From 65d7ce9c9c4fad5b1af7e5fde275db56ec93c745 Mon Sep 17 00:00:00 2001 From: Callum Waters Date: Tue, 23 Jun 2020 17:09:14 +0200 Subject: [PATCH] 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 --- consensus/reactor_test.go | 4 +- consensus/replay_stubs.go | 2 +- consensus/state.go | 4 +- consensus/state_test.go | 8 +- evidence/doc.go | 30 +- evidence/pool.go | 258 +++++++++++------ evidence/pool_test.go | 166 ++++++++--- light/client.go | 4 +- light/client_test.go | 2 +- rpc/client/evidence_test.go | 2 +- state/validation.go | 13 +- state/validation_test.go | 104 +++++-- types/block.go | 4 +- types/evidence.go | 554 +++++++++++++++++++----------------- types/evidence_test.go | 210 ++++++++------ 15 files changed, 827 insertions(+), 538 deletions(-) diff --git a/consensus/reactor_test.go b/consensus/reactor_test.go index 069350a11..cc45150ed 100644 --- a/consensus/reactor_test.go +++ b/consensus/reactor_test.go @@ -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 } //------------------------------------ diff --git a/consensus/replay_stubs.go b/consensus/replay_stubs.go index 8560af2d3..c83543f2a 100644 --- a/consensus/replay_stubs.go +++ b/consensus/replay_stubs.go @@ -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 } //----------------------------------------------------------------------------- diff --git a/consensus/state.go b/consensus/state.go index 86962a88e..0d2d28c9b 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -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 diff --git a/consensus/state_test.go b/consensus/state_test.go index 8fe483591..f0999a86d 100644 --- a/consensus/state_test.go +++ b/consensus/state_test.go @@ -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) diff --git a/evidence/doc.go b/evidence/doc.go index 77bd8ef75..3ac0d67fe 100644 --- a/evidence/doc.go +++ b/evidence/doc.go @@ -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 diff --git a/evidence/pool.go b/evidence/pool.go index cfeb2981b..d425ab183 100644 --- a/evidence/pool.go +++ b/evidence/pool.go @@ -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()) } diff --git a/evidence/pool_test.go b/evidence/pool_test.go index cfcf189e0..d761be931 100644 --- a/evidence/pool_test.go +++ b/evidence/pool_test.go @@ -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, } diff --git a/light/client.go b/light/client.go index 8c9e1e2e6..5e88f7813 100644 --- a/light/client.go +++ b/light/client.go @@ -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) diff --git a/light/client_test.go b/light/client_test.go index 215e03704..8b39ee819 100644 --- a/light/client_test.go +++ b/light/client_test.go @@ -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)) } diff --git a/rpc/client/evidence_test.go b/rpc/client/evidence_test.go index f51e9b3f2..6f61b4356 100644 --- a/rpc/client/evidence_test.go +++ b/rpc/client/evidence_test.go @@ -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, } diff --git a/state/validation.go b/state/validation.go index 609f66b50..3d21b7c83 100644 --- a/state/validation.go +++ b/state/validation.go @@ -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) diff --git a/state/validation_test.go b/state/validation_test.go index 3c3dad7db..560a3489b 100644 --- a/state/validation_test.go +++ b/state/validation_test.go @@ -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, } diff --git a/types/block.go b/types/block.go index 6ee3540e9..47e3c9906 100644 --- a/types/block.go +++ b/types/block.go @@ -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) diff --git a/types/evidence.go b/types/evidence.go index 9b9cadbea..7f88e6cce 100644 --- a/types/evidence.go +++ b/types/evidence.go @@ -92,26 +92,17 @@ func EvidenceToProto(evidence Evidence) (*tmproto.Evidence, error) { pbevi := evi.ToProto() tp := &tmproto.Evidence{ Sum: &tmproto.Evidence_DuplicateVoteEvidence{ - DuplicateVoteEvidence: &pbevi, + DuplicateVoteEvidence: pbevi, }, } return tp, nil - case ConflictingHeadersEvidence: - pbevi := evi.ToProto() - tp := &tmproto.Evidence{ - Sum: &tmproto.Evidence_ConflictingHeadersEvidence{ - ConflictingHeadersEvidence: &pbevi, - }, - } - - return tp, nil case *ConflictingHeadersEvidence: pbevi := evi.ToProto() tp := &tmproto.Evidence{ Sum: &tmproto.Evidence_ConflictingHeadersEvidence{ - ConflictingHeadersEvidence: &pbevi, + ConflictingHeadersEvidence: pbevi, }, } @@ -121,66 +112,44 @@ func EvidenceToProto(evidence Evidence) (*tmproto.Evidence, error) { tp := &tmproto.Evidence{ Sum: &tmproto.Evidence_LunaticValidatorEvidence{ - LunaticValidatorEvidence: &pbevi, + LunaticValidatorEvidence: pbevi, }, } return tp, nil - case LunaticValidatorEvidence: - pbevi := evi.ToProto() - tp := &tmproto.Evidence{ - Sum: &tmproto.Evidence_LunaticValidatorEvidence{ - LunaticValidatorEvidence: &pbevi, - }, - } - - return tp, nil case *PhantomValidatorEvidence: pbevi := evi.ToProto() tp := &tmproto.Evidence{ Sum: &tmproto.Evidence_PhantomValidatorEvidence{ - PhantomValidatorEvidence: &pbevi, + PhantomValidatorEvidence: pbevi, }, } return tp, nil - case PhantomValidatorEvidence: - pbevi := evi.ToProto() - tp := &tmproto.Evidence{ - Sum: &tmproto.Evidence_PhantomValidatorEvidence{ - PhantomValidatorEvidence: &pbevi, - }, - } - - return tp, nil case *PotentialAmnesiaEvidence: pbevi := evi.ToProto() tp := &tmproto.Evidence{ Sum: &tmproto.Evidence_PotentialAmnesiaEvidence{ - PotentialAmnesiaEvidence: &pbevi, + PotentialAmnesiaEvidence: pbevi, }, } return tp, nil - case PotentialAmnesiaEvidence: - pbevi := evi.ToProto() + + case *AmnesiaEvidence: + aepb := evi.ToProto() tp := &tmproto.Evidence{ - Sum: &tmproto.Evidence_PotentialAmnesiaEvidence{ - PotentialAmnesiaEvidence: &pbevi, + Sum: &tmproto.Evidence_AmnesiaEvidence{ + AmnesiaEvidence: aepb, }, } - return tp, nil - - case AmnesiaEvidence: - return AmnesiaEvidenceToProto(evi) - case *AmnesiaEvidence: - return AmnesiaEvidenceToProto(*evi) + return tp, nil case MockEvidence: if err := evi.ValidateBasic(); err != nil { @@ -234,7 +203,7 @@ func EvidenceFromProto(evidence *tmproto.Evidence) (Evidence, error) { case *tmproto.Evidence_PotentialAmnesiaEvidence: return PotentialAmnesiaEvidenceFromProto(evi.PotentialAmnesiaEvidence) case *tmproto.Evidence_AmnesiaEvidence: - return AmensiaEvidenceFromProto(evi.AmnesiaEvidence) + return AmnesiaEvidenceFromProto(evi.AmnesiaEvidence) case *tmproto.Evidence_PhantomValidatorEvidence: return PhantomValidatorEvidenceFromProto(evi.PhantomValidatorEvidence) case *tmproto.Evidence_MockEvidence: @@ -428,6 +397,10 @@ func (dve *DuplicateVoteEvidence) Equal(ev Evidence) bool { // ValidateBasic performs basic validation. func (dve *DuplicateVoteEvidence) ValidateBasic() error { + if dve == nil { + return errors.New("empty duplicate vote evidence") + } + if dve.VoteA == nil || dve.VoteB == nil { return fmt.Errorf("one or both of the votes are empty %v, %v", dve.VoteA, dve.VoteB) } @@ -444,14 +417,14 @@ func (dve *DuplicateVoteEvidence) ValidateBasic() error { return nil } -func (dve DuplicateVoteEvidence) ToProto() tmproto.DuplicateVoteEvidence { +func (dve *DuplicateVoteEvidence) ToProto() *tmproto.DuplicateVoteEvidence { voteB := dve.VoteB.ToProto() voteA := dve.VoteA.ToProto() tp := tmproto.DuplicateVoteEvidence{ VoteA: voteA, VoteB: voteB, } - return tp + return &tp } func DuplicateVoteEvidenceFromProto(pb *tmproto.DuplicateVoteEvidence) (*DuplicateVoteEvidence, error) { @@ -477,43 +450,6 @@ func DuplicateVoteEvidenceFromProto(pb *tmproto.DuplicateVoteEvidence) (*Duplica return dve, dve.ValidateBasic() } -//------------------------------------------- - -// 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.HashFromByteSlices(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. @@ -522,10 +458,15 @@ type ConflictingHeadersEvidence struct { H2 *SignedHeader `json:"h_2"` } -var _ Evidence = ConflictingHeadersEvidence{} -var _ CompositeEvidence = ConflictingHeadersEvidence{} +var _ Evidence = &ConflictingHeadersEvidence{} +var _ CompositeEvidence = &ConflictingHeadersEvidence{} + +// NewConflictingHeadersEvidence creates a new instance of the respective evidence +func NewConflictingHeadersEvidence(h1, h2 *SignedHeader) *ConflictingHeadersEvidence { + return &ConflictingHeadersEvidence{H1: h1, H2: h2} +} -// Split breaks up eviddence into smaller chunks (one per validator except for +// Split breaks up evidence into smaller chunks (one per validator except for // PotentialAmnesiaEvidence): PhantomValidatorEvidence, // LunaticValidatorEvidence, DuplicateVoteEvidence and // PotentialAmnesiaEvidence. @@ -533,7 +474,7 @@ var _ CompositeEvidence = ConflictingHeadersEvidence{} // 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, +func (ev *ConflictingHeadersEvidence) Split(committedHeader *Header, valSet *ValidatorSet, valToLastHeight map[string]int64) []Evidence { evList := make([]Evidence, 0) @@ -640,21 +581,11 @@ OUTER_LOOP: // immediately slashable. firstVote := ev.H1.Commit.GetVote(int32(i)) secondVote := ev.H2.Commit.GetVote(int32(j)) - var newEv PotentialAmnesiaEvidence - if firstVote.Timestamp.Before(secondVote.Timestamp) { - newEv = PotentialAmnesiaEvidence{ - VoteA: firstVote, - VoteB: secondVote, - } - } else { - newEv = PotentialAmnesiaEvidence{ - VoteA: secondVote, - VoteB: firstVote, - } - } + newEv := NewPotentialAmnesiaEvidence(firstVote, secondVote) + // has the validator incorrectly voted for a previous round if newEv.VoteA.Round > newEv.VoteB.Round { - evList = append(evList, MakeAmnesiaEvidence(newEv, EmptyPOLC())) + evList = append(evList, NewAmnesiaEvidence(newEv, NewEmptyPOLC())) } else { evList = append(evList, newEv) } @@ -675,18 +606,18 @@ OUTER_LOOP: return evList } -func (ev ConflictingHeadersEvidence) Height() int64 { return ev.H1.Height } +func (ev *ConflictingHeadersEvidence) Height() int64 { return ev.H1.Height } // Time returns time of the latest header. -func (ev ConflictingHeadersEvidence) Time() time.Time { +func (ev *ConflictingHeadersEvidence) Time() time.Time { return maxTime(ev.H1.Time, ev.H2.Time) } -func (ev ConflictingHeadersEvidence) Address() []byte { +func (ev *ConflictingHeadersEvidence) Address() []byte { panic("use ConflictingHeadersEvidence#Split to split evidence into individual pieces") } -func (ev ConflictingHeadersEvidence) Bytes() []byte { +func (ev *ConflictingHeadersEvidence) Bytes() []byte { pbe := ev.ToProto() bz, err := pbe.Marshal() @@ -697,20 +628,20 @@ func (ev ConflictingHeadersEvidence) Bytes() []byte { return bz } -func (ev ConflictingHeadersEvidence) Hash() []byte { +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 { +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 { +func (ev *ConflictingHeadersEvidence) VerifyComposite(committedHeader *Header, valSet *ValidatorSet) error { var alternativeHeader *SignedHeader switch { case bytes.Equal(committedHeader.Hash(), ev.H1.Hash()): @@ -755,18 +686,19 @@ func (ev ConflictingHeadersEvidence) VerifyComposite(committedHeader *Header, va return nil } -func (ev ConflictingHeadersEvidence) Equal(ev2 Evidence) bool { - switch e2 := ev2.(type) { - case ConflictingHeadersEvidence: +func (ev *ConflictingHeadersEvidence) Equal(ev2 Evidence) bool { + if e2, ok := ev2.(*ConflictingHeadersEvidence); ok { 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 } + + return false } -func (ev ConflictingHeadersEvidence) ValidateBasic() error { +func (ev *ConflictingHeadersEvidence) ValidateBasic() error { + if ev == nil { + return errors.New("empty conflicting headers evidence") + } + if ev.H1 == nil { return errors.New("first header is missing") } @@ -784,37 +716,37 @@ func (ev ConflictingHeadersEvidence) ValidateBasic() error { return nil } -func (ev ConflictingHeadersEvidence) String() string { +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()) } -func (ev ConflictingHeadersEvidence) ToProto() tmproto.ConflictingHeadersEvidence { +func (ev *ConflictingHeadersEvidence) ToProto() *tmproto.ConflictingHeadersEvidence { pbh1 := ev.H1.ToProto() pbh2 := ev.H2.ToProto() - tp := tmproto.ConflictingHeadersEvidence{ + tp := &tmproto.ConflictingHeadersEvidence{ H1: pbh1, H2: pbh2, } return tp } -func ConflictingHeadersEvidenceFromProto(pb *tmproto.ConflictingHeadersEvidence) (ConflictingHeadersEvidence, error) { +func ConflictingHeadersEvidenceFromProto(pb *tmproto.ConflictingHeadersEvidence) (*ConflictingHeadersEvidence, error) { if pb == nil { - return ConflictingHeadersEvidence{}, errors.New("nil ConflictingHeadersEvidence") + return &ConflictingHeadersEvidence{}, errors.New("nil ConflictingHeadersEvidence") } h1, err := SignedHeaderFromProto(pb.H1) if err != nil { - return ConflictingHeadersEvidence{}, fmt.Errorf("from proto err: %w", err) + return &ConflictingHeadersEvidence{}, fmt.Errorf("from proto err: %w", err) } h2, err := SignedHeaderFromProto(pb.H2) if err != nil { - return ConflictingHeadersEvidence{}, fmt.Errorf("from proto err: %w", err) + return &ConflictingHeadersEvidence{}, fmt.Errorf("from proto err: %w", err) } - tp := ConflictingHeadersEvidence{ + tp := &ConflictingHeadersEvidence{ H1: h1, H2: h2, } @@ -829,22 +761,29 @@ type PhantomValidatorEvidence struct { LastHeightValidatorWasInSet int64 `json:"last_height_validator_was_in_set"` } -var _ Evidence = PhantomValidatorEvidence{} +var _ Evidence = &PhantomValidatorEvidence{} + +// NewPhantomValidatorEvidence creates a new instance of the respective evidence +func NewPhantomValidatorEvidence(vote *Vote, lastHeightValidatorWasInSet int64) *PhantomValidatorEvidence { + return &PhantomValidatorEvidence{ + Vote: vote, + LastHeightValidatorWasInSet: lastHeightValidatorWasInSet, + } +} -func (e PhantomValidatorEvidence) Height() int64 { +func (e *PhantomValidatorEvidence) Height() int64 { return e.Vote.Height } -// Time returns the Vote's timestamp. -func (e PhantomValidatorEvidence) Time() time.Time { +func (e *PhantomValidatorEvidence) Time() time.Time { return e.Vote.Timestamp } -func (e PhantomValidatorEvidence) Address() []byte { +func (e *PhantomValidatorEvidence) Address() []byte { return e.Vote.ValidatorAddress } -func (e PhantomValidatorEvidence) Hash() []byte { +func (e *PhantomValidatorEvidence) Hash() []byte { pbe := e.ToProto() bz, err := pbe.Marshal() @@ -854,7 +793,7 @@ func (e PhantomValidatorEvidence) Hash() []byte { return tmhash.Sum(bz) } -func (e PhantomValidatorEvidence) Bytes() []byte { +func (e *PhantomValidatorEvidence) Bytes() []byte { pbe := e.ToProto() bz, err := pbe.Marshal() @@ -865,7 +804,7 @@ func (e PhantomValidatorEvidence) Bytes() []byte { return bz } -func (e PhantomValidatorEvidence) Verify(chainID string, pubKey crypto.PubKey) error { +func (e *PhantomValidatorEvidence) Verify(chainID string, pubKey crypto.PubKey) error { v := e.Vote.ToProto() if !pubKey.VerifyBytes(VoteSignBytes(chainID, v), e.Vote.Signature) { @@ -875,27 +814,26 @@ func (e PhantomValidatorEvidence) Verify(chainID string, pubKey crypto.PubKey) e return nil } -func (e PhantomValidatorEvidence) Equal(ev Evidence) bool { - switch e2 := ev.(type) { - case PhantomValidatorEvidence: - return e.Vote.Height == e2.Vote.Height && - bytes.Equal(e.Vote.ValidatorAddress, e2.Vote.ValidatorAddress) - case *PhantomValidatorEvidence: +func (e *PhantomValidatorEvidence) Equal(ev Evidence) bool { + if e2, ok := ev.(*PhantomValidatorEvidence); ok { return e.Vote.Height == e2.Vote.Height && bytes.Equal(e.Vote.ValidatorAddress, e2.Vote.ValidatorAddress) - default: - return false } + + return false } -func (e PhantomValidatorEvidence) ValidateBasic() error { +func (e *PhantomValidatorEvidence) ValidateBasic() error { + if e == nil { + return errors.New("empty phantom validator evidence") + } if e.Vote == nil { return errors.New("empty vote") } if err := e.Vote.ValidateBasic(); err != nil { - return fmt.Errorf("invalid signature: %v", err) + return fmt.Errorf("invalid vote: %w", err) } if !e.Vote.BlockID.IsComplete() { @@ -909,15 +847,15 @@ func (e PhantomValidatorEvidence) ValidateBasic() error { return nil } -func (e PhantomValidatorEvidence) String() string { +func (e *PhantomValidatorEvidence) String() string { return fmt.Sprintf("PhantomValidatorEvidence{%X voted at height %d}", e.Vote.ValidatorAddress, e.Vote.Height) } -func (e PhantomValidatorEvidence) ToProto() tmproto.PhantomValidatorEvidence { +func (e *PhantomValidatorEvidence) ToProto() *tmproto.PhantomValidatorEvidence { vpb := e.Vote.ToProto() - tp := tmproto.PhantomValidatorEvidence{ + tp := &tmproto.PhantomValidatorEvidence{ Vote: vpb, LastHeightValidatorWasInSet: e.LastHeightValidatorWasInSet, } @@ -925,17 +863,17 @@ func (e PhantomValidatorEvidence) ToProto() tmproto.PhantomValidatorEvidence { return tp } -func PhantomValidatorEvidenceFromProto(pb *tmproto.PhantomValidatorEvidence) (PhantomValidatorEvidence, error) { +func PhantomValidatorEvidenceFromProto(pb *tmproto.PhantomValidatorEvidence) (*PhantomValidatorEvidence, error) { if pb == nil { - return PhantomValidatorEvidence{}, errors.New("nil PhantomValidatorEvidence") + return nil, errors.New("nil PhantomValidatorEvidence") } vpb, err := VoteFromProto(pb.Vote) if err != nil { - return PhantomValidatorEvidence{}, err + return nil, err } - tp := PhantomValidatorEvidence{ + tp := &PhantomValidatorEvidence{ Vote: vpb, LastHeightValidatorWasInSet: pb.LastHeightValidatorWasInSet, } @@ -951,29 +889,38 @@ type LunaticValidatorEvidence struct { InvalidHeaderField string `json:"invalid_header_field"` } -var _ Evidence = LunaticValidatorEvidence{} +var _ Evidence = &LunaticValidatorEvidence{} -func (e LunaticValidatorEvidence) Height() int64 { +// NewLunaticValidatorEvidence creates a new instance of the respective evidence +func NewLunaticValidatorEvidence(header *Header, vote *Vote, invalidHeaderField string) *LunaticValidatorEvidence { + return &LunaticValidatorEvidence{ + Header: header, + Vote: vote, + InvalidHeaderField: invalidHeaderField, + } +} + +func (e *LunaticValidatorEvidence) Height() int64 { return e.Header.Height } // Time returns the maximum between the header's time and vote's time. -func (e LunaticValidatorEvidence) Time() time.Time { +func (e *LunaticValidatorEvidence) Time() time.Time { return maxTime(e.Header.Time, e.Vote.Timestamp) } -func (e LunaticValidatorEvidence) Address() []byte { +func (e *LunaticValidatorEvidence) Address() []byte { return e.Vote.ValidatorAddress } -func (e LunaticValidatorEvidence) Hash() []byte { +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 { +func (e *LunaticValidatorEvidence) Bytes() []byte { pbe := e.ToProto() bz, err := pbe.Marshal() @@ -984,7 +931,7 @@ func (e LunaticValidatorEvidence) Bytes() []byte { return bz } -func (e LunaticValidatorEvidence) Verify(chainID string, pubKey crypto.PubKey) error { +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", @@ -1001,20 +948,19 @@ func (e LunaticValidatorEvidence) Verify(chainID string, pubKey crypto.PubKey) e return nil } -func (e LunaticValidatorEvidence) Equal(ev Evidence) bool { - switch e2 := ev.(type) { - case LunaticValidatorEvidence: +func (e *LunaticValidatorEvidence) Equal(ev Evidence) bool { + if e2, ok := ev.(*LunaticValidatorEvidence); ok { 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 } + return false } -func (e LunaticValidatorEvidence) ValidateBasic() error { +func (e *LunaticValidatorEvidence) ValidateBasic() error { + if e == nil { + return errors.New("empty lunatic validator evidence") + } + if e.Header == nil { return errors.New("empty header") } @@ -1050,12 +996,12 @@ func (e LunaticValidatorEvidence) ValidateBasic() error { } } -func (e LunaticValidatorEvidence) String() string { +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 { +func (e *LunaticValidatorEvidence) VerifyHeader(committedHeader *Header) error { matchErr := func(field string) error { return fmt.Errorf("%s matches committed hash", field) } @@ -1092,11 +1038,11 @@ func (e LunaticValidatorEvidence) VerifyHeader(committedHeader *Header) error { return nil } -func (e LunaticValidatorEvidence) ToProto() tmproto.LunaticValidatorEvidence { +func (e *LunaticValidatorEvidence) ToProto() *tmproto.LunaticValidatorEvidence { h := e.Header.ToProto() v := e.Vote.ToProto() - tp := tmproto.LunaticValidatorEvidence{ + tp := &tmproto.LunaticValidatorEvidence{ Header: h, Vote: v, InvalidHeaderField: e.InvalidHeaderField, @@ -1132,7 +1078,7 @@ func LunaticValidatorEvidenceFromProto(pb *tmproto.LunaticValidatorEvidence) (*L //------------------------------------------- // PotentialAmnesiaEvidence is constructed when a validator votes on two different blocks at different rounds -// in the same height. PotentialAmnesiaEvidence can then evolve into AmensiaEvidence if the indicted validator +// in the same height. PotentialAmnesiaEvidence can then evolve into AmnesiaEvidence if the indicted validator // is incapable of providing the proof of lock change that validates voting twice in the allotted trial period. // Heightstamp is used for each node to keep a track of how much time has passed so as to know when the trial period // is finished and is set when the node first receives the evidence. @@ -1143,33 +1089,48 @@ type PotentialAmnesiaEvidence struct { HeightStamp int64 } -var _ Evidence = PotentialAmnesiaEvidence{} +var _ Evidence = &PotentialAmnesiaEvidence{} + +// NewPotentialAmnesiaEvidence creates a new instance of the evidence and orders the votes correctly +func NewPotentialAmnesiaEvidence(voteA *Vote, voteB *Vote) *PotentialAmnesiaEvidence { + if voteA == nil || voteB == nil { + return nil + } + + if voteA.Timestamp.Before(voteB.Timestamp) { + return &PotentialAmnesiaEvidence{VoteA: voteA, VoteB: voteB} + } + return &PotentialAmnesiaEvidence{VoteA: voteB, VoteB: voteA} +} -func (e PotentialAmnesiaEvidence) Height() int64 { +func (e *PotentialAmnesiaEvidence) Height() int64 { return e.VoteA.Height } -// Time returns time of the latest vote. -func (e PotentialAmnesiaEvidence) Time() time.Time { - return maxTime(e.VoteA.Timestamp, e.VoteB.Timestamp) +func (e *PotentialAmnesiaEvidence) Time() time.Time { + return e.VoteB.Timestamp } -func (e PotentialAmnesiaEvidence) Address() []byte { +func (e *PotentialAmnesiaEvidence) Address() []byte { return e.VoteA.ValidatorAddress } -func (e PotentialAmnesiaEvidence) Hash() []byte { - pbe := e.ToProto() +// NOTE: Heightstamp must not be included in hash +func (e *PotentialAmnesiaEvidence) Hash() []byte { + v1, err := e.VoteA.ToProto().Marshal() + if err != nil { + panic(fmt.Errorf("trying to hash potential amnesia evidence, err: %w", err)) + } - bz, err := pbe.Marshal() + v2, err := e.VoteB.ToProto().Marshal() if err != nil { - panic(err) + panic(fmt.Errorf("trying to hash potential amnesia evidence, err: %w", err)) } - return tmhash.Sum(bz) + return tmhash.Sum(append(v1, v2...)) } -func (e PotentialAmnesiaEvidence) Bytes() []byte { +func (e *PotentialAmnesiaEvidence) Bytes() []byte { pbe := e.ToProto() bz, err := pbe.Marshal() @@ -1180,7 +1141,7 @@ func (e PotentialAmnesiaEvidence) Bytes() []byte { return bz } -func (e PotentialAmnesiaEvidence) Verify(chainID string, pubKey crypto.PubKey) error { +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) { @@ -1202,23 +1163,23 @@ func (e PotentialAmnesiaEvidence) Verify(chainID string, pubKey crypto.PubKey) e return nil } -func (e PotentialAmnesiaEvidence) Equal(ev Evidence) bool { - switch e2 := ev.(type) { - case PotentialAmnesiaEvidence: +func (e *PotentialAmnesiaEvidence) Equal(ev Evidence) bool { + if e2, ok := ev.(*PotentialAmnesiaEvidence); ok { return e.Height() == e2.Height() && e.VoteA.Round == e2.VoteA.Round && e.VoteB.Round == e2.VoteB.Round && bytes.Equal(e.Address(), e2.Address()) - case *PotentialAmnesiaEvidence: - return e.Height() == e2.Height() && e.VoteA.Round == e2.VoteA.Round && e.VoteB.Round == e2.VoteB.Round && - bytes.Equal(e.Address(), e2.Address()) - default: - return false } + return false } -func (e PotentialAmnesiaEvidence) ValidateBasic() error { +func (e *PotentialAmnesiaEvidence) ValidateBasic() error { + if e == nil { + return errors.New("empty potential amnesia evidence") + } + 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) } @@ -1268,14 +1229,14 @@ func (e PotentialAmnesiaEvidence) ValidateBasic() error { return nil } -func (e PotentialAmnesiaEvidence) String() string { +func (e *PotentialAmnesiaEvidence) String() string { return fmt.Sprintf("PotentialAmnesiaEvidence{VoteA: %v, VoteB: %v}", e.VoteA, e.VoteB) } // Primed finds whether the PotentialAmnesiaEvidence is ready to be upgraded to Amnesia Evidence. It is decided if // either the prosecuted node voted in the past or if the allocated trial period has expired without a proof of lock // change having been provided. -func (e PotentialAmnesiaEvidence) Primed(trialPeriod, currentHeight int64) bool { +func (e *PotentialAmnesiaEvidence) Primed(trialPeriod, currentHeight int64) bool { // voted in the past can be instantly punishable if e.VoteA.Round > e.VoteB.Round { return true @@ -1287,11 +1248,11 @@ func (e PotentialAmnesiaEvidence) Primed(trialPeriod, currentHeight int64) bool return false } -func (e PotentialAmnesiaEvidence) ToProto() tmproto.PotentialAmnesiaEvidence { +func (e *PotentialAmnesiaEvidence) ToProto() *tmproto.PotentialAmnesiaEvidence { voteB := e.VoteB.ToProto() voteA := e.VoteA.ToProto() - tp := tmproto.PotentialAmnesiaEvidence{ + tp := &tmproto.PotentialAmnesiaEvidence{ VoteA: voteA, VoteB: voteB, HeightStamp: e.HeightStamp, @@ -1306,27 +1267,35 @@ func (e PotentialAmnesiaEvidence) ToProto() tmproto.PotentialAmnesiaEvidence { // different rounds because the node received a majority of votes for a different block in the latter round. In cases of // amnesia evidence, a suspected node will need ProofOfLockChange to prove that the node did not break protocol. type ProofOfLockChange struct { - Votes []Vote `json:"votes"` + Votes []*Vote `json:"votes"` PubKey crypto.PubKey `json:"pubkey"` } // MakePOLCFromVoteSet can be used when a majority of prevotes or precommits for a block is seen // that the node has itself not yet voted for in order to process the vote set into a proof of lock change -func MakePOLCFromVoteSet(voteSet *VoteSet, pubKey crypto.PubKey, blockID BlockID) (ProofOfLockChange, error) { - polc := makePOLCFromVoteSet(voteSet, pubKey, blockID) +func NewPOLCFromVoteSet(voteSet *VoteSet, pubKey crypto.PubKey, blockID BlockID) (*ProofOfLockChange, error) { + polc := newPOLCFromVoteSet(voteSet, pubKey, blockID) return polc, polc.ValidateBasic() } -func makePOLCFromVoteSet(voteSet *VoteSet, pubKey crypto.PubKey, blockID BlockID) ProofOfLockChange { - var votes []Vote +func newPOLCFromVoteSet(voteSet *VoteSet, pubKey crypto.PubKey, blockID BlockID) *ProofOfLockChange { + if voteSet == nil { + return nil + } + var votes []*Vote valSetSize := voteSet.Size() for valIdx := int32(0); int(valIdx) < valSetSize; valIdx++ { vote := voteSet.GetByIndex(valIdx) if vote != nil && vote.BlockID.Equals(blockID) { - votes = append(votes, *vote) + votes = append(votes, vote) } } - return ProofOfLockChange{ + return NewPOLC(votes, pubKey) +} + +// NewPOLC creates a POLC +func NewPOLC(votes []*Vote, pubKey crypto.PubKey) *ProofOfLockChange { + return &ProofOfLockChange{ Votes: votes, PubKey: pubKey, } @@ -1334,19 +1303,19 @@ func makePOLCFromVoteSet(voteSet *VoteSet, pubKey crypto.PubKey, blockID BlockID // EmptyPOLC returns an empty polc. This is used when no polc has been provided in the allocated trial period time // and the node now needs to move to upgrading to AmnesiaEvidence and hence uses an empty polc -func EmptyPOLC() ProofOfLockChange { - return ProofOfLockChange{ +func NewEmptyPOLC() *ProofOfLockChange { + return &ProofOfLockChange{ nil, nil, } } -func (e ProofOfLockChange) Height() int64 { +func (e *ProofOfLockChange) Height() int64 { return e.Votes[0].Height } // Time returns time of the latest vote. -func (e ProofOfLockChange) Time() time.Time { +func (e *ProofOfLockChange) Time() time.Time { latest := e.Votes[0].Timestamp for _, vote := range e.Votes { if vote.Timestamp.After(latest) { @@ -1356,21 +1325,21 @@ func (e ProofOfLockChange) Time() time.Time { return latest } -func (e ProofOfLockChange) Round() int32 { +func (e *ProofOfLockChange) Round() int32 { return e.Votes[0].Round } -func (e ProofOfLockChange) Address() []byte { +func (e *ProofOfLockChange) Address() []byte { return e.PubKey.Address() } -func (e ProofOfLockChange) BlockID() BlockID { +func (e *ProofOfLockChange) BlockID() BlockID { return e.Votes[0].BlockID } // ValidateVotes checks the polc against the validator set of that height. The function makes sure that the polc // contains a majority of votes and that each -func (e ProofOfLockChange) ValidateVotes(valSet *ValidatorSet, chainID string) error { +func (e *ProofOfLockChange) ValidateVotes(valSet *ValidatorSet, chainID string) error { if e.IsAbsent() { return errors.New("polc is empty") } @@ -1403,12 +1372,16 @@ func (e ProofOfLockChange) ValidateVotes(valSet *ValidatorSet, chainID string) e return nil } -func (e ProofOfLockChange) Equal(e2 ProofOfLockChange) bool { +func (e *ProofOfLockChange) Equal(e2 *ProofOfLockChange) bool { return bytes.Equal(e.Address(), e2.Address()) && e.Height() == e2.Height() && e.Round() == e2.Round() } -func (e ProofOfLockChange) ValidateBasic() error { +func (e *ProofOfLockChange) ValidateBasic() error { + if e == nil { + return errors.New("empty proof of lock change") + } + // first check if the polc is absent / empty if e.IsAbsent() { return nil @@ -1418,7 +1391,7 @@ func (e ProofOfLockChange) ValidateBasic() error { return errors.New("missing public key") } // validate basic doesn't count the number of votes and their voting power, this is to be done by VerifyEvidence - if e.Votes == nil { + if e.Votes == nil || len(e.Votes) == 0 { return errors.New("missing votes") } // height, round and vote type must be the same for all votes @@ -1429,6 +1402,10 @@ func (e ProofOfLockChange) ValidateBasic() error { } voteType := e.Votes[0].Type for idx, vote := range e.Votes { + if vote == nil { + return fmt.Errorf("nil vote at index: %d", idx) + } + if err := vote.ValidateBasic(); err != nil { return fmt.Errorf("invalid vote#%d: %w", idx, err) } @@ -1464,7 +1441,7 @@ func (e ProofOfLockChange) ValidateBasic() error { return nil } -func (e ProofOfLockChange) String() string { +func (e *ProofOfLockChange) String() string { if e.IsAbsent() { return "Empty ProofOfLockChange" } @@ -1473,14 +1450,11 @@ func (e ProofOfLockChange) String() string { } // IsAbsent checks if the polc is empty -func (e ProofOfLockChange) IsAbsent() bool { +func (e *ProofOfLockChange) IsAbsent() bool { return e.Votes == nil && e.PubKey == nil } func (e *ProofOfLockChange) ToProto() (*tmproto.ProofOfLockChange, error) { - if e == nil { - return nil, errors.New("nil proof of lock change") - } plc := new(tmproto.ProofOfLockChange) vpb := make([]*tmproto.Vote, len(e.Votes)) @@ -1490,7 +1464,7 @@ func (e *ProofOfLockChange) ToProto() (*tmproto.ProofOfLockChange, error) { } if e.Votes == nil { - return nil, errors.New("polc is not absent but has no votes") + return plc, errors.New("invalid proof of lock change (no votes), did you forget to validate?") } for i, v := range e.Votes { pb := v.ToProto() @@ -1501,7 +1475,7 @@ func (e *ProofOfLockChange) ToProto() (*tmproto.ProofOfLockChange, error) { pk, err := cryptoenc.PubKeyToProto(e.PubKey) if err != nil { - return nil, err + return plc, fmt.Errorf("invalid proof of lock change (err: %w), did you forget to validate?", err) } plc.PubKey = &pk plc.Votes = vpb @@ -1512,30 +1486,47 @@ func (e *ProofOfLockChange) ToProto() (*tmproto.ProofOfLockChange, error) { // AmnesiaEvidence is the progression of PotentialAmnesiaEvidence and is used to prove an infringement of the // Tendermint consensus when a validator incorrectly sends a vote in a later round without correctly changing the lock type AmnesiaEvidence struct { - PotentialAmnesiaEvidence - Polc ProofOfLockChange + *PotentialAmnesiaEvidence + Polc *ProofOfLockChange } -// Height, Time, Address and Verify functions are all inherited by the PotentialAmnesiaEvidence struct +// Height, Time, Address, and Verify, and Hash functions are all inherited by the PotentialAmnesiaEvidence struct var _ Evidence = &AmnesiaEvidence{} -var _ Evidence = AmnesiaEvidence{} -func MakeAmnesiaEvidence(pe PotentialAmnesiaEvidence, proof ProofOfLockChange) AmnesiaEvidence { - return AmnesiaEvidence{ +func NewAmnesiaEvidence(pe *PotentialAmnesiaEvidence, proof *ProofOfLockChange) *AmnesiaEvidence { + return &AmnesiaEvidence{ pe, proof, } } -func (e AmnesiaEvidence) Equal(ev Evidence) bool { - e2, ok := ev.(AmnesiaEvidence) - if !ok { - return false +// Note: Amnesia evidence with or without a polc are considered the same +func (e *AmnesiaEvidence) Equal(ev Evidence) bool { + if e2, ok := ev.(*AmnesiaEvidence); ok { + return e.PotentialAmnesiaEvidence.Equal(e2.PotentialAmnesiaEvidence) } - return e.PotentialAmnesiaEvidence.Equal(e2.PotentialAmnesiaEvidence) + return false } -func (e AmnesiaEvidence) ValidateBasic() error { +func (e *AmnesiaEvidence) Bytes() []byte { + pbe := e.ToProto() + + bz, err := pbe.Marshal() + if err != nil { + panic(fmt.Errorf("converting amnesia evidence to bytes, err: %w", err)) + } + + return bz +} + +func (e *AmnesiaEvidence) ValidateBasic() error { + if e == nil { + return errors.New("empty amnesia evidence") + } + if e.Polc == nil || e.PotentialAmnesiaEvidence == nil { + return errors.New("amnesia evidence is missing either the polc or the potential amnesia evidence") + } + if err := e.PotentialAmnesiaEvidence.ValidateBasic(); err != nil { return fmt.Errorf("invalid potential amnesia evidence: %w", err) } @@ -1571,10 +1562,10 @@ func (e AmnesiaEvidence) ValidateBasic() error { return nil } -// ViolatedConsensus assess on the basis of the AmensiaEvidence whether the validator has violated the +// ViolatedConsensus assess on the basis of the AmnesiaEvidence whether the validator has violated the // Tendermint consensus. Evidence must be validated first (see ValidateBasic). // We are only interested in proving that the latter of the votes in terms of time was correctly done. -func (e AmnesiaEvidence) ViolatedConsensus() (bool, string) { +func (e *AmnesiaEvidence) ViolatedConsensus() (bool, string) { // a validator having voted cannot go back and vote on an earlier round if e.PotentialAmnesiaEvidence.VoteA.Round > e.PotentialAmnesiaEvidence.VoteB.Round { return true, "validator went back and voted on a previous round" @@ -1588,10 +1579,24 @@ func (e AmnesiaEvidence) ViolatedConsensus() (bool, string) { return false, "" } -func (e AmnesiaEvidence) String() string { +func (e *AmnesiaEvidence) String() string { return fmt.Sprintf("AmnesiaEvidence{ %v, polc: %v }", e.PotentialAmnesiaEvidence, e.Polc) } +func (e *AmnesiaEvidence) ToProto() *tmproto.AmnesiaEvidence { + paepb := e.PotentialAmnesiaEvidence.ToProto() + + polc, err := e.Polc.ToProto() + if err != nil { + polc, _ = NewEmptyPOLC().ToProto() + } + + return &tmproto.AmnesiaEvidence{ + PotentialAmnesiaEvidence: paepb, + Polc: polc, + } +} + func ProofOfLockChangeFromProto(pb *tmproto.ProofOfLockChange) (*ProofOfLockChange, error) { if pb == nil { return nil, errors.New("nil proof of lock change") @@ -1608,17 +1613,17 @@ func ProofOfLockChangeFromProto(pb *tmproto.ProofOfLockChange) (*ProofOfLockChan return nil, errors.New("proofOfLockChange: is not absent but has no votes") } - vpb := make([]Vote, len(pb.Votes)) + vpb := make([]*Vote, len(pb.Votes)) for i, v := range pb.Votes { vi, err := VoteFromProto(v) if err != nil { return nil, err } - vpb[i] = *vi + vpb[i] = vi } if pb.PubKey == nil { - return nil, errors.New("proofOfLockChange: is not abest but has nil PubKey") + return nil, errors.New("proofOfLockChange: is not absent but has nil PubKey") } pk, err := cryptoenc.PubKeyFromProto(*pb.PubKey) if err != nil { @@ -1628,7 +1633,7 @@ func ProofOfLockChangeFromProto(pb *tmproto.ProofOfLockChange) (*ProofOfLockChan plc.PubKey = pk plc.Votes = vpb - return plc, nil + return plc, plc.ValidateBasic() } func PotentialAmnesiaEvidenceFromProto(pb *tmproto.PotentialAmnesiaEvidence) (*PotentialAmnesiaEvidence, error) { @@ -1650,49 +1655,64 @@ func PotentialAmnesiaEvidenceFromProto(pb *tmproto.PotentialAmnesiaEvidence) (*P return &tp, tp.ValidateBasic() } -func AmnesiaEvidenceToProto(evi AmnesiaEvidence) (*tmproto.Evidence, error) { - paepb := evi.PotentialAmnesiaEvidence.ToProto() - - polc, err := evi.Polc.ToProto() - if err != nil { - return nil, err - } - - tp := &tmproto.Evidence{ - Sum: &tmproto.Evidence_AmnesiaEvidence{ - AmnesiaEvidence: &tmproto.AmnesiaEvidence{ - PotentialAmnesiaEvidence: &paepb, - Polc: polc, - }, - }, - } - - return tp, nil -} - -func AmensiaEvidenceFromProto(pb *tmproto.AmnesiaEvidence) (AmnesiaEvidence, error) { +func AmnesiaEvidenceFromProto(pb *tmproto.AmnesiaEvidence) (*AmnesiaEvidence, error) { if pb == nil { - return AmnesiaEvidence{}, errors.New("nil duplicate vote evidence") + return nil, errors.New("nil amnesia evidence") } pae, err := PotentialAmnesiaEvidenceFromProto(pb.PotentialAmnesiaEvidence) if err != nil { - return AmnesiaEvidence{}, err + return nil, fmt.Errorf("decoding to amnesia evidence, err: %w", err) } polc, err := ProofOfLockChangeFromProto(pb.Polc) if err != nil { - return AmnesiaEvidence{}, err + return nil, fmt.Errorf("decoding to amnesia evidence, err: %w", err) } - tp := AmnesiaEvidence{ - PotentialAmnesiaEvidence: *pae, - Polc: *polc, + tp := &AmnesiaEvidence{ + PotentialAmnesiaEvidence: pae, + Polc: polc, } return tp, tp.ValidateBasic() } -//----------------------------------------------------------------- +//-------------------------------------------------------------- + +// 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.HashFromByteSlices(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 +} + +//-------------------------------------------------- // UNSTABLE type MockRandomEvidence struct { @@ -1771,7 +1791,7 @@ func NewMockPOLC(height int64, time time.Time, pubKey crypto.PubKey) ProofOfLock vote.Signature = v.Signature return ProofOfLockChange{ - Votes: []Vote{vote}, + Votes: []*Vote{&vote}, PubKey: pubKey, } } diff --git a/types/evidence_test.go b/types/evidence_test.go index 3ead30731..036b54414 100644 --- a/types/evidence_test.go +++ b/types/evidence_test.go @@ -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) }