Skip to content

Commit

Permalink
fix: defend ddos voting attack with other mini fix according to audit
Browse files Browse the repository at this point in the history
  • Loading branch information
NathanBSC committed Jul 10, 2023
1 parent dda8e8d commit 221f30e
Show file tree
Hide file tree
Showing 4 changed files with 41 additions and 6 deletions.
6 changes: 3 additions & 3 deletions consensus/parlia/parlia.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const (

systemRewardPercent = 4 // it means 1/2^4 = 1/16 percentage of gas fee incoming will be distributed to system

collectAdditionalVotesRewardRatio = float64(1) // ratio of additional reward for collecting more votes than needed
collectAdditionalVotesRewardRatio = 100 // ratio of additional reward for collecting more votes than needed, the denominator is 100
)

var (
Expand Down Expand Up @@ -1027,7 +1027,7 @@ func (p *Parlia) distributeFinalityReward(chain consensus.ChainHeaderReader, sta
}
quorum := cmath.CeilDiv(len(snap.Validators)*2, 3)
if validVoteCount > quorum {
accumulatedWeights[head.Coinbase] += uint64(float64(validVoteCount-quorum) * collectAdditionalVotesRewardRatio)
accumulatedWeights[head.Coinbase] += uint64((validVoteCount - quorum) * collectAdditionalVotesRewardRatio / 100)
}
}

Expand Down Expand Up @@ -1788,7 +1788,7 @@ func encodeSigHeader(w io.Writer, header *types.Header, chainId *big.Int) {
header.GasLimit,
header.GasUsed,
header.Time,
header.Extra[:len(header.Extra)-65], // this will panic if extra is too short, should check before calling encodeSigHeader
header.Extra[:len(header.Extra)-extraSeal], // this will panic if extra is too short, should check before calling encodeSigHeader
header.MixDigest,
header.Nonce,
})
Expand Down
2 changes: 2 additions & 0 deletions eth/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -944,6 +944,8 @@ func (h *handler) voteBroadcastLoop() {
for {
select {
case event := <-h.voteCh:
// The timeliness of votes is very important,
// so one vote will be sent instantly without waiting for other votes for batch sending by design.
h.BroadcastVote(event.Vote)
case <-h.votesSub.Err():
return
Expand Down
12 changes: 9 additions & 3 deletions eth/handler_bsc.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,15 @@ func (h *bscHandler) Handle(peer *bsc.Peer, packet bsc.Packet) error {
// handleVotesBroadcast is invoked from a peer's message handler when it transmits a
// votes broadcast for the local node to process.
func (h *bscHandler) handleVotesBroadcast(peer *bsc.Peer, votes []*types.VoteEnvelope) error {
// Try to put votes into votepool
for _, vote := range votes {
h.votepool.PutVote(vote)
if peer.IsOverLimitAfterReceiving() {
peer.Log().Warn("peer sending votes too much, votes dropped; it may be a ddos attack, please check!")
return nil
}
// Here we only put the first vote, to avoid ddos attack by sending a large batch of votes.
// This won't abandon any valid vote, because one vote is sent every time referring to func voteBroadcastLoop
if len(votes) > 0 {
h.votepool.PutVote(votes[0])
}

return nil
}
27 changes: 27 additions & 0 deletions eth/protocols/bsc/peer.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package bsc

import (
"time"

mapset "github.com/deckarep/golang-set"

"github.com/ethereum/go-ethereum/common"
Expand All @@ -16,6 +18,15 @@ const (

// voteBufferSize is the maximum number of batch votes can be hold before sending
voteBufferSize = 21 * 2

// used to avoid of DDOS attack
// It's the max number of received votes per second from one peer
// 21 validators exist now, so 21 votes will be produced every one block interval
// so the limit is 7 = 21/3, here set it to 10 with a buffer.
receiveRateLimitPerSecond = 10

// the time span of one period
secondsPerPeriod = float64(10)
)

// max is a helper function which returns the larger of the two given integers.
Expand All @@ -31,6 +42,8 @@ type Peer struct {
id string // Unique ID for the peer, cached
knownVotes *knownCache // Set of vote hashes known to be known by this peer
voteBroadcast chan []*types.VoteEnvelope // Channel used to queue votes propagation requests
periodBegin time.Time // Begin time of the latest period for votes counting
periodCounter uint // Votes number in the latest period

*p2p.Peer // The embedded P2P package peer
rw p2p.MsgReadWriter // Input/output streams for bsc
Expand All @@ -47,6 +60,8 @@ func NewPeer(version uint, p *p2p.Peer, rw p2p.MsgReadWriter) *Peer {
id: id,
knownVotes: newKnownCache(maxKnownVotes),
voteBroadcast: make(chan []*types.VoteEnvelope, voteBufferSize),
periodBegin: time.Now(),
periodCounter: 0,
Peer: p,
rw: rw,
version: version,
Expand Down Expand Up @@ -114,6 +129,18 @@ func (p *Peer) AsyncSendVotes(votes []*types.VoteEnvelope) {
}
}

// Step into the next period when secondsPerPeriod seconds passed,
// Otherwise, check whether the number of received votes extra (secondsPerPeriod * receiveRateLimitPerSecond)
func (p *Peer) IsOverLimitAfterReceiving() bool {
if timeInterval := time.Since(p.periodBegin).Seconds(); timeInterval >= secondsPerPeriod {
p.periodBegin = time.Now()
p.periodCounter = 0
return false
}
p.periodCounter += 1
return p.periodCounter > uint(secondsPerPeriod*receiveRateLimitPerSecond)
}

// broadcastVotes is a write loop that schedules votes broadcasts
// to the remote peer. The goal is to have an async writer that does not lock up
// node internals and at the same time rate limits queued data.
Expand Down

0 comments on commit 221f30e

Please sign in to comment.