eth, eth/downloader: handle a potential unknown parent attack

This commit is contained in:
Péter Szilágyi 2015-05-14 15:24:18 +03:00
parent 7cb0e24245
commit a4246c2da6
3 changed files with 112 additions and 31 deletions

View File

@ -37,6 +37,7 @@ var (
errCancelHashFetch = errors.New("hash fetching cancelled (requested)") errCancelHashFetch = errors.New("hash fetching cancelled (requested)")
errCancelBlockFetch = errors.New("block downloading cancelled (requested)") errCancelBlockFetch = errors.New("block downloading cancelled (requested)")
errNoSyncActive = errors.New("no sync active") errNoSyncActive = errors.New("no sync active")
ErrUnknownParent = errors.New("block has unknown parent")
) )
type hashCheckFn func(common.Hash) bool type hashCheckFn func(common.Hash) bool
@ -142,16 +143,19 @@ func (d *Downloader) Synchronise(id string, hash common.Hash) error {
return d.syncWithPeer(p, hash) return d.syncWithPeer(p, hash)
} }
// TakeBlocks takes blocks from the queue and yields them to the blockTaker handler // TakeBlocks takes blocks from the queue and yields them to the caller.
// it's possible it yields no blocks func (d *Downloader) TakeBlocks() (types.Blocks, error) {
func (d *Downloader) TakeBlocks() types.Blocks { // If the head block is missing, no blocks are ready
// Check that there are blocks available and its parents are known
head := d.queue.GetHeadBlock() head := d.queue.GetHeadBlock()
if head == nil || !d.hasBlock(head.ParentHash()) { if head == nil {
return nil return nil, nil
} }
// Retrieve a full batch of blocks // If the parent hash of the head is unknown, notify the caller
return d.queue.TakeBlocks(head) if !d.hasBlock(head.ParentHash()) {
return nil, ErrUnknownParent
}
// Otherwise retrieve a full batch of blocks
return d.queue.TakeBlocks(head), nil
} }
func (d *Downloader) Has(hash common.Hash) bool { func (d *Downloader) Has(hash common.Hash) bool {

View File

@ -10,7 +10,10 @@ import (
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
) )
var knownHash = common.Hash{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} var (
knownHash = common.Hash{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
unknownHash = common.Hash{9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9}
)
func createHashes(start, amount int) (hashes []common.Hash) { func createHashes(start, amount int) (hashes []common.Hash) {
hashes = make([]common.Hash, amount+1) hashes = make([]common.Hash, amount+1)
@ -27,7 +30,7 @@ func createBlock(i int, prevHash, hash common.Hash) *types.Block {
header := &types.Header{Number: big.NewInt(int64(i))} header := &types.Header{Number: big.NewInt(int64(i))}
block := types.NewBlockWithHeader(header) block := types.NewBlockWithHeader(header)
block.HeaderHash = hash block.HeaderHash = hash
block.ParentHeaderHash = knownHash block.ParentHeaderHash = prevHash
return block return block
} }
@ -42,9 +45,12 @@ func createBlocksFromHashes(hashes []common.Hash) map[common.Hash]*types.Block {
} }
type downloadTester struct { type downloadTester struct {
downloader *Downloader downloader *Downloader
hashes []common.Hash
blocks map[common.Hash]*types.Block hashes []common.Hash // Chain of hashes simulating
blocks map[common.Hash]*types.Block // Blocks associated with the hashes
chain []common.Hash // Block-chain being constructed
t *testing.T t *testing.T
pcount int pcount int
done chan bool done chan bool
@ -52,7 +58,15 @@ type downloadTester struct {
} }
func newTester(t *testing.T, hashes []common.Hash, blocks map[common.Hash]*types.Block) *downloadTester { func newTester(t *testing.T, hashes []common.Hash, blocks map[common.Hash]*types.Block) *downloadTester {
tester := &downloadTester{t: t, hashes: hashes, blocks: blocks, done: make(chan bool)} tester := &downloadTester{
t: t,
hashes: hashes,
blocks: blocks,
chain: []common.Hash{knownHash},
done: make(chan bool),
}
downloader := New(tester.hasBlock, tester.getBlock) downloader := New(tester.hasBlock, tester.getBlock)
tester.downloader = downloader tester.downloader = downloader
@ -64,9 +78,17 @@ func (dl *downloadTester) sync(peerId string, hash common.Hash) error {
return dl.downloader.Synchronise(peerId, hash) return dl.downloader.Synchronise(peerId, hash)
} }
func (dl *downloadTester) insertBlocks(blocks types.Blocks) {
for _, block := range blocks {
dl.chain = append(dl.chain, block.Hash())
}
}
func (dl *downloadTester) hasBlock(hash common.Hash) bool { func (dl *downloadTester) hasBlock(hash common.Hash) bool {
if knownHash == hash { for _, h := range dl.chain {
return true if h == hash {
return true
}
} }
return false return false
} }
@ -175,10 +197,12 @@ func TestTaking(t *testing.T) {
if err != nil { if err != nil {
t.Error("download error", err) t.Error("download error", err)
} }
bs, err := tester.downloader.TakeBlocks()
bs1 := tester.downloader.TakeBlocks() if err != nil {
if len(bs1) != 1000 { t.Fatalf("failed to take blocks: %v", err)
t.Error("expected to take 1000, got", len(bs1)) }
if len(bs) != targetBlocks {
t.Error("retrieved block mismatch: have %v, want %v", len(bs), targetBlocks)
} }
} }
@ -248,17 +272,18 @@ func TestThrottling(t *testing.T) {
done := make(chan struct{}) done := make(chan struct{})
took := []*types.Block{} took := []*types.Block{}
go func() { go func() {
for { for running := true; running; {
select { select {
case <-done: case <-done:
took = append(took, tester.downloader.TakeBlocks()...) running = false
done <- struct{}{}
return
default: default:
took = append(took, tester.downloader.TakeBlocks()...)
time.Sleep(time.Millisecond) time.Sleep(time.Millisecond)
} }
// Take a batch of blocks and accumulate
blocks, _ := tester.downloader.TakeBlocks()
took = append(took, blocks...)
} }
done <- struct{}{}
}() }()
// Synchronise the two threads and verify // Synchronise the two threads and verify
@ -273,3 +298,44 @@ func TestThrottling(t *testing.T) {
t.Fatalf("downloaded block mismatch: have %v, want %v", len(took), targetBlocks) t.Fatalf("downloaded block mismatch: have %v, want %v", len(took), targetBlocks)
} }
} }
// Tests that if a peer returns an invalid chain with a block pointing to a non-
// existing parent, it is correctly detected and handled.
func TestNonExistingParentAttack(t *testing.T) {
// Forge a single-link chain with a forged header
hashes := createHashes(0, 1)
blocks := createBlocksFromHashes(hashes)
forged := blocks[hashes[0]]
forged.ParentHeaderHash = unknownHash
// Try and sync with the malicious node and check that it fails
tester := newTester(t, hashes, blocks)
tester.newPeer("attack", big.NewInt(10000), hashes[0])
if err := tester.sync("attack", hashes[0]); err != nil {
t.Fatalf("failed to synchronise blocks: %v", err)
}
bs, err := tester.downloader.TakeBlocks()
if err != ErrUnknownParent {
t.Fatalf("take error mismatch: have %v, want %v", err, ErrUnknownParent)
}
if len(bs) != 0 {
t.Error("retrieved block mismatch: have %v, want %v", len(bs), 0)
}
// Cancel the download due to the parent attack
tester.downloader.Cancel()
// Reconstruct a valid chain, and try to synchronize with it
forged.ParentHeaderHash = knownHash
tester.newPeer("valid", big.NewInt(20000), hashes[0])
if err := tester.sync("valid", hashes[0]); err != nil {
t.Fatalf("failed to synchronise blocks: %v", err)
}
bs, err = tester.downloader.TakeBlocks()
if err != nil {
t.Fatalf("failed to retrieve blocks: %v", err)
}
if len(bs) != 1 {
t.Error("retrieved block mismatch: have %v, want %v", len(bs), 1)
}
}

View File

@ -2,6 +2,7 @@ package eth
import ( import (
"math" "math"
"sync/atomic"
"time" "time"
"github.com/ethereum/go-ethereum/eth/downloader" "github.com/ethereum/go-ethereum/eth/downloader"
@ -14,6 +15,7 @@ import (
func (pm *ProtocolManager) update() { func (pm *ProtocolManager) update() {
forceSync := time.Tick(forceSyncCycle) forceSync := time.Tick(forceSyncCycle)
blockProc := time.Tick(blockProcCycle) blockProc := time.Tick(blockProcCycle)
blockProcPend := int32(0)
for { for {
select { select {
@ -36,7 +38,14 @@ func (pm *ProtocolManager) update() {
} }
case <-blockProc: case <-blockProc:
// Try to pull some blocks from the downloaded // Try to pull some blocks from the downloaded
go pm.processBlocks() if atomic.CompareAndSwapInt32(&blockProcPend, 0, 1) {
go func() {
if err := pm.processBlocks(); err != nil {
pm.downloader.Cancel()
}
atomic.StoreInt32(&blockProcPend, 0)
}()
}
case <-pm.quitSync: case <-pm.quitSync:
return return
@ -52,8 +61,12 @@ func (pm *ProtocolManager) processBlocks() error {
pm.wg.Add(1) pm.wg.Add(1)
defer pm.wg.Done() defer pm.wg.Done()
// Take a batch of blocks (will return nil if a previous batch has not reached the chain yet) // Take a batch of blocks, but abort if there's an invalid head or if the chain's empty
blocks := pm.downloader.TakeBlocks() blocks, err := pm.downloader.TakeBlocks()
if err != nil {
glog.V(logger.Warn).Infof("Block processing failed: %v", err)
return err
}
if len(blocks) == 0 { if len(blocks) == 0 {
return nil return nil
} }
@ -63,9 +76,7 @@ func (pm *ProtocolManager) processBlocks() error {
max := int(math.Min(float64(len(blocks)), float64(blockProcAmount))) max := int(math.Min(float64(len(blocks)), float64(blockProcAmount)))
_, err := pm.chainman.InsertChain(blocks[:max]) _, err := pm.chainman.InsertChain(blocks[:max])
if err != nil { if err != nil {
// cancel download process glog.V(logger.Warn).Infof("Block insertion failed: %v", err)
pm.downloader.Cancel()
return err return err
} }
blocks = blocks[max:] blocks = blocks[max:]