From 221f30e8f143696519599ec40784aa5b7e0dec0e Mon Sep 17 00:00:00 2001 From: NathanBSC Date: Wed, 28 Jun 2023 10:17:00 +0800 Subject: [PATCH] fix: defend ddos voting attack with other mini fix according to audit --- consensus/parlia/parlia.go | 6 +++--- eth/handler.go | 2 ++ eth/handler_bsc.go | 12 +++++++++--- eth/protocols/bsc/peer.go | 27 +++++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/consensus/parlia/parlia.go b/consensus/parlia/parlia.go index aed1052c1f..7522f555ac 100644 --- a/consensus/parlia/parlia.go +++ b/consensus/parlia/parlia.go @@ -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 ( @@ -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) } } @@ -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, }) diff --git a/eth/handler.go b/eth/handler.go index f3e05b2240..88afa3afef 100644 --- a/eth/handler.go +++ b/eth/handler.go @@ -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 diff --git a/eth/handler_bsc.go b/eth/handler_bsc.go index 195691989b..d01a475ed0 100644 --- a/eth/handler_bsc.go +++ b/eth/handler_bsc.go @@ -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 } diff --git a/eth/protocols/bsc/peer.go b/eth/protocols/bsc/peer.go index 202502a4b8..77ac11599f 100644 --- a/eth/protocols/bsc/peer.go +++ b/eth/protocols/bsc/peer.go @@ -1,6 +1,8 @@ package bsc import ( + "time" + mapset "github.com/deckarep/golang-set" "github.com/ethereum/go-ethereum/common" @@ -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. @@ -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 @@ -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, @@ -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.