eth/downloader: cap the hash ban set, add test for it
This commit is contained in:
parent
4b2dd44711
commit
63c6cedb14
|
@ -17,18 +17,17 @@ import (
|
||||||
"gopkg.in/fatih/set.v0"
|
"gopkg.in/fatih/set.v0"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
var (
|
||||||
MinHashFetch = 512 // Minimum amount of hashes to not consider a peer stalling
|
MinHashFetch = 512 // Minimum amount of hashes to not consider a peer stalling
|
||||||
MaxHashFetch = 2048 // Amount of hashes to be fetched per retrieval request
|
MaxHashFetch = 2048 // Amount of hashes to be fetched per retrieval request
|
||||||
MaxBlockFetch = 128 // Amount of blocks to be fetched per retrieval request
|
MaxBlockFetch = 128 // Amount of blocks to be fetched per retrieval request
|
||||||
|
|
||||||
hashTTL = 5 * time.Second // Time it takes for a hash request to time out
|
hashTTL = 5 * time.Second // Time it takes for a hash request to time out
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
blockSoftTTL = 3 * time.Second // Request completion threshold for increasing or decreasing a peer's bandwidth
|
blockSoftTTL = 3 * time.Second // Request completion threshold for increasing or decreasing a peer's bandwidth
|
||||||
blockHardTTL = 3 * blockSoftTTL // Maximum time allowance before a block request is considered expired
|
blockHardTTL = 3 * blockSoftTTL // Maximum time allowance before a block request is considered expired
|
||||||
crossCheckCycle = time.Second // Period after which to check for expired cross checks
|
crossCheckCycle = time.Second // Period after which to check for expired cross checks
|
||||||
|
|
||||||
|
maxBannedHashes = 4096 // Number of bannable hashes before phasing old ones out
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -602,9 +601,19 @@ func (d *Downloader) banBlocks(peerId string, head common.Hash) error {
|
||||||
}
|
}
|
||||||
index++
|
index++
|
||||||
}
|
}
|
||||||
|
// Ban the head hash and phase out any excess
|
||||||
d.banned.Add(blocks[index].Hash())
|
d.banned.Add(blocks[index].Hash())
|
||||||
|
for d.banned.Size() > maxBannedHashes {
|
||||||
glog.V(logger.Debug).Infof("Banned %d blocks from: %s\n", index+1, peerId)
|
d.banned.Each(func(item interface{}) bool {
|
||||||
|
// Skip any hard coded bans
|
||||||
|
if core.BadHashes[item.(common.Hash)] {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
d.banned.Remove(item)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
glog.V(logger.Debug).Infof("Banned %d blocks from: %s", index+1, peerId)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core"
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
"github.com/ethereum/go-ethereum/event"
|
"github.com/ethereum/go-ethereum/event"
|
||||||
)
|
)
|
||||||
|
@ -559,3 +560,45 @@ func TestBannedChainStarvationAttack(t *testing.T) {
|
||||||
banned = bans
|
banned = bans
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests that if a peer sends excessively many/large invalid chains that are
|
||||||
|
// gradually banned, it will have an upper limit on the consumed memory and also
|
||||||
|
// the origin bad hashes will not be evacuated.
|
||||||
|
func TestBannedChainMemoryExhaustionAttack(t *testing.T) {
|
||||||
|
// Reduce the test size a bit
|
||||||
|
MaxBlockFetch = 4
|
||||||
|
maxBannedHashes = 256
|
||||||
|
|
||||||
|
// Construct a banned chain with more chunks than the ban limit
|
||||||
|
hashes := createHashes(0, maxBannedHashes*MaxBlockFetch)
|
||||||
|
hashes[len(hashes)-1] = bannedHash // weird index to have non multiple of ban chunk size
|
||||||
|
|
||||||
|
blocks := createBlocksFromHashes(hashes)
|
||||||
|
|
||||||
|
// Create the tester and ban the selected hash
|
||||||
|
tester := newTester(t, hashes, blocks)
|
||||||
|
tester.downloader.banned.Add(bannedHash)
|
||||||
|
|
||||||
|
// Iteratively try to sync, and verify that the banned hash list grows until
|
||||||
|
// the head of the invalid chain is blocked too.
|
||||||
|
tester.newPeer("attack", big.NewInt(10000), hashes[0])
|
||||||
|
for {
|
||||||
|
// Try to sync with the attacker, check hash chain failure
|
||||||
|
if _, err := tester.syncTake("attack", hashes[0]); err != ErrInvalidChain {
|
||||||
|
t.Fatalf("synchronisation error mismatch: have %v, want %v", err, ErrInvalidChain)
|
||||||
|
}
|
||||||
|
// Short circuit if the entire chain was banned
|
||||||
|
if tester.downloader.banned.Has(hashes[0]) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Otherwise ensure we never exceed the memory allowance and the hard coded bans are untouched
|
||||||
|
if bans := tester.downloader.banned.Size(); bans > maxBannedHashes {
|
||||||
|
t.Fatalf("ban cap exceeded: have %v, want max %v", bans, maxBannedHashes)
|
||||||
|
}
|
||||||
|
for hash, _ := range core.BadHashes {
|
||||||
|
if !tester.downloader.banned.Has(hash) {
|
||||||
|
t.Fatalf("hard coded ban evacuated: %x", hash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -94,7 +94,7 @@ func (p *peer) SetIdle() {
|
||||||
for {
|
for {
|
||||||
// Calculate the new download bandwidth allowance
|
// Calculate the new download bandwidth allowance
|
||||||
prev := atomic.LoadInt32(&p.capacity)
|
prev := atomic.LoadInt32(&p.capacity)
|
||||||
next := int32(math.Max(1, math.Min(MaxBlockFetch, float64(prev)*scale)))
|
next := int32(math.Max(1, math.Min(float64(MaxBlockFetch), float64(prev)*scale)))
|
||||||
|
|
||||||
// Try to update the old value
|
// Try to update the old value
|
||||||
if atomic.CompareAndSwapInt32(&p.capacity, prev, next) {
|
if atomic.CompareAndSwapInt32(&p.capacity, prev, next) {
|
||||||
|
|
|
@ -16,7 +16,7 @@ import (
|
||||||
"gopkg.in/karalabe/cookiejar.v2/collections/prque"
|
"gopkg.in/karalabe/cookiejar.v2/collections/prque"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
var (
|
||||||
blockCacheLimit = 8 * MaxBlockFetch // Maximum number of blocks to cache before throttling the download
|
blockCacheLimit = 8 * MaxBlockFetch // Maximum number of blocks to cache before throttling the download
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -213,8 +213,8 @@ func (self *ProtocolManager) handleMsg(p *peer) error {
|
||||||
return errResp(ErrDecode, "->msg %v: %v", msg, err)
|
return errResp(ErrDecode, "->msg %v: %v", msg, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if request.Amount > downloader.MaxHashFetch {
|
if request.Amount > uint64(downloader.MaxHashFetch) {
|
||||||
request.Amount = downloader.MaxHashFetch
|
request.Amount = uint64(downloader.MaxHashFetch)
|
||||||
}
|
}
|
||||||
|
|
||||||
hashes := self.chainman.GetBlockHashesFromHash(request.Hash, request.Amount)
|
hashes := self.chainman.GetBlockHashesFromHash(request.Hash, request.Amount)
|
||||||
|
|
|
@ -102,7 +102,7 @@ func (p *peer) sendTransaction(tx *types.Transaction) error {
|
||||||
|
|
||||||
func (p *peer) requestHashes(from common.Hash) error {
|
func (p *peer) requestHashes(from common.Hash) error {
|
||||||
glog.V(logger.Debug).Infof("[%s] fetching hashes (%d) %x...\n", p.id, downloader.MaxHashFetch, from[:4])
|
glog.V(logger.Debug).Infof("[%s] fetching hashes (%d) %x...\n", p.id, downloader.MaxHashFetch, from[:4])
|
||||||
return p2p.Send(p.rw, GetBlockHashesMsg, getBlockHashesMsgData{from, downloader.MaxHashFetch})
|
return p2p.Send(p.rw, GetBlockHashesMsg, getBlockHashesMsgData{from, uint64(downloader.MaxHashFetch)})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *peer) requestBlocks(hashes []common.Hash) error {
|
func (p *peer) requestBlocks(hashes []common.Hash) error {
|
||||||
|
|
Loading…
Reference in New Issue