diff --git a/CHANGELOG_OF_TENDERMINT.md b/CHANGELOG_OF_TENDERMINT.md index 9fd6bd205..c588224be 100644 --- a/CHANGELOG_OF_TENDERMINT.md +++ b/CHANGELOG_OF_TENDERMINT.md @@ -1,5 +1,65 @@ # Changelog +## v0.33.6 + +*July 2, 2020* + +This security release fixes: + +### Denial of service + +Tendermint 0.33.0 and above allow block proposers to include signatures for the +wrong block. This may happen naturally if you start a network, have it run for +some time and restart it **without changing the chainID**. (It is a +[misconfiguration](https://docs.tendermint.com/master/tendermint-core/using-tendermint.html) +to reuse chainIDs.) Correct block proposers will accidentally include signatures +for the wrong block if they see these signatures, and then commits won't validate, +making all proposed blocks invalid. A malicious validator (even with a minimal +amount of stake) can use this vulnerability to completely halt the network. + +Tendermint 0.33.6 checks all the signatures are for the block with +2/3 +majority before creating a commit. + +### False Witness + +Tendermint 0.33.1 and above are no longer fully verifying commit signatures +during block execution - they stop after +2/3. This means proposers can propose +blocks that contain valid +2/3 signatures and then the rest of the signatures +can be whatever they want. They can claim that all the other validators signed +just by including a CommitSig with arbitrary signature data. While this doesn't +seem to impact safety of Tendermint per se, it means that Commits may contain a +lot of invalid data. + +_This was already true of blocks, since they could include invalid txs filled +with garbage, but in that case the application knew that they are invalid and +could punish the proposer. But since applications didn't--and don't-- +verify commit signatures directly (they trust Tendermint to do that), +they won't be able to detect it._ + +This can impact incentivization logic in the application that depends on the +LastCommitInfo sent in BeginBlock, which includes which validators signed. For +instance, Gaia incentivizes proposers with a bonus for including more than +2/3 +of the signatures. But a proposer can now claim that bonus just by including +arbitrary data for the final -1/3 of validators without actually waiting for +their signatures. There may be other tricks that can be played because of this. + +Tendermint 0.33.6 verifies all the signatures during block execution. + +_Please note that the light client does not check nil votes and exits as soon +as 2/3+ of the signatures are checked._ + +**All clients are recommended to upgrade.** + +Special thanks to @njmurarka at Bluzelle Networks for reporting this. + +Friendly reminder, we have a [bug bounty +program](https://hackerone.com/tendermint). + +### SECURITY: + +- [consensus] Do not allow signatures for a wrong block in commits (@ebuchman) +- [consensus] Verify all the signatures during block execution (@melekes) + ## v.0.33.5 Special thanks to our external contributor on this release: @tau3 diff --git a/consensus/invalid_test.go b/consensus/invalid_test.go new file mode 100644 index 000000000..1ae0a69ec --- /dev/null +++ b/consensus/invalid_test.go @@ -0,0 +1,94 @@ +package consensus + +import ( + "testing" + + "github.com/tendermint/tendermint/libs/bytes" + "github.com/tendermint/tendermint/libs/log" + tmrand "github.com/tendermint/tendermint/libs/rand" + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/types" +) + +//---------------------------------------------- +// byzantine failures + +// one byz val sends a precommit for a random block at each height +// Ensure a testnet makes blocks +func TestReactorInvalidPrecommit(t *testing.T) { + N := 4 + css, cleanup := randConsensusNet(N, "consensus_reactor_test", newMockTickerFunc(true), newCounter) + defer cleanup() + + for i := 0; i < 4; i++ { + ticker := NewTimeoutTicker() + ticker.SetLogger(css[i].Logger) + css[i].SetTimeoutTicker(ticker) + + } + + reactors, blocksSubs, eventBuses := startConsensusNet(t, css, N) + + // this val sends a random precommit at each height + byzValIdx := 0 + byzVal := css[byzValIdx] + byzR := reactors[byzValIdx] + + // update the doPrevote function to just send a valid precommit for a random block + // and otherwise disable the priv validator + byzVal.mtx.Lock() + pv := byzVal.privValidator + byzVal.doPrevote = func(height int64, round int) { + invalidDoPrevoteFunc(t, height, round, byzVal, byzR.Switch, pv) + } + byzVal.mtx.Unlock() + defer stopConsensusNet(log.TestingLogger(), reactors, eventBuses) + + // wait for a bunch of blocks + // TODO: make this tighter by ensuring the halt happens by block 2 + for i := 0; i < 10; i++ { + timeoutWaitGroup(t, N, func(j int) { + <-blocksSubs[j].Out() + }, css) + } +} + +func invalidDoPrevoteFunc(t *testing.T, height int64, round int, cs *State, sw *p2p.Switch, pv types.PrivValidator) { + // routine to: + // - precommit for a random block + // - send precommit to all peers + // - disable privValidator (so we don't do normal precommits) + go func() { + cs.mtx.Lock() + cs.privValidator = pv + pubKey, err := cs.privValidator.GetPubKey() + if err != nil { + panic(err) + } + addr := pubKey.Address() + valIndex, _ := cs.Validators.GetByAddress(addr) + + // precommit a random block + blockHash := bytes.HexBytes(tmrand.Bytes(32)) + precommit := &types.Vote{ + ValidatorAddress: addr, + ValidatorIndex: valIndex, + Height: cs.Height, + Round: cs.Round, + Timestamp: cs.voteTime(), + Type: types.PrecommitType, + BlockID: types.BlockID{ + Hash: blockHash, + PartsHeader: types.PartSetHeader{Total: 1, Hash: tmrand.Bytes(32)}}, + } + cs.privValidator.SignVote(cs.state.ChainID, precommit) + cs.privValidator = nil // disable priv val so we don't do normal votes + cs.mtx.Unlock() + + peers := sw.Peers().List() + for _, peer := range peers { + cs.Logger.Info("Sending bad vote", "block", blockHash, "peer", peer) + peer.Send(VoteChannel, cdc.MustMarshalBinaryBare(&VoteMessage{precommit})) + } + }() +} diff --git a/lite2/client.go b/lite2/client.go index 0ff6caf8e..c514eba4b 100644 --- a/lite2/client.go +++ b/lite2/client.go @@ -404,7 +404,7 @@ func (c *Client) initializeWithTrustOptions(options TrustOptions) error { voters := types.SelectVoter(vals, proofHash, c.voterParams) // Ensure that +2/3 of voters signed correctly. - err = voters.VerifyCommit(c.chainID, h.Commit.BlockID, h.Height, h.Commit) + err = voters.VerifyCommitLight(c.chainID, h.Commit.BlockID, h.Height, h.Commit) if err != nil { return fmt.Errorf("invalid commit: %w", err) } @@ -977,7 +977,7 @@ func (c *Client) compareNewHeaderWithWitnesses(h *types.SignedHeader) error { c.logger.Error("Witness sent us incorrect header; invalid proof", "err", err) } voters := types.SelectVoter(c.latestTrustedValidators, proofHash, c.voterParams) - if err = voters.VerifyCommitTrusting(c.chainID, altH.Commit.BlockID, + if err = voters.VerifyCommitLightTrusting(c.chainID, altH.Commit.BlockID, altH.Height, altH.Commit, c.trustLevel); err != nil { c.logger.Error("Witness sent us incorrect header", "err", err, "witness", witness) witnessesToRemove = append(witnessesToRemove, i) diff --git a/lite2/verifier.go b/lite2/verifier.go index ad12622a9..fc17caedd 100644 --- a/lite2/verifier.go +++ b/lite2/verifier.go @@ -69,7 +69,7 @@ func VerifyNonAdjacent( } // Ensure that +`trustLevel` (default 1/3) or more of last trusted validators signed correctly. - err = trustedVoters.VerifyCommitTrusting( + err = trustedVoters.VerifyCommitLightTrusting( chainID, untrustedHeader.Commit.BlockID, untrustedHeader.Height, @@ -89,7 +89,7 @@ func VerifyNonAdjacent( // NOTE: this should always be the last check because untrustedVals can be // intentionally made very large to DOS the light client. not the case for // VerifyAdjacent, where validator set is known in advance. - if err := untrustedVoters.VerifyCommit( + if err := untrustedVoters.VerifyCommitLight( chainID, untrustedHeader.Commit.BlockID, untrustedHeader.Height, @@ -159,7 +159,7 @@ func VerifyAdjacent( } // Ensure that +2/3 of new validators signed correctly. - if err := untrustedVoters.VerifyCommit( + if err := untrustedVoters.VerifyCommitLight( chainID, untrustedHeader.Commit.BlockID, untrustedHeader.Height, diff --git a/types/validator_set_test.go b/types/validator_set_test.go index b18ef2d64..3190d065f 100644 --- a/types/validator_set_test.go +++ b/types/validator_set_test.go @@ -19,7 +19,6 @@ import ( "github.com/tendermint/tendermint/crypto/ed25519" tmmath "github.com/tendermint/tendermint/libs/math" tmrand "github.com/tendermint/tendermint/libs/rand" - tmtime "github.com/tendermint/tendermint/types/time" ) func TestMaxVotingPowerTest(t *testing.T) { @@ -600,62 +599,147 @@ func TestSafeSubClip(t *testing.T) { //------------------------------------------------------------------- -func TestValidatorSetVerifyCommit(t *testing.T) { - privKey := ed25519.GenPrivKey() - pubKey := privKey.PubKey() - v1 := NewValidator(pubKey, 1000) - vset := ToVoterAll([]*Validator{v1}) - - // good +// Check VerifyCommit, VerifyCommitLight and VerifyCommitLightTrusting basic +// verification. +func TestValidatorSet_VerifyCommit_All(t *testing.T) { var ( - chainID = "mychainID" - blockID = makeBlockIDRandom() - height = int64(5) + privKey = ed25519.GenPrivKey() + pubKey = privKey.PubKey() + v1 = NewValidator(pubKey, 1000) + vset = ToVoterAll([]*Validator{v1}) + + chainID = "Lalande21185" ) - vote := &Vote{ - ValidatorAddress: v1.Address, - ValidatorIndex: 0, - Height: height, - Round: 0, - Timestamp: tmtime.Now(), - Type: PrecommitType, - BlockID: blockID, - } + + vote := examplePrecommit() + vote.ValidatorAddress = pubKey.Address() sig, err := privKey.Sign(vote.SignBytes(chainID)) assert.NoError(t, err) vote.Signature = sig - commit := NewCommit(vote.Height, vote.Round, blockID, []CommitSig{vote.CommitSig()}) - // bad - var ( - badChainID = "notmychainID" - badBlockID = BlockID{Hash: []byte("goodbye")} - badHeight = height + 1 - badCommit = NewCommit(badHeight, 0, blockID, []CommitSig{{BlockIDFlag: BlockIDFlagAbsent}}) - ) + commit := NewCommit(vote.Height, vote.Round, vote.BlockID, []CommitSig{vote.CommitSig()}) - // test some error cases - // TODO: test more cases! - cases := []struct { - chainID string - blockID BlockID - height int64 - commit *Commit + vote2 := *vote + sig2, err := privKey.Sign(vote2.SignBytes("EpsilonEridani")) + require.NoError(t, err) + vote2.Signature = sig2 + + testCases := []struct { + description string + chainID string + blockID BlockID + height int64 + commit *Commit + expErr bool }{ - {badChainID, blockID, height, commit}, - {chainID, badBlockID, height, commit}, - {chainID, blockID, badHeight, commit}, - {chainID, blockID, height, badCommit}, + {"good", chainID, vote.BlockID, vote.Height, commit, false}, + + {"wrong signature (#0)", "EpsilonEridani", vote.BlockID, vote.Height, commit, true}, + {"wrong block ID", chainID, makeBlockIDRandom(), vote.Height, commit, true}, + {"wrong height", chainID, vote.BlockID, vote.Height - 1, commit, true}, + + {"wrong set size: 1 vs 0", chainID, vote.BlockID, vote.Height, + NewCommit(vote.Height, vote.Round, vote.BlockID, []CommitSig{}), true}, + + {"wrong set size: 1 vs 2", chainID, vote.BlockID, vote.Height, + NewCommit(vote.Height, vote.Round, vote.BlockID, + []CommitSig{vote.CommitSig(), {BlockIDFlag: BlockIDFlagAbsent}}), true}, + + {"insufficient voting power: got 0, needed more than 666", chainID, vote.BlockID, vote.Height, + NewCommit(vote.Height, vote.Round, vote.BlockID, []CommitSig{{BlockIDFlag: BlockIDFlagAbsent}}), true}, + + {"wrong signature (#0)", chainID, vote.BlockID, vote.Height, + NewCommit(vote.Height, vote.Round, vote.BlockID, []CommitSig{vote2.CommitSig()}), true}, } - for i, c := range cases { - err := vset.VerifyCommit(c.chainID, c.blockID, c.height, c.commit) - assert.NotNil(t, err, i) + for _, tc := range testCases { + tc := tc + t.Run(tc.description, func(t *testing.T) { + err := vset.VerifyCommit(tc.chainID, tc.blockID, tc.height, tc.commit) + if tc.expErr { + if assert.Error(t, err, "VerifyCommit") { + assert.Contains(t, err.Error(), tc.description, "VerifyCommit") + } + } else { + assert.NoError(t, err, "VerifyCommit") + } + + err = vset.VerifyCommitLight(tc.chainID, tc.blockID, tc.height, tc.commit) + if tc.expErr { + if assert.Error(t, err, "VerifyCommitLight") { + assert.Contains(t, err.Error(), tc.description, "VerifyCommitLight") + } + } else { + assert.NoError(t, err, "VerifyCommitLight") + } + + }) + } +} + +func TestValidatorSet_VerifyCommit_CheckAllSignatures(t *testing.T) { + var ( + chainID = "test_chain_id" + h = int64(3) + blockID = makeBlockIDRandom() + ) + + voteSet, _, vset, vals := randVoteSet(h, 0, PrecommitType, 4, 10) + commit, err := MakeCommit(blockID, h, 0, voteSet, vals, time.Now()) + require.NoError(t, err) + + // malleate 4th signature + vote := voteSet.GetByIndex(3) + err = vals[3].SignVote("CentaurusA", vote) + require.NoError(t, err) + commit.Signatures[3] = vote.CommitSig() + + err = vset.VerifyCommit(chainID, blockID, h, commit) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "wrong signature (#3)") } +} - // test a good one - err = vset.VerifyCommit(chainID, blockID, height, commit) - assert.Nil(t, err) +func TestValidatorSet_VerifyCommitLight_ReturnsAsSoonAsMajorityOfVotingPowerSigned(t *testing.T) { + var ( + chainID = "test_chain_id" + h = int64(3) + blockID = makeBlockIDRandom() + ) + + voteSet, _, vset, vals := randVoteSet(h, 0, PrecommitType, 4, 10) + commit, err := MakeCommit(blockID, h, 0, voteSet, vals, time.Now()) + require.NoError(t, err) + + // malleate 4th signature (3 signatures are enough for 2/3+) + vote := voteSet.GetByIndex(3) + err = vals[3].SignVote("CentaurusA", vote) + require.NoError(t, err) + commit.Signatures[3] = vote.CommitSig() + + err = vset.VerifyCommitLight(chainID, blockID, h, commit) + assert.NoError(t, err) +} + +func TestValidatorSet_VerifyCommitLightTrusting_ReturnsAsSoonAsTrustLevelOfVotingPowerSigned(t *testing.T) { + var ( + chainID = "test_chain_id" + h = int64(3) + blockID = makeBlockIDRandom() + ) + + voteSet, _, vset, vals := randVoteSet(h, 0, PrecommitType, 4, 10) + commit, err := MakeCommit(blockID, h, 0, voteSet, vals, time.Now()) + require.NoError(t, err) + + // malleate 3rd signature (2 signatures are enough for 1/3+ trust level) + vote := voteSet.GetByIndex(2) + err = vals[2].SignVote("CentaurusA", vote) + require.NoError(t, err) + commit.Signatures[2] = vote.CommitSig() + + err = vset.VerifyCommitLightTrusting(chainID, blockID, h, commit, tmmath.Fraction{Numerator: 1, Denominator: 3}) + assert.NoError(t, err) } func TestEmptySet(t *testing.T) { @@ -1343,7 +1427,7 @@ func TestValSetUpdateOverflowRelated(t *testing.T) { } } -func TestVerifyCommitTrusting(t *testing.T) { +func TestValidatorSet_VerifyCommitLightTrusting(t *testing.T) { var ( blockID = makeBlockIDRandom() voteSet, _, originalVoterSet, privValidators = randVoteSet(1, 1, PrecommitType, 6, 1) @@ -1387,7 +1471,7 @@ func TestVerifyCommitTrusting(t *testing.T) { } } -func TestVerifyCommitTrustingErrorsOnOverflow(t *testing.T) { +func TestValidatorSet_VerifyCommitLightTrustingErrorsOnOverflow(t *testing.T) { var ( blockID = makeBlockIDRandom() voteSet, _, voterSet, vals = randVoteSet(1, 1, PrecommitType, 1, MaxTotalStakingPower) diff --git a/types/vote_set.go b/types/vote_set.go index 1825bffe0..5bcb885f5 100644 --- a/types/vote_set.go +++ b/types/vote_set.go @@ -27,35 +27,27 @@ type P2PID string /* VoteSet helps collect signatures from validators at each height+round for a predefined vote type. - We need VoteSet to be able to keep track of conflicting votes when validators double-sign. Yet, we can't keep track of *all* the votes seen, as that could be a DoS attack vector. - There are two storage areas for votes. 1. voteSet.votes 2. voteSet.votesByBlock - `.votes` is the "canonical" list of votes. It always has at least one vote, if a vote from a validator had been seen at all. Usually it keeps track of the first vote seen, but when a 2/3 majority is found, votes for that get priority and are copied over from `.votesByBlock`. - `.votesByBlock` keeps track of a list of votes for a particular block. There are two ways a &blockVotes{} gets created in `.votesByBlock`. 1. the first vote seen by a validator was for the particular block. 2. a peer claims to have seen 2/3 majority for the particular block. - Since the first vote from a validator will always get added in `.votesByBlock` , all votes in `.votes` will have a corresponding entry in `.votesByBlock`. - When a &blockVotes{} in `.votesByBlock` reaches a 2/3 majority quorum, its votes are copied into `.votes`. - All this is memory bounded because conflicting votes only get added if a peer told us to track that block, each peer only gets to tell us 1 such block, and, there's only a limited number of peers. - NOTE: Assumes that the sum total of voting power does not exceed MaxUInt64. */ type VoteSet struct { @@ -557,9 +549,11 @@ func (voteSet *VoteSet) sumTotalFrac() (int64, int64, float64) { //-------------------------------------------------------------------------------- // Commit -// MakeCommit constructs a Commit from the VoteSet. -// Panics if the vote type is not PrecommitType or if -// there's no +2/3 votes for a single block. +// MakeCommit constructs a Commit from the VoteSet. It only includes precommits +// for the block, which has 2/3+ majority, and nil. +// +// Panics if the vote type is not PrecommitType or if there's no +2/3 votes for +// a single block. func (voteSet *VoteSet) MakeCommit() *Commit { if voteSet.signedMsgType != PrecommitType { panic("Cannot MakeCommit() unless VoteSet.Type is PrecommitType") @@ -575,7 +569,12 @@ func (voteSet *VoteSet) MakeCommit() *Commit { // For every validator, get the precommit commitSigs := make([]CommitSig, len(voteSet.votes)) for i, v := range voteSet.votes { - commitSigs[i] = v.CommitSig() + commitSig := v.CommitSig() + // if block ID exists but doesn't match, exclude sig + if commitSig.ForBlock() && !v.BlockID.Equals(*voteSet.maj23) { + commitSig = NewCommitSigAbsent() + } + commitSigs[i] = commitSig } return NewCommit(voteSet.GetHeight(), voteSet.GetRound(), *voteSet.maj23, commitSigs) diff --git a/types/voter_set.go b/types/voter_set.go index de98c861e..f0fe5e710 100644 --- a/types/voter_set.go +++ b/types/voter_set.go @@ -129,7 +129,14 @@ func (voters *VoterSet) Hash() []byte { } // VerifyCommit verifies +2/3 of the set had signed the given commit. -func (voters *VoterSet) VerifyCommit(chainID string, blockID BlockID, height int64, commit *Commit) error { +// +// It checks all the signatures! While it's safe to exit as soon as we have +// 2/3+ signatures, doing so would impact incentivization logic in the ABCI +// application that depends on the LastCommitInfo sent in BeginBlock, which +// includes which validators signed. For instance, Gaia incentivizes proposers +// with a bonus for including more than +2/3 of the signatures. +func (voters *VoterSet) VerifyCommit(chainID string, blockID BlockID, + height int64, commit *Commit) error { if voters.Size() != len(commit.Signatures) { return NewErrInvalidCommitSignatures(voters.Size(), len(commit.Signatures)) @@ -162,6 +169,58 @@ func (voters *VoterSet) VerifyCommit(chainID string, blockID BlockID, height int // It's OK that the BlockID doesn't match. We include stray // signatures (~votes for nil) to measure validator availability. // } + } + + if got, needed := talliedVotingPower, votingPowerNeeded; got <= needed { + return ErrNotEnoughVotingPowerSigned{Got: got, Needed: needed} + } + + return nil +} + +/////////////////////////////////////////////////////////////////////////////// +// LIGHT CLIENT VERIFICATION METHODS +/////////////////////////////////////////////////////////////////////////////// + +// VerifyCommitLight verifies +2/3 of the set had signed the given commit. +// +// This method is primarily used by the light client and does not check all the +// signatures. +func (voters *VoterSet) VerifyCommitLight(chainID string, blockID BlockID, + height int64, commit *Commit) error { + + if voters.Size() != len(commit.Signatures) { + return NewErrInvalidCommitSignatures(voters.Size(), len(commit.Signatures)) + } + + // Validate Height and BlockID. + if height != commit.Height { + return NewErrInvalidCommitHeight(height, commit.Height) + } + if !blockID.Equals(commit.BlockID) { + return fmt.Errorf("invalid commit -- wrong block ID: want %v, got %v", + blockID, commit.BlockID) + } + + talliedVotingPower := int64(0) + votingPowerNeeded := voters.TotalVotingPower() * 2 / 3 + for idx, commitSig := range commit.Signatures { + // No need to verify absent or nil votes. + if !commitSig.ForBlock() { + continue + } + + // The vals and commit have a 1-to-1 correspondance. + // This means we don't need the validator address or to do any lookup. + val := voters.Voters[idx] + + // Validate signature. + voteSignBytes := commit.VoteSignBytes(chainID, idx) + if !val.PubKey.VerifyBytes(voteSignBytes, commitSig.Signature) { + return fmt.Errorf("wrong signature (#%d): %X", idx, commitSig.Signature) + } + + talliedVotingPower += val.VotingPower // return as soon as +2/3 of the signatures are verified if talliedVotingPower > votingPowerNeeded { @@ -169,7 +228,6 @@ func (voters *VoterSet) VerifyCommit(chainID string, blockID BlockID, height int } } - // talliedVotingPower <= needed, thus return error return ErrNotEnoughVotingPowerSigned{Got: talliedVotingPower, Needed: votingPowerNeeded} } @@ -317,6 +375,76 @@ func (voters *VoterSet) VerifyCommitTrusting(chainID string, blockID BlockID, return ErrNotEnoughVotingPowerSigned{Got: talliedVotingPower, Needed: votingPowerNeeded} } +// VerifyCommitLightTrusting verifies that trustLevel of the validator set signed +// this commit. +// +// This method is primarily used by the light client and does not check all the +// signatures. +// +// NOTE the given validators do not necessarily correspond to the validator set +// for this commit, but there may be some intersection. +// +// Panics if trustLevel is invalid. +func (voters *VoterSet) VerifyCommitLightTrusting(chainID string, blockID BlockID, + height int64, commit *Commit, trustLevel tmmath.Fraction) error { + + // sanity check + if trustLevel.Numerator*3 < trustLevel.Denominator || // < 1/3 + trustLevel.Numerator > trustLevel.Denominator { // > 1 + panic(fmt.Sprintf("trustLevel must be within [1/3, 1], given %v", trustLevel)) + } + + if err := verifyCommitBasic(commit, height, blockID); err != nil { + return err + } + + var ( + talliedVotingPower int64 + seenVals = make(map[int]int, len(commit.Signatures)) // validator index -> commit index + ) + + // Safely calculate voting power needed. + totalVotingPowerMulByNumerator, overflow := safeMul(voters.TotalVotingPower(), trustLevel.Numerator) + if overflow { + return errors.New("int64 overflow while calculating voting power needed. please provide smaller trustLevel numerator") + } + votingPowerNeeded := totalVotingPowerMulByNumerator / trustLevel.Denominator + + for idx, commitSig := range commit.Signatures { + // No need to verify absent or nil votes. + if !commitSig.ForBlock() { + continue + } + + // We don't know the validators that committed this block, so we have to + // check for each vote if its validator is already known. + valIdx, val := voters.GetByAddress(commitSig.ValidatorAddress) + + if val != nil { + // check for double vote of validator on the same commit + if firstIndex, ok := seenVals[valIdx]; ok { + secondIndex := idx + return errors.Errorf("double vote from %v (%d and %d)", val, firstIndex, secondIndex) + } + seenVals[valIdx] = idx + + // Validate signature. + voteSignBytes := commit.VoteSignBytes(chainID, idx) + if !val.PubKey.VerifyBytes(voteSignBytes, commitSig.Signature) { + return errors.Errorf("wrong signature (#%d): %X", idx, commitSig.Signature) + } + + talliedVotingPower += val.VotingPower + + if talliedVotingPower > votingPowerNeeded { + return nil + } + } + } + + return ErrNotEnoughVotingPowerSigned{Got: talliedVotingPower, Needed: votingPowerNeeded} +} + // ToProto converts VoterSet to protobuf func (voters *VoterSet) ToProto() (*tmproto.VoterSet, error) { if voters == nil { diff --git a/version/version.go b/version/version.go index c0b98a96d..fb9388b57 100644 --- a/version/version.go +++ b/version/version.go @@ -21,7 +21,7 @@ const ( // XXX: Don't change the name of this variable or you will break // automation :) - TMCoreSemVer = "0.33.5" + TMCoreSemVer = "0.33.6" // LINECoreSemVer is the current version of LINE Tendermint Core. LINECoreSemVer = "0.2"