Skip to content

track evidence, include in block#592

Merged
ebuchman merged 35 commits intodevelopfrom
evidence
Dec 27, 2017
Merged

track evidence, include in block#592
ebuchman merged 35 commits intodevelopfrom
evidence

Conversation

@ebuchman
Copy link
Contributor

@ebuchman ebuchman commented Jul 25, 2017

// TODO: track evidence for inclusion in a block
cs.Logger.Error("Found conflicting vote. Recording evidence in the RoundState", "height", vote.Height, "round", vote.Round, "type", vote.Type, "valAddr", vote.ValidatorAddress, "valIndex", vote.ValidatorIndex)

// TODO: ensure we haven't seen this evidence already !
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't that be the case already since we've already reported a conflicting vote and below are adding voteErr.DuplicateVoteEvidence? Or are you saying that before adding new evidence, we should check for the presence of DuplicateVoteEvidence?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible we already saw this particular evidence (ie. from the same validator) and added it to the cs.Evidence.

if !bytes.Equal(dve.VoteA.ValidatorAddress, dve.VoteB.ValidatorAddress) {
return fmt.Errorf("DuplicateVoteEvidence Error: Validator addresses do not match. Got %X and %X", dve.VoteA.ValidatorAddress, dve.VoteB.ValidatorAddress)
}
// XXX: Should we enforce index is the same ?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we? checking the address should be enough. although, validator set is sorted, so it does not matter

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're right, and we don't need the index.

@melekes
Copy link
Contributor

melekes commented Sep 19, 2017

Looking good so far )

valIdx, val := valset.GetByAddress(addr)
if val == nil {
return fmt.Errorf("Address %X was not a validator at height %d", addr, height)
} else if idx != valIdx {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we still want to punish even if indexes are wrong?!

@ebuchman ebuchman changed the title WIP: track evidence, include in block track evidence, include in block Nov 20, 2017
@ebuchman
Copy link
Contributor Author

Only thing left to do is add the Evidence to BeginBlock and add some simple tests in the state package.

But this is ready for more serious review.

evidence/pool.go Outdated
}

// EvidenceChan returns an unbuffered channel on which new evidence can be received.
func (evpool *EvidencePool) EvidenceChan() chan types.Evidence {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about encoding the direction of the channel here.
func (evpool *EvidencePool) EvidenceChan() <-chan types.Evidence

https://gobyexample.com/channel-directions

// reverse the order so highest priority is first
l := store.ListEvidence(baseKeyOutqueue)
l2 := make([]types.Evidence, len(l))
for i, _ := range l {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The underscore can be omitted here.

return ei
}

// AddNewEvidence adds the given evidence to the database.
Copy link
Contributor

@adrianbrink adrianbrink Nov 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra doc comment: Returns false if the evidence is already in the database.

if len(val) == 0 {
return nil
}
ei := new(EvidenceInfo)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are at least two cases of var ei EvidenceInfo and &ei elsewhere however this is the only case of ei := new(EvidenceInfo) in this PR. since the variable is named the same maybe it is easier reading to convert this last one to the var form also for parallel consistency.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference here is we return the pointer. But sure I'll be consistent and just return &ei


// add it to the store
key := keyOutqueue(evidence, priority)
store.db.Set(key, eiBytes)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if the same problem applies here If we crash after committed but before removing/updating (i.e. if one of the set op crashes)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These operations seem idempotent... see my other comment on idempotency

@ebuchman
Copy link
Contributor Author

Also needs a reactor_test.go which I forgot to check in and just accidentally deleted. Awesome :). It's mostly just a copy of mempool/reactor_test.go though so not that much work

@codecov-io
Copy link

codecov-io commented Nov 30, 2017

Codecov Report

Merging #592 into develop will decrease coverage by 0.07%.
The diff coverage is 62.21%.

@@             Coverage Diff             @@
##           develop     #592      +/-   ##
===========================================
- Coverage    60.12%   60.05%   -0.08%     
===========================================
  Files          110      114       +4     
  Lines        10273    10596     +323     
===========================================
+ Hits          6177     6363     +186     
- Misses        3535     3658     +123     
- Partials       561      575      +14

// XXX: is this thread safe ?
priority, err := evpool.state.VerifyEvidence(evidence)
if err != nil {
return err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// TODO if err is that we can't check it before we pruned, then ignore.
// TODO if err is that evidence is bad, return (and upstream mark peer as bad)

evidence/pool.go Outdated
// Blocks on the EvidenceChan.
func (evpool *EvidencePool) AddEvidence(evidence types.Evidence) (err error) {

// XXX: is this thread safe ?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

State is not thread safe... it's actually a bit of a mess.
(e.g. we should probably move state.LoadValidators out into a global function and keep state a singleton snapshot). Is AddEvidence thread safe?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. This is actually broken because this state never gets updated. The goal with evidence pool was to be thread safe thanks to the threadsafety of the underlying db. But with needing to update the state, we probably need something more


evpool.logger.Info("Verified new evidence of byzantine behaviour", "evidence", evidence)

evpool.evidenceChan <- evidence
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: how do you know that evidenceChan isn't closed? (since it would panic)...
answer: because we don't close it. But that should be OK because channels are garbage collected as long as there are no hanging goroutines waiting on them.

func (evR *EvidenceReactor) AddPeer(peer p2p.Peer) {
// send the peer our high-priority evidence.
// the rest will be sent by the broadcastRoutine
evidence := evR.evpool.PriorityEvidence()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why a single high one?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh it's a list. that wasn't clear.

}

// broadcast new evidence to all peers.
// broadcasts must be non-blocking so routine is always available to read off EvidenceChan.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

capitalization please :)

- Once committed, atomically remove from pending and update lookup.
- TODO: If we crash after committed but before removing/updating,
we'll be stuck broadcasting evidence we never know we committed.
so either share the state db and atomically MarkCommitted
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't have to be atomic, but if we make updating this evidence store happen first before (or during) state db commit, and make sure the operation on the evidence store is idempotent, then that works too.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(would have to comment CONTRACT: needs to be idempotent in that case)

}

// big endian padded hex
func be(h int) string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bE()?

// It is wrapped by PriorityEvidence and PendingEvidence for convenience.
func (store *EvidenceStore) ListEvidence(prefixKey string) (evidence []types.Evidence) {
iter := store.db.IteratorPrefix([]byte(prefixKey))
for iter.Next() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're skipping the first item by calling Next().

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See documentation for tmlibs/db/Iterator:

    Usage:

    for itr.Seek(mykey); itr.Valid(); itr.Next() {
        k, v := itr.Key(); itr.Value()
        ....
    }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case for ;itr.Valid(); itr.Next() {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but how itr.Valid() changes this skipping the first item by calling Next()? if calling Next() progresses itr, then we must do something like

iter := store.db.IteratorPrefix([]byte(prefixKey))
// handle first item
iter.Key()
iter.Value()
for iter.Next()

I am confused

Copy link
Contributor

@melekes melekes Nov 30, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and there is no docs for iterator in tmlibs https://github.com/tendermint/tmlibs/tree/develop/db. none that I can find

// It returns false if the evidence is already stored.
func (store *EvidenceStore) AddNewEvidence(evidence types.Evidence, priority int) bool {
// check if we already have seen it
ei_ := store.GetEvidence(evidence.Height(), evidence.Hash())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if a byzantine validator signed a million votes for the same height/round?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for simple vote evidence, we could search for whether we have evidence for a validator at a given height, and not worry about rounds or blockhashes etc. So if we already have evidence to slash a validator at a height, no need to keep collecting evidence. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds good. So we should change the lookup scheme to include the validator-address in the key so we can quickly check if theres already evidence for the validator at the height?

}
eiBytes := wire.BinaryBytes(ei)

// add it to the store
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this function could be refactored out right underneath.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean?

ei.Committed = true

// TODO: we should use the state db and db.Sync in state.Save instead.
// Else, if we call this before state.Save, we may never mark committed evidence as committed.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should just mark it as committed before state.Save. It'll get committed again if we restart.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a case during replay if the app is multiple blocks behind that we don't construct historical state and call state.ApplyBlock, we use sm.ExecCommitBlock instead, so eg. MarkEvidenceAsCommitted would never get called.

But now with historical validators etc we should be able to construct the historical state to use ApplyBlock after all.

return s.LastValidators, s.Validators
}

// VerifyEvidence verifies the evidence fully by checking it is internally
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my other comment about State... I think we should move these kinds of functions out of State and keep State a simple snapshot. LMK what you think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok - I think that's a nice idea. All of the LoadXxx just need the db, but nothing else from the state.

This VerifyEvidence does require a bit more. Do we just pass in a state as an arg here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's deal with it in a separate PR: #1010

@ebuchman ebuchman mentioned this pull request Dec 11, 2017
12 tasks
@ebuchman ebuchman mentioned this pull request Dec 27, 2017
6 tasks
@ebuchman
Copy link
Contributor Author

Further issues moved to #1012

@ebuchman ebuchman merged commit 53eb9ac into develop Dec 27, 2017
@ebuchman ebuchman deleted the evidence branch December 27, 2017 20:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants