diff options
author | Jeffrey Wilcke <geffobscura@gmail.com> | 2015-11-03 18:47:07 +0800 |
---|---|---|
committer | Jeffrey Wilcke <geffobscura@gmail.com> | 2015-11-03 18:47:07 +0800 |
commit | e5532154a50114d5ffb1ffd850b746cab00cb899 (patch) | |
tree | 0042cc997ccf4166b9b52464339d52d37d7a8ad6 /eth/downloader | |
parent | 9666db2a442887ccf8ec2d81f5e2fedc1a3a3d3e (diff) | |
parent | f75becc264f8bde0f58391fc226243d03e78aa7b (diff) | |
download | go-tangerine-e5532154a50114d5ffb1ffd850b746cab00cb899.tar go-tangerine-e5532154a50114d5ffb1ffd850b746cab00cb899.tar.gz go-tangerine-e5532154a50114d5ffb1ffd850b746cab00cb899.tar.bz2 go-tangerine-e5532154a50114d5ffb1ffd850b746cab00cb899.tar.lz go-tangerine-e5532154a50114d5ffb1ffd850b746cab00cb899.tar.xz go-tangerine-e5532154a50114d5ffb1ffd850b746cab00cb899.tar.zst go-tangerine-e5532154a50114d5ffb1ffd850b746cab00cb899.zip |
Merge branch 'release/1.3.0'
Conflicts:
VERSION
cmd/geth/main.go
Diffstat (limited to 'eth/downloader')
-rw-r--r-- | eth/downloader/downloader.go | 1051 | ||||
-rw-r--r-- | eth/downloader/downloader_test.go | 1122 | ||||
-rw-r--r-- | eth/downloader/metrics.go | 10 | ||||
-rw-r--r-- | eth/downloader/modes.go | 26 | ||||
-rw-r--r-- | eth/downloader/peer.go | 261 | ||||
-rw-r--r-- | eth/downloader/queue.go | 881 | ||||
-rw-r--r-- | eth/downloader/types.go | 140 |
7 files changed, 2445 insertions, 1046 deletions
diff --git a/eth/downloader/downloader.go b/eth/downloader/downloader.go index d1a716c5f..153427ee4 100644 --- a/eth/downloader/downloader.go +++ b/eth/downloader/downloader.go @@ -18,111 +18,85 @@ package downloader import ( + "crypto/rand" "errors" + "fmt" "math" "math/big" + "strings" "sync" "sync/atomic" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/logger" "github.com/ethereum/go-ethereum/logger/glog" -) - -const ( - eth61 = 61 // Constant to check for old protocol support - eth62 = 62 // Constant to check for new protocol support + "github.com/rcrowley/go-metrics" ) var ( - MaxHashFetch = 512 // Amount of hashes to be fetched per retrieval request - MaxBlockFetch = 128 // Amount of blocks to be fetched per retrieval request - MaxHeaderFetch = 192 // Amount of block headers to be fetched per retrieval request - MaxBodyFetch = 128 // Amount of block bodies to be fetched per retrieval request - MaxStateFetch = 384 // Amount of node state values to allow fetching per request - MaxReceiptsFetch = 384 // Amount of transaction receipts to allow fetching per request - - hashTTL = 5 * time.Second // [eth/61] Time it takes for a hash request to time out - blockSoftTTL = 3 * time.Second // [eth/61] Request completion threshold for increasing or decreasing a peer's bandwidth - blockHardTTL = 3 * blockSoftTTL // [eth/61] Maximum time allowance before a block request is considered expired - headerTTL = 5 * time.Second // [eth/62] Time it takes for a header request to time out - bodySoftTTL = 3 * time.Second // [eth/62] Request completion threshold for increasing or decreasing a peer's bandwidth - bodyHardTTL = 3 * bodySoftTTL // [eth/62] Maximum time allowance before a block body request is considered expired - - maxQueuedHashes = 256 * 1024 // [eth/61] Maximum number of hashes to queue for import (DOS protection) - maxQueuedHeaders = 256 * 1024 // [eth/62] Maximum number of headers to queue for import (DOS protection) - maxBlockProcess = 256 // Number of blocks to import at once into the chain + MaxHashFetch = 512 // Amount of hashes to be fetched per retrieval request + MaxBlockFetch = 128 // Amount of blocks to be fetched per retrieval request + MaxHeaderFetch = 192 // Amount of block headers to be fetched per retrieval request + MaxBodyFetch = 128 // Amount of block bodies to be fetched per retrieval request + MaxReceiptFetch = 256 // Amount of transaction receipts to allow fetching per request + MaxStateFetch = 384 // Amount of node state values to allow fetching per request + + hashTTL = 5 * time.Second // [eth/61] Time it takes for a hash request to time out + blockSoftTTL = 3 * time.Second // [eth/61] Request completion threshold for increasing or decreasing a peer's bandwidth + blockHardTTL = 3 * blockSoftTTL // [eth/61] Maximum time allowance before a block request is considered expired + headerTTL = 5 * time.Second // [eth/62] Time it takes for a header request to time out + bodySoftTTL = 3 * time.Second // [eth/62] Request completion threshold for increasing or decreasing a peer's bandwidth + bodyHardTTL = 3 * bodySoftTTL // [eth/62] Maximum time allowance before a block body request is considered expired + receiptSoftTTL = 3 * time.Second // [eth/63] Request completion threshold for increasing or decreasing a peer's bandwidth + receiptHardTTL = 3 * receiptSoftTTL // [eth/63] Maximum time allowance before a receipt request is considered expired + stateSoftTTL = 2 * time.Second // [eth/63] Request completion threshold for increasing or decreasing a peer's bandwidth + stateHardTTL = 3 * stateSoftTTL // [eth/63] Maximum time allowance before a node data request is considered expired + + maxQueuedHashes = 256 * 1024 // [eth/61] Maximum number of hashes to queue for import (DOS protection) + maxQueuedHeaders = 256 * 1024 // [eth/62] Maximum number of headers to queue for import (DOS protection) + maxQueuedStates = 256 * 1024 // [eth/63] Maximum number of state requests to queue (DOS protection) + maxResultsProcess = 256 // Number of download results to import at once into the chain + + fsHeaderCheckFrequency = 100 // Verification frequency of the downloaded headers during fast sync + fsHeaderSafetyNet = 2048 // Number of headers to discard in case a chain violation is detected + fsHeaderForceVerify = 24 // Number of headers to verify before and after the pivot to accept it + fsPivotInterval = 512 // Number of headers out of which to randomize the pivot point + fsMinFullBlocks = 1024 // Number of blocks to retrieve fully even in fast sync ) var ( - errBusy = errors.New("busy") - errUnknownPeer = errors.New("peer is unknown or unhealthy") - errBadPeer = errors.New("action from bad peer ignored") - errStallingPeer = errors.New("peer is stalling") - errNoPeers = errors.New("no peers to keep download active") - errPendingQueue = errors.New("pending items in queue") - errTimeout = errors.New("timeout") - errEmptyHashSet = errors.New("empty hash set by peer") - errEmptyHeaderSet = errors.New("empty header set by peer") - errPeersUnavailable = errors.New("no peers available or all peers tried for block download process") - errAlreadyInPool = errors.New("hash already in pool") - errInvalidChain = errors.New("retrieved hash chain is invalid") - errInvalidBody = errors.New("retrieved block body is invalid") - errCancelHashFetch = errors.New("hash fetching canceled (requested)") - errCancelBlockFetch = errors.New("block downloading canceled (requested)") - errCancelHeaderFetch = errors.New("block header fetching canceled (requested)") - errCancelBodyFetch = errors.New("block body downloading canceled (requested)") - errNoSyncActive = errors.New("no sync active") + errBusy = errors.New("busy") + errUnknownPeer = errors.New("peer is unknown or unhealthy") + errBadPeer = errors.New("action from bad peer ignored") + errStallingPeer = errors.New("peer is stalling") + errNoPeers = errors.New("no peers to keep download active") + errPendingQueue = errors.New("pending items in queue") + errTimeout = errors.New("timeout") + errEmptyHashSet = errors.New("empty hash set by peer") + errEmptyHeaderSet = errors.New("empty header set by peer") + errPeersUnavailable = errors.New("no peers available or all tried for download") + errAlreadyInPool = errors.New("hash already in pool") + errInvalidChain = errors.New("retrieved hash chain is invalid") + errInvalidBlock = errors.New("retrieved block is invalid") + errInvalidBody = errors.New("retrieved block body is invalid") + errInvalidReceipt = errors.New("retrieved receipt is invalid") + errCancelHashFetch = errors.New("hash download canceled (requested)") + errCancelBlockFetch = errors.New("block download canceled (requested)") + errCancelHeaderFetch = errors.New("block header download canceled (requested)") + errCancelBodyFetch = errors.New("block body download canceled (requested)") + errCancelReceiptFetch = errors.New("receipt download canceled (requested)") + errCancelStateFetch = errors.New("state data download canceled (requested)") + errNoSyncActive = errors.New("no sync active") ) -// hashCheckFn is a callback type for verifying a hash's presence in the local chain. -type hashCheckFn func(common.Hash) bool - -// blockRetrievalFn is a callback type for retrieving a block from the local chain. -type blockRetrievalFn func(common.Hash) *types.Block - -// headRetrievalFn is a callback type for retrieving the head block from the local chain. -type headRetrievalFn func() *types.Block - -// tdRetrievalFn is a callback type for retrieving the total difficulty of a local block. -type tdRetrievalFn func(common.Hash) *big.Int - -// chainInsertFn is a callback type to insert a batch of blocks into the local chain. -type chainInsertFn func(types.Blocks) (int, error) - -// peerDropFn is a callback type for dropping a peer detected as malicious. -type peerDropFn func(id string) - -// hashPack is a batch of block hashes returned by a peer (eth/61). -type hashPack struct { - peerId string - hashes []common.Hash -} - -// blockPack is a batch of blocks returned by a peer (eth/61). -type blockPack struct { - peerId string - blocks []*types.Block -} - -// headerPack is a batch of block headers returned by a peer. -type headerPack struct { - peerId string - headers []*types.Header -} - -// bodyPack is a batch of block bodies returned by a peer. -type bodyPack struct { - peerId string - transactions [][]*types.Transaction - uncles [][]*types.Header -} - type Downloader struct { - mux *event.TypeMux + mode SyncMode // Synchronisation mode defining the strategy used (per sync cycle) + noFast bool // Flag to disable fast syncing in case of a security error + mux *event.TypeMux // Event multiplexer to announce sync operation events queue *queue // Scheduler for selecting the hashes to download peers *peerSet // Set of active peers from which download can proceed @@ -130,17 +104,27 @@ type Downloader struct { interrupt int32 // Atomic boolean to signal termination // Statistics - syncStatsOrigin uint64 // Origin block number where syncing started at - syncStatsHeight uint64 // Highest block number known when syncing started - syncStatsLock sync.RWMutex // Lock protecting the sync stats fields + syncStatsChainOrigin uint64 // Origin block number where syncing started at + syncStatsChainHeight uint64 // Highest block number known when syncing started + syncStatsStateTotal uint64 // Total number of node state entries known so far + syncStatsStateDone uint64 // Number of state trie entries already pulled + syncStatsLock sync.RWMutex // Lock protecting the sync stats fields // Callbacks - hasBlock hashCheckFn // Checks if a block is present in the chain - getBlock blockRetrievalFn // Retrieves a block from the chain - headBlock headRetrievalFn // Retrieves the head block from the chain - getTd tdRetrievalFn // Retrieves the TD of a block from the chain - insertChain chainInsertFn // Injects a batch of blocks into the chain - dropPeer peerDropFn // Drops a peer for misbehaving + hasHeader headerCheckFn // Checks if a header is present in the chain + hasBlock blockCheckFn // Checks if a block is present in the chain + getHeader headerRetrievalFn // Retrieves a header from the chain + getBlock blockRetrievalFn // Retrieves a block from the chain + headHeader headHeaderRetrievalFn // Retrieves the head header from the chain + headBlock headBlockRetrievalFn // Retrieves the head block from the chain + headFastBlock headFastBlockRetrievalFn // Retrieves the head fast-sync block from the chain + commitHeadBlock headBlockCommitterFn // Commits a manually assembled block as the chain head + getTd tdRetrievalFn // Retrieves the TD of a block from the chain + insertHeaders headerChainInsertFn // Injects a batch of headers into the chain + insertBlocks blockChainInsertFn // Injects a batch of blocks into the chain + insertReceipts receiptChainInsertFn // Injects a batch of blocks and their receipts into the chain + rollback chainRollbackFn // Removes a batch of recently added chain links + dropPeer peerDropFn // Drops a peer for misbehaving // Status synchroniseMock func(id string, hash common.Hash) error // Replacement for synchronise during testing @@ -149,72 +133,100 @@ type Downloader struct { notified int32 // Channels - newPeerCh chan *peer - hashCh chan hashPack // [eth/61] Channel receiving inbound hashes - blockCh chan blockPack // [eth/61] Channel receiving inbound blocks - headerCh chan headerPack // [eth/62] Channel receiving inbound block headers - bodyCh chan bodyPack // [eth/62] Channel receiving inbound block bodies - wakeCh chan bool // Channel to signal the block/body fetcher of new tasks + newPeerCh chan *peer + hashCh chan dataPack // [eth/61] Channel receiving inbound hashes + blockCh chan dataPack // [eth/61] Channel receiving inbound blocks + headerCh chan dataPack // [eth/62] Channel receiving inbound block headers + bodyCh chan dataPack // [eth/62] Channel receiving inbound block bodies + receiptCh chan dataPack // [eth/63] Channel receiving inbound receipts + stateCh chan dataPack // [eth/63] Channel receiving inbound node state data + blockWakeCh chan bool // [eth/61] Channel to signal the block fetcher of new tasks + bodyWakeCh chan bool // [eth/62] Channel to signal the block body fetcher of new tasks + receiptWakeCh chan bool // [eth/63] Channel to signal the receipt fetcher of new tasks + stateWakeCh chan bool // [eth/63] Channel to signal the state fetcher of new tasks cancelCh chan struct{} // Channel to cancel mid-flight syncs cancelLock sync.RWMutex // Lock to protect the cancel channel in delivers // Testing hooks - syncInitHook func(uint64, uint64) // Method to call upon initiating a new sync run - bodyFetchHook func([]*types.Header) // Method to call upon starting a block body fetch - chainInsertHook func([]*Block) // Method to call upon inserting a chain of blocks (possibly in multiple invocations) -} - -// Block is an origin-tagged blockchain block. -type Block struct { - RawBlock *types.Block - OriginPeer string + syncInitHook func(uint64, uint64) // Method to call upon initiating a new sync run + bodyFetchHook func([]*types.Header) // Method to call upon starting a block body fetch + receiptFetchHook func([]*types.Header) // Method to call upon starting a receipt fetch + chainInsertHook func([]*fetchResult) // Method to call upon inserting a chain of blocks (possibly in multiple invocations) } // New creates a new downloader to fetch hashes and blocks from remote peers. -func New(mux *event.TypeMux, hasBlock hashCheckFn, getBlock blockRetrievalFn, headBlock headRetrievalFn, getTd tdRetrievalFn, insertChain chainInsertFn, dropPeer peerDropFn) *Downloader { +func New(stateDb ethdb.Database, mux *event.TypeMux, hasHeader headerCheckFn, hasBlock blockCheckFn, getHeader headerRetrievalFn, + getBlock blockRetrievalFn, headHeader headHeaderRetrievalFn, headBlock headBlockRetrievalFn, headFastBlock headFastBlockRetrievalFn, + commitHeadBlock headBlockCommitterFn, getTd tdRetrievalFn, insertHeaders headerChainInsertFn, insertBlocks blockChainInsertFn, + insertReceipts receiptChainInsertFn, rollback chainRollbackFn, dropPeer peerDropFn) *Downloader { + return &Downloader{ - mux: mux, - queue: newQueue(), - peers: newPeerSet(), - hasBlock: hasBlock, - getBlock: getBlock, - headBlock: headBlock, - getTd: getTd, - insertChain: insertChain, - dropPeer: dropPeer, - newPeerCh: make(chan *peer, 1), - hashCh: make(chan hashPack, 1), - blockCh: make(chan blockPack, 1), - headerCh: make(chan headerPack, 1), - bodyCh: make(chan bodyPack, 1), - wakeCh: make(chan bool, 1), + mode: FullSync, + mux: mux, + queue: newQueue(stateDb), + peers: newPeerSet(), + hasHeader: hasHeader, + hasBlock: hasBlock, + getHeader: getHeader, + getBlock: getBlock, + headHeader: headHeader, + headBlock: headBlock, + headFastBlock: headFastBlock, + commitHeadBlock: commitHeadBlock, + getTd: getTd, + insertHeaders: insertHeaders, + insertBlocks: insertBlocks, + insertReceipts: insertReceipts, + rollback: rollback, + dropPeer: dropPeer, + newPeerCh: make(chan *peer, 1), + hashCh: make(chan dataPack, 1), + blockCh: make(chan dataPack, 1), + headerCh: make(chan dataPack, 1), + bodyCh: make(chan dataPack, 1), + receiptCh: make(chan dataPack, 1), + stateCh: make(chan dataPack, 1), + blockWakeCh: make(chan bool, 1), + bodyWakeCh: make(chan bool, 1), + receiptWakeCh: make(chan bool, 1), + stateWakeCh: make(chan bool, 1), } } -// Boundaries retrieves the synchronisation boundaries, specifically the origin -// block where synchronisation started at (may have failed/suspended) and the -// latest known block which the synchonisation targets. -func (d *Downloader) Boundaries() (uint64, uint64) { +// Progress retrieves the synchronisation boundaries, specifically the origin +// block where synchronisation started at (may have failed/suspended); the block +// or header sync is currently at; and the latest known block which the sync targets. +func (d *Downloader) Progress() (uint64, uint64, uint64) { d.syncStatsLock.RLock() defer d.syncStatsLock.RUnlock() - return d.syncStatsOrigin, d.syncStatsHeight + current := uint64(0) + switch d.mode { + case FullSync: + current = d.headBlock().NumberU64() + case FastSync: + current = d.headFastBlock().NumberU64() + case LightSync: + current = d.headHeader().Number.Uint64() + } + return d.syncStatsChainOrigin, current, d.syncStatsChainHeight } // Synchronising returns whether the downloader is currently retrieving blocks. func (d *Downloader) Synchronising() bool { - return atomic.LoadInt32(&d.synchronising) > 0 + return atomic.LoadInt32(&d.synchronising) > 0 || atomic.LoadInt32(&d.processing) > 0 } // RegisterPeer injects a new download peer into the set of block source to be // used for fetching hashes and blocks from. func (d *Downloader) RegisterPeer(id string, version int, head common.Hash, getRelHashes relativeHashFetcherFn, getAbsHashes absoluteHashFetcherFn, getBlocks blockFetcherFn, // eth/61 callbacks, remove when upgrading - getRelHeaders relativeHeaderFetcherFn, getAbsHeaders absoluteHeaderFetcherFn, getBlockBodies blockBodyFetcherFn) error { + getRelHeaders relativeHeaderFetcherFn, getAbsHeaders absoluteHeaderFetcherFn, getBlockBodies blockBodyFetcherFn, + getReceipts receiptFetcherFn, getNodeData stateFetcherFn) error { glog.V(logger.Detail).Infoln("Registering peer", id) - if err := d.peers.Register(newPeer(id, version, head, getRelHashes, getAbsHashes, getBlocks, getRelHeaders, getAbsHeaders, getBlockBodies)); err != nil { + if err := d.peers.Register(newPeer(id, version, head, getRelHashes, getAbsHashes, getBlocks, getRelHeaders, getAbsHeaders, getBlockBodies, getReceipts, getNodeData)); err != nil { glog.V(logger.Error).Infoln("Register failed:", err) return err } @@ -222,22 +234,25 @@ func (d *Downloader) RegisterPeer(id string, version int, head common.Hash, } // UnregisterPeer remove a peer from the known list, preventing any action from -// the specified peer. +// the specified peer. An effort is also made to return any pending fetches into +// the queue. func (d *Downloader) UnregisterPeer(id string) error { glog.V(logger.Detail).Infoln("Unregistering peer", id) if err := d.peers.Unregister(id); err != nil { glog.V(logger.Error).Infoln("Unregister failed:", err) return err } + d.queue.Revoke(id) return nil } // Synchronise tries to sync up our local block chain with a remote peer, both // adding various sanity checks as well as wrapping it with various log entries. -func (d *Downloader) Synchronise(id string, head common.Hash, td *big.Int) { +func (d *Downloader) Synchronise(id string, head common.Hash, td *big.Int, mode SyncMode) error { glog.V(logger.Detail).Infof("Attempting synchronisation: %v, head [%x…], TD %v", id, head[:4], td) - switch err := d.synchronise(id, head, td); err { + err := d.synchronise(id, head, td, mode) + switch err { case nil: glog.V(logger.Detail).Infof("Synchronisation completed") @@ -254,12 +269,13 @@ func (d *Downloader) Synchronise(id string, head common.Hash, td *big.Int) { default: glog.V(logger.Warn).Infof("Synchronisation failed: %v", err) } + return err } // synchronise will select the peer and use it for synchronising. If an empty string is given // it will use the best peer possible and synchronize if it's TD is higher than our own. If any of the // checks fail an error will be returned. This method is synchronous -func (d *Downloader) synchronise(id string, hash common.Hash, td *big.Int) error { +func (d *Downloader) synchronise(id string, hash common.Hash, td *big.Int, mode SyncMode) error { // Mock out the synchonisation if testing if d.synchroniseMock != nil { return d.synchroniseMock(id, hash) @@ -275,22 +291,35 @@ func (d *Downloader) synchronise(id string, hash common.Hash, td *big.Int) error glog.V(logger.Info).Infoln("Block synchronisation started") } // Abort if the queue still contains some leftover data - if _, cached := d.queue.Size(); cached > 0 && d.queue.GetHeadBlock() != nil { + if d.queue.GetHeadResult() != nil { return errPendingQueue } - // Reset the queue and peer set to clean any internal leftover state + // Reset the queue, peer set and wake channels to clean any internal leftover state d.queue.Reset() d.peers.Reset() - select { - case <-d.wakeCh: - default: + for _, ch := range []chan bool{d.blockWakeCh, d.bodyWakeCh, d.receiptWakeCh, d.stateWakeCh} { + select { + case <-ch: + default: + } } + // Reset and ephemeral sync statistics + d.syncStatsLock.Lock() + d.syncStatsStateTotal = 0 + d.syncStatsStateDone = 0 + d.syncStatsLock.Unlock() + // Create cancel channel for aborting mid-flight d.cancelLock.Lock() d.cancelCh = make(chan struct{}) d.cancelLock.Unlock() + // Set the requested sync mode, unless it's forbidden + d.mode = mode + if d.mode == FastSync && d.noFast { + d.mode = FullSync + } // Retrieve the origin peer and initiate the downloading process p := d.peers.Peer(id) if p == nil { @@ -299,12 +328,6 @@ func (d *Downloader) synchronise(id string, hash common.Hash, td *big.Int) error return d.syncWithPeer(p, hash, td) } -// Has checks if the downloader knows about a particular hash, meaning that its -// either already downloaded of pending retrieval. -func (d *Downloader) Has(hash common.Hash) bool { - return d.queue.Has(hash) -} - // syncWithPeer starts a block synchronization based on the hash chain from the // specified peer and head hash. func (d *Downloader) syncWithPeer(p *peer, hash common.Hash, td *big.Int) (err error) { @@ -320,10 +343,12 @@ func (d *Downloader) syncWithPeer(p *peer, hash common.Hash, td *big.Int) (err e }() glog.V(logger.Debug).Infof("Synchronising with the network using: %s [eth/%d]", p.id, p.version) - defer glog.V(logger.Debug).Infof("Synchronisation terminated") + defer func(start time.Time) { + glog.V(logger.Debug).Infof("Synchronisation terminated after %v", time.Since(start)) + }(time.Now()) switch { - case p.version == eth61: + case p.version == 61: // Look up the sync boundaries: the common ancestor and the target block latest, err := d.fetchHeight61(p) if err != nil { @@ -334,16 +359,18 @@ func (d *Downloader) syncWithPeer(p *peer, hash common.Hash, td *big.Int) (err e return err } d.syncStatsLock.Lock() - if d.syncStatsHeight <= origin || d.syncStatsOrigin > origin { - d.syncStatsOrigin = origin + if d.syncStatsChainHeight <= origin || d.syncStatsChainOrigin > origin { + d.syncStatsChainOrigin = origin } - d.syncStatsHeight = latest + d.syncStatsChainHeight = latest d.syncStatsLock.Unlock() // Initiate the sync using a concurrent hash and block retrieval algorithm if d.syncInitHook != nil { d.syncInitHook(origin, latest) } + d.queue.Prepare(origin+1, d.mode, 0) + errc := make(chan error, 2) go func() { errc <- d.fetchHashes61(p, td, origin+1) }() go func() { errc <- d.fetchBlocks61(origin + 1) }() @@ -356,7 +383,7 @@ func (d *Downloader) syncWithPeer(p *peer, hash common.Hash, td *big.Int) (err e } return <-errc - case p.version >= eth62: + case p.version >= 62: // Look up the sync boundaries: the common ancestor and the target block latest, err := d.fetchHeight(p) if err != nil { @@ -367,27 +394,59 @@ func (d *Downloader) syncWithPeer(p *peer, hash common.Hash, td *big.Int) (err e return err } d.syncStatsLock.Lock() - if d.syncStatsHeight <= origin || d.syncStatsOrigin > origin { - d.syncStatsOrigin = origin + if d.syncStatsChainHeight <= origin || d.syncStatsChainOrigin > origin { + d.syncStatsChainOrigin = origin } - d.syncStatsHeight = latest + d.syncStatsChainHeight = latest d.syncStatsLock.Unlock() - // Initiate the sync using a concurrent hash and block retrieval algorithm + // Initiate the sync using a concurrent header and content retrieval algorithm + pivot := uint64(0) + switch d.mode { + case LightSync: + pivot = latest + + case FastSync: + // Calculate the new fast/slow sync pivot point + pivotOffset, err := rand.Int(rand.Reader, big.NewInt(int64(fsPivotInterval))) + if err != nil { + panic(fmt.Sprintf("Failed to access crypto random source: %v", err)) + } + if latest > uint64(fsMinFullBlocks)+pivotOffset.Uint64() { + pivot = latest - uint64(fsMinFullBlocks) - pivotOffset.Uint64() + } + // If the point is below the origin, move origin back to ensure state download + if pivot < origin { + if pivot > 0 { + origin = pivot - 1 + } else { + origin = 0 + } + } + glog.V(logger.Debug).Infof("Fast syncing until pivot block #%d", pivot) + } + d.queue.Prepare(origin+1, d.mode, pivot) + if d.syncInitHook != nil { d.syncInitHook(origin, latest) } - errc := make(chan error, 2) - go func() { errc <- d.fetchHeaders(p, td, origin+1) }() - go func() { errc <- d.fetchBodies(origin + 1) }() - - // If any fetcher fails, cancel the other - if err := <-errc; err != nil { - d.cancel() - <-errc - return err + errc := make(chan error, 4) + go func() { errc <- d.fetchHeaders(p, td, origin+1) }() // Headers are always retrieved + go func() { errc <- d.fetchBodies(origin + 1) }() // Bodies are retrieved during normal and fast sync + go func() { errc <- d.fetchReceipts(origin + 1) }() // Receipts are retrieved during fast sync + go func() { errc <- d.fetchNodeData() }() // Node state data is retrieved during fast sync + + // If any fetcher fails, cancel the others + var fail error + for i := 0; i < cap(errc); i++ { + if err := <-errc; err != nil { + if fail == nil { + fail = err + d.cancel() + } + } } - return <-errc + return fail default: // Something very wrong, stop right here @@ -445,14 +504,14 @@ func (d *Downloader) fetchHeight61(p *peer) (uint64, error) { case <-d.hashCh: // Out of bounds hashes received, ignore them - case blockPack := <-d.blockCh: + case packet := <-d.blockCh: // Discard anything not from the origin peer - if blockPack.peerId != p.id { - glog.V(logger.Debug).Infof("Received blocks from incorrect peer(%s)", blockPack.peerId) + if packet.PeerId() != p.id { + glog.V(logger.Debug).Infof("Received blocks from incorrect peer(%s)", packet.PeerId()) break } // Make sure the peer actually gave something valid - blocks := blockPack.blocks + blocks := packet.(*blockPack).blocks if len(blocks) != 1 { glog.V(logger.Debug).Infof("%v: invalid number of head blocks: %d != 1", p, len(blocks)) return 0, errBadPeer @@ -491,14 +550,14 @@ func (d *Downloader) findAncestor61(p *peer) (uint64, error) { case <-d.cancelCh: return 0, errCancelHashFetch - case hashPack := <-d.hashCh: + case packet := <-d.hashCh: // Discard anything not from the origin peer - if hashPack.peerId != p.id { - glog.V(logger.Debug).Infof("Received hashes from incorrect peer(%s)", hashPack.peerId) + if packet.PeerId() != p.id { + glog.V(logger.Debug).Infof("Received hashes from incorrect peer(%s)", packet.PeerId()) break } // Make sure the peer actually gave something valid - hashes := hashPack.hashes + hashes := packet.(*hashPack).hashes if len(hashes) == 0 { glog.V(logger.Debug).Infof("%v: empty head hash set", p) return 0, errEmptyHashSet @@ -546,14 +605,14 @@ func (d *Downloader) findAncestor61(p *peer) (uint64, error) { case <-d.cancelCh: return 0, errCancelHashFetch - case hashPack := <-d.hashCh: + case packet := <-d.hashCh: // Discard anything not from the origin peer - if hashPack.peerId != p.id { - glog.V(logger.Debug).Infof("Received hashes from incorrect peer(%s)", hashPack.peerId) + if packet.PeerId() != p.id { + glog.V(logger.Debug).Infof("Received hashes from incorrect peer(%s)", packet.PeerId()) break } // Make sure the peer actually gave something valid - hashes := hashPack.hashes + hashes := packet.(*hashPack).hashes if len(hashes) != 1 { glog.V(logger.Debug).Infof("%v: invalid search hash set (%d)", p, len(hashes)) return 0, errBadPeer @@ -623,21 +682,21 @@ func (d *Downloader) fetchHashes61(p *peer, td *big.Int, from uint64) error { case <-d.bodyCh: // Out of bounds eth/62 block bodies received, ignore them - case hashPack := <-d.hashCh: + case packet := <-d.hashCh: // Make sure the active peer is giving us the hashes - if hashPack.peerId != p.id { - glog.V(logger.Debug).Infof("Received hashes from incorrect peer(%s)", hashPack.peerId) + if packet.PeerId() != p.id { + glog.V(logger.Debug).Infof("Received hashes from incorrect peer(%s)", packet.PeerId()) break } hashReqTimer.UpdateSince(request) timeout.Stop() // If no more hashes are inbound, notify the block fetcher and return - if len(hashPack.hashes) == 0 { + if packet.Items() == 0 { glog.V(logger.Debug).Infof("%v: no available hashes", p) select { - case d.wakeCh <- false: + case d.blockWakeCh <- false: case <-d.cancelCh: } // If no hashes were retrieved at all, the peer violated it's TD promise that it had a @@ -658,32 +717,33 @@ func (d *Downloader) fetchHashes61(p *peer, td *big.Int, from uint64) error { return nil } gotHashes = true + hashes := packet.(*hashPack).hashes // Otherwise insert all the new hashes, aborting in case of junk - glog.V(logger.Detail).Infof("%v: inserting %d hashes from #%d", p, len(hashPack.hashes), from) + glog.V(logger.Detail).Infof("%v: scheduling %d hashes from #%d", p, len(hashes), from) - inserts := d.queue.Insert61(hashPack.hashes, true) - if len(inserts) != len(hashPack.hashes) { + inserts := d.queue.Schedule61(hashes, true) + if len(inserts) != len(hashes) { glog.V(logger.Debug).Infof("%v: stale hashes", p) return errBadPeer } // Notify the block fetcher of new hashes, but stop if queue is full - if d.queue.Pending() < maxQueuedHashes { + if d.queue.PendingBlocks() < maxQueuedHashes { // We still have hashes to fetch, send continuation wake signal (potential) select { - case d.wakeCh <- true: + case d.blockWakeCh <- true: default: } } else { // Hash limit reached, send a termination wake signal (enforced) select { - case d.wakeCh <- false: + case d.blockWakeCh <- false: case <-d.cancelCh: } return nil } // Queue not yet full, fetch the next batch - from += uint64(len(hashPack.hashes)) + from += uint64(len(hashes)) getHashes(from) case <-timeout.C: @@ -707,10 +767,8 @@ func (d *Downloader) fetchBlocks61(from uint64) error { update := make(chan struct{}, 1) - // Prepare the queue and fetch blocks until the hash fetcher's done - d.queue.Prepare(from) + // Fetch blocks until the hash fetcher's done finished := false - for { select { case <-d.cancelCh: @@ -722,25 +780,26 @@ func (d *Downloader) fetchBlocks61(from uint64) error { case <-d.bodyCh: // Out of bounds eth/62 block bodies received, ignore them - case blockPack := <-d.blockCh: + case packet := <-d.blockCh: // If the peer was previously banned and failed to deliver it's pack // in a reasonable time frame, ignore it's message. - if peer := d.peers.Peer(blockPack.peerId); peer != nil { + if peer := d.peers.Peer(packet.PeerId()); peer != nil { // Deliver the received chunk of blocks, and demote in case of errors - err := d.queue.Deliver61(blockPack.peerId, blockPack.blocks) + blocks := packet.(*blockPack).blocks + err := d.queue.DeliverBlocks(peer.id, blocks) switch err { case nil: // If no blocks were delivered, demote the peer (need the delivery above) - if len(blockPack.blocks) == 0 { + if len(blocks) == 0 { peer.Demote() - peer.SetIdle61() + peer.SetBlocksIdle() glog.V(logger.Detail).Infof("%s: no blocks delivered", peer) break } // All was successful, promote the peer and potentially start processing peer.Promote() - peer.SetIdle61() - glog.V(logger.Detail).Infof("%s: delivered %d blocks", peer, len(blockPack.blocks)) + peer.SetBlocksIdle() + glog.V(logger.Detail).Infof("%s: delivered %d blocks", peer, len(blocks)) go d.process() case errInvalidChain: @@ -751,7 +810,7 @@ func (d *Downloader) fetchBlocks61(from uint64) error { // Peer probably timed out with its delivery but came through // in the end, demote, but allow to to pull from this peer. peer.Demote() - peer.SetIdle61() + peer.SetBlocksIdle() glog.V(logger.Detail).Infof("%s: out of bound delivery", peer) case errStaleDelivery: @@ -765,7 +824,7 @@ func (d *Downloader) fetchBlocks61(from uint64) error { default: // Peer did something semi-useful, demote but keep it around peer.Demote() - peer.SetIdle61() + peer.SetBlocksIdle() glog.V(logger.Detail).Infof("%s: delivery partially failed: %v", peer, err) go d.process() } @@ -776,7 +835,7 @@ func (d *Downloader) fetchBlocks61(from uint64) error { default: } - case cont := <-d.wakeCh: + case cont := <-d.blockWakeCh: // The hash fetcher sent a continuation flag, check if it's done if !cont { finished = true @@ -800,15 +859,15 @@ func (d *Downloader) fetchBlocks61(from uint64) error { return errNoPeers } // Check for block request timeouts and demote the responsible peers - for _, pid := range d.queue.Expire(blockHardTTL) { + for _, pid := range d.queue.ExpireBlocks(blockHardTTL) { if peer := d.peers.Peer(pid); peer != nil { peer.Demote() glog.V(logger.Detail).Infof("%s: block delivery timeout", peer) } } - // If there's noting more to fetch, wait or terminate - if d.queue.Pending() == 0 { - if d.queue.InFlight() == 0 && finished { + // If there's nothing more to fetch, wait or terminate + if d.queue.PendingBlocks() == 0 { + if !d.queue.InFlightBlocks() && finished { glog.V(logger.Debug).Infof("Block fetching completed") return nil } @@ -816,16 +875,18 @@ func (d *Downloader) fetchBlocks61(from uint64) error { } // Send a download request to all idle peers, until throttled throttled := false - for _, peer := range d.peers.IdlePeers() { + idles, total := d.peers.BlockIdlePeers() + + for _, peer := range idles { // Short circuit if throttling activated - if d.queue.Throttle() { + if d.queue.ShouldThrottleBlocks() { throttled = true break } // Reserve a chunk of hashes for a peer. A nil can mean either that // no more hashes are available, or that the peer is known not to // have them. - request := d.queue.Reserve61(peer, peer.Capacity()) + request := d.queue.ReserveBlocks(peer, peer.BlockCapacity()) if request == nil { continue } @@ -834,13 +895,18 @@ func (d *Downloader) fetchBlocks61(from uint64) error { } // Fetch the chunk and make sure any errors return the hashes to the queue if err := peer.Fetch61(request); err != nil { - glog.V(logger.Error).Infof("%v: fetch failed, rescheduling", peer) - d.queue.Cancel(request) + // Although we could try and make an attempt to fix this, this error really + // means that we've double allocated a fetch task to a peer. If that is the + // case, the internal state of the downloader and the queue is very wrong so + // better hard crash and note the error instead of silently accumulating into + // a much bigger issue. + panic(fmt.Sprintf("%v: fetch assignment failed, hard panic", peer)) + d.queue.CancelBlocks(request) // noop for now } } // Make sure that we have peers available for fetching. If all peers have been tried // and all failed throw an error - if !throttled && d.queue.InFlight() == 0 { + if !throttled && !d.queue.InFlightBlocks() && len(idles) == total { return errPeersUnavailable } } @@ -861,14 +927,14 @@ func (d *Downloader) fetchHeight(p *peer) (uint64, error) { case <-d.cancelCh: return 0, errCancelBlockFetch - case headerPack := <-d.headerCh: + case packet := <-d.headerCh: // Discard anything not from the origin peer - if headerPack.peerId != p.id { - glog.V(logger.Debug).Infof("Received headers from incorrect peer(%s)", headerPack.peerId) + if packet.PeerId() != p.id { + glog.V(logger.Debug).Infof("Received headers from incorrect peer(%s)", packet.PeerId()) break } // Make sure the peer actually gave something valid - headers := headerPack.headers + headers := packet.(*headerPack).headers if len(headers) != 1 { glog.V(logger.Debug).Infof("%v: invalid number of head headers: %d != 1", p, len(headers)) return 0, errBadPeer @@ -891,16 +957,21 @@ func (d *Downloader) fetchHeight(p *peer) (uint64, error) { } } -// findAncestor tries to locate the common ancestor block of the local chain and +// findAncestor tries to locate the common ancestor link of the local chain and // a remote peers blockchain. In the general case when our node was in sync and -// on the correct chain, checking the top N blocks should already get us a match. +// on the correct chain, checking the top N links should already get us a match. // In the rare scenario when we ended up on a long reorganization (i.e. none of -// the head blocks match), we do a binary search to find the common ancestor. +// the head links match), we do a binary search to find the common ancestor. func (d *Downloader) findAncestor(p *peer) (uint64, error) { glog.V(logger.Debug).Infof("%v: looking for common ancestor", p) - // Request our head blocks to short circuit ancestor location - head := d.headBlock().NumberU64() + // Request our head headers to short circuit ancestor location + head := d.headHeader().Number.Uint64() + if d.mode == FullSync { + head = d.headBlock().NumberU64() + } else if d.mode == FastSync { + head = d.headFastBlock().NumberU64() + } from := int64(head) - int64(MaxHeaderFetch) + 1 if from < 0 { from = 0 @@ -916,14 +987,14 @@ func (d *Downloader) findAncestor(p *peer) (uint64, error) { case <-d.cancelCh: return 0, errCancelHashFetch - case headerPack := <-d.headerCh: + case packet := <-d.headerCh: // Discard anything not from the origin peer - if headerPack.peerId != p.id { - glog.V(logger.Debug).Infof("Received headers from incorrect peer(%s)", headerPack.peerId) + if packet.PeerId() != p.id { + glog.V(logger.Debug).Infof("Received headers from incorrect peer(%s)", packet.PeerId()) break } // Make sure the peer actually gave something valid - headers := headerPack.headers + headers := packet.(*headerPack).headers if len(headers) == 0 { glog.V(logger.Debug).Infof("%v: empty head header set", p) return 0, errEmptyHeaderSet @@ -931,7 +1002,7 @@ func (d *Downloader) findAncestor(p *peer) (uint64, error) { // Check if a common ancestor was found finished = true for i := len(headers) - 1; i >= 0; i-- { - if d.hasBlock(headers[i].Hash()) { + if (d.mode != LightSync && d.hasBlock(headers[i].Hash())) || (d.mode == LightSync && d.hasHeader(headers[i].Hash())) { number, hash = headers[i].Number.Uint64(), headers[i].Hash() break } @@ -971,14 +1042,14 @@ func (d *Downloader) findAncestor(p *peer) (uint64, error) { case <-d.cancelCh: return 0, errCancelHashFetch - case headerPack := <-d.headerCh: + case packer := <-d.headerCh: // Discard anything not from the origin peer - if headerPack.peerId != p.id { - glog.V(logger.Debug).Infof("Received headers from incorrect peer(%s)", headerPack.peerId) + if packer.PeerId() != p.id { + glog.V(logger.Debug).Infof("Received headers from incorrect peer(%s)", packer.PeerId()) break } // Make sure the peer actually gave something valid - headers := headerPack.headers + headers := packer.(*headerPack).headers if len(headers) != 1 { glog.V(logger.Debug).Infof("%v: invalid search header set (%d)", p, len(headers)) return 0, errBadPeer @@ -986,13 +1057,13 @@ func (d *Downloader) findAncestor(p *peer) (uint64, error) { arrived = true // Modify the search interval based on the response - block := d.getBlock(headers[0].Hash()) - if block == nil { + if (d.mode == FullSync && !d.hasBlock(headers[0].Hash())) || (d.mode != FullSync && !d.hasHeader(headers[0].Hash())) { end = check break } - if block.NumberU64() != check { - glog.V(logger.Debug).Infof("%v: non requested header #%d [%x…], instead of #%d", p, block.NumberU64(), block.Hash().Bytes()[:4], check) + header := d.getHeader(headers[0].Hash()) // Independent of sync mode, header surely exists + if header.Number.Uint64() != check { + glog.V(logger.Debug).Infof("%v: non requested header #%d [%x…], instead of #%d", p, header.Number, header.Hash().Bytes()[:4], check) return 0, errBadPeer } start = check @@ -1017,10 +1088,37 @@ func (d *Downloader) findAncestor(p *peer) (uint64, error) { // fetchHeaders keeps retrieving headers from the requested number, until no more // are returned, potentially throttling on the way. +// +// The queue parameter can be used to switch between queuing headers for block +// body download too, or directly import as pure header chains. func (d *Downloader) fetchHeaders(p *peer, td *big.Int, from uint64) error { glog.V(logger.Debug).Infof("%v: downloading headers from #%d", p, from) defer glog.V(logger.Debug).Infof("%v: header download terminated", p) + // Calculate the pivoting point for switching from fast to slow sync + pivot := d.queue.FastSyncPivot() + + // Keep a count of uncertain headers to roll back + rollback := []*types.Header{} + defer func() { + if len(rollback) > 0 { + // Flatten the headers and roll them back + hashes := make([]common.Hash, len(rollback)) + for i, header := range rollback { + hashes[i] = header.Hash() + } + lh, lfb, lb := d.headHeader().Number, d.headFastBlock().Number(), d.headBlock().Number() + d.rollback(hashes) + glog.V(logger.Warn).Infof("Rolled back %d headers (LH: %d->%d, FB: %d->%d, LB: %d->%d)", + len(hashes), lh, d.headHeader().Number, lfb, d.headFastBlock().Number(), lb, d.headBlock().Number()) + + // If we're already past the pivot point, this could be an attack, disable fast sync + if rollback[len(rollback)-1].Number.Uint64() > pivot { + d.noFast = true + } + } + }() + // Create a timeout timer, and the associated hash fetcher request := time.Now() // time of the last fetch request timeout := time.NewTimer(0) // timer to dump a non-responsive active peer @@ -1049,22 +1147,24 @@ func (d *Downloader) fetchHeaders(p *peer, td *big.Int, from uint64) error { case <-d.blockCh: // Out of bounds eth/61 blocks received, ignore them - case headerPack := <-d.headerCh: + case packet := <-d.headerCh: // Make sure the active peer is giving us the headers - if headerPack.peerId != p.id { - glog.V(logger.Debug).Infof("Received headers from incorrect peer (%s)", headerPack.peerId) + if packet.PeerId() != p.id { + glog.V(logger.Debug).Infof("Received headers from incorrect peer (%s)", packet.PeerId()) break } headerReqTimer.UpdateSince(request) timeout.Stop() - // If no more headers are inbound, notify the body fetcher and return - if len(headerPack.headers) == 0 { + // If no more headers are inbound, notify the content fetchers and return + if packet.Items() == 0 { glog.V(logger.Debug).Infof("%v: no available headers", p) - select { - case d.wakeCh <- false: - case <-d.cancelCh: + for _, ch := range []chan bool{d.bodyWakeCh, d.receiptWakeCh, d.stateWakeCh} { + select { + case ch <- false: + case <-d.cancelCh: + } } // If no headers were retrieved at all, the peer violated it's TD promise that it had a // better chain compared to ours. The only exception is if it's promised blocks were @@ -1081,35 +1181,77 @@ func (d *Downloader) fetchHeaders(p *peer, td *big.Int, from uint64) error { if !gotHeaders && td.Cmp(d.getTd(d.headBlock().Hash())) > 0 { return errStallingPeer } + // If fast or light syncing, ensure promised headers are indeed delivered. This is + // needed to detect scenarios where an attacker feeds a bad pivot and then bails out + // of delivering the post-pivot blocks that would flag the invalid content. + // + // This check cannot be executed "as is" for full imports, since blocks may still be + // queued for processing when the header download completes. However, as long as the + // peer gave us something useful, we're already happy/progressed (above check). + if d.mode == FastSync || d.mode == LightSync { + if td.Cmp(d.getTd(d.headHeader().Hash())) > 0 { + return errStallingPeer + } + } + rollback = nil return nil } gotHeaders = true + headers := packet.(*headerPack).headers // Otherwise insert all the new headers, aborting in case of junk - glog.V(logger.Detail).Infof("%v: inserting %d headers from #%d", p, len(headerPack.headers), from) - - inserts := d.queue.Insert(headerPack.headers, from) - if len(inserts) != len(headerPack.headers) { - glog.V(logger.Debug).Infof("%v: stale headers", p) - return errBadPeer + glog.V(logger.Detail).Infof("%v: schedule %d headers from #%d", p, len(headers), from) + + if d.mode == FastSync || d.mode == LightSync { + // Collect the yet unknown headers to mark them as uncertain + unknown := make([]*types.Header, 0, len(headers)) + for _, header := range headers { + if !d.hasHeader(header.Hash()) { + unknown = append(unknown, header) + } + } + // If we're importing pure headers, verify based on their recentness + frequency := fsHeaderCheckFrequency + if headers[len(headers)-1].Number.Uint64()+uint64(fsHeaderForceVerify) > pivot { + frequency = 1 + } + if n, err := d.insertHeaders(headers, frequency); err != nil { + glog.V(logger.Debug).Infof("%v: invalid header #%d [%x…]: %v", p, headers[n].Number, headers[n].Hash().Bytes()[:4], err) + return errInvalidChain + } + // All verifications passed, store newly found uncertain headers + rollback = append(rollback, unknown...) + if len(rollback) > fsHeaderSafetyNet { + rollback = append(rollback[:0], rollback[len(rollback)-fsHeaderSafetyNet:]...) + } } - // Notify the block fetcher of new headers, but stop if queue is full - if d.queue.Pending() < maxQueuedHeaders { - // We still have headers to fetch, send continuation wake signal (potential) - select { - case d.wakeCh <- true: - default: + if d.mode == FullSync || d.mode == FastSync { + inserts := d.queue.Schedule(headers, from) + if len(inserts) != len(headers) { + glog.V(logger.Debug).Infof("%v: stale headers", p) + return errBadPeer } - } else { - // Header limit reached, send a termination wake signal (enforced) - select { - case d.wakeCh <- false: - case <-d.cancelCh: + } + // Notify the content fetchers of new headers, but stop if queue is full + cont := d.queue.PendingBlocks() < maxQueuedHeaders || d.queue.PendingReceipts() < maxQueuedHeaders + for _, ch := range []chan bool{d.bodyWakeCh, d.receiptWakeCh, d.stateWakeCh} { + if cont { + // We still have headers to fetch, send continuation wake signal (potential) + select { + case ch <- true: + default: + } + } else { + // Header limit reached, send a termination wake signal (enforced) + select { + case ch <- false: + case <-d.cancelCh: + } + return nil } - return nil } // Queue not yet full, fetch the next batch - from += uint64(len(headerPack.headers)) + from += uint64(len(headers)) getHeaders(from) case <-timeout.C: @@ -1119,9 +1261,11 @@ func (d *Downloader) fetchHeaders(p *peer, td *big.Int, from uint64) error { d.dropPeer(p.id) // Finish the sync gracefully instead of dumping the gathered data though - select { - case d.wakeCh <- false: - case <-d.cancelCh: + for _, ch := range []chan bool{d.bodyWakeCh, d.receiptWakeCh, d.stateWakeCh} { + select { + case ch <- false: + case <-d.cancelCh: + } } return nil } @@ -1133,22 +1277,119 @@ func (d *Downloader) fetchHeaders(p *peer, td *big.Int, from uint64) error { // and also periodically checking for timeouts. func (d *Downloader) fetchBodies(from uint64) error { glog.V(logger.Debug).Infof("Downloading block bodies from #%d", from) - defer glog.V(logger.Debug).Infof("Block body download terminated") - // Create a timeout timer for scheduling expiration tasks + var ( + deliver = func(packet dataPack) error { + pack := packet.(*bodyPack) + return d.queue.DeliverBodies(pack.peerId, pack.transactions, pack.uncles) + } + expire = func() []string { return d.queue.ExpireBodies(bodyHardTTL) } + fetch = func(p *peer, req *fetchRequest) error { return p.FetchBodies(req) } + capacity = func(p *peer) int { return p.BlockCapacity() } + setIdle = func(p *peer) { p.SetBodiesIdle() } + ) + err := d.fetchParts(errCancelBodyFetch, d.bodyCh, deliver, d.bodyWakeCh, expire, + d.queue.PendingBlocks, d.queue.InFlightBlocks, d.queue.ShouldThrottleBlocks, d.queue.ReserveBodies, + d.bodyFetchHook, fetch, d.queue.CancelBodies, capacity, d.peers.BodyIdlePeers, setIdle, "Body") + + glog.V(logger.Debug).Infof("Block body download terminated: %v", err) + return err +} + +// fetchReceipts iteratively downloads the scheduled block receipts, taking any +// available peers, reserving a chunk of receipts for each, waiting for delivery +// and also periodically checking for timeouts. +func (d *Downloader) fetchReceipts(from uint64) error { + glog.V(logger.Debug).Infof("Downloading receipts from #%d", from) + + var ( + deliver = func(packet dataPack) error { + pack := packet.(*receiptPack) + return d.queue.DeliverReceipts(pack.peerId, pack.receipts) + } + expire = func() []string { return d.queue.ExpireReceipts(receiptHardTTL) } + fetch = func(p *peer, req *fetchRequest) error { return p.FetchReceipts(req) } + capacity = func(p *peer) int { return p.ReceiptCapacity() } + setIdle = func(p *peer) { p.SetReceiptsIdle() } + ) + err := d.fetchParts(errCancelReceiptFetch, d.receiptCh, deliver, d.receiptWakeCh, expire, + d.queue.PendingReceipts, d.queue.InFlightReceipts, d.queue.ShouldThrottleReceipts, d.queue.ReserveReceipts, + d.receiptFetchHook, fetch, d.queue.CancelReceipts, capacity, d.peers.ReceiptIdlePeers, setIdle, "Receipt") + + glog.V(logger.Debug).Infof("Receipt download terminated: %v", err) + return err +} + +// fetchNodeData iteratively downloads the scheduled state trie nodes, taking any +// available peers, reserving a chunk of nodes for each, waiting for delivery and +// also periodically checking for timeouts. +func (d *Downloader) fetchNodeData() error { + glog.V(logger.Debug).Infof("Downloading node state data") + + var ( + deliver = func(packet dataPack) error { + start := time.Now() + return d.queue.DeliverNodeData(packet.PeerId(), packet.(*statePack).states, func(err error, delivered int) { + if err != nil { + // If the node data processing failed, the root hash is very wrong, abort + glog.V(logger.Error).Infof("peer %d: state processing failed: %v", packet.PeerId(), err) + d.cancel() + return + } + // Processing succeeded, notify state fetcher and processor of continuation + if d.queue.PendingNodeData() == 0 { + go d.process() + } else { + select { + case d.stateWakeCh <- true: + default: + } + } + // Log a message to the user and return + d.syncStatsLock.Lock() + defer d.syncStatsLock.Unlock() + + d.syncStatsStateDone += uint64(delivered) + glog.V(logger.Info).Infof("imported %d state entries in %v: processed %d in total", delivered, time.Since(start), d.syncStatsStateDone) + }) + } + expire = func() []string { return d.queue.ExpireNodeData(stateHardTTL) } + throttle = func() bool { return false } + reserve = func(p *peer, count int) (*fetchRequest, bool, error) { + return d.queue.ReserveNodeData(p, count), false, nil + } + fetch = func(p *peer, req *fetchRequest) error { return p.FetchNodeData(req) } + capacity = func(p *peer) int { return p.NodeDataCapacity() } + setIdle = func(p *peer) { p.SetNodeDataIdle() } + ) + err := d.fetchParts(errCancelStateFetch, d.stateCh, deliver, d.stateWakeCh, expire, + d.queue.PendingNodeData, d.queue.InFlightNodeData, throttle, reserve, nil, fetch, + d.queue.CancelNodeData, capacity, d.peers.NodeDataIdlePeers, setIdle, "State") + + glog.V(logger.Debug).Infof("Node state data download terminated: %v", err) + return err +} + +// fetchParts iteratively downloads scheduled block parts, taking any available +// peers, reserving a chunk of fetch requests for each, waiting for delivery and +// also periodically checking for timeouts. +func (d *Downloader) fetchParts(errCancel error, deliveryCh chan dataPack, deliver func(packet dataPack) error, wakeCh chan bool, + expire func() []string, pending func() int, inFlight func() bool, throttle func() bool, reserve func(*peer, int) (*fetchRequest, bool, error), + fetchHook func([]*types.Header), fetch func(*peer, *fetchRequest) error, cancel func(*fetchRequest), capacity func(*peer) int, + idle func() ([]*peer, int), setIdle func(*peer), kind string) error { + + // Create a ticker to detect expired retrieval tasks ticker := time.NewTicker(100 * time.Millisecond) defer ticker.Stop() update := make(chan struct{}, 1) - // Prepare the queue and fetch block bodies until the block header fetcher's done - d.queue.Prepare(from) + // Prepare the queue and fetch block parts until the block header fetcher's done finished := false - for { select { case <-d.cancelCh: - return errCancelBlockFetch + return errCancel case <-d.hashCh: // Out of bounds eth/61 hashes received, ignore them @@ -1156,42 +1397,36 @@ func (d *Downloader) fetchBodies(from uint64) error { case <-d.blockCh: // Out of bounds eth/61 blocks received, ignore them - case bodyPack := <-d.bodyCh: + case packet := <-deliveryCh: // If the peer was previously banned and failed to deliver it's pack // in a reasonable time frame, ignore it's message. - if peer := d.peers.Peer(bodyPack.peerId); peer != nil { - // Deliver the received chunk of bodies, and demote in case of errors - err := d.queue.Deliver(bodyPack.peerId, bodyPack.transactions, bodyPack.uncles) - switch err { + if peer := d.peers.Peer(packet.PeerId()); peer != nil { + // Deliver the received chunk of data, and demote in case of errors + switch err := deliver(packet); err { case nil: - // If no blocks were delivered, demote the peer (need the delivery above) - if len(bodyPack.transactions) == 0 || len(bodyPack.uncles) == 0 { + // If no blocks were delivered, demote the peer (need the delivery above to clean internal queue!) + if packet.Items() == 0 { peer.Demote() - peer.SetIdle() - glog.V(logger.Detail).Infof("%s: no block bodies delivered", peer) + setIdle(peer) + glog.V(logger.Detail).Infof("%s: no %s delivered", peer, strings.ToLower(kind)) break } // All was successful, promote the peer and potentially start processing peer.Promote() - peer.SetIdle() - glog.V(logger.Detail).Infof("%s: delivered %d:%d block bodies", peer, len(bodyPack.transactions), len(bodyPack.uncles)) + setIdle(peer) + glog.V(logger.Detail).Infof("%s: delivered %s %s(s)", peer, packet.Stats(), strings.ToLower(kind)) go d.process() case errInvalidChain: // The hash chain is invalid (blocks are not ordered properly), abort return err - case errInvalidBody: - // The peer delivered something very bad, drop immediately - glog.V(logger.Error).Infof("%s: delivered invalid block, dropping", peer) - d.dropPeer(peer.id) - case errNoFetchesPending: // Peer probably timed out with its delivery but came through // in the end, demote, but allow to to pull from this peer. peer.Demote() - peer.SetIdle() - glog.V(logger.Detail).Infof("%s: out of bound delivery", peer) + setIdle(peer) + glog.V(logger.Detail).Infof("%s: out of bound %s delivery", peer, strings.ToLower(kind)) case errStaleDelivery: // Delivered something completely else than requested, usually @@ -1199,13 +1434,13 @@ func (d *Downloader) fetchBodies(from uint64) error { // Don't set it to idle as the original request should still be // in flight. peer.Demote() - glog.V(logger.Detail).Infof("%s: stale delivery", peer) + glog.V(logger.Detail).Infof("%s: %s stale delivery", peer, strings.ToLower(kind)) default: // Peer did something semi-useful, demote but keep it around peer.Demote() - peer.SetIdle() - glog.V(logger.Detail).Infof("%s: delivery partially failed: %v", peer, err) + setIdle(peer) + glog.V(logger.Detail).Infof("%s: %s delivery partially failed: %v", peer, strings.ToLower(kind), err) go d.process() } } @@ -1215,7 +1450,7 @@ func (d *Downloader) fetchBodies(from uint64) error { default: } - case cont := <-d.wakeCh: + case cont := <-wakeCh: // The header fetcher sent a continuation flag, check if it's done if !cont { finished = true @@ -1238,65 +1473,80 @@ func (d *Downloader) fetchBodies(from uint64) error { if d.peers.Len() == 0 { return errNoPeers } - // Check for block body request timeouts and demote the responsible peers - for _, pid := range d.queue.Expire(bodyHardTTL) { + // Check for fetch request timeouts and demote the responsible peers + for _, pid := range expire() { if peer := d.peers.Peer(pid); peer != nil { peer.Demote() - glog.V(logger.Detail).Infof("%s: block body delivery timeout", peer) + setIdle(peer) + glog.V(logger.Detail).Infof("%s: %s delivery timeout", peer, strings.ToLower(kind)) } } - // If there's noting more to fetch, wait or terminate - if d.queue.Pending() == 0 { - if d.queue.InFlight() == 0 && finished { - glog.V(logger.Debug).Infof("Block body fetching completed") + // If there's nothing more to fetch, wait or terminate + if pending() == 0 { + if !inFlight() && finished { + glog.V(logger.Debug).Infof("%s fetching completed", kind) return nil } break } // Send a download request to all idle peers, until throttled - queuedEmptyBlocks, throttled := false, false - for _, peer := range d.peers.IdlePeers() { + progressed, throttled, running := false, false, inFlight() + idles, total := idle() + + for _, peer := range idles { // Short circuit if throttling activated - if d.queue.Throttle() { + if throttle() { throttled = true break } - // Reserve a chunk of hashes for a peer. A nil can mean either that - // no more hashes are available, or that the peer is known not to + // Reserve a chunk of fetches for a peer. A nil can mean either that + // no more headers are available, or that the peer is known not to // have them. - request, process, err := d.queue.Reserve(peer, peer.Capacity()) + request, progress, err := reserve(peer, capacity(peer)) if err != nil { return err } - if process { - queuedEmptyBlocks = true + if progress { + progressed = true go d.process() } if request == nil { continue } if glog.V(logger.Detail) { - glog.Infof("%s: requesting %d block bodies", peer, len(request.Headers)) + if len(request.Headers) > 0 { + glog.Infof("%s: requesting %d %s(s), first at #%d", peer, len(request.Headers), strings.ToLower(kind), request.Headers[0].Number) + } else { + glog.Infof("%s: requesting %d %s(s)", peer, len(request.Hashes), strings.ToLower(kind)) + } } // Fetch the chunk and make sure any errors return the hashes to the queue - if d.bodyFetchHook != nil { - d.bodyFetchHook(request.Headers) + if fetchHook != nil { + fetchHook(request.Headers) } - if err := peer.Fetch(request); err != nil { - glog.V(logger.Error).Infof("%v: fetch failed, rescheduling", peer) - d.queue.Cancel(request) + if err := fetch(peer, request); err != nil { + // Although we could try and make an attempt to fix this, this error really + // means that we've double allocated a fetch task to a peer. If that is the + // case, the internal state of the downloader and the queue is very wrong so + // better hard crash and note the error instead of silently accumulating into + // a much bigger issue. + panic(fmt.Sprintf("%v: %s fetch assignment failed, hard panic", peer, strings.ToLower(kind))) + cancel(request) // noop for now } + running = true } // Make sure that we have peers available for fetching. If all peers have been tried // and all failed throw an error - if !queuedEmptyBlocks && !throttled && d.queue.InFlight() == 0 { + if !progressed && !throttled && !running && len(idles) == total && pending() > 0 { return errPeersUnavailable } } } } -// process takes blocks from the queue and tries to import them into the chain. +// process takes fetch results from the queue and tries to import them into the +// chain. The type of import operation will depend on the result contents: +// - // // The algorithmic flow is as follows: // - The `processing` flag is swapped to 1 to ensure singleton access @@ -1317,10 +1567,10 @@ func (d *Downloader) process() { } // If the processor just exited, but there are freshly pending items, try to // reenter. This is needed because the goroutine spinned up for processing - // the fresh blocks might have been rejected entry to to this present thread + // the fresh results might have been rejected entry to to this present thread // not yet releasing the `processing` state. defer func() { - if atomic.LoadInt32(&d.interrupt) == 0 && d.queue.GetHeadBlock() != nil { + if atomic.LoadInt32(&d.interrupt) == 0 && d.queue.GetHeadResult() != nil { d.process() } }() @@ -1328,134 +1578,111 @@ func (d *Downloader) process() { // the import statistics to zero. defer atomic.StoreInt32(&d.processing, 0) - // Repeat the processing as long as there are blocks to import + // Repeat the processing as long as there are results to process for { - // Fetch the next batch of blocks - blocks := d.queue.TakeBlocks() - if len(blocks) == 0 { + // Fetch the next batch of results + pivot := d.queue.FastSyncPivot() // Fetch pivot before results to prevent reset race + results := d.queue.TakeResults() + if len(results) == 0 { return } if d.chainInsertHook != nil { - d.chainInsertHook(blocks) + d.chainInsertHook(results) } // Actually import the blocks - glog.V(logger.Debug).Infof("Inserting chain with %d blocks (#%v - #%v)\n", len(blocks), blocks[0].RawBlock.Number(), blocks[len(blocks)-1].RawBlock.Number()) - for len(blocks) != 0 { + if glog.V(logger.Debug) { + first, last := results[0].Header, results[len(results)-1].Header + glog.Infof("Inserting chain with %d items (#%d [%x…] - #%d [%x…])", len(results), first.Number, first.Hash().Bytes()[:4], last.Number, last.Hash().Bytes()[:4]) + } + for len(results) != 0 { // Check for any termination requests if atomic.LoadInt32(&d.interrupt) == 1 { return } - // Retrieve the first batch of blocks to insert - max := int(math.Min(float64(len(blocks)), float64(maxBlockProcess))) - raw := make(types.Blocks, 0, max) - for _, block := range blocks[:max] { - raw = append(raw, block.RawBlock) + // Retrieve the a batch of results to import + var ( + blocks = make([]*types.Block, 0, maxResultsProcess) + receipts = make([]types.Receipts, 0, maxResultsProcess) + ) + items := int(math.Min(float64(len(results)), float64(maxResultsProcess))) + for _, result := range results[:items] { + switch { + case d.mode == FullSync: + blocks = append(blocks, types.NewBlockWithHeader(result.Header).WithBody(result.Transactions, result.Uncles)) + case d.mode == FastSync: + blocks = append(blocks, types.NewBlockWithHeader(result.Header).WithBody(result.Transactions, result.Uncles)) + if result.Header.Number.Uint64() <= pivot { + receipts = append(receipts, result.Receipts) + } + } + } + // Try to process the results, aborting if there's an error + var ( + err error + index int + ) + switch { + case len(receipts) > 0: + index, err = d.insertReceipts(blocks, receipts) + if err == nil && blocks[len(blocks)-1].NumberU64() == pivot { + glog.V(logger.Debug).Infof("Committing block #%d [%x…] as the new head", blocks[len(blocks)-1].Number(), blocks[len(blocks)-1].Hash().Bytes()[:4]) + index, err = len(blocks)-1, d.commitHeadBlock(blocks[len(blocks)-1].Hash()) + } + default: + index, err = d.insertBlocks(blocks) } - // Try to inset the blocks, drop the originating peer if there's an error - index, err := d.insertChain(raw) if err != nil { - glog.V(logger.Debug).Infof("Block #%d import failed: %v", raw[index].NumberU64(), err) - d.dropPeer(blocks[index].OriginPeer) + glog.V(logger.Debug).Infof("Result #%d [%x…] processing failed: %v", results[index].Header.Number, results[index].Header.Hash().Bytes()[:4], err) d.cancel() return } - blocks = blocks[max:] + // Shift the results to the next batch + results = results[items:] } } } -// DeliverHashes61 injects a new batch of hashes received from a remote node into +// DeliverHashes injects a new batch of hashes received from a remote node into // the download schedule. This is usually invoked through the BlockHashesMsg by // the protocol handler. -func (d *Downloader) DeliverHashes61(id string, hashes []common.Hash) (err error) { - // Update the delivery metrics for both good and failed deliveries - hashInMeter.Mark(int64(len(hashes))) - defer func() { - if err != nil { - hashDropMeter.Mark(int64(len(hashes))) - } - }() - // Make sure the downloader is active - if atomic.LoadInt32(&d.synchronising) == 0 { - return errNoSyncActive - } - // Deliver or abort if the sync is canceled while queuing - d.cancelLock.RLock() - cancel := d.cancelCh - d.cancelLock.RUnlock() - - select { - case d.hashCh <- hashPack{id, hashes}: - return nil - - case <-cancel: - return errNoSyncActive - } +func (d *Downloader) DeliverHashes(id string, hashes []common.Hash) (err error) { + return d.deliver(id, d.hashCh, &hashPack{id, hashes}, hashInMeter, hashDropMeter) } -// DeliverBlocks61 injects a new batch of blocks received from a remote node. +// DeliverBlocks injects a new batch of blocks received from a remote node. // This is usually invoked through the BlocksMsg by the protocol handler. -func (d *Downloader) DeliverBlocks61(id string, blocks []*types.Block) (err error) { - // Update the delivery metrics for both good and failed deliveries - blockInMeter.Mark(int64(len(blocks))) - defer func() { - if err != nil { - blockDropMeter.Mark(int64(len(blocks))) - } - }() - // Make sure the downloader is active - if atomic.LoadInt32(&d.synchronising) == 0 { - return errNoSyncActive - } - // Deliver or abort if the sync is canceled while queuing - d.cancelLock.RLock() - cancel := d.cancelCh - d.cancelLock.RUnlock() - - select { - case d.blockCh <- blockPack{id, blocks}: - return nil - - case <-cancel: - return errNoSyncActive - } +func (d *Downloader) DeliverBlocks(id string, blocks []*types.Block) (err error) { + return d.deliver(id, d.blockCh, &blockPack{id, blocks}, blockInMeter, blockDropMeter) } // DeliverHeaders injects a new batch of blck headers received from a remote // node into the download schedule. func (d *Downloader) DeliverHeaders(id string, headers []*types.Header) (err error) { - // Update the delivery metrics for both good and failed deliveries - headerInMeter.Mark(int64(len(headers))) - defer func() { - if err != nil { - headerDropMeter.Mark(int64(len(headers))) - } - }() - // Make sure the downloader is active - if atomic.LoadInt32(&d.synchronising) == 0 { - return errNoSyncActive - } - // Deliver or abort if the sync is canceled while queuing - d.cancelLock.RLock() - cancel := d.cancelCh - d.cancelLock.RUnlock() - - select { - case d.headerCh <- headerPack{id, headers}: - return nil - - case <-cancel: - return errNoSyncActive - } + return d.deliver(id, d.headerCh, &headerPack{id, headers}, headerInMeter, headerDropMeter) } // DeliverBodies injects a new batch of block bodies received from a remote node. func (d *Downloader) DeliverBodies(id string, transactions [][]*types.Transaction, uncles [][]*types.Header) (err error) { + return d.deliver(id, d.bodyCh, &bodyPack{id, transactions, uncles}, bodyInMeter, bodyDropMeter) +} + +// DeliverReceipts injects a new batch of receipts received from a remote node. +func (d *Downloader) DeliverReceipts(id string, receipts [][]*types.Receipt) (err error) { + return d.deliver(id, d.receiptCh, &receiptPack{id, receipts}, receiptInMeter, receiptDropMeter) +} + +// DeliverNodeData injects a new batch of node state data received from a remote node. +func (d *Downloader) DeliverNodeData(id string, data [][]byte) (err error) { + return d.deliver(id, d.stateCh, &statePack{id, data}, stateInMeter, stateDropMeter) +} + +// deliver injects a new batch of data received from a remote node. +func (d *Downloader) deliver(id string, destCh chan dataPack, packet dataPack, inMeter, dropMeter metrics.Meter) (err error) { // Update the delivery metrics for both good and failed deliveries - bodyInMeter.Mark(int64(len(transactions))) + inMeter.Mark(int64(packet.Items())) defer func() { if err != nil { - bodyDropMeter.Mark(int64(len(transactions))) + dropMeter.Mark(int64(packet.Items())) } }() // Make sure the downloader is active @@ -1468,7 +1695,7 @@ func (d *Downloader) DeliverBodies(id string, transactions [][]*types.Transactio d.cancelLock.RUnlock() select { - case d.bodyCh <- bodyPack{id, transactions, uncles}: + case destCh <- packet: return nil case <-cancel: diff --git a/eth/downloader/downloader_test.go b/eth/downloader/downloader_test.go index 885fab8bd..ef6f74a6b 100644 --- a/eth/downloader/downloader_test.go +++ b/eth/downloader/downloader_test.go @@ -27,11 +27,13 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/trie" ) var ( @@ -45,8 +47,9 @@ var ( // the returned hash chain is ordered head->parent. In addition, every 3rd block // contains a transaction and every 5th an uncle to allow testing correct block // reassembly. -func makeChain(n int, seed byte, parent *types.Block) ([]common.Hash, map[common.Hash]*types.Block) { - blocks := core.GenerateChain(parent, testdb, n, func(i int, block *core.BlockGen) { +func makeChain(n int, seed byte, parent *types.Block, parentReceipts types.Receipts) ([]common.Hash, map[common.Hash]*types.Header, map[common.Hash]*types.Block, map[common.Hash]types.Receipts) { + // Generate the block chain + blocks, receipts := core.GenerateChain(parent, testdb, n, func(i int, block *core.BlockGen) { block.SetCoinbase(common.Address{seed}) // If the block number is multiple of 3, send a bonus transaction to the miner @@ -62,44 +65,72 @@ func makeChain(n int, seed byte, parent *types.Block) ([]common.Hash, map[common block.AddUncle(&types.Header{ParentHash: block.PrevBlock(i - 1).Hash(), Number: big.NewInt(int64(i - 1))}) } }) + // Convert the block-chain into a hash-chain and header/block maps hashes := make([]common.Hash, n+1) hashes[len(hashes)-1] = parent.Hash() + + headerm := make(map[common.Hash]*types.Header, n+1) + headerm[parent.Hash()] = parent.Header() + blockm := make(map[common.Hash]*types.Block, n+1) blockm[parent.Hash()] = parent + + receiptm := make(map[common.Hash]types.Receipts, n+1) + receiptm[parent.Hash()] = parentReceipts + for i, b := range blocks { hashes[len(hashes)-i-2] = b.Hash() + headerm[b.Hash()] = b.Header() blockm[b.Hash()] = b + receiptm[b.Hash()] = receipts[i] } - return hashes, blockm + return hashes, headerm, blockm, receiptm } // makeChainFork creates two chains of length n, such that h1[:f] and // h2[:f] are different but have a common suffix of length n-f. -func makeChainFork(n, f int, parent *types.Block) (h1, h2 []common.Hash, b1, b2 map[common.Hash]*types.Block) { - // Create the common suffix. - h, b := makeChain(n-f, 0, parent) - // Create the forks. - h1, b1 = makeChain(f, 1, b[h[0]]) - h1 = append(h1, h[1:]...) - h2, b2 = makeChain(f, 2, b[h[0]]) - h2 = append(h2, h[1:]...) - for hash, block := range b { - b1[hash] = block - b2[hash] = block - } - return h1, h2, b1, b2 +func makeChainFork(n, f int, parent *types.Block, parentReceipts types.Receipts) ([]common.Hash, []common.Hash, map[common.Hash]*types.Header, map[common.Hash]*types.Header, map[common.Hash]*types.Block, map[common.Hash]*types.Block, map[common.Hash]types.Receipts, map[common.Hash]types.Receipts) { + // Create the common suffix + hashes, headers, blocks, receipts := makeChain(n-f, 0, parent, parentReceipts) + + // Create the forks + hashes1, headers1, blocks1, receipts1 := makeChain(f, 1, blocks[hashes[0]], receipts[hashes[0]]) + hashes1 = append(hashes1, hashes[1:]...) + + hashes2, headers2, blocks2, receipts2 := makeChain(f, 2, blocks[hashes[0]], receipts[hashes[0]]) + hashes2 = append(hashes2, hashes[1:]...) + + for hash, header := range headers { + headers1[hash] = header + headers2[hash] = header + } + for hash, block := range blocks { + blocks1[hash] = block + blocks2[hash] = block + } + for hash, receipt := range receipts { + receipts1[hash] = receipt + receipts2[hash] = receipt + } + return hashes1, hashes2, headers1, headers2, blocks1, blocks2, receipts1, receipts2 } // downloadTester is a test simulator for mocking out local block chain. type downloadTester struct { + stateDb ethdb.Database downloader *Downloader - ownHashes []common.Hash // Hash chain belonging to the tester - ownBlocks map[common.Hash]*types.Block // Blocks belonging to the tester - ownChainTd map[common.Hash]*big.Int // Total difficulties of the blocks in the local chain - peerHashes map[string][]common.Hash // Hash chain belonging to different test peers - peerBlocks map[string]map[common.Hash]*types.Block // Blocks belonging to different test peers - peerChainTds map[string]map[common.Hash]*big.Int // Total difficulties of the blocks in the peer chains + ownHashes []common.Hash // Hash chain belonging to the tester + ownHeaders map[common.Hash]*types.Header // Headers belonging to the tester + ownBlocks map[common.Hash]*types.Block // Blocks belonging to the tester + ownReceipts map[common.Hash]types.Receipts // Receipts belonging to the tester + ownChainTd map[common.Hash]*big.Int // Total difficulties of the blocks in the local chain + + peerHashes map[string][]common.Hash // Hash chain belonging to different test peers + peerHeaders map[string]map[common.Hash]*types.Header // Headers belonging to different test peers + peerBlocks map[string]map[common.Hash]*types.Block // Blocks belonging to different test peers + peerReceipts map[string]map[common.Hash]types.Receipts // Receipts belonging to different test peers + peerChainTds map[string]map[common.Hash]*big.Int // Total difficulties of the blocks in the peer chains lock sync.RWMutex } @@ -108,19 +139,26 @@ type downloadTester struct { func newTester() *downloadTester { tester := &downloadTester{ ownHashes: []common.Hash{genesis.Hash()}, + ownHeaders: map[common.Hash]*types.Header{genesis.Hash(): genesis.Header()}, ownBlocks: map[common.Hash]*types.Block{genesis.Hash(): genesis}, + ownReceipts: map[common.Hash]types.Receipts{genesis.Hash(): nil}, ownChainTd: map[common.Hash]*big.Int{genesis.Hash(): genesis.Difficulty()}, peerHashes: make(map[string][]common.Hash), + peerHeaders: make(map[string]map[common.Hash]*types.Header), peerBlocks: make(map[string]map[common.Hash]*types.Block), + peerReceipts: make(map[string]map[common.Hash]types.Receipts), peerChainTds: make(map[string]map[common.Hash]*big.Int), } - tester.downloader = New(new(event.TypeMux), tester.hasBlock, tester.getBlock, tester.headBlock, tester.getTd, tester.insertChain, tester.dropPeer) + tester.stateDb, _ = ethdb.NewMemDatabase() + tester.downloader = New(tester.stateDb, new(event.TypeMux), tester.hasHeader, tester.hasBlock, tester.getHeader, + tester.getBlock, tester.headHeader, tester.headBlock, tester.headFastBlock, tester.commitHeadBlock, tester.getTd, + tester.insertHeaders, tester.insertBlocks, tester.insertReceipts, tester.rollback, tester.dropPeer) return tester } // sync starts synchronizing with a remote peer, blocking until it completes. -func (dl *downloadTester) sync(id string, td *big.Int) error { +func (dl *downloadTester) sync(id string, td *big.Int, mode SyncMode) error { dl.lock.RLock() hash := dl.peerHashes[id][0] // If no particular TD was requested, load from the peer's blockchain @@ -132,11 +170,10 @@ func (dl *downloadTester) sync(id string, td *big.Int) error { } dl.lock.RUnlock() - err := dl.downloader.synchronise(id, hash, td) + err := dl.downloader.synchronise(id, hash, td, mode) for { // If the queue is empty and processing stopped, break - hashes, blocks := dl.downloader.queue.Size() - if hashes+blocks == 0 && atomic.LoadInt32(&dl.downloader.processing) == 0 { + if dl.downloader.queue.Idle() && atomic.LoadInt32(&dl.downloader.processing) == 0 { break } // Otherwise sleep a bit and retry @@ -145,12 +182,22 @@ func (dl *downloadTester) sync(id string, td *big.Int) error { return err } -// hasBlock checks if a block is pres ent in the testers canonical chain. +// hasHeader checks if a header is present in the testers canonical chain. +func (dl *downloadTester) hasHeader(hash common.Hash) bool { + return dl.getHeader(hash) != nil +} + +// hasBlock checks if a block is present in the testers canonical chain. func (dl *downloadTester) hasBlock(hash common.Hash) bool { + return dl.getBlock(hash) != nil +} + +// getHeader retrieves a header from the testers canonical chain. +func (dl *downloadTester) getHeader(hash common.Hash) *types.Header { dl.lock.RLock() defer dl.lock.RUnlock() - return dl.getBlock(hash) != nil + return dl.ownHeaders[hash] } // getBlock retrieves a block from the testers canonical chain. @@ -161,12 +208,55 @@ func (dl *downloadTester) getBlock(hash common.Hash) *types.Block { return dl.ownBlocks[hash] } +// headHeader retrieves the current head header from the canonical chain. +func (dl *downloadTester) headHeader() *types.Header { + dl.lock.RLock() + defer dl.lock.RUnlock() + + for i := len(dl.ownHashes) - 1; i >= 0; i-- { + if header := dl.ownHeaders[dl.ownHashes[i]]; header != nil { + return header + } + } + return genesis.Header() +} + // headBlock retrieves the current head block from the canonical chain. func (dl *downloadTester) headBlock() *types.Block { dl.lock.RLock() defer dl.lock.RUnlock() - return dl.getBlock(dl.ownHashes[len(dl.ownHashes)-1]) + for i := len(dl.ownHashes) - 1; i >= 0; i-- { + if block := dl.ownBlocks[dl.ownHashes[i]]; block != nil { + if _, err := dl.stateDb.Get(block.Root().Bytes()); err == nil { + return block + } + } + } + return genesis +} + +// headFastBlock retrieves the current head fast-sync block from the canonical chain. +func (dl *downloadTester) headFastBlock() *types.Block { + dl.lock.RLock() + defer dl.lock.RUnlock() + + for i := len(dl.ownHashes) - 1; i >= 0; i-- { + if block := dl.ownBlocks[dl.ownHashes[i]]; block != nil { + return block + } + } + return genesis +} + +// commitHeadBlock manually sets the head block to a given hash. +func (dl *downloadTester) commitHeadBlock(hash common.Hash) error { + // For now only check that the state trie is correct + if block := dl.getBlock(hash); block != nil { + _, err := trie.NewSecure(block.Root(), dl.stateDb) + return err + } + return fmt.Errorf("non existent block: %x", hash[:4]) } // getTd retrieves the block's total difficulty from the canonical chain. @@ -177,8 +267,37 @@ func (dl *downloadTester) getTd(hash common.Hash) *big.Int { return dl.ownChainTd[hash] } -// insertChain injects a new batch of blocks into the simulated chain. -func (dl *downloadTester) insertChain(blocks types.Blocks) (int, error) { +// insertHeaders injects a new batch of headers into the simulated chain. +func (dl *downloadTester) insertHeaders(headers []*types.Header, checkFreq int) (int, error) { + dl.lock.Lock() + defer dl.lock.Unlock() + + // Do a quick check, as the blockchain.InsertHeaderChain doesn't insert anthing in case of errors + if _, ok := dl.ownHeaders[headers[0].ParentHash]; !ok { + return 0, errors.New("unknown parent") + } + for i := 1; i < len(headers); i++ { + if headers[i].ParentHash != headers[i-1].Hash() { + return i, errors.New("unknown parent") + } + } + // Do a full insert if pre-checks passed + for i, header := range headers { + if _, ok := dl.ownHeaders[header.Hash()]; ok { + continue + } + if _, ok := dl.ownHeaders[header.ParentHash]; !ok { + return i, errors.New("unknown parent") + } + dl.ownHashes = append(dl.ownHashes, header.Hash()) + dl.ownHeaders[header.Hash()] = header + dl.ownChainTd[header.Hash()] = new(big.Int).Add(dl.ownChainTd[header.ParentHash], header.Difficulty) + } + return len(headers), nil +} + +// insertBlocks injects a new batch of blocks into the simulated chain. +func (dl *downloadTester) insertBlocks(blocks types.Blocks) (int, error) { dl.lock.Lock() defer dl.lock.Unlock() @@ -186,42 +305,112 @@ func (dl *downloadTester) insertChain(blocks types.Blocks) (int, error) { if _, ok := dl.ownBlocks[block.ParentHash()]; !ok { return i, errors.New("unknown parent") } - dl.ownHashes = append(dl.ownHashes, block.Hash()) + if _, ok := dl.ownHeaders[block.Hash()]; !ok { + dl.ownHashes = append(dl.ownHashes, block.Hash()) + dl.ownHeaders[block.Hash()] = block.Header() + } dl.ownBlocks[block.Hash()] = block - dl.ownChainTd[block.Hash()] = dl.ownChainTd[block.ParentHash()] + dl.stateDb.Put(block.Root().Bytes(), []byte{0x00}) + dl.ownChainTd[block.Hash()] = new(big.Int).Add(dl.ownChainTd[block.ParentHash()], block.Difficulty()) + } + return len(blocks), nil +} + +// insertReceipts injects a new batch of blocks into the simulated chain. +func (dl *downloadTester) insertReceipts(blocks types.Blocks, receipts []types.Receipts) (int, error) { + dl.lock.Lock() + defer dl.lock.Unlock() + + for i := 0; i < len(blocks) && i < len(receipts); i++ { + if _, ok := dl.ownHeaders[blocks[i].Hash()]; !ok { + return i, errors.New("unknown owner") + } + if _, ok := dl.ownBlocks[blocks[i].ParentHash()]; !ok { + return i, errors.New("unknown parent") + } + dl.ownBlocks[blocks[i].Hash()] = blocks[i] + dl.ownReceipts[blocks[i].Hash()] = receipts[i] } return len(blocks), nil } +// rollback removes some recently added elements from the chain. +func (dl *downloadTester) rollback(hashes []common.Hash) { + dl.lock.Lock() + defer dl.lock.Unlock() + + for i := len(hashes) - 1; i >= 0; i-- { + if dl.ownHashes[len(dl.ownHashes)-1] == hashes[i] { + dl.ownHashes = dl.ownHashes[:len(dl.ownHashes)-1] + } + delete(dl.ownChainTd, hashes[i]) + delete(dl.ownHeaders, hashes[i]) + delete(dl.ownReceipts, hashes[i]) + delete(dl.ownBlocks, hashes[i]) + } +} + // newPeer registers a new block download source into the downloader. -func (dl *downloadTester) newPeer(id string, version int, hashes []common.Hash, blocks map[common.Hash]*types.Block) error { - return dl.newSlowPeer(id, version, hashes, blocks, 0) +func (dl *downloadTester) newPeer(id string, version int, hashes []common.Hash, headers map[common.Hash]*types.Header, blocks map[common.Hash]*types.Block, receipts map[common.Hash]types.Receipts) error { + return dl.newSlowPeer(id, version, hashes, headers, blocks, receipts, 0) } // newSlowPeer registers a new block download source into the downloader, with a // specific delay time on processing the network packets sent to it, simulating // potentially slow network IO. -func (dl *downloadTester) newSlowPeer(id string, version int, hashes []common.Hash, blocks map[common.Hash]*types.Block, delay time.Duration) error { +func (dl *downloadTester) newSlowPeer(id string, version int, hashes []common.Hash, headers map[common.Hash]*types.Header, blocks map[common.Hash]*types.Block, receipts map[common.Hash]types.Receipts, delay time.Duration) error { dl.lock.Lock() defer dl.lock.Unlock() - err := dl.downloader.RegisterPeer(id, version, hashes[0], - dl.peerGetRelHashesFn(id, delay), dl.peerGetAbsHashesFn(id, delay), dl.peerGetBlocksFn(id, delay), - dl.peerGetRelHeadersFn(id, delay), dl.peerGetAbsHeadersFn(id, delay), dl.peerGetBodiesFn(id, delay)) + var err error + switch version { + case 61: + err = dl.downloader.RegisterPeer(id, version, hashes[0], dl.peerGetRelHashesFn(id, delay), dl.peerGetAbsHashesFn(id, delay), dl.peerGetBlocksFn(id, delay), nil, nil, nil, nil, nil) + case 62: + err = dl.downloader.RegisterPeer(id, version, hashes[0], nil, nil, nil, dl.peerGetRelHeadersFn(id, delay), dl.peerGetAbsHeadersFn(id, delay), dl.peerGetBodiesFn(id, delay), nil, nil) + case 63: + err = dl.downloader.RegisterPeer(id, version, hashes[0], nil, nil, nil, dl.peerGetRelHeadersFn(id, delay), dl.peerGetAbsHeadersFn(id, delay), dl.peerGetBodiesFn(id, delay), dl.peerGetReceiptsFn(id, delay), dl.peerGetNodeDataFn(id, delay)) + case 64: + err = dl.downloader.RegisterPeer(id, version, hashes[0], nil, nil, nil, dl.peerGetRelHeadersFn(id, delay), dl.peerGetAbsHeadersFn(id, delay), dl.peerGetBodiesFn(id, delay), dl.peerGetReceiptsFn(id, delay), dl.peerGetNodeDataFn(id, delay)) + } if err == nil { - // Assign the owned hashes and blocks to the peer (deep copy) + // Assign the owned hashes, headers and blocks to the peer (deep copy) dl.peerHashes[id] = make([]common.Hash, len(hashes)) copy(dl.peerHashes[id], hashes) + dl.peerHeaders[id] = make(map[common.Hash]*types.Header) dl.peerBlocks[id] = make(map[common.Hash]*types.Block) + dl.peerReceipts[id] = make(map[common.Hash]types.Receipts) dl.peerChainTds[id] = make(map[common.Hash]*big.Int) - for _, hash := range hashes { + + genesis := hashes[len(hashes)-1] + if header := headers[genesis]; header != nil { + dl.peerHeaders[id][genesis] = header + dl.peerChainTds[id][genesis] = header.Difficulty + } + if block := blocks[genesis]; block != nil { + dl.peerBlocks[id][genesis] = block + dl.peerChainTds[id][genesis] = block.Difficulty() + } + + for i := len(hashes) - 2; i >= 0; i-- { + hash := hashes[i] + + if header, ok := headers[hash]; ok { + dl.peerHeaders[id][hash] = header + if _, ok := dl.peerHeaders[id][header.ParentHash]; ok { + dl.peerChainTds[id][hash] = new(big.Int).Add(header.Difficulty, dl.peerChainTds[id][header.ParentHash]) + } + } if block, ok := blocks[hash]; ok { dl.peerBlocks[id][hash] = block - if parent, ok := dl.peerBlocks[id][block.ParentHash()]; ok { - dl.peerChainTds[id][hash] = new(big.Int).Add(block.Difficulty(), dl.peerChainTds[id][parent.Hash()]) + if _, ok := dl.peerBlocks[id][block.ParentHash()]; ok { + dl.peerChainTds[id][hash] = new(big.Int).Add(block.Difficulty(), dl.peerChainTds[id][block.ParentHash()]) } } + if receipt, ok := receipts[hash]; ok { + dl.peerReceipts[id][hash] = receipt + } } } return err @@ -233,6 +422,7 @@ func (dl *downloadTester) dropPeer(id string) { defer dl.lock.Unlock() delete(dl.peerHashes, id) + delete(dl.peerHeaders, id) delete(dl.peerBlocks, id) delete(dl.peerChainTds, id) @@ -265,7 +455,7 @@ func (dl *downloadTester) peerGetRelHashesFn(id string, delay time.Duration) fun // Delay delivery a bit to allow attacks to unfold go func() { time.Sleep(time.Millisecond) - dl.downloader.DeliverHashes61(id, result) + dl.downloader.DeliverHashes(id, result) }() return nil } @@ -290,7 +480,7 @@ func (dl *downloadTester) peerGetAbsHashesFn(id string, delay time.Duration) fun // Delay delivery a bit to allow attacks to unfold go func() { time.Sleep(time.Millisecond) - dl.downloader.DeliverHashes61(id, result) + dl.downloader.DeliverHashes(id, result) }() return nil } @@ -313,7 +503,7 @@ func (dl *downloadTester) peerGetBlocksFn(id string, delay time.Duration) func([ result = append(result, block) } } - go dl.downloader.DeliverBlocks61(id, result) + go dl.downloader.DeliverBlocks(id, result) return nil } @@ -350,13 +540,13 @@ func (dl *downloadTester) peerGetAbsHeadersFn(id string, delay time.Duration) fu dl.lock.RLock() defer dl.lock.RUnlock() - // Gather the next batch of hashes + // Gather the next batch of headers hashes := dl.peerHashes[id] - blocks := dl.peerBlocks[id] + headers := dl.peerHeaders[id] result := make([]*types.Header, 0, amount) for i := 0; i < amount && len(hashes)-int(origin)-1-i >= 0; i++ { - if block, ok := blocks[hashes[len(hashes)-int(origin)-1-i]]; ok { - result = append(result, block.Header()) + if header, ok := headers[hashes[len(hashes)-int(origin)-1-i]]; ok { + result = append(result, header) } } // Delay delivery a bit to allow attacks to unfold @@ -395,56 +585,163 @@ func (dl *downloadTester) peerGetBodiesFn(id string, delay time.Duration) func([ } } +// peerGetReceiptsFn constructs a getReceipts method associated with a particular +// peer in the download tester. The returned function can be used to retrieve +// batches of block receipts from the particularly requested peer. +func (dl *downloadTester) peerGetReceiptsFn(id string, delay time.Duration) func([]common.Hash) error { + return func(hashes []common.Hash) error { + time.Sleep(delay) + + dl.lock.RLock() + defer dl.lock.RUnlock() + + receipts := dl.peerReceipts[id] + + results := make([][]*types.Receipt, 0, len(hashes)) + for _, hash := range hashes { + if receipt, ok := receipts[hash]; ok { + results = append(results, receipt) + } + } + go dl.downloader.DeliverReceipts(id, results) + + return nil + } +} + +// peerGetNodeDataFn constructs a getNodeData method associated with a particular +// peer in the download tester. The returned function can be used to retrieve +// batches of node state data from the particularly requested peer. +func (dl *downloadTester) peerGetNodeDataFn(id string, delay time.Duration) func([]common.Hash) error { + return func(hashes []common.Hash) error { + time.Sleep(delay) + + dl.lock.RLock() + defer dl.lock.RUnlock() + + results := make([][]byte, 0, len(hashes)) + for _, hash := range hashes { + if data, err := testdb.Get(hash.Bytes()); err == nil { + results = append(results, data) + } + } + go dl.downloader.DeliverNodeData(id, results) + + return nil + } +} + +// assertOwnChain checks if the local chain contains the correct number of items +// of the various chain components. +func assertOwnChain(t *testing.T, tester *downloadTester, length int) { + assertOwnForkedChain(t, tester, 1, []int{length}) +} + +// assertOwnForkedChain checks if the local forked chain contains the correct +// number of items of the various chain components. +func assertOwnForkedChain(t *testing.T, tester *downloadTester, common int, lengths []int) { + // Initialize the counters for the first fork + headers, blocks := lengths[0], lengths[0] + + minReceipts, maxReceipts := lengths[0]-fsMinFullBlocks-fsPivotInterval, lengths[0]-fsMinFullBlocks + if minReceipts < 0 { + minReceipts = 1 + } + if maxReceipts < 0 { + maxReceipts = 1 + } + // Update the counters for each subsequent fork + for _, length := range lengths[1:] { + headers += length - common + blocks += length - common + + minReceipts += length - common - fsMinFullBlocks - fsPivotInterval + maxReceipts += length - common - fsMinFullBlocks + } + switch tester.downloader.mode { + case FullSync: + minReceipts, maxReceipts = 1, 1 + case LightSync: + blocks, minReceipts, maxReceipts = 1, 1, 1 + } + if hs := len(tester.ownHeaders); hs != headers { + t.Fatalf("synchronised headers mismatch: have %v, want %v", hs, headers) + } + if bs := len(tester.ownBlocks); bs != blocks { + t.Fatalf("synchronised blocks mismatch: have %v, want %v", bs, blocks) + } + if rs := len(tester.ownReceipts); rs < minReceipts || rs > maxReceipts { + t.Fatalf("synchronised receipts mismatch: have %v, want between [%v, %v]", rs, minReceipts, maxReceipts) + } + // Verify the state trie too for fast syncs + if tester.downloader.mode == FastSync { + index := 0 + if pivot := int(tester.downloader.queue.fastSyncPivot); pivot < common { + index = pivot + } else { + index = len(tester.ownHashes) - lengths[len(lengths)-1] + int(tester.downloader.queue.fastSyncPivot) + } + if index > 0 { + if statedb, err := state.New(tester.ownHeaders[tester.ownHashes[index]].Root, tester.stateDb); statedb == nil || err != nil { + t.Fatalf("state reconstruction failed: %v", err) + } + } + } +} + // Tests that simple synchronization against a canonical chain works correctly. // In this test common ancestor lookup should be short circuited and not require // binary searching. -func TestCanonicalSynchronisation61(t *testing.T) { testCanonicalSynchronisation(t, 61) } -func TestCanonicalSynchronisation62(t *testing.T) { testCanonicalSynchronisation(t, 62) } -func TestCanonicalSynchronisation63(t *testing.T) { testCanonicalSynchronisation(t, 63) } -func TestCanonicalSynchronisation64(t *testing.T) { testCanonicalSynchronisation(t, 64) } - -func testCanonicalSynchronisation(t *testing.T, protocol int) { +func TestCanonicalSynchronisation61(t *testing.T) { testCanonicalSynchronisation(t, 61, FullSync) } +func TestCanonicalSynchronisation62(t *testing.T) { testCanonicalSynchronisation(t, 62, FullSync) } +func TestCanonicalSynchronisation63Full(t *testing.T) { testCanonicalSynchronisation(t, 63, FullSync) } +func TestCanonicalSynchronisation63Fast(t *testing.T) { testCanonicalSynchronisation(t, 63, FastSync) } +func TestCanonicalSynchronisation64Full(t *testing.T) { testCanonicalSynchronisation(t, 64, FullSync) } +func TestCanonicalSynchronisation64Fast(t *testing.T) { testCanonicalSynchronisation(t, 64, FastSync) } +func TestCanonicalSynchronisation64Light(t *testing.T) { testCanonicalSynchronisation(t, 64, LightSync) } + +func testCanonicalSynchronisation(t *testing.T, protocol int, mode SyncMode) { // Create a small enough block chain to download targetBlocks := blockCacheLimit - 15 - hashes, blocks := makeChain(targetBlocks, 0, genesis) + hashes, headers, blocks, receipts := makeChain(targetBlocks, 0, genesis, nil) tester := newTester() - tester.newPeer("peer", protocol, hashes, blocks) + tester.newPeer("peer", protocol, hashes, headers, blocks, receipts) - // Synchronise with the peer and make sure all blocks were retrieved - if err := tester.sync("peer", nil); err != nil { + // Synchronise with the peer and make sure all relevant data was retrieved + if err := tester.sync("peer", nil, mode); err != nil { t.Fatalf("failed to synchronise blocks: %v", err) } - if imported := len(tester.ownBlocks); imported != targetBlocks+1 { - t.Fatalf("synchronised block mismatch: have %v, want %v", imported, targetBlocks+1) - } + assertOwnChain(t, tester, targetBlocks+1) } // Tests that if a large batch of blocks are being downloaded, it is throttled // until the cached blocks are retrieved. -func TestThrottling61(t *testing.T) { testThrottling(t, 61) } -func TestThrottling62(t *testing.T) { testThrottling(t, 62) } -func TestThrottling63(t *testing.T) { testThrottling(t, 63) } -func TestThrottling64(t *testing.T) { testThrottling(t, 64) } - -func testThrottling(t *testing.T, protocol int) { +func TestThrottling61(t *testing.T) { testThrottling(t, 61, FullSync) } +func TestThrottling62(t *testing.T) { testThrottling(t, 62, FullSync) } +func TestThrottling63Full(t *testing.T) { testThrottling(t, 63, FullSync) } +func TestThrottling63Fast(t *testing.T) { testThrottling(t, 63, FastSync) } +func TestThrottling64Full(t *testing.T) { testThrottling(t, 64, FullSync) } +func TestThrottling64Fast(t *testing.T) { testThrottling(t, 64, FastSync) } + +func testThrottling(t *testing.T, protocol int, mode SyncMode) { // Create a long block chain to download and the tester targetBlocks := 8 * blockCacheLimit - hashes, blocks := makeChain(targetBlocks, 0, genesis) + hashes, headers, blocks, receipts := makeChain(targetBlocks, 0, genesis, nil) tester := newTester() - tester.newPeer("peer", protocol, hashes, blocks) + tester.newPeer("peer", protocol, hashes, headers, blocks, receipts) // Wrap the importer to allow stepping blocked, proceed := uint32(0), make(chan struct{}) - tester.downloader.chainInsertHook = func(blocks []*Block) { - atomic.StoreUint32(&blocked, uint32(len(blocks))) + tester.downloader.chainInsertHook = func(results []*fetchResult) { + atomic.StoreUint32(&blocked, uint32(len(results))) <-proceed } // Start a synchronisation concurrently errc := make(chan error) go func() { - errc <- tester.sync("peer", nil) + errc <- tester.sync("peer", nil, mode) }() // Iteratively take some blocks, always checking the retrieval count for { @@ -456,22 +753,37 @@ func testThrottling(t *testing.T, protocol int) { break } // Wait a bit for sync to throttle itself - var cached int + var cached, frozen int for start := time.Now(); time.Since(start) < time.Second; { time.Sleep(25 * time.Millisecond) + tester.lock.RLock() tester.downloader.queue.lock.RLock() - cached = len(tester.downloader.queue.blockPool) + cached = len(tester.downloader.queue.blockDonePool) + if mode == FastSync { + if receipts := len(tester.downloader.queue.receiptDonePool); receipts < cached { + if tester.downloader.queue.resultCache[receipts].Header.Number.Uint64() < tester.downloader.queue.fastSyncPivot { + cached = receipts + } + } + } + frozen = int(atomic.LoadUint32(&blocked)) + retrieved = len(tester.ownBlocks) tester.downloader.queue.lock.RUnlock() + tester.lock.RUnlock() - if cached == blockCacheLimit || len(tester.ownBlocks)+cached+int(atomic.LoadUint32(&blocked)) == targetBlocks+1 { + if cached == blockCacheLimit || retrieved+cached+frozen == targetBlocks+1 { break } } // Make sure we filled up the cache, then exhaust it time.Sleep(25 * time.Millisecond) // give it a chance to screw up - if cached != blockCacheLimit && len(tester.ownBlocks)+cached+int(atomic.LoadUint32(&blocked)) != targetBlocks+1 { - t.Fatalf("block count mismatch: have %v, want %v (owned %v, target %v)", cached, blockCacheLimit, len(tester.ownBlocks), targetBlocks+1) + + tester.lock.RLock() + retrieved = len(tester.ownBlocks) + tester.lock.RUnlock() + if cached != blockCacheLimit && retrieved+cached+frozen != targetBlocks+1 { + t.Fatalf("block count mismatch: have %v, want %v (owned %v, blocked %v, target %v)", cached, blockCacheLimit, retrieved, frozen, targetBlocks+1) } // Permit the blocked blocks to import if atomic.LoadUint32(&blocked) > 0 { @@ -480,9 +792,7 @@ func testThrottling(t *testing.T, protocol int) { } } // Check that we haven't pulled more blocks than available - if len(tester.ownBlocks) > targetBlocks+1 { - t.Fatalf("target block count mismatch: have %v, want %v", len(tester.ownBlocks), targetBlocks+1) - } + assertOwnChain(t, tester, targetBlocks+1) if err := <-errc; err != nil { t.Fatalf("block synchronization failed: %v", err) } @@ -491,34 +801,34 @@ func testThrottling(t *testing.T, protocol int) { // Tests that simple synchronization against a forked chain works correctly. In // this test common ancestor lookup should *not* be short circuited, and a full // binary search should be executed. -func TestForkedSynchronisation61(t *testing.T) { testForkedSynchronisation(t, 61) } -func TestForkedSynchronisation62(t *testing.T) { testForkedSynchronisation(t, 62) } -func TestForkedSynchronisation63(t *testing.T) { testForkedSynchronisation(t, 63) } -func TestForkedSynchronisation64(t *testing.T) { testForkedSynchronisation(t, 64) } - -func testForkedSynchronisation(t *testing.T, protocol int) { +func TestForkedSynchronisation61(t *testing.T) { testForkedSynchronisation(t, 61, FullSync) } +func TestForkedSynchronisation62(t *testing.T) { testForkedSynchronisation(t, 62, FullSync) } +func TestForkedSynchronisation63Full(t *testing.T) { testForkedSynchronisation(t, 63, FullSync) } +func TestForkedSynchronisation63Fast(t *testing.T) { testForkedSynchronisation(t, 63, FastSync) } +func TestForkedSynchronisation64Full(t *testing.T) { testForkedSynchronisation(t, 64, FullSync) } +func TestForkedSynchronisation64Fast(t *testing.T) { testForkedSynchronisation(t, 64, FastSync) } +func TestForkedSynchronisation64Light(t *testing.T) { testForkedSynchronisation(t, 64, LightSync) } + +func testForkedSynchronisation(t *testing.T, protocol int, mode SyncMode) { // Create a long enough forked chain common, fork := MaxHashFetch, 2*MaxHashFetch - hashesA, hashesB, blocksA, blocksB := makeChainFork(common+fork, fork, genesis) + hashesA, hashesB, headersA, headersB, blocksA, blocksB, receiptsA, receiptsB := makeChainFork(common+fork, fork, genesis, nil) tester := newTester() - tester.newPeer("fork A", protocol, hashesA, blocksA) - tester.newPeer("fork B", protocol, hashesB, blocksB) + tester.newPeer("fork A", protocol, hashesA, headersA, blocksA, receiptsA) + tester.newPeer("fork B", protocol, hashesB, headersB, blocksB, receiptsB) // Synchronise with the peer and make sure all blocks were retrieved - if err := tester.sync("fork A", nil); err != nil { + if err := tester.sync("fork A", nil, mode); err != nil { t.Fatalf("failed to synchronise blocks: %v", err) } - if imported := len(tester.ownBlocks); imported != common+fork+1 { - t.Fatalf("synchronised block mismatch: have %v, want %v", imported, common+fork+1) - } + assertOwnChain(t, tester, common+fork+1) + // Synchronise with the second peer and make sure that fork is pulled too - if err := tester.sync("fork B", nil); err != nil { + if err := tester.sync("fork B", nil, mode); err != nil { t.Fatalf("failed to synchronise blocks: %v", err) } - if imported := len(tester.ownBlocks); imported != common+2*fork+1 { - t.Fatalf("synchronised block mismatch: have %v, want %v", imported, common+2*fork+1) - } + assertOwnForkedChain(t, tester, common+1, []int{common + fork + 1, common + fork + 1}) } // Tests that an inactive downloader will not accept incoming hashes and blocks. @@ -526,15 +836,16 @@ func TestInactiveDownloader61(t *testing.T) { tester := newTester() // Check that neither hashes nor blocks are accepted - if err := tester.downloader.DeliverHashes61("bad peer", []common.Hash{}); err != errNoSyncActive { + if err := tester.downloader.DeliverHashes("bad peer", []common.Hash{}); err != errNoSyncActive { t.Errorf("error mismatch: have %v, want %v", err, errNoSyncActive) } - if err := tester.downloader.DeliverBlocks61("bad peer", []*types.Block{}); err != errNoSyncActive { + if err := tester.downloader.DeliverBlocks("bad peer", []*types.Block{}); err != errNoSyncActive { t.Errorf("error mismatch: have %v, want %v", err, errNoSyncActive) } } -// Tests that an inactive downloader will not accept incoming block headers and bodies. +// Tests that an inactive downloader will not accept incoming block headers and +// bodies. func TestInactiveDownloader62(t *testing.T) { tester := newTester() @@ -547,13 +858,33 @@ func TestInactiveDownloader62(t *testing.T) { } } -// Tests that a canceled download wipes all previously accumulated state. -func TestCancel61(t *testing.T) { testCancel(t, 61) } -func TestCancel62(t *testing.T) { testCancel(t, 62) } -func TestCancel63(t *testing.T) { testCancel(t, 63) } -func TestCancel64(t *testing.T) { testCancel(t, 64) } +// Tests that an inactive downloader will not accept incoming block headers, +// bodies and receipts. +func TestInactiveDownloader63(t *testing.T) { + tester := newTester() + + // Check that neither block headers nor bodies are accepted + if err := tester.downloader.DeliverHeaders("bad peer", []*types.Header{}); err != errNoSyncActive { + t.Errorf("error mismatch: have %v, want %v", err, errNoSyncActive) + } + if err := tester.downloader.DeliverBodies("bad peer", [][]*types.Transaction{}, [][]*types.Header{}); err != errNoSyncActive { + t.Errorf("error mismatch: have %v, want %v", err, errNoSyncActive) + } + if err := tester.downloader.DeliverReceipts("bad peer", [][]*types.Receipt{}); err != errNoSyncActive { + t.Errorf("error mismatch: have %v, want %v", err, errNoSyncActive) + } +} -func testCancel(t *testing.T, protocol int) { +// Tests that a canceled download wipes all previously accumulated state. +func TestCancel61(t *testing.T) { testCancel(t, 61, FullSync) } +func TestCancel62(t *testing.T) { testCancel(t, 62, FullSync) } +func TestCancel63Full(t *testing.T) { testCancel(t, 63, FullSync) } +func TestCancel63Fast(t *testing.T) { testCancel(t, 63, FastSync) } +func TestCancel64Full(t *testing.T) { testCancel(t, 64, FullSync) } +func TestCancel64Fast(t *testing.T) { testCancel(t, 64, FastSync) } +func TestCancel64Light(t *testing.T) { testCancel(t, 64, LightSync) } + +func testCancel(t *testing.T, protocol int, mode SyncMode) { // Create a small enough block chain to download and the tester targetBlocks := blockCacheLimit - 15 if targetBlocks >= MaxHashFetch { @@ -562,207 +893,306 @@ func testCancel(t *testing.T, protocol int) { if targetBlocks >= MaxHeaderFetch { targetBlocks = MaxHeaderFetch - 15 } - hashes, blocks := makeChain(targetBlocks, 0, genesis) + hashes, headers, blocks, receipts := makeChain(targetBlocks, 0, genesis, nil) tester := newTester() - tester.newPeer("peer", protocol, hashes, blocks) + tester.newPeer("peer", protocol, hashes, headers, blocks, receipts) // Make sure canceling works with a pristine downloader tester.downloader.cancel() - downloading, importing := tester.downloader.queue.Size() - if downloading > 0 || importing > 0 { - t.Errorf("download or import count mismatch: %d downloading, %d importing, want 0", downloading, importing) + if !tester.downloader.queue.Idle() { + t.Errorf("download queue not idle") } // Synchronise with the peer, but cancel afterwards - if err := tester.sync("peer", nil); err != nil { + if err := tester.sync("peer", nil, mode); err != nil { t.Fatalf("failed to synchronise blocks: %v", err) } tester.downloader.cancel() - downloading, importing = tester.downloader.queue.Size() - if downloading > 0 || importing > 0 { - t.Errorf("download or import count mismatch: %d downloading, %d importing, want 0", downloading, importing) + if !tester.downloader.queue.Idle() { + t.Errorf("download queue not idle") } } // Tests that synchronisation from multiple peers works as intended (multi thread sanity test). -func TestMultiSynchronisation61(t *testing.T) { testMultiSynchronisation(t, 61) } -func TestMultiSynchronisation62(t *testing.T) { testMultiSynchronisation(t, 62) } -func TestMultiSynchronisation63(t *testing.T) { testMultiSynchronisation(t, 63) } -func TestMultiSynchronisation64(t *testing.T) { testMultiSynchronisation(t, 64) } - -func testMultiSynchronisation(t *testing.T, protocol int) { +func TestMultiSynchronisation61(t *testing.T) { testMultiSynchronisation(t, 61, FullSync) } +func TestMultiSynchronisation62(t *testing.T) { testMultiSynchronisation(t, 62, FullSync) } +func TestMultiSynchronisation63Full(t *testing.T) { testMultiSynchronisation(t, 63, FullSync) } +func TestMultiSynchronisation63Fast(t *testing.T) { testMultiSynchronisation(t, 63, FastSync) } +func TestMultiSynchronisation64Full(t *testing.T) { testMultiSynchronisation(t, 64, FullSync) } +func TestMultiSynchronisation64Fast(t *testing.T) { testMultiSynchronisation(t, 64, FastSync) } +func TestMultiSynchronisation64Light(t *testing.T) { testMultiSynchronisation(t, 64, LightSync) } + +func testMultiSynchronisation(t *testing.T, protocol int, mode SyncMode) { // Create various peers with various parts of the chain targetPeers := 8 targetBlocks := targetPeers*blockCacheLimit - 15 - hashes, blocks := makeChain(targetBlocks, 0, genesis) + hashes, headers, blocks, receipts := makeChain(targetBlocks, 0, genesis, nil) tester := newTester() for i := 0; i < targetPeers; i++ { id := fmt.Sprintf("peer #%d", i) - tester.newPeer(id, protocol, hashes[i*blockCacheLimit:], blocks) + tester.newPeer(id, protocol, hashes[i*blockCacheLimit:], headers, blocks, receipts) } - // Synchronise with the middle peer and make sure half of the blocks were retrieved - id := fmt.Sprintf("peer #%d", targetPeers/2) - if err := tester.sync(id, nil); err != nil { + if err := tester.sync("peer #0", nil, mode); err != nil { t.Fatalf("failed to synchronise blocks: %v", err) } - if imported := len(tester.ownBlocks); imported != len(tester.peerHashes[id]) { - t.Fatalf("synchronised block mismatch: have %v, want %v", imported, len(tester.peerHashes[id])) - } - // Synchronise with the best peer and make sure everything is retrieved - if err := tester.sync("peer #0", nil); err != nil { + assertOwnChain(t, tester, targetBlocks+1) +} + +// Tests that synchronisations behave well in multi-version protocol environments +// and not wreak havok on other nodes in the network. +func TestMultiProtoSynchronisation61(t *testing.T) { testMultiProtoSync(t, 61, FullSync) } +func TestMultiProtoSynchronisation62(t *testing.T) { testMultiProtoSync(t, 62, FullSync) } +func TestMultiProtoSynchronisation63Full(t *testing.T) { testMultiProtoSync(t, 63, FullSync) } +func TestMultiProtoSynchronisation63Fast(t *testing.T) { testMultiProtoSync(t, 63, FastSync) } +func TestMultiProtoSynchronisation64Full(t *testing.T) { testMultiProtoSync(t, 64, FullSync) } +func TestMultiProtoSynchronisation64Fast(t *testing.T) { testMultiProtoSync(t, 64, FastSync) } +func TestMultiProtoSynchronisation64Light(t *testing.T) { testMultiProtoSync(t, 64, LightSync) } + +func testMultiProtoSync(t *testing.T, protocol int, mode SyncMode) { + // Create a small enough block chain to download + targetBlocks := blockCacheLimit - 15 + hashes, headers, blocks, receipts := makeChain(targetBlocks, 0, genesis, nil) + + // Create peers of every type + tester := newTester() + tester.newPeer("peer 61", 61, hashes, nil, blocks, nil) + tester.newPeer("peer 62", 62, hashes, headers, blocks, nil) + tester.newPeer("peer 63", 63, hashes, headers, blocks, receipts) + tester.newPeer("peer 64", 64, hashes, headers, blocks, receipts) + + // Synchronise with the requested peer and make sure all blocks were retrieved + if err := tester.sync(fmt.Sprintf("peer %d", protocol), nil, mode); err != nil { t.Fatalf("failed to synchronise blocks: %v", err) } - if imported := len(tester.ownBlocks); imported != targetBlocks+1 { - t.Fatalf("synchronised block mismatch: have %v, want %v", imported, targetBlocks+1) + assertOwnChain(t, tester, targetBlocks+1) + + // Check that no peers have been dropped off + for _, version := range []int{61, 62, 63, 64} { + peer := fmt.Sprintf("peer %d", version) + if _, ok := tester.peerHashes[peer]; !ok { + t.Errorf("%s dropped", peer) + } } } -// Tests that if a block is empty (i.e. header only), no body request should be +// Tests that if a block is empty (e.g. header only), no body request should be // made, and instead the header should be assembled into a whole block in itself. -func TestEmptyBlockShortCircuit62(t *testing.T) { testEmptyBlockShortCircuit(t, 62) } -func TestEmptyBlockShortCircuit63(t *testing.T) { testEmptyBlockShortCircuit(t, 63) } -func TestEmptyBlockShortCircuit64(t *testing.T) { testEmptyBlockShortCircuit(t, 64) } - -func testEmptyBlockShortCircuit(t *testing.T, protocol int) { - // Create a small enough block chain to download - targetBlocks := blockCacheLimit - 15 - hashes, blocks := makeChain(targetBlocks, 0, genesis) +func TestEmptyShortCircuit62(t *testing.T) { testEmptyShortCircuit(t, 62, FullSync) } +func TestEmptyShortCircuit63Full(t *testing.T) { testEmptyShortCircuit(t, 63, FullSync) } +func TestEmptyShortCircuit63Fast(t *testing.T) { testEmptyShortCircuit(t, 63, FastSync) } +func TestEmptyShortCircuit64Full(t *testing.T) { testEmptyShortCircuit(t, 64, FullSync) } +func TestEmptyShortCircuit64Fast(t *testing.T) { testEmptyShortCircuit(t, 64, FastSync) } +func TestEmptyShortCircuit64Light(t *testing.T) { testEmptyShortCircuit(t, 64, LightSync) } + +func testEmptyShortCircuit(t *testing.T, protocol int, mode SyncMode) { + // Create a block chain to download + targetBlocks := 2*blockCacheLimit - 15 + hashes, headers, blocks, receipts := makeChain(targetBlocks, 0, genesis, nil) tester := newTester() - tester.newPeer("peer", protocol, hashes, blocks) + tester.newPeer("peer", protocol, hashes, headers, blocks, receipts) // Instrument the downloader to signal body requests - requested := int32(0) + bodiesHave, receiptsHave := int32(0), int32(0) tester.downloader.bodyFetchHook = func(headers []*types.Header) { - atomic.AddInt32(&requested, int32(len(headers))) + atomic.AddInt32(&bodiesHave, int32(len(headers))) + } + tester.downloader.receiptFetchHook = func(headers []*types.Header) { + atomic.AddInt32(&receiptsHave, int32(len(headers))) } // Synchronise with the peer and make sure all blocks were retrieved - if err := tester.sync("peer", nil); err != nil { + if err := tester.sync("peer", nil, mode); err != nil { t.Fatalf("failed to synchronise blocks: %v", err) } - if imported := len(tester.ownBlocks); imported != targetBlocks+1 { - t.Fatalf("synchronised block mismatch: have %v, want %v", imported, targetBlocks+1) - } + assertOwnChain(t, tester, targetBlocks+1) + // Validate the number of block bodies that should have been requested - needed := 0 + bodiesNeeded, receiptsNeeded := 0, 0 for _, block := range blocks { - if block != genesis && (len(block.Transactions()) > 0 || len(block.Uncles()) > 0) { - needed++ + if mode != LightSync && block != genesis && (len(block.Transactions()) > 0 || len(block.Uncles()) > 0) { + bodiesNeeded++ } } - if int(requested) != needed { - t.Fatalf("block body retrieval count mismatch: have %v, want %v", requested, needed) + for hash, receipt := range receipts { + if mode == FastSync && len(receipt) > 0 && headers[hash].Number.Uint64() <= tester.downloader.queue.fastSyncPivot { + receiptsNeeded++ + } + } + if int(bodiesHave) != bodiesNeeded { + t.Errorf("body retrieval count mismatch: have %v, want %v", bodiesHave, bodiesNeeded) + } + if int(receiptsHave) != receiptsNeeded { + t.Errorf("receipt retrieval count mismatch: have %v, want %v", receiptsHave, receiptsNeeded) } } // Tests that headers are enqueued continuously, preventing malicious nodes from // stalling the downloader by feeding gapped header chains. -func TestMissingHeaderAttack62(t *testing.T) { testMissingHeaderAttack(t, 62) } -func TestMissingHeaderAttack63(t *testing.T) { testMissingHeaderAttack(t, 63) } -func TestMissingHeaderAttack64(t *testing.T) { testMissingHeaderAttack(t, 64) } - -func testMissingHeaderAttack(t *testing.T, protocol int) { +func TestMissingHeaderAttack62(t *testing.T) { testMissingHeaderAttack(t, 62, FullSync) } +func TestMissingHeaderAttack63Full(t *testing.T) { testMissingHeaderAttack(t, 63, FullSync) } +func TestMissingHeaderAttack63Fast(t *testing.T) { testMissingHeaderAttack(t, 63, FastSync) } +func TestMissingHeaderAttack64Full(t *testing.T) { testMissingHeaderAttack(t, 64, FullSync) } +func TestMissingHeaderAttack64Fast(t *testing.T) { testMissingHeaderAttack(t, 64, FastSync) } +func TestMissingHeaderAttack64Light(t *testing.T) { testMissingHeaderAttack(t, 64, LightSync) } + +func testMissingHeaderAttack(t *testing.T, protocol int, mode SyncMode) { // Create a small enough block chain to download targetBlocks := blockCacheLimit - 15 - hashes, blocks := makeChain(targetBlocks, 0, genesis) + hashes, headers, blocks, receipts := makeChain(targetBlocks, 0, genesis, nil) tester := newTester() // Attempt a full sync with an attacker feeding gapped headers - tester.newPeer("attack", protocol, hashes, blocks) + tester.newPeer("attack", protocol, hashes, headers, blocks, receipts) missing := targetBlocks / 2 - delete(tester.peerBlocks["attack"], hashes[missing]) + delete(tester.peerHeaders["attack"], hashes[missing]) - if err := tester.sync("attack", nil); err == nil { + if err := tester.sync("attack", nil, mode); err == nil { t.Fatalf("succeeded attacker synchronisation") } // Synchronise with the valid peer and make sure sync succeeds - tester.newPeer("valid", protocol, hashes, blocks) - if err := tester.sync("valid", nil); err != nil { + tester.newPeer("valid", protocol, hashes, headers, blocks, receipts) + if err := tester.sync("valid", nil, mode); err != nil { t.Fatalf("failed to synchronise blocks: %v", err) } - if imported := len(tester.ownBlocks); imported != len(hashes) { - t.Fatalf("synchronised block mismatch: have %v, want %v", imported, len(hashes)) - } + assertOwnChain(t, tester, targetBlocks+1) } // Tests that if requested headers are shifted (i.e. first is missing), the queue // detects the invalid numbering. -func TestShiftedHeaderAttack62(t *testing.T) { testShiftedHeaderAttack(t, 62) } -func TestShiftedHeaderAttack63(t *testing.T) { testShiftedHeaderAttack(t, 63) } -func TestShiftedHeaderAttack64(t *testing.T) { testShiftedHeaderAttack(t, 64) } - -func testShiftedHeaderAttack(t *testing.T, protocol int) { +func TestShiftedHeaderAttack62(t *testing.T) { testShiftedHeaderAttack(t, 62, FullSync) } +func TestShiftedHeaderAttack63Full(t *testing.T) { testShiftedHeaderAttack(t, 63, FullSync) } +func TestShiftedHeaderAttack63Fast(t *testing.T) { testShiftedHeaderAttack(t, 63, FastSync) } +func TestShiftedHeaderAttack64Full(t *testing.T) { testShiftedHeaderAttack(t, 64, FullSync) } +func TestShiftedHeaderAttack64Fast(t *testing.T) { testShiftedHeaderAttack(t, 64, FastSync) } +func TestShiftedHeaderAttack64Light(t *testing.T) { testShiftedHeaderAttack(t, 64, LightSync) } + +func testShiftedHeaderAttack(t *testing.T, protocol int, mode SyncMode) { // Create a small enough block chain to download targetBlocks := blockCacheLimit - 15 - hashes, blocks := makeChain(targetBlocks, 0, genesis) + hashes, headers, blocks, receipts := makeChain(targetBlocks, 0, genesis, nil) tester := newTester() // Attempt a full sync with an attacker feeding shifted headers - tester.newPeer("attack", protocol, hashes, blocks) + tester.newPeer("attack", protocol, hashes, headers, blocks, receipts) + delete(tester.peerHeaders["attack"], hashes[len(hashes)-2]) delete(tester.peerBlocks["attack"], hashes[len(hashes)-2]) + delete(tester.peerReceipts["attack"], hashes[len(hashes)-2]) - if err := tester.sync("attack", nil); err == nil { + if err := tester.sync("attack", nil, mode); err == nil { t.Fatalf("succeeded attacker synchronisation") } // Synchronise with the valid peer and make sure sync succeeds - tester.newPeer("valid", protocol, hashes, blocks) - if err := tester.sync("valid", nil); err != nil { + tester.newPeer("valid", protocol, hashes, headers, blocks, receipts) + if err := tester.sync("valid", nil, mode); err != nil { t.Fatalf("failed to synchronise blocks: %v", err) } - if imported := len(tester.ownBlocks); imported != len(hashes) { - t.Fatalf("synchronised block mismatch: have %v, want %v", imported, len(hashes)) - } + assertOwnChain(t, tester, targetBlocks+1) } -// Tests that if a peer sends an invalid body for a requested block, it gets -// dropped immediately by the downloader. -func TestInvalidBlockBodyAttack62(t *testing.T) { testInvalidBlockBodyAttack(t, 62) } -func TestInvalidBlockBodyAttack63(t *testing.T) { testInvalidBlockBodyAttack(t, 63) } -func TestInvalidBlockBodyAttack64(t *testing.T) { testInvalidBlockBodyAttack(t, 64) } +// Tests that upon detecting an invalid header, the recent ones are rolled back +func TestInvalidHeaderRollback63Fast(t *testing.T) { testInvalidHeaderRollback(t, 63, FastSync) } +func TestInvalidHeaderRollback64Fast(t *testing.T) { testInvalidHeaderRollback(t, 64, FastSync) } +func TestInvalidHeaderRollback64Light(t *testing.T) { testInvalidHeaderRollback(t, 64, LightSync) } + +func testInvalidHeaderRollback(t *testing.T, protocol int, mode SyncMode) { + // Create a small enough block chain to download + targetBlocks := 3*fsHeaderSafetyNet + fsMinFullBlocks + hashes, headers, blocks, receipts := makeChain(targetBlocks, 0, genesis, nil) + + tester := newTester() + + // Attempt to sync with an attacker that feeds junk during the fast sync phase. + // This should result in the last fsHeaderSafetyNet headers being rolled back. + tester.newPeer("fast-attack", protocol, hashes, headers, blocks, receipts) + missing := fsHeaderSafetyNet + MaxHeaderFetch + 1 + delete(tester.peerHeaders["fast-attack"], hashes[len(hashes)-missing]) -func testInvalidBlockBodyAttack(t *testing.T, protocol int) { - // Create two peers, one feeding invalid block bodies - targetBlocks := 4*blockCacheLimit - 15 - hashes, validBlocks := makeChain(targetBlocks, 0, genesis) + if err := tester.sync("fast-attack", nil, mode); err == nil { + t.Fatalf("succeeded fast attacker synchronisation") + } + if head := tester.headHeader().Number.Int64(); int(head) > MaxHeaderFetch { + t.Errorf("rollback head mismatch: have %v, want at most %v", head, MaxHeaderFetch) + } + // Attempt to sync with an attacker that feeds junk during the block import phase. + // This should result in both the last fsHeaderSafetyNet number of headers being + // rolled back, and also the pivot point being reverted to a non-block status. + tester.newPeer("block-attack", protocol, hashes, headers, blocks, receipts) + missing = 3*fsHeaderSafetyNet + MaxHeaderFetch + 1 + delete(tester.peerHeaders["block-attack"], hashes[len(hashes)-missing]) - invalidBlocks := make(map[common.Hash]*types.Block) - for hash, block := range validBlocks { - invalidBlocks[hash] = types.NewBlockWithHeader(block.Header()) + if err := tester.sync("block-attack", nil, mode); err == nil { + t.Fatalf("succeeded block attacker synchronisation") + } + if head := tester.headHeader().Number.Int64(); int(head) > 2*fsHeaderSafetyNet+MaxHeaderFetch { + t.Errorf("rollback head mismatch: have %v, want at most %v", head, 2*fsHeaderSafetyNet+MaxHeaderFetch) } + if mode == FastSync { + if head := tester.headBlock().NumberU64(); head != 0 { + t.Errorf("fast sync pivot block #%d not rolled back", head) + } + } + // Attempt to sync with an attacker that withholds promised blocks after the + // fast sync pivot point. This could be a trial to leave the node with a bad + // but already imported pivot block. + tester.newPeer("withhold-attack", protocol, hashes, headers, blocks, receipts) + missing = 3*fsHeaderSafetyNet + MaxHeaderFetch + 1 - tester := newTester() - tester.newPeer("valid", protocol, hashes, validBlocks) - tester.newPeer("attack", protocol, hashes, invalidBlocks) + tester.downloader.noFast = false + tester.downloader.syncInitHook = func(uint64, uint64) { + for i := missing; i <= len(hashes); i++ { + delete(tester.peerHeaders["withhold-attack"], hashes[len(hashes)-i]) + } + tester.downloader.syncInitHook = nil + } - // Synchronise with the valid peer (will pull contents from the attacker too) - if err := tester.sync("valid", nil); err != nil { + if err := tester.sync("withhold-attack", nil, mode); err == nil { + t.Fatalf("succeeded withholding attacker synchronisation") + } + if head := tester.headHeader().Number.Int64(); int(head) > 2*fsHeaderSafetyNet+MaxHeaderFetch { + t.Errorf("rollback head mismatch: have %v, want at most %v", head, 2*fsHeaderSafetyNet+MaxHeaderFetch) + } + if mode == FastSync { + if head := tester.headBlock().NumberU64(); head != 0 { + t.Errorf("fast sync pivot block #%d not rolled back", head) + } + } + // Synchronise with the valid peer and make sure sync succeeds. Since the last + // rollback should also disable fast syncing for this process, verify that we + // did a fresh full sync. Note, we can't assert anything about the receipts + // since we won't purge the database of them, hence we can't use asserOwnChain. + tester.newPeer("valid", protocol, hashes, headers, blocks, receipts) + if err := tester.sync("valid", nil, mode); err != nil { t.Fatalf("failed to synchronise blocks: %v", err) } - if imported := len(tester.ownBlocks); imported != len(hashes) { - t.Fatalf("synchronised block mismatch: have %v, want %v", imported, len(hashes)) + if hs := len(tester.ownHeaders); hs != len(headers) { + t.Fatalf("synchronised headers mismatch: have %v, want %v", hs, len(headers)) } - // Make sure the attacker was detected and dropped in the mean time - if _, ok := tester.peerHashes["attack"]; ok { - t.Fatalf("block body attacker not detected/dropped") + if mode != LightSync { + if bs := len(tester.ownBlocks); bs != len(blocks) { + t.Fatalf("synchronised blocks mismatch: have %v, want %v", bs, len(blocks)) + } } } // Tests that a peer advertising an high TD doesn't get to stall the downloader // afterwards by not sending any useful hashes. -func TestHighTDStarvationAttack61(t *testing.T) { testHighTDStarvationAttack(t, 61) } -func TestHighTDStarvationAttack62(t *testing.T) { testHighTDStarvationAttack(t, 62) } -func TestHighTDStarvationAttack63(t *testing.T) { testHighTDStarvationAttack(t, 63) } -func TestHighTDStarvationAttack64(t *testing.T) { testHighTDStarvationAttack(t, 64) } - -func testHighTDStarvationAttack(t *testing.T, protocol int) { +func TestHighTDStarvationAttack61(t *testing.T) { testHighTDStarvationAttack(t, 61, FullSync) } +func TestHighTDStarvationAttack62(t *testing.T) { testHighTDStarvationAttack(t, 62, FullSync) } +func TestHighTDStarvationAttack63Full(t *testing.T) { testHighTDStarvationAttack(t, 63, FullSync) } +func TestHighTDStarvationAttack63Fast(t *testing.T) { testHighTDStarvationAttack(t, 63, FastSync) } +func TestHighTDStarvationAttack64Full(t *testing.T) { testHighTDStarvationAttack(t, 64, FullSync) } +func TestHighTDStarvationAttack64Fast(t *testing.T) { testHighTDStarvationAttack(t, 64, FastSync) } +func TestHighTDStarvationAttack64Light(t *testing.T) { testHighTDStarvationAttack(t, 64, LightSync) } + +func testHighTDStarvationAttack(t *testing.T, protocol int, mode SyncMode) { tester := newTester() - hashes, blocks := makeChain(0, 0, genesis) + hashes, headers, blocks, receipts := makeChain(0, 0, genesis, nil) - tester.newPeer("attack", protocol, []common.Hash{hashes[0]}, blocks) - if err := tester.sync("attack", big.NewInt(1000000)); err != errStallingPeer { + tester.newPeer("attack", protocol, []common.Hash{hashes[0]}, headers, blocks, receipts) + if err := tester.sync("attack", big.NewInt(1000000), mode); err != errStallingPeer { t.Fatalf("synchronisation error mismatch: have %v, want %v", err, errStallingPeer) } } @@ -791,7 +1221,9 @@ func testBlockHeaderAttackerDropping(t *testing.T, protocol int) { {errEmptyHeaderSet, true}, // No headers were returned as a response, drop as it's a dead end {errPeersUnavailable, true}, // Nobody had the advertised blocks, drop the advertiser {errInvalidChain, true}, // Hash chain was detected as invalid, definitely drop + {errInvalidBlock, false}, // A bad peer was detected, but not the sync origin {errInvalidBody, false}, // A bad peer was detected, but not the sync origin + {errInvalidReceipt, false}, // A bad peer was detected, but not the sync origin {errCancelHashFetch, false}, // Synchronisation was canceled, origin may be innocent, don't drop {errCancelBlockFetch, false}, // Synchronisation was canceled, origin may be innocent, don't drop {errCancelHeaderFetch, false}, // Synchronisation was canceled, origin may be innocent, don't drop @@ -802,7 +1234,7 @@ func testBlockHeaderAttackerDropping(t *testing.T, protocol int) { for i, tt := range tests { // Register a new peer and ensure it's presence id := fmt.Sprintf("test %d", i) - if err := tester.newPeer(id, protocol, []common.Hash{genesis.Hash()}, nil); err != nil { + if err := tester.newPeer(id, protocol, []common.Hash{genesis.Hash()}, nil, nil, nil); err != nil { t.Fatalf("test %d: failed to register new peer: %v", i, err) } if _, ok := tester.peerHashes[id]; !ok { @@ -811,70 +1243,29 @@ func testBlockHeaderAttackerDropping(t *testing.T, protocol int) { // Simulate a synchronisation and check the required result tester.downloader.synchroniseMock = func(string, common.Hash) error { return tt.result } - tester.downloader.Synchronise(id, genesis.Hash(), big.NewInt(1000)) + tester.downloader.Synchronise(id, genesis.Hash(), big.NewInt(1000), FullSync) if _, ok := tester.peerHashes[id]; !ok != tt.drop { t.Errorf("test %d: peer drop mismatch for %v: have %v, want %v", i, tt.result, !ok, tt.drop) } } } -// Tests that feeding bad blocks will result in a peer drop. -func TestBlockBodyAttackerDropping61(t *testing.T) { testBlockBodyAttackerDropping(t, 61) } -func TestBlockBodyAttackerDropping62(t *testing.T) { testBlockBodyAttackerDropping(t, 62) } -func TestBlockBodyAttackerDropping63(t *testing.T) { testBlockBodyAttackerDropping(t, 63) } -func TestBlockBodyAttackerDropping64(t *testing.T) { testBlockBodyAttackerDropping(t, 64) } - -func testBlockBodyAttackerDropping(t *testing.T, protocol int) { - // Define the disconnection requirement for individual block import errors - tests := []struct { - failure bool - drop bool - }{ - {true, true}, - {false, false}, - } - - // Run the tests and check disconnection status - tester := newTester() - for i, tt := range tests { - // Register a new peer and ensure it's presence - id := fmt.Sprintf("test %d", i) - if err := tester.newPeer(id, protocol, []common.Hash{common.Hash{}}, nil); err != nil { - t.Fatalf("test %d: failed to register new peer: %v", i, err) - } - if _, ok := tester.peerHashes[id]; !ok { - t.Fatalf("test %d: registered peer not found", i) - } - // Assemble a good or bad block, depending of the test - raw := core.GenerateChain(genesis, testdb, 1, nil)[0] - if tt.failure { - parent := types.NewBlock(&types.Header{}, nil, nil, nil) - raw = core.GenerateChain(parent, testdb, 1, nil)[0] - } - block := &Block{OriginPeer: id, RawBlock: raw} - - // Simulate block processing and check the result - tester.downloader.queue.blockCache[0] = block - tester.downloader.process() - if _, ok := tester.peerHashes[id]; !ok != tt.drop { - t.Errorf("test %d: peer drop mismatch for %v: have %v, want %v", i, tt.failure, !ok, tt.drop) - } - } -} - -// Tests that synchronisation boundaries (origin block number and highest block -// number) is tracked and updated correctly. -func TestSyncBoundaries61(t *testing.T) { testSyncBoundaries(t, 61) } -func TestSyncBoundaries62(t *testing.T) { testSyncBoundaries(t, 62) } -func TestSyncBoundaries63(t *testing.T) { testSyncBoundaries(t, 63) } -func TestSyncBoundaries64(t *testing.T) { testSyncBoundaries(t, 64) } - -func testSyncBoundaries(t *testing.T, protocol int) { +// Tests that synchronisation progress (origin block number, current block number +// and highest block number) is tracked and updated correctly. +func TestSyncProgress61(t *testing.T) { testSyncProgress(t, 61, FullSync) } +func TestSyncProgress62(t *testing.T) { testSyncProgress(t, 62, FullSync) } +func TestSyncProgress63Full(t *testing.T) { testSyncProgress(t, 63, FullSync) } +func TestSyncProgress63Fast(t *testing.T) { testSyncProgress(t, 63, FastSync) } +func TestSyncProgress64Full(t *testing.T) { testSyncProgress(t, 64, FullSync) } +func TestSyncProgress64Fast(t *testing.T) { testSyncProgress(t, 64, FastSync) } +func TestSyncProgress64Light(t *testing.T) { testSyncProgress(t, 64, LightSync) } + +func testSyncProgress(t *testing.T, protocol int, mode SyncMode) { // Create a small enough block chain to download targetBlocks := blockCacheLimit - 15 - hashes, blocks := makeChain(targetBlocks, 0, genesis) + hashes, headers, blocks, receipts := makeChain(targetBlocks, 0, genesis, nil) - // Set a sync init hook to catch boundary changes + // Set a sync init hook to catch progress changes starting := make(chan struct{}) progress := make(chan struct{}) @@ -883,60 +1274,68 @@ func testSyncBoundaries(t *testing.T, protocol int) { starting <- struct{}{} <-progress } - // Retrieve the sync boundaries and ensure they are zero (pristine sync) - if origin, latest := tester.downloader.Boundaries(); origin != 0 || latest != 0 { - t.Fatalf("Pristine boundary mismatch: have %v/%v, want %v/%v", origin, latest, 0, 0) + // Retrieve the sync progress and ensure they are zero (pristine sync) + if origin, current, latest := tester.downloader.Progress(); origin != 0 || current != 0 || latest != 0 { + t.Fatalf("Pristine progress mismatch: have %v/%v/%v, want %v/%v/%v", origin, current, latest, 0, 0, 0) } - // Synchronise half the blocks and check initial boundaries - tester.newPeer("peer-half", protocol, hashes[targetBlocks/2:], blocks) + // Synchronise half the blocks and check initial progress + tester.newPeer("peer-half", protocol, hashes[targetBlocks/2:], headers, blocks, receipts) pending := new(sync.WaitGroup) pending.Add(1) go func() { defer pending.Done() - if err := tester.sync("peer-half", nil); err != nil { + if err := tester.sync("peer-half", nil, mode); err != nil { t.Fatalf("failed to synchronise blocks: %v", err) } }() <-starting - if origin, latest := tester.downloader.Boundaries(); origin != 0 || latest != uint64(targetBlocks/2+1) { - t.Fatalf("Initial boundary mismatch: have %v/%v, want %v/%v", origin, latest, 0, targetBlocks/2+1) + if origin, current, latest := tester.downloader.Progress(); origin != 0 || current != 0 || latest != uint64(targetBlocks/2+1) { + t.Fatalf("Initial progress mismatch: have %v/%v/%v, want %v/%v/%v", origin, current, latest, 0, 0, targetBlocks/2+1) } progress <- struct{}{} pending.Wait() - // Synchronise all the blocks and check continuation boundaries - tester.newPeer("peer-full", protocol, hashes, blocks) + // Synchronise all the blocks and check continuation progress + tester.newPeer("peer-full", protocol, hashes, headers, blocks, receipts) pending.Add(1) go func() { defer pending.Done() - if err := tester.sync("peer-full", nil); err != nil { + if err := tester.sync("peer-full", nil, mode); err != nil { t.Fatalf("failed to synchronise blocks: %v", err) } }() <-starting - if origin, latest := tester.downloader.Boundaries(); origin != uint64(targetBlocks/2+1) || latest != uint64(targetBlocks) { - t.Fatalf("Completing boundary mismatch: have %v/%v, want %v/%v", origin, latest, targetBlocks/2+1, targetBlocks) + if origin, current, latest := tester.downloader.Progress(); origin != uint64(targetBlocks/2+1) || current != uint64(targetBlocks/2+1) || latest != uint64(targetBlocks) { + t.Fatalf("Completing progress mismatch: have %v/%v/%v, want %v/%v/%v", origin, current, latest, targetBlocks/2+1, targetBlocks/2+1, targetBlocks) } progress <- struct{}{} pending.Wait() + + // Check final progress after successful sync + if origin, current, latest := tester.downloader.Progress(); origin != uint64(targetBlocks/2+1) || current != uint64(targetBlocks) || latest != uint64(targetBlocks) { + t.Fatalf("Final progress mismatch: have %v/%v/%v, want %v/%v/%v", origin, current, latest, targetBlocks/2+1, targetBlocks, targetBlocks) + } } -// Tests that synchronisation boundaries (origin block number and highest block +// Tests that synchronisation progress (origin block number and highest block // number) is tracked and updated correctly in case of a fork (or manual head // revertal). -func TestForkedSyncBoundaries61(t *testing.T) { testForkedSyncBoundaries(t, 61) } -func TestForkedSyncBoundaries62(t *testing.T) { testForkedSyncBoundaries(t, 62) } -func TestForkedSyncBoundaries63(t *testing.T) { testForkedSyncBoundaries(t, 63) } -func TestForkedSyncBoundaries64(t *testing.T) { testForkedSyncBoundaries(t, 64) } - -func testForkedSyncBoundaries(t *testing.T, protocol int) { +func TestForkedSyncProgress61(t *testing.T) { testForkedSyncProgress(t, 61, FullSync) } +func TestForkedSyncProgress62(t *testing.T) { testForkedSyncProgress(t, 62, FullSync) } +func TestForkedSyncProgress63Full(t *testing.T) { testForkedSyncProgress(t, 63, FullSync) } +func TestForkedSyncProgress63Fast(t *testing.T) { testForkedSyncProgress(t, 63, FastSync) } +func TestForkedSyncProgress64Full(t *testing.T) { testForkedSyncProgress(t, 64, FullSync) } +func TestForkedSyncProgress64Fast(t *testing.T) { testForkedSyncProgress(t, 64, FastSync) } +func TestForkedSyncProgress64Light(t *testing.T) { testForkedSyncProgress(t, 64, LightSync) } + +func testForkedSyncProgress(t *testing.T, protocol int, mode SyncMode) { // Create a forked chain to simulate origin revertal common, fork := MaxHashFetch, 2*MaxHashFetch - hashesA, hashesB, blocksA, blocksB := makeChainFork(common+fork, fork, genesis) + hashesA, hashesB, headersA, headersB, blocksA, blocksB, receiptsA, receiptsB := makeChainFork(common+fork, fork, genesis, nil) - // Set a sync init hook to catch boundary changes + // Set a sync init hook to catch progress changes starting := make(chan struct{}) progress := make(chan struct{}) @@ -945,63 +1344,71 @@ func testForkedSyncBoundaries(t *testing.T, protocol int) { starting <- struct{}{} <-progress } - // Retrieve the sync boundaries and ensure they are zero (pristine sync) - if origin, latest := tester.downloader.Boundaries(); origin != 0 || latest != 0 { - t.Fatalf("Pristine boundary mismatch: have %v/%v, want %v/%v", origin, latest, 0, 0) + // Retrieve the sync progress and ensure they are zero (pristine sync) + if origin, current, latest := tester.downloader.Progress(); origin != 0 || current != 0 || latest != 0 { + t.Fatalf("Pristine progress mismatch: have %v/%v/%v, want %v/%v/%v", origin, current, latest, 0, 0, 0) } - // Synchronise with one of the forks and check boundaries - tester.newPeer("fork A", protocol, hashesA, blocksA) + // Synchronise with one of the forks and check progress + tester.newPeer("fork A", protocol, hashesA, headersA, blocksA, receiptsA) pending := new(sync.WaitGroup) pending.Add(1) go func() { defer pending.Done() - if err := tester.sync("fork A", nil); err != nil { + if err := tester.sync("fork A", nil, mode); err != nil { t.Fatalf("failed to synchronise blocks: %v", err) } }() <-starting - if origin, latest := tester.downloader.Boundaries(); origin != 0 || latest != uint64(len(hashesA)-1) { - t.Fatalf("Initial boundary mismatch: have %v/%v, want %v/%v", origin, latest, 0, len(hashesA)-1) + if origin, current, latest := tester.downloader.Progress(); origin != 0 || current != 0 || latest != uint64(len(hashesA)-1) { + t.Fatalf("Initial progress mismatch: have %v/%v/%v, want %v/%v/%v", origin, current, latest, 0, 0, len(hashesA)-1) } progress <- struct{}{} pending.Wait() // Simulate a successful sync above the fork - tester.downloader.syncStatsOrigin = tester.downloader.syncStatsHeight + tester.downloader.syncStatsChainOrigin = tester.downloader.syncStatsChainHeight - // Synchronise with the second fork and check boundary resets - tester.newPeer("fork B", protocol, hashesB, blocksB) + // Synchronise with the second fork and check progress resets + tester.newPeer("fork B", protocol, hashesB, headersB, blocksB, receiptsB) pending.Add(1) go func() { defer pending.Done() - if err := tester.sync("fork B", nil); err != nil { + if err := tester.sync("fork B", nil, mode); err != nil { t.Fatalf("failed to synchronise blocks: %v", err) } }() <-starting - if origin, latest := tester.downloader.Boundaries(); origin != uint64(common) || latest != uint64(len(hashesB)-1) { - t.Fatalf("Forking boundary mismatch: have %v/%v, want %v/%v", origin, latest, common, len(hashesB)-1) + if origin, current, latest := tester.downloader.Progress(); origin != uint64(common) || current != uint64(len(hashesA)-1) || latest != uint64(len(hashesB)-1) { + t.Fatalf("Forking progress mismatch: have %v/%v/%v, want %v/%v/%v", origin, current, latest, common, len(hashesA)-1, len(hashesB)-1) } progress <- struct{}{} pending.Wait() + + // Check final progress after successful sync + if origin, current, latest := tester.downloader.Progress(); origin != uint64(common) || current != uint64(len(hashesB)-1) || latest != uint64(len(hashesB)-1) { + t.Fatalf("Final progress mismatch: have %v/%v/%v, want %v/%v/%v", origin, current, latest, common, len(hashesB)-1, len(hashesB)-1) + } } -// Tests that if synchronisation is aborted due to some failure, then the boundary +// Tests that if synchronisation is aborted due to some failure, then the progress // origin is not updated in the next sync cycle, as it should be considered the // continuation of the previous sync and not a new instance. -func TestFailedSyncBoundaries61(t *testing.T) { testFailedSyncBoundaries(t, 61) } -func TestFailedSyncBoundaries62(t *testing.T) { testFailedSyncBoundaries(t, 62) } -func TestFailedSyncBoundaries63(t *testing.T) { testFailedSyncBoundaries(t, 63) } -func TestFailedSyncBoundaries64(t *testing.T) { testFailedSyncBoundaries(t, 64) } - -func testFailedSyncBoundaries(t *testing.T, protocol int) { +func TestFailedSyncProgress61(t *testing.T) { testFailedSyncProgress(t, 61, FullSync) } +func TestFailedSyncProgress62(t *testing.T) { testFailedSyncProgress(t, 62, FullSync) } +func TestFailedSyncProgress63Full(t *testing.T) { testFailedSyncProgress(t, 63, FullSync) } +func TestFailedSyncProgress63Fast(t *testing.T) { testFailedSyncProgress(t, 63, FastSync) } +func TestFailedSyncProgress64Full(t *testing.T) { testFailedSyncProgress(t, 64, FullSync) } +func TestFailedSyncProgress64Fast(t *testing.T) { testFailedSyncProgress(t, 64, FastSync) } +func TestFailedSyncProgress64Light(t *testing.T) { testFailedSyncProgress(t, 64, LightSync) } + +func testFailedSyncProgress(t *testing.T, protocol int, mode SyncMode) { // Create a small enough block chain to download targetBlocks := blockCacheLimit - 15 - hashes, blocks := makeChain(targetBlocks, 0, genesis) + hashes, headers, blocks, receipts := makeChain(targetBlocks, 0, genesis, nil) - // Set a sync init hook to catch boundary changes + // Set a sync init hook to catch progress changes starting := make(chan struct{}) progress := make(chan struct{}) @@ -1010,62 +1417,72 @@ func testFailedSyncBoundaries(t *testing.T, protocol int) { starting <- struct{}{} <-progress } - // Retrieve the sync boundaries and ensure they are zero (pristine sync) - if origin, latest := tester.downloader.Boundaries(); origin != 0 || latest != 0 { - t.Fatalf("Pristine boundary mismatch: have %v/%v, want %v/%v", origin, latest, 0, 0) + // Retrieve the sync progress and ensure they are zero (pristine sync) + if origin, current, latest := tester.downloader.Progress(); origin != 0 || current != 0 || latest != 0 { + t.Fatalf("Pristine progress mismatch: have %v/%v/%v, want %v/%v/%v", origin, current, latest, 0, 0, 0) } // Attempt a full sync with a faulty peer - tester.newPeer("faulty", protocol, hashes, blocks) + tester.newPeer("faulty", protocol, hashes, headers, blocks, receipts) missing := targetBlocks / 2 + delete(tester.peerHeaders["faulty"], hashes[missing]) delete(tester.peerBlocks["faulty"], hashes[missing]) + delete(tester.peerReceipts["faulty"], hashes[missing]) pending := new(sync.WaitGroup) pending.Add(1) go func() { defer pending.Done() - if err := tester.sync("faulty", nil); err == nil { + if err := tester.sync("faulty", nil, mode); err == nil { t.Fatalf("succeeded faulty synchronisation") } }() <-starting - if origin, latest := tester.downloader.Boundaries(); origin != 0 || latest != uint64(targetBlocks) { - t.Fatalf("Initial boundary mismatch: have %v/%v, want %v/%v", origin, latest, 0, targetBlocks) + if origin, current, latest := tester.downloader.Progress(); origin != 0 || current != 0 || latest != uint64(targetBlocks) { + t.Fatalf("Initial progress mismatch: have %v/%v/%v, want %v/%v/%v", origin, current, latest, 0, 0, targetBlocks) } progress <- struct{}{} pending.Wait() - // Synchronise with a good peer and check that the boundary origin remind the same after a failure - tester.newPeer("valid", protocol, hashes, blocks) + // Synchronise with a good peer and check that the progress origin remind the same after a failure + tester.newPeer("valid", protocol, hashes, headers, blocks, receipts) pending.Add(1) go func() { defer pending.Done() - if err := tester.sync("valid", nil); err != nil { + if err := tester.sync("valid", nil, mode); err != nil { t.Fatalf("failed to synchronise blocks: %v", err) } }() <-starting - if origin, latest := tester.downloader.Boundaries(); origin != 0 || latest != uint64(targetBlocks) { - t.Fatalf("Completing boundary mismatch: have %v/%v, want %v/%v", origin, latest, 0, targetBlocks) + if origin, current, latest := tester.downloader.Progress(); origin != 0 || current > uint64(targetBlocks/2) || latest != uint64(targetBlocks) { + t.Fatalf("Completing progress mismatch: have %v/%v/%v, want %v/0-%v/%v", origin, current, latest, 0, targetBlocks/2, targetBlocks) } progress <- struct{}{} pending.Wait() + + // Check final progress after successful sync + if origin, current, latest := tester.downloader.Progress(); origin > uint64(targetBlocks/2) || current != uint64(targetBlocks) || latest != uint64(targetBlocks) { + t.Fatalf("Final progress mismatch: have %v/%v/%v, want 0-%v/%v/%v", origin, current, latest, targetBlocks/2, targetBlocks, targetBlocks) + } } // Tests that if an attacker fakes a chain height, after the attack is detected, -// the boundary height is successfully reduced at the next sync invocation. -func TestFakedSyncBoundaries61(t *testing.T) { testFakedSyncBoundaries(t, 61) } -func TestFakedSyncBoundaries62(t *testing.T) { testFakedSyncBoundaries(t, 62) } -func TestFakedSyncBoundaries63(t *testing.T) { testFakedSyncBoundaries(t, 63) } -func TestFakedSyncBoundaries64(t *testing.T) { testFakedSyncBoundaries(t, 64) } - -func testFakedSyncBoundaries(t *testing.T, protocol int) { +// the progress height is successfully reduced at the next sync invocation. +func TestFakedSyncProgress61(t *testing.T) { testFakedSyncProgress(t, 61, FullSync) } +func TestFakedSyncProgress62(t *testing.T) { testFakedSyncProgress(t, 62, FullSync) } +func TestFakedSyncProgress63Full(t *testing.T) { testFakedSyncProgress(t, 63, FullSync) } +func TestFakedSyncProgress63Fast(t *testing.T) { testFakedSyncProgress(t, 63, FastSync) } +func TestFakedSyncProgress64Full(t *testing.T) { testFakedSyncProgress(t, 64, FullSync) } +func TestFakedSyncProgress64Fast(t *testing.T) { testFakedSyncProgress(t, 64, FastSync) } +func TestFakedSyncProgress64Light(t *testing.T) { testFakedSyncProgress(t, 64, LightSync) } + +func testFakedSyncProgress(t *testing.T, protocol int, mode SyncMode) { // Create a small block chain targetBlocks := blockCacheLimit - 15 - hashes, blocks := makeChain(targetBlocks+3, 0, genesis) + hashes, headers, blocks, receipts := makeChain(targetBlocks+3, 0, genesis, nil) - // Set a sync init hook to catch boundary changes + // Set a sync init hook to catch progress changes starting := make(chan struct{}) progress := make(chan struct{}) @@ -1074,14 +1491,16 @@ func testFakedSyncBoundaries(t *testing.T, protocol int) { starting <- struct{}{} <-progress } - // Retrieve the sync boundaries and ensure they are zero (pristine sync) - if origin, latest := tester.downloader.Boundaries(); origin != 0 || latest != 0 { - t.Fatalf("Pristine boundary mismatch: have %v/%v, want %v/%v", origin, latest, 0, 0) + // Retrieve the sync progress and ensure they are zero (pristine sync) + if origin, current, latest := tester.downloader.Progress(); origin != 0 || current != 0 || latest != 0 { + t.Fatalf("Pristine progress mismatch: have %v/%v/%v, want %v/%v/%v", origin, current, latest, 0, 0, 0) } // Create and sync with an attacker that promises a higher chain than available - tester.newPeer("attack", protocol, hashes, blocks) + tester.newPeer("attack", protocol, hashes, headers, blocks, receipts) for i := 1; i < 3; i++ { + delete(tester.peerHeaders["attack"], hashes[i]) delete(tester.peerBlocks["attack"], hashes[i]) + delete(tester.peerReceipts["attack"], hashes[i]) } pending := new(sync.WaitGroup) @@ -1089,31 +1508,36 @@ func testFakedSyncBoundaries(t *testing.T, protocol int) { go func() { defer pending.Done() - if err := tester.sync("attack", nil); err == nil { + if err := tester.sync("attack", nil, mode); err == nil { t.Fatalf("succeeded attacker synchronisation") } }() <-starting - if origin, latest := tester.downloader.Boundaries(); origin != 0 || latest != uint64(targetBlocks+3) { - t.Fatalf("Initial boundary mismatch: have %v/%v, want %v/%v", origin, latest, 0, targetBlocks+3) + if origin, current, latest := tester.downloader.Progress(); origin != 0 || current != 0 || latest != uint64(targetBlocks+3) { + t.Fatalf("Initial progress mismatch: have %v/%v/%v, want %v/%v/%v", origin, current, latest, 0, 0, targetBlocks+3) } progress <- struct{}{} pending.Wait() - // Synchronise with a good peer and check that the boundary height has been reduced to the true value - tester.newPeer("valid", protocol, hashes[3:], blocks) + // Synchronise with a good peer and check that the progress height has been reduced to the true value + tester.newPeer("valid", protocol, hashes[3:], headers, blocks, receipts) pending.Add(1) go func() { defer pending.Done() - if err := tester.sync("valid", nil); err != nil { + if err := tester.sync("valid", nil, mode); err != nil { t.Fatalf("failed to synchronise blocks: %v", err) } }() <-starting - if origin, latest := tester.downloader.Boundaries(); origin != 0 || latest != uint64(targetBlocks) { - t.Fatalf("Initial boundary mismatch: have %v/%v, want %v/%v", origin, latest, 0, targetBlocks) + if origin, current, latest := tester.downloader.Progress(); origin != 0 || current > uint64(targetBlocks) || latest != uint64(targetBlocks) { + t.Fatalf("Completing progress mismatch: have %v/%v/%v, want %v/0-%v/%v", origin, current, latest, 0, targetBlocks, targetBlocks) } progress <- struct{}{} pending.Wait() + + // Check final progress after successful sync + if origin, current, latest := tester.downloader.Progress(); origin > uint64(targetBlocks) || current != uint64(targetBlocks) || latest != uint64(targetBlocks) { + t.Fatalf("Final progress mismatch: have %v/%v/%v, want 0-%v/%v/%v", origin, current, latest, targetBlocks, targetBlocks, targetBlocks) + } } diff --git a/eth/downloader/metrics.go b/eth/downloader/metrics.go index fd926affd..d6fcfa25c 100644 --- a/eth/downloader/metrics.go +++ b/eth/downloader/metrics.go @@ -42,4 +42,14 @@ var ( bodyReqTimer = metrics.NewTimer("eth/downloader/bodies/req") bodyDropMeter = metrics.NewMeter("eth/downloader/bodies/drop") bodyTimeoutMeter = metrics.NewMeter("eth/downloader/bodies/timeout") + + receiptInMeter = metrics.NewMeter("eth/downloader/receipts/in") + receiptReqTimer = metrics.NewTimer("eth/downloader/receipts/req") + receiptDropMeter = metrics.NewMeter("eth/downloader/receipts/drop") + receiptTimeoutMeter = metrics.NewMeter("eth/downloader/receipts/timeout") + + stateInMeter = metrics.NewMeter("eth/downloader/states/in") + stateReqTimer = metrics.NewTimer("eth/downloader/states/req") + stateDropMeter = metrics.NewMeter("eth/downloader/states/drop") + stateTimeoutMeter = metrics.NewMeter("eth/downloader/states/timeout") ) diff --git a/eth/downloader/modes.go b/eth/downloader/modes.go new file mode 100644 index 000000000..ec339c074 --- /dev/null +++ b/eth/downloader/modes.go @@ -0,0 +1,26 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. + +package downloader + +// SyncMode represents the synchronisation mode of the downloader. +type SyncMode int + +const ( + FullSync SyncMode = iota // Synchronise the entire blockchain history from full blocks + FastSync // Quickly download the headers, full sync only at the chain head + LightSync // Download only the headers and terminate afterwards +) diff --git a/eth/downloader/peer.go b/eth/downloader/peer.go index 8fd1f9a99..1f457cb15 100644 --- a/eth/downloader/peer.go +++ b/eth/downloader/peer.go @@ -36,10 +36,12 @@ type relativeHashFetcherFn func(common.Hash) error type absoluteHashFetcherFn func(uint64, int) error type blockFetcherFn func([]common.Hash) error -// Block header and body fethers belonging to eth/62 and above +// Block header and body fetchers belonging to eth/62 and above type relativeHeaderFetcherFn func(common.Hash, int, int, bool) error type absoluteHeaderFetcherFn func(uint64, int, int, bool) error type blockBodyFetcherFn func([]common.Hash) error +type receiptFetcherFn func([]common.Hash) error +type stateFetcherFn func([]common.Hash) error var ( errAlreadyFetching = errors.New("already fetching blocks from peer") @@ -52,11 +54,18 @@ type peer struct { id string // Unique identifier of the peer head common.Hash // Hash of the peers latest known block - idle int32 // Current activity state of the peer (idle = 0, active = 1) - rep int32 // Simple peer reputation + blockIdle int32 // Current block activity state of the peer (idle = 0, active = 1) + receiptIdle int32 // Current receipt activity state of the peer (idle = 0, active = 1) + stateIdle int32 // Current node data activity state of the peer (idle = 0, active = 1) + rep int32 // Simple peer reputation - capacity int32 // Number of blocks allowed to fetch per request - started time.Time // Time instance when the last fetch was started + blockCapacity int32 // Number of blocks (bodies) allowed to fetch per request + receiptCapacity int32 // Number of receipts allowed to fetch per request + stateCapacity int32 // Number of node data pieces allowed to fetch per request + + blockStarted time.Time // Time instance when the last block (body)fetch was started + receiptStarted time.Time // Time instance when the last receipt fetch was started + stateStarted time.Time // Time instance when the last node data fetch was started ignored *set.Set // Set of hashes not to request (didn't have previously) @@ -68,6 +77,9 @@ type peer struct { getAbsHeaders absoluteHeaderFetcherFn // [eth/62] Method to retrieve a batch of headers from an absolute position getBlockBodies blockBodyFetcherFn // [eth/62] Method to retrieve a batch of block bodies + getReceipts receiptFetcherFn // [eth/63] Method to retrieve a batch of block transaction receipts + getNodeData stateFetcherFn // [eth/63] Method to retrieve a batch of state trie data + version int // Eth protocol version number to switch strategies } @@ -75,12 +87,15 @@ type peer struct { // mechanisms. func newPeer(id string, version int, head common.Hash, getRelHashes relativeHashFetcherFn, getAbsHashes absoluteHashFetcherFn, getBlocks blockFetcherFn, // eth/61 callbacks, remove when upgrading - getRelHeaders relativeHeaderFetcherFn, getAbsHeaders absoluteHeaderFetcherFn, getBlockBodies blockBodyFetcherFn) *peer { + getRelHeaders relativeHeaderFetcherFn, getAbsHeaders absoluteHeaderFetcherFn, getBlockBodies blockBodyFetcherFn, + getReceipts receiptFetcherFn, getNodeData stateFetcherFn) *peer { return &peer{ - id: id, - head: head, - capacity: 1, - ignored: set.New(), + id: id, + head: head, + blockCapacity: 1, + receiptCapacity: 1, + stateCapacity: 1, + ignored: set.New(), getRelHashes: getRelHashes, getAbsHashes: getAbsHashes, @@ -90,24 +105,34 @@ func newPeer(id string, version int, head common.Hash, getAbsHeaders: getAbsHeaders, getBlockBodies: getBlockBodies, + getReceipts: getReceipts, + getNodeData: getNodeData, + version: version, } } // Reset clears the internal state of a peer entity. func (p *peer) Reset() { - atomic.StoreInt32(&p.idle, 0) - atomic.StoreInt32(&p.capacity, 1) + atomic.StoreInt32(&p.blockIdle, 0) + atomic.StoreInt32(&p.receiptIdle, 0) + atomic.StoreInt32(&p.blockCapacity, 1) + atomic.StoreInt32(&p.receiptCapacity, 1) + atomic.StoreInt32(&p.stateCapacity, 1) p.ignored.Clear() } // Fetch61 sends a block retrieval request to the remote peer. func (p *peer) Fetch61(request *fetchRequest) error { + // Sanity check the protocol version + if p.version != 61 { + panic(fmt.Sprintf("block fetch [eth/61] requested on eth/%d", p.version)) + } // Short circuit if the peer is already fetching - if !atomic.CompareAndSwapInt32(&p.idle, 0, 1) { + if !atomic.CompareAndSwapInt32(&p.blockIdle, 0, 1) { return errAlreadyFetching } - p.started = time.Now() + p.blockStarted = time.Now() // Convert the hash set to a retrievable slice hashes := make([]common.Hash, 0, len(request.Hashes)) @@ -119,13 +144,17 @@ func (p *peer) Fetch61(request *fetchRequest) error { return nil } -// Fetch sends a block body retrieval request to the remote peer. -func (p *peer) Fetch(request *fetchRequest) error { +// FetchBodies sends a block body retrieval request to the remote peer. +func (p *peer) FetchBodies(request *fetchRequest) error { + // Sanity check the protocol version + if p.version < 62 { + panic(fmt.Sprintf("body fetch [eth/62+] requested on eth/%d", p.version)) + } // Short circuit if the peer is already fetching - if !atomic.CompareAndSwapInt32(&p.idle, 0, 1) { + if !atomic.CompareAndSwapInt32(&p.blockIdle, 0, 1) { return errAlreadyFetching } - p.started = time.Now() + p.blockStarted = time.Now() // Convert the header set to a retrievable slice hashes := make([]common.Hash, 0, len(request.Headers)) @@ -137,55 +166,97 @@ func (p *peer) Fetch(request *fetchRequest) error { return nil } -// SetIdle61 sets the peer to idle, allowing it to execute new retrieval requests. -// Its block retrieval allowance will also be updated either up- or downwards, -// depending on whether the previous fetch completed in time or not. -func (p *peer) SetIdle61() { - // Update the peer's download allowance based on previous performance - scale := 2.0 - if time.Since(p.started) > blockSoftTTL { - scale = 0.5 - if time.Since(p.started) > blockHardTTL { - scale = 1 / float64(MaxBlockFetch) // reduces capacity to 1 - } +// FetchReceipts sends a receipt retrieval request to the remote peer. +func (p *peer) FetchReceipts(request *fetchRequest) error { + // Sanity check the protocol version + if p.version < 63 { + panic(fmt.Sprintf("body fetch [eth/63+] requested on eth/%d", p.version)) } - for { - // Calculate the new download bandwidth allowance - prev := atomic.LoadInt32(&p.capacity) - next := int32(math.Max(1, math.Min(float64(MaxBlockFetch), float64(prev)*scale))) + // Short circuit if the peer is already fetching + if !atomic.CompareAndSwapInt32(&p.receiptIdle, 0, 1) { + return errAlreadyFetching + } + p.receiptStarted = time.Now() - // Try to update the old value - if atomic.CompareAndSwapInt32(&p.capacity, prev, next) { - // If we're having problems at 1 capacity, try to find better peers - if next == 1 { - p.Demote() - } - break - } + // Convert the header set to a retrievable slice + hashes := make([]common.Hash, 0, len(request.Headers)) + for _, header := range request.Headers { + hashes = append(hashes, header.Hash()) + } + go p.getReceipts(hashes) + + return nil +} + +// FetchNodeData sends a node state data retrieval request to the remote peer. +func (p *peer) FetchNodeData(request *fetchRequest) error { + // Sanity check the protocol version + if p.version < 63 { + panic(fmt.Sprintf("node data fetch [eth/63+] requested on eth/%d", p.version)) + } + // Short circuit if the peer is already fetching + if !atomic.CompareAndSwapInt32(&p.stateIdle, 0, 1) { + return errAlreadyFetching + } + p.stateStarted = time.Now() + + // Convert the hash set to a retrievable slice + hashes := make([]common.Hash, 0, len(request.Hashes)) + for hash, _ := range request.Hashes { + hashes = append(hashes, hash) } - // Set the peer to idle to allow further block requests - atomic.StoreInt32(&p.idle, 0) + go p.getNodeData(hashes) + + return nil +} + +// SetBlocksIdle sets the peer to idle, allowing it to execute new retrieval requests. +// Its block retrieval allowance will also be updated either up- or downwards, +// depending on whether the previous fetch completed in time. +func (p *peer) SetBlocksIdle() { + p.setIdle(p.blockStarted, blockSoftTTL, blockHardTTL, MaxBlockFetch, &p.blockCapacity, &p.blockIdle) } -// SetIdle sets the peer to idle, allowing it to execute new retrieval requests. +// SetBodiesIdle sets the peer to idle, allowing it to execute new retrieval requests. // Its block body retrieval allowance will also be updated either up- or downwards, -// depending on whether the previous fetch completed in time or not. -func (p *peer) SetIdle() { +// depending on whether the previous fetch completed in time. +func (p *peer) SetBodiesIdle() { + p.setIdle(p.blockStarted, bodySoftTTL, bodyHardTTL, MaxBodyFetch, &p.blockCapacity, &p.blockIdle) +} + +// SetReceiptsIdle sets the peer to idle, allowing it to execute new retrieval requests. +// Its receipt retrieval allowance will also be updated either up- or downwards, +// depending on whether the previous fetch completed in time. +func (p *peer) SetReceiptsIdle() { + p.setIdle(p.receiptStarted, receiptSoftTTL, receiptHardTTL, MaxReceiptFetch, &p.receiptCapacity, &p.receiptIdle) +} + +// SetNodeDataIdle sets the peer to idle, allowing it to execute new retrieval +// requests. Its node data retrieval allowance will also be updated either up- or +// downwards, depending on whether the previous fetch completed in time. +func (p *peer) SetNodeDataIdle() { + p.setIdle(p.stateStarted, stateSoftTTL, stateSoftTTL, MaxStateFetch, &p.stateCapacity, &p.stateIdle) +} + +// setIdle sets the peer to idle, allowing it to execute new retrieval requests. +// Its data retrieval allowance will also be updated either up- or downwards, +// depending on whether the previous fetch completed in time. +func (p *peer) setIdle(started time.Time, softTTL, hardTTL time.Duration, maxFetch int, capacity, idle *int32) { // Update the peer's download allowance based on previous performance scale := 2.0 - if time.Since(p.started) > bodySoftTTL { + if time.Since(started) > softTTL { scale = 0.5 - if time.Since(p.started) > bodyHardTTL { - scale = 1 / float64(MaxBodyFetch) // reduces capacity to 1 + if time.Since(started) > hardTTL { + scale = 1 / float64(maxFetch) // reduces capacity to 1 } } for { // Calculate the new download bandwidth allowance - prev := atomic.LoadInt32(&p.capacity) - next := int32(math.Max(1, math.Min(float64(MaxBodyFetch), float64(prev)*scale))) + prev := atomic.LoadInt32(capacity) + next := int32(math.Max(1, math.Min(float64(maxFetch), float64(prev)*scale))) // Try to update the old value - if atomic.CompareAndSwapInt32(&p.capacity, prev, next) { + if atomic.CompareAndSwapInt32(capacity, prev, next) { // If we're having problems at 1 capacity, try to find better peers if next == 1 { p.Demote() @@ -193,14 +264,26 @@ func (p *peer) SetIdle() { break } } - // Set the peer to idle to allow further block requests - atomic.StoreInt32(&p.idle, 0) + // Set the peer to idle to allow further fetch requests + atomic.StoreInt32(idle, 0) +} + +// BlockCapacity retrieves the peers block download allowance based on its +// previously discovered bandwidth capacity. +func (p *peer) BlockCapacity() int { + return int(atomic.LoadInt32(&p.blockCapacity)) } -// Capacity retrieves the peers block download allowance based on its previously -// discovered bandwidth capacity. -func (p *peer) Capacity() int { - return int(atomic.LoadInt32(&p.capacity)) +// ReceiptCapacity retrieves the peers block download allowance based on its +// previously discovered bandwidth capacity. +func (p *peer) ReceiptCapacity() int { + return int(atomic.LoadInt32(&p.receiptCapacity)) +} + +// NodeDataCapacity retrieves the peers block download allowance based on its +// previously discovered bandwidth capacity. +func (p *peer) NodeDataCapacity() int { + return int(atomic.LoadInt32(&p.stateCapacity)) } // Promote increases the peer's reputation. @@ -226,7 +309,8 @@ func (p *peer) Demote() { func (p *peer) String() string { return fmt.Sprintf("Peer %s [%s]", p.id, fmt.Sprintf("reputation %3d, ", atomic.LoadInt32(&p.rep))+ - fmt.Sprintf("capacity %3d, ", atomic.LoadInt32(&p.capacity))+ + fmt.Sprintf("block cap %3d, ", atomic.LoadInt32(&p.blockCapacity))+ + fmt.Sprintf("receipt cap %3d, ", atomic.LoadInt32(&p.receiptCapacity))+ fmt.Sprintf("ignored %4d", p.ignored.Size()), ) } @@ -310,24 +394,63 @@ func (ps *peerSet) AllPeers() []*peer { return list } -// IdlePeers retrieves a flat list of all the currently idle peers within the +// BlockIdlePeers retrieves a flat list of all the currently idle peers within the // active peer set, ordered by their reputation. -func (ps *peerSet) IdlePeers() []*peer { +func (ps *peerSet) BlockIdlePeers() ([]*peer, int) { + idle := func(p *peer) bool { + return atomic.LoadInt32(&p.blockIdle) == 0 + } + return ps.idlePeers(61, 61, idle) +} + +// BodyIdlePeers retrieves a flat list of all the currently body-idle peers within +// the active peer set, ordered by their reputation. +func (ps *peerSet) BodyIdlePeers() ([]*peer, int) { + idle := func(p *peer) bool { + return atomic.LoadInt32(&p.blockIdle) == 0 + } + return ps.idlePeers(62, 64, idle) +} + +// ReceiptIdlePeers retrieves a flat list of all the currently receipt-idle peers +// within the active peer set, ordered by their reputation. +func (ps *peerSet) ReceiptIdlePeers() ([]*peer, int) { + idle := func(p *peer) bool { + return atomic.LoadInt32(&p.receiptIdle) == 0 + } + return ps.idlePeers(63, 64, idle) +} + +// NodeDataIdlePeers retrieves a flat list of all the currently node-data-idle +// peers within the active peer set, ordered by their reputation. +func (ps *peerSet) NodeDataIdlePeers() ([]*peer, int) { + idle := func(p *peer) bool { + return atomic.LoadInt32(&p.stateIdle) == 0 + } + return ps.idlePeers(63, 64, idle) +} + +// idlePeers retrieves a flat list of all currently idle peers satisfying the +// protocol version constraints, using the provided function to check idleness. +func (ps *peerSet) idlePeers(minProtocol, maxProtocol int, idleCheck func(*peer) bool) ([]*peer, int) { ps.lock.RLock() defer ps.lock.RUnlock() - list := make([]*peer, 0, len(ps.peers)) + idle, total := make([]*peer, 0, len(ps.peers)), 0 for _, p := range ps.peers { - if atomic.LoadInt32(&p.idle) == 0 { - list = append(list, p) + if p.version >= minProtocol && p.version <= maxProtocol { + if idleCheck(p) { + idle = append(idle, p) + } + total++ } } - for i := 0; i < len(list); i++ { - for j := i + 1; j < len(list); j++ { - if atomic.LoadInt32(&list[i].rep) < atomic.LoadInt32(&list[j].rep) { - list[i], list[j] = list[j], list[i] + for i := 0; i < len(idle); i++ { + for j := i + 1; j < len(idle); j++ { + if atomic.LoadInt32(&idle[i].rep) < atomic.LoadInt32(&idle[j].rep) { + idle[i], idle[j] = idle[j], idle[i] } } } - return list + return idle, total } diff --git a/eth/downloader/queue.go b/eth/downloader/queue.go index 49d1046fb..56b46e285 100644 --- a/eth/downloader/queue.go +++ b/eth/downloader/queue.go @@ -23,25 +23,32 @@ import ( "errors" "fmt" "sync" + "sync/atomic" "time" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/logger" "github.com/ethereum/go-ethereum/logger/glog" + "github.com/ethereum/go-ethereum/trie" + "github.com/rcrowley/go-metrics" "gopkg.in/karalabe/cookiejar.v2/collections/prque" ) var ( - blockCacheLimit = 8 * MaxBlockFetch // Maximum number of blocks to cache before throttling the download + blockCacheLimit = 1024 // Maximum number of blocks to cache before throttling the download ) var ( errNoFetchesPending = errors.New("no fetches pending") + errStateSyncPending = errors.New("state trie sync already scheduled") errStaleDelivery = errors.New("stale delivery") ) -// fetchRequest is a currently running block retrieval operation. +// fetchRequest is a currently running data retrieval operation. type fetchRequest struct { Peer *peer // Peer to which the request was sent Hashes map[common.Hash]int // [eth/61] Requested hashes with their insertion index (priority) @@ -49,35 +56,72 @@ type fetchRequest struct { Time time.Time // Time when the request was made } +// fetchResult is a struct collecting partial results from data fetchers until +// all outstanding pieces complete and the result as a whole can be processed. +type fetchResult struct { + Pending int // Number of data fetches still pending + + Header *types.Header + Uncles []*types.Header + Transactions types.Transactions + Receipts types.Receipts +} + // queue represents hashes that are either need fetching or are being fetched type queue struct { + mode SyncMode // Synchronisation mode to decide on the block parts to schedule for fetching + fastSyncPivot uint64 // Block number where the fast sync pivots into archive synchronisation mode + hashPool map[common.Hash]int // [eth/61] Pending hashes, mapping to their insertion index (priority) hashQueue *prque.Prque // [eth/61] Priority queue of the block hashes to fetch hashCounter int // [eth/61] Counter indexing the added hashes to ensure retrieval order - headerPool map[common.Hash]*types.Header // [eth/62] Pending headers, mapping from their hashes - headerQueue *prque.Prque // [eth/62] Priority queue of the headers to fetch the bodies for - headerHead common.Hash // [eth/62] Hash of the last queued header to verify order + headerHead common.Hash // [eth/62] Hash of the last queued header to verify order + + blockTaskPool map[common.Hash]*types.Header // [eth/62] Pending block (body) retrieval tasks, mapping hashes to headers + blockTaskQueue *prque.Prque // [eth/62] Priority queue of the headers to fetch the blocks (bodies) for + blockPendPool map[string]*fetchRequest // [eth/62] Currently pending block (body) retrieval operations + blockDonePool map[common.Hash]struct{} // [eth/62] Set of the completed block (body) fetches + + receiptTaskPool map[common.Hash]*types.Header // [eth/63] Pending receipt retrieval tasks, mapping hashes to headers + receiptTaskQueue *prque.Prque // [eth/63] Priority queue of the headers to fetch the receipts for + receiptPendPool map[string]*fetchRequest // [eth/63] Currently pending receipt retrieval operations + receiptDonePool map[common.Hash]struct{} // [eth/63] Set of the completed receipt fetches + + stateTaskIndex int // [eth/63] Counter indexing the added hashes to ensure prioritised retrieval order + stateTaskPool map[common.Hash]int // [eth/63] Pending node data retrieval tasks, mapping to their priority + stateTaskQueue *prque.Prque // [eth/63] Priority queue of the hashes to fetch the node data for + statePendPool map[string]*fetchRequest // [eth/63] Currently pending node data retrieval operations - pendPool map[string]*fetchRequest // Currently pending block retrieval operations + stateDatabase ethdb.Database // [eth/63] Trie database to populate during state reassembly + stateScheduler *state.StateSync // [eth/63] State trie synchronisation scheduler and integrator + stateProcessors int32 // [eth/63] Number of currently running state processors + stateSchedLock sync.RWMutex // [eth/63] Lock serialising access to the state scheduler - blockPool map[common.Hash]uint64 // Hash-set of the downloaded data blocks, mapping to cache indexes - blockCache []*Block // Downloaded but not yet delivered blocks - blockOffset uint64 // Offset of the first cached block in the block-chain + resultCache []*fetchResult // Downloaded but not yet delivered fetch results + resultOffset uint64 // Offset of the first cached fetch result in the block chain lock sync.RWMutex } // newQueue creates a new download queue for scheduling block retrieval. -func newQueue() *queue { +func newQueue(stateDb ethdb.Database) *queue { return &queue{ - hashPool: make(map[common.Hash]int), - hashQueue: prque.New(), - headerPool: make(map[common.Hash]*types.Header), - headerQueue: prque.New(), - pendPool: make(map[string]*fetchRequest), - blockPool: make(map[common.Hash]uint64), - blockCache: make([]*Block, blockCacheLimit), + hashPool: make(map[common.Hash]int), + hashQueue: prque.New(), + blockTaskPool: make(map[common.Hash]*types.Header), + blockTaskQueue: prque.New(), + blockPendPool: make(map[string]*fetchRequest), + blockDonePool: make(map[common.Hash]struct{}), + receiptTaskPool: make(map[common.Hash]*types.Header), + receiptTaskQueue: prque.New(), + receiptPendPool: make(map[string]*fetchRequest), + receiptDonePool: make(map[common.Hash]struct{}), + stateTaskPool: make(map[common.Hash]int), + stateTaskQueue: prque.New(), + statePendPool: make(map[string]*fetchRequest), + stateDatabase: stateDb, + resultCache: make([]*fetchResult, blockCacheLimit), } } @@ -86,85 +130,156 @@ func (q *queue) Reset() { q.lock.Lock() defer q.lock.Unlock() + q.stateSchedLock.Lock() + defer q.stateSchedLock.Unlock() + + q.mode = FullSync + q.fastSyncPivot = 0 + q.hashPool = make(map[common.Hash]int) q.hashQueue.Reset() q.hashCounter = 0 - q.headerPool = make(map[common.Hash]*types.Header) - q.headerQueue.Reset() q.headerHead = common.Hash{} - q.pendPool = make(map[string]*fetchRequest) + q.blockTaskPool = make(map[common.Hash]*types.Header) + q.blockTaskQueue.Reset() + q.blockPendPool = make(map[string]*fetchRequest) + q.blockDonePool = make(map[common.Hash]struct{}) + + q.receiptTaskPool = make(map[common.Hash]*types.Header) + q.receiptTaskQueue.Reset() + q.receiptPendPool = make(map[string]*fetchRequest) + q.receiptDonePool = make(map[common.Hash]struct{}) + + q.stateTaskIndex = 0 + q.stateTaskPool = make(map[common.Hash]int) + q.stateTaskQueue.Reset() + q.statePendPool = make(map[string]*fetchRequest) + q.stateScheduler = nil + + q.resultCache = make([]*fetchResult, blockCacheLimit) + q.resultOffset = 0 +} + +// PendingBlocks retrieves the number of block (body) requests pending for retrieval. +func (q *queue) PendingBlocks() int { + q.lock.RLock() + defer q.lock.RUnlock() + + return q.hashQueue.Size() + q.blockTaskQueue.Size() +} + +// PendingReceipts retrieves the number of block receipts pending for retrieval. +func (q *queue) PendingReceipts() int { + q.lock.RLock() + defer q.lock.RUnlock() + + return q.receiptTaskQueue.Size() +} + +// PendingNodeData retrieves the number of node data entries pending for retrieval. +func (q *queue) PendingNodeData() int { + q.stateSchedLock.RLock() + defer q.stateSchedLock.RUnlock() + + if q.stateScheduler != nil { + return q.stateScheduler.Pending() + } + return 0 +} + +// InFlightBlocks retrieves whether there are block fetch requests currently in +// flight. +func (q *queue) InFlightBlocks() bool { + q.lock.RLock() + defer q.lock.RUnlock() - q.blockPool = make(map[common.Hash]uint64) - q.blockOffset = 0 - q.blockCache = make([]*Block, blockCacheLimit) + return len(q.blockPendPool) > 0 } -// Size retrieves the number of blocks in the queue, returning separately for -// pending and already downloaded. -func (q *queue) Size() (int, int) { +// InFlightReceipts retrieves whether there are receipt fetch requests currently +// in flight. +func (q *queue) InFlightReceipts() bool { q.lock.RLock() defer q.lock.RUnlock() - return len(q.hashPool) + len(q.headerPool), len(q.blockPool) + return len(q.receiptPendPool) > 0 } -// Pending retrieves the number of blocks pending for retrieval. -func (q *queue) Pending() int { +// InFlightNodeData retrieves whether there are node data entry fetch requests +// currently in flight. +func (q *queue) InFlightNodeData() bool { q.lock.RLock() defer q.lock.RUnlock() - return q.hashQueue.Size() + q.headerQueue.Size() + return len(q.statePendPool)+int(atomic.LoadInt32(&q.stateProcessors)) > 0 } -// InFlight retrieves the number of fetch requests currently in flight. -func (q *queue) InFlight() int { +// Idle returns if the queue is fully idle or has some data still inside. This +// method is used by the tester to detect termination events. +func (q *queue) Idle() bool { q.lock.RLock() defer q.lock.RUnlock() - return len(q.pendPool) + queued := q.hashQueue.Size() + q.blockTaskQueue.Size() + q.receiptTaskQueue.Size() + q.stateTaskQueue.Size() + pending := len(q.blockPendPool) + len(q.receiptPendPool) + len(q.statePendPool) + cached := len(q.blockDonePool) + len(q.receiptDonePool) + + q.stateSchedLock.RLock() + if q.stateScheduler != nil { + queued += q.stateScheduler.Pending() + } + q.stateSchedLock.RUnlock() + + return (queued + pending + cached) == 0 } -// Throttle checks if the download should be throttled (active block fetches -// exceed block cache). -func (q *queue) Throttle() bool { +// FastSyncPivot retrieves the currently used fast sync pivot point. +func (q *queue) FastSyncPivot() uint64 { q.lock.RLock() defer q.lock.RUnlock() - // Calculate the currently in-flight block requests + return q.fastSyncPivot +} + +// ShouldThrottleBlocks checks if the download should be throttled (active block (body) +// fetches exceed block cache). +func (q *queue) ShouldThrottleBlocks() bool { + q.lock.RLock() + defer q.lock.RUnlock() + + // Calculate the currently in-flight block (body) requests pending := 0 - for _, request := range q.pendPool { + for _, request := range q.blockPendPool { pending += len(request.Hashes) + len(request.Headers) } - // Throttle if more blocks are in-flight than free space in the cache - return pending >= len(q.blockCache)-len(q.blockPool) + // Throttle if more blocks (bodies) are in-flight than free space in the cache + return pending >= len(q.resultCache)-len(q.blockDonePool) } -// Has checks if a hash is within the download queue or not. -func (q *queue) Has(hash common.Hash) bool { +// ShouldThrottleReceipts checks if the download should be throttled (active receipt +// fetches exceed block cache). +func (q *queue) ShouldThrottleReceipts() bool { q.lock.RLock() defer q.lock.RUnlock() - if _, ok := q.hashPool[hash]; ok { - return true - } - if _, ok := q.headerPool[hash]; ok { - return true - } - if _, ok := q.blockPool[hash]; ok { - return true + // Calculate the currently in-flight receipt requests + pending := 0 + for _, request := range q.receiptPendPool { + pending += len(request.Headers) } - return false + // Throttle if more receipts are in-flight than free space in the cache + return pending >= len(q.resultCache)-len(q.receiptDonePool) } -// Insert61 adds a set of hashes for the download queue for scheduling, returning +// Schedule61 adds a set of hashes for the download queue for scheduling, returning // the new hashes encountered. -func (q *queue) Insert61(hashes []common.Hash, fifo bool) []common.Hash { +func (q *queue) Schedule61(hashes []common.Hash, fifo bool) []common.Hash { q.lock.Lock() defer q.lock.Unlock() - // Insert all the hashes prioritized in the arrival order + // Insert all the hashes prioritised in the arrival order inserts := make([]common.Hash, 0, len(hashes)) for _, hash := range hashes { // Skip anything we already have @@ -186,22 +301,17 @@ func (q *queue) Insert61(hashes []common.Hash, fifo bool) []common.Hash { return inserts } -// Insert adds a set of headers for the download queue for scheduling, returning +// Schedule adds a set of headers for the download queue for scheduling, returning // the new headers encountered. -func (q *queue) Insert(headers []*types.Header, from uint64) []*types.Header { +func (q *queue) Schedule(headers []*types.Header, from uint64) []*types.Header { q.lock.Lock() defer q.lock.Unlock() - // Insert all the headers prioritized by the contained block number + // Insert all the headers prioritised by the contained block number inserts := make([]*types.Header, 0, len(headers)) for _, header := range headers { - // Make sure no duplicate requests are executed + // Make sure chain order is honoured and preserved throughout hash := header.Hash() - if _, ok := q.headerPool[hash]; ok { - glog.V(logger.Warn).Infof("Header #%d [%x] already scheduled", header.Number.Uint64(), hash[:4]) - continue - } - // Make sure chain order is honored and preserved throughout if header.Number == nil || header.Number.Uint64() != from { glog.V(logger.Warn).Infof("Header #%v [%x] broke chain ordering, expected %d", header.Number, hash[:4], from) break @@ -210,96 +320,187 @@ func (q *queue) Insert(headers []*types.Header, from uint64) []*types.Header { glog.V(logger.Warn).Infof("Header #%v [%x] broke chain ancestry", header.Number, hash[:4]) break } - // Queue the header for body retrieval + // Make sure no duplicate requests are executed + if _, ok := q.blockTaskPool[hash]; ok { + glog.V(logger.Warn).Infof("Header #%d [%x] already scheduled for block fetch", header.Number.Uint64(), hash[:4]) + continue + } + if _, ok := q.receiptTaskPool[hash]; ok { + glog.V(logger.Warn).Infof("Header #%d [%x] already scheduled for receipt fetch", header.Number.Uint64(), hash[:4]) + continue + } + // Queue the header for content retrieval + q.blockTaskPool[hash] = header + q.blockTaskQueue.Push(header, -float32(header.Number.Uint64())) + + if q.mode == FastSync && header.Number.Uint64() <= q.fastSyncPivot { + // Fast phase of the fast sync, retrieve receipts too + q.receiptTaskPool[hash] = header + q.receiptTaskQueue.Push(header, -float32(header.Number.Uint64())) + } + if q.mode == FastSync && header.Number.Uint64() == q.fastSyncPivot { + // Pivoting point of the fast sync, retrieve the state tries + q.stateSchedLock.Lock() + q.stateScheduler = state.NewStateSync(header.Root, q.stateDatabase) + q.stateSchedLock.Unlock() + } inserts = append(inserts, header) - q.headerPool[hash] = header - q.headerQueue.Push(header, -float32(header.Number.Uint64())) q.headerHead = hash from++ } return inserts } -// GetHeadBlock retrieves the first block from the cache, or nil if it hasn't +// GetHeadResult retrieves the first fetch result from the cache, or nil if it hasn't // been downloaded yet (or simply non existent). -func (q *queue) GetHeadBlock() *Block { +func (q *queue) GetHeadResult() *fetchResult { q.lock.RLock() defer q.lock.RUnlock() - if len(q.blockCache) == 0 { + // If there are no results pending, return nil + if len(q.resultCache) == 0 || q.resultCache[0] == nil { return nil } - return q.blockCache[0] -} - -// GetBlock retrieves a downloaded block, or nil if non-existent. -func (q *queue) GetBlock(hash common.Hash) *Block { - q.lock.RLock() - defer q.lock.RUnlock() - - // Short circuit if the block hasn't been downloaded yet - index, ok := q.blockPool[hash] - if !ok { + // If the next result is still incomplete, return nil + if q.resultCache[0].Pending > 0 { return nil } - // Return the block if it's still available in the cache - if q.blockOffset <= index && index < q.blockOffset+uint64(len(q.blockCache)) { - return q.blockCache[index-q.blockOffset] + // If the next result is the fast sync pivot... + if q.mode == FastSync && q.resultCache[0].Header.Number.Uint64() == q.fastSyncPivot { + // If the pivot state trie is still being pulled, return nil + if len(q.stateTaskPool) > 0 { + return nil + } + if q.PendingNodeData() > 0 { + return nil + } + // If the state is done, but not enough post-pivot headers were verified, stall... + for i := 0; i < fsHeaderForceVerify; i++ { + if i+1 >= len(q.resultCache) || q.resultCache[i+1] == nil { + return nil + } + } } - return nil + return q.resultCache[0] } -// TakeBlocks retrieves and permanently removes a batch of blocks from the cache. -func (q *queue) TakeBlocks() []*Block { +// TakeResults retrieves and permanently removes a batch of fetch results from +// the cache. +func (q *queue) TakeResults() []*fetchResult { q.lock.Lock() defer q.lock.Unlock() - // Accumulate all available blocks - blocks := []*Block{} - for _, block := range q.blockCache { - if block == nil { + // Accumulate all available results + results := []*fetchResult{} + for i, result := range q.resultCache { + // Stop if no more results are ready + if result == nil || result.Pending > 0 { + break + } + // The fast sync pivot block may only be processed after state fetch completes + if q.mode == FastSync && result.Header.Number.Uint64() == q.fastSyncPivot { + if len(q.stateTaskPool) > 0 { + break + } + if q.PendingNodeData() > 0 { + break + } + // Even is state fetch is done, ensure post-pivot headers passed verifications + safe := true + for j := 0; j < fsHeaderForceVerify; j++ { + if i+j+1 >= len(q.resultCache) || q.resultCache[i+j+1] == nil { + safe = false + } + } + if !safe { + break + } + } + // If we've just inserted the fast sync pivot, stop as the following batch needs different insertion + if q.mode == FastSync && result.Header.Number.Uint64() == q.fastSyncPivot+1 && len(results) > 0 { break } - blocks = append(blocks, block) - delete(q.blockPool, block.RawBlock.Hash()) + results = append(results, result) + + hash := result.Header.Hash() + delete(q.blockDonePool, hash) + delete(q.receiptDonePool, hash) } - // Delete the blocks from the slice and let them be garbage collected - // without this slice trick the blocks would stay in memory until nil - // would be assigned to q.blocks - copy(q.blockCache, q.blockCache[len(blocks):]) - for k, n := len(q.blockCache)-len(blocks), len(q.blockCache); k < n; k++ { - q.blockCache[k] = nil + // Delete the results from the slice and let them be garbage collected + // without this slice trick the results would stay in memory until nil + // would be assigned to them. + copy(q.resultCache, q.resultCache[len(results):]) + for k, n := len(q.resultCache)-len(results), len(q.resultCache); k < n; k++ { + q.resultCache[k] = nil } - q.blockOffset += uint64(len(blocks)) + q.resultOffset += uint64(len(results)) - return blocks + return results } -// Reserve61 reserves a set of hashes for the given peer, skipping any previously -// failed download. -func (q *queue) Reserve61(p *peer, count int) *fetchRequest { +// ReserveBlocks reserves a set of block hashes for the given peer, skipping any +// previously failed download. +func (q *queue) ReserveBlocks(p *peer, count int) *fetchRequest { q.lock.Lock() defer q.lock.Unlock() - // Short circuit if the pool has been depleted, or if the peer's already - // downloading something (sanity check not to corrupt state) - if q.hashQueue.Empty() { - return nil + return q.reserveHashes(p, count, q.hashQueue, nil, q.blockPendPool, len(q.resultCache)-len(q.blockDonePool)) +} + +// ReserveNodeData reserves a set of node data hashes for the given peer, skipping +// any previously failed download. +func (q *queue) ReserveNodeData(p *peer, count int) *fetchRequest { + // Create a task generator to fetch status-fetch tasks if all schedules ones are done + generator := func(max int) { + q.stateSchedLock.Lock() + defer q.stateSchedLock.Unlock() + + if q.stateScheduler != nil { + for _, hash := range q.stateScheduler.Missing(max) { + q.stateTaskPool[hash] = q.stateTaskIndex + q.stateTaskQueue.Push(hash, -float32(q.stateTaskIndex)) + q.stateTaskIndex++ + } + } } - if _, ok := q.pendPool[p.id]; ok { + q.lock.Lock() + defer q.lock.Unlock() + + return q.reserveHashes(p, count, q.stateTaskQueue, generator, q.statePendPool, count) +} + +// reserveHashes reserves a set of hashes for the given peer, skipping previously +// failed ones. +// +// Note, this method expects the queue lock to be already held for writing. The +// reason the lock is not obtained in here is because the parameters already need +// to access the queue, so they already need a lock anyway. +func (q *queue) reserveHashes(p *peer, count int, taskQueue *prque.Prque, taskGen func(int), pendPool map[string]*fetchRequest, maxPending int) *fetchRequest { + // Short circuit if the peer's already downloading something (sanity check to + // not corrupt state) + if _, ok := pendPool[p.id]; ok { return nil } // Calculate an upper limit on the hashes we might fetch (i.e. throttling) - space := len(q.blockCache) - len(q.blockPool) - for _, request := range q.pendPool { - space -= len(request.Hashes) + allowance := maxPending + if allowance > 0 { + for _, request := range pendPool { + allowance -= len(request.Hashes) + } + } + // If there's a task generator, ask it to fill our task queue + if taskGen != nil && taskQueue.Size() < allowance { + taskGen(allowance - taskQueue.Size()) + } + if taskQueue.Empty() { + return nil } // Retrieve a batch of hashes, skipping previously failed ones send := make(map[common.Hash]int) skip := make(map[common.Hash]int) - for proc := 0; proc < space && len(send) < count && !q.hashQueue.Empty(); proc++ { - hash, priority := q.hashQueue.Pop() + for proc := 0; (allowance == 0 || proc < allowance) && len(send) < count && !taskQueue.Empty(); proc++ { + hash, priority := taskQueue.Pop() if p.ignored.Has(hash) { skip[hash.(common.Hash)] = int(priority) } else { @@ -308,7 +509,7 @@ func (q *queue) Reserve61(p *peer, count int) *fetchRequest { } // Merge all the skipped hashes back for hash, index := range skip { - q.hashQueue.Push(hash, float32(index)) + taskQueue.Push(hash, float32(index)) } // Assemble and return the block download request if len(send) == 0 { @@ -319,49 +520,93 @@ func (q *queue) Reserve61(p *peer, count int) *fetchRequest { Hashes: send, Time: time.Now(), } - q.pendPool[p.id] = request + pendPool[p.id] = request return request } -// Reserve reserves a set of headers for the given peer, skipping any previously -// failed download. Beside the next batch of needed fetches, it also returns a -// flag whether empty blocks were queued requiring processing. -func (q *queue) Reserve(p *peer, count int) (*fetchRequest, bool, error) { +// ReserveBodies reserves a set of body fetches for the given peer, skipping any +// previously failed downloads. Beside the next batch of needed fetches, it also +// returns a flag whether empty blocks were queued requiring processing. +func (q *queue) ReserveBodies(p *peer, count int) (*fetchRequest, bool, error) { + isNoop := func(header *types.Header) bool { + return header.TxHash == types.EmptyRootHash && header.UncleHash == types.EmptyUncleHash + } q.lock.Lock() defer q.lock.Unlock() + return q.reserveHeaders(p, count, q.blockTaskPool, q.blockTaskQueue, q.blockPendPool, q.blockDonePool, isNoop) +} + +// ReserveReceipts reserves a set of receipt fetches for the given peer, skipping +// any previously failed downloads. Beside the next batch of needed fetches, it +// also returns a flag whether empty receipts were queued requiring importing. +func (q *queue) ReserveReceipts(p *peer, count int) (*fetchRequest, bool, error) { + isNoop := func(header *types.Header) bool { + return header.ReceiptHash == types.EmptyRootHash + } + q.lock.Lock() + defer q.lock.Unlock() + + return q.reserveHeaders(p, count, q.receiptTaskPool, q.receiptTaskQueue, q.receiptPendPool, q.receiptDonePool, isNoop) +} + +// reserveHeaders reserves a set of data download operations for a given peer, +// skipping any previously failed ones. This method is a generic version used +// by the individual special reservation functions. +// +// Note, this method expects the queue lock to be already held for writing. The +// reason the lock is not obtained in here is because the parameters already need +// to access the queue, so they already need a lock anyway. +func (q *queue) reserveHeaders(p *peer, count int, taskPool map[common.Hash]*types.Header, taskQueue *prque.Prque, + pendPool map[string]*fetchRequest, donePool map[common.Hash]struct{}, isNoop func(*types.Header) bool) (*fetchRequest, bool, error) { // Short circuit if the pool has been depleted, or if the peer's already // downloading something (sanity check not to corrupt state) - if q.headerQueue.Empty() { + if taskQueue.Empty() { return nil, false, nil } - if _, ok := q.pendPool[p.id]; ok { + if _, ok := pendPool[p.id]; ok { return nil, false, nil } - // Calculate an upper limit on the bodies we might fetch (i.e. throttling) - space := len(q.blockCache) - len(q.blockPool) - for _, request := range q.pendPool { + // Calculate an upper limit on the items we might fetch (i.e. throttling) + space := len(q.resultCache) - len(donePool) + for _, request := range pendPool { space -= len(request.Headers) } - // Retrieve a batch of headers, skipping previously failed ones + // Retrieve a batch of tasks, skipping previously failed ones send := make([]*types.Header, 0, count) skip := make([]*types.Header, 0) - process := false - for proc := 0; proc < space && len(send) < count && !q.headerQueue.Empty(); proc++ { - header := q.headerQueue.PopItem().(*types.Header) + progress := false + for proc := 0; proc < space && len(send) < count && !taskQueue.Empty(); proc++ { + header := taskQueue.PopItem().(*types.Header) - // If the header defines an empty block, deliver straight - if header.TxHash == types.DeriveSha(types.Transactions{}) && header.UncleHash == types.CalcUncleHash([]*types.Header{}) { - if err := q.enqueue("", types.NewBlockWithHeader(header)); err != nil { - return nil, false, errInvalidChain + // If we're the first to request this task, initialise the result container + index := int(header.Number.Int64() - int64(q.resultOffset)) + if index >= len(q.resultCache) || index < 0 { + return nil, false, errInvalidChain + } + if q.resultCache[index] == nil { + components := 1 + if q.mode == FastSync && header.Number.Uint64() <= q.fastSyncPivot { + components = 2 } - delete(q.headerPool, header.Hash()) - process, space, proc = true, space-1, proc-1 + q.resultCache[index] = &fetchResult{ + Pending: components, + Header: header, + } + } + // If this fetch task is a noop, skip this fetch operation + if isNoop(header) { + donePool[header.Hash()] = struct{}{} + delete(taskPool, header.Hash()) + + space, proc = space-1, proc-1 + q.resultCache[index].Pending-- + progress = true continue } - // If it's a content block, add to the body fetch request + // Otherwise unless the peer is known not to have the data, add to the retrieve list if p.ignored.Has(header.Hash()) { skip = append(skip, header) } else { @@ -370,81 +615,168 @@ func (q *queue) Reserve(p *peer, count int) (*fetchRequest, bool, error) { } // Merge all the skipped headers back for _, header := range skip { - q.headerQueue.Push(header, -float32(header.Number.Uint64())) + taskQueue.Push(header, -float32(header.Number.Uint64())) } // Assemble and return the block download request if len(send) == 0 { - return nil, process, nil + return nil, progress, nil } request := &fetchRequest{ Peer: p, Headers: send, Time: time.Now(), } - q.pendPool[p.id] = request + pendPool[p.id] = request + + return request, progress, nil +} + +// CancelBlocks aborts a fetch request, returning all pending hashes to the queue. +func (q *queue) CancelBlocks(request *fetchRequest) { + q.cancel(request, q.hashQueue, q.blockPendPool) +} - return request, process, nil +// CancelBodies aborts a body fetch request, returning all pending headers to the +// task queue. +func (q *queue) CancelBodies(request *fetchRequest) { + q.cancel(request, q.blockTaskQueue, q.blockPendPool) } -// Cancel aborts a fetch request, returning all pending hashes to the queue. -func (q *queue) Cancel(request *fetchRequest) { +// CancelReceipts aborts a body fetch request, returning all pending headers to +// the task queue. +func (q *queue) CancelReceipts(request *fetchRequest) { + q.cancel(request, q.receiptTaskQueue, q.receiptPendPool) +} + +// CancelNodeData aborts a node state data fetch request, returning all pending +// hashes to the task queue. +func (q *queue) CancelNodeData(request *fetchRequest) { + q.cancel(request, q.stateTaskQueue, q.statePendPool) +} + +// Cancel aborts a fetch request, returning all pending hashes to the task queue. +func (q *queue) cancel(request *fetchRequest, taskQueue *prque.Prque, pendPool map[string]*fetchRequest) { q.lock.Lock() defer q.lock.Unlock() for hash, index := range request.Hashes { - q.hashQueue.Push(hash, float32(index)) + taskQueue.Push(hash, float32(index)) } for _, header := range request.Headers { - q.headerQueue.Push(header, -float32(header.Number.Uint64())) + taskQueue.Push(header, -float32(header.Number.Uint64())) + } + delete(pendPool, request.Peer.id) +} + +// Revoke cancels all pending requests belonging to a given peer. This method is +// meant to be called during a peer drop to quickly reassign owned data fetches +// to remaining nodes. +func (q *queue) Revoke(peerId string) { + q.lock.Lock() + defer q.lock.Unlock() + + if request, ok := q.blockPendPool[peerId]; ok { + for hash, index := range request.Hashes { + q.hashQueue.Push(hash, float32(index)) + } + for _, header := range request.Headers { + q.blockTaskQueue.Push(header, -float32(header.Number.Uint64())) + } + delete(q.blockPendPool, peerId) + } + if request, ok := q.receiptPendPool[peerId]; ok { + for _, header := range request.Headers { + q.receiptTaskQueue.Push(header, -float32(header.Number.Uint64())) + } + delete(q.receiptPendPool, peerId) + } + if request, ok := q.statePendPool[peerId]; ok { + for hash, index := range request.Hashes { + q.stateTaskQueue.Push(hash, float32(index)) + } + delete(q.statePendPool, peerId) } - delete(q.pendPool, request.Peer.id) } -// Expire checks for in flight requests that exceeded a timeout allowance, -// canceling them and returning the responsible peers for penalization. -func (q *queue) Expire(timeout time.Duration) []string { +// ExpireBlocks checks for in flight requests that exceeded a timeout allowance, +// canceling them and returning the responsible peers for penalisation. +func (q *queue) ExpireBlocks(timeout time.Duration) []string { q.lock.Lock() defer q.lock.Unlock() + return q.expire(timeout, q.blockPendPool, q.hashQueue, blockTimeoutMeter) +} + +// ExpireBodies checks for in flight block body requests that exceeded a timeout +// allowance, canceling them and returning the responsible peers for penalisation. +func (q *queue) ExpireBodies(timeout time.Duration) []string { + q.lock.Lock() + defer q.lock.Unlock() + + return q.expire(timeout, q.blockPendPool, q.blockTaskQueue, bodyTimeoutMeter) +} + +// ExpireReceipts checks for in flight receipt requests that exceeded a timeout +// allowance, canceling them and returning the responsible peers for penalisation. +func (q *queue) ExpireReceipts(timeout time.Duration) []string { + q.lock.Lock() + defer q.lock.Unlock() + + return q.expire(timeout, q.receiptPendPool, q.receiptTaskQueue, receiptTimeoutMeter) +} + +// ExpireNodeData checks for in flight node data requests that exceeded a timeout +// allowance, canceling them and returning the responsible peers for penalisation. +func (q *queue) ExpireNodeData(timeout time.Duration) []string { + q.lock.Lock() + defer q.lock.Unlock() + + return q.expire(timeout, q.statePendPool, q.stateTaskQueue, stateTimeoutMeter) +} + +// expire is the generic check that move expired tasks from a pending pool back +// into a task pool, returning all entities caught with expired tasks. +// +// Note, this method expects the queue lock to be already held for writing. The +// reason the lock is not obtained in here is because the parameters already need +// to access the queue, so they already need a lock anyway. +func (q *queue) expire(timeout time.Duration, pendPool map[string]*fetchRequest, taskQueue *prque.Prque, timeoutMeter metrics.Meter) []string { // Iterate over the expired requests and return each to the queue peers := []string{} - for id, request := range q.pendPool { + for id, request := range pendPool { if time.Since(request.Time) > timeout { // Update the metrics with the timeout - if len(request.Hashes) > 0 { - blockTimeoutMeter.Mark(1) - } else { - bodyTimeoutMeter.Mark(1) - } + timeoutMeter.Mark(1) + // Return any non satisfied requests to the pool for hash, index := range request.Hashes { - q.hashQueue.Push(hash, float32(index)) + taskQueue.Push(hash, float32(index)) } for _, header := range request.Headers { - q.headerQueue.Push(header, -float32(header.Number.Uint64())) + taskQueue.Push(header, -float32(header.Number.Uint64())) } peers = append(peers, id) } } // Remove the expired requests from the pending pool for _, id := range peers { - delete(q.pendPool, id) + delete(pendPool, id) } return peers } -// Deliver61 injects a block retrieval response into the download queue. -func (q *queue) Deliver61(id string, blocks []*types.Block) (err error) { +// DeliverBlocks injects a block retrieval response into the download queue. +func (q *queue) DeliverBlocks(id string, blocks []*types.Block) error { q.lock.Lock() defer q.lock.Unlock() // Short circuit if the blocks were never requested - request := q.pendPool[id] + request := q.blockPendPool[id] if request == nil { return errNoFetchesPending } blockReqTimer.UpdateSince(request.Time) - delete(q.pendPool, id) + delete(q.blockPendPool, id) // If no blocks were retrieved, mark them as unavailable for the origin peer if len(blocks) == 0 { @@ -461,10 +793,19 @@ func (q *queue) Deliver61(id string, blocks []*types.Block) (err error) { errs = append(errs, fmt.Errorf("non-requested block %x", hash)) continue } - // Queue the block up for processing - if err := q.enqueue(id, block); err != nil { - return err + // Reconstruct the next result if contents match up + index := int(block.Number().Int64() - int64(q.resultOffset)) + if index >= len(q.resultCache) || index < 0 { + errs = []error{errInvalidChain} + break + } + q.resultCache[index] = &fetchResult{ + Header: block.Header(), + Transactions: block.Transactions(), + Uncles: block.Uncles(), } + q.blockDonePool[block.Hash()] = struct{}{} + delete(request.Hashes, hash) delete(q.hashPool, hash) } @@ -473,74 +814,171 @@ func (q *queue) Deliver61(id string, blocks []*types.Block) (err error) { q.hashQueue.Push(hash, float32(index)) } // If none of the blocks were good, it's a stale delivery - if len(errs) != 0 { - if len(errs) == len(blocks) { - return errStaleDelivery - } + switch { + case len(errs) == 0: + return nil + + case len(errs) == 1 && (errs[0] == errInvalidChain || errs[0] == errInvalidBlock): + return errs[0] + + case len(errs) == len(blocks): + return errStaleDelivery + + default: return fmt.Errorf("multiple failures: %v", errs) } - return nil } -// Deliver injects a block body retrieval response into the download queue. -func (q *queue) Deliver(id string, txLists [][]*types.Transaction, uncleLists [][]*types.Header) error { +// DeliverBodies injects a block body retrieval response into the results queue. +func (q *queue) DeliverBodies(id string, txLists [][]*types.Transaction, uncleLists [][]*types.Header) error { + q.lock.Lock() + defer q.lock.Unlock() + + reconstruct := func(header *types.Header, index int, result *fetchResult) error { + if types.DeriveSha(types.Transactions(txLists[index])) != header.TxHash || types.CalcUncleHash(uncleLists[index]) != header.UncleHash { + return errInvalidBody + } + result.Transactions = txLists[index] + result.Uncles = uncleLists[index] + return nil + } + return q.deliver(id, q.blockTaskPool, q.blockTaskQueue, q.blockPendPool, q.blockDonePool, bodyReqTimer, len(txLists), reconstruct) +} + +// DeliverReceipts injects a receipt retrieval response into the results queue. +func (q *queue) DeliverReceipts(id string, receiptList [][]*types.Receipt) error { q.lock.Lock() defer q.lock.Unlock() - // Short circuit if the block bodies were never requested - request := q.pendPool[id] + reconstruct := func(header *types.Header, index int, result *fetchResult) error { + if types.DeriveSha(types.Receipts(receiptList[index])) != header.ReceiptHash { + return errInvalidReceipt + } + result.Receipts = receiptList[index] + return nil + } + return q.deliver(id, q.receiptTaskPool, q.receiptTaskQueue, q.receiptPendPool, q.receiptDonePool, receiptReqTimer, len(receiptList), reconstruct) +} + +// deliver injects a data retrieval response into the results queue. +// +// Note, this method expects the queue lock to be already held for writing. The +// reason the lock is not obtained in here is because the parameters already need +// to access the queue, so they already need a lock anyway. +func (q *queue) deliver(id string, taskPool map[common.Hash]*types.Header, taskQueue *prque.Prque, pendPool map[string]*fetchRequest, + donePool map[common.Hash]struct{}, reqTimer metrics.Timer, results int, reconstruct func(header *types.Header, index int, result *fetchResult) error) error { + // Short circuit if the data was never requested + request := pendPool[id] if request == nil { return errNoFetchesPending } - bodyReqTimer.UpdateSince(request.Time) - delete(q.pendPool, id) + reqTimer.UpdateSince(request.Time) + delete(pendPool, id) - // If no block bodies were retrieved, mark them as unavailable for the origin peer - if len(txLists) == 0 || len(uncleLists) == 0 { + // If no data items were retrieved, mark them as unavailable for the origin peer + if results == 0 { for hash, _ := range request.Headers { request.Peer.ignored.Add(hash) } } - // Assemble each of the block bodies with their headers and queue for processing - errs := make([]error, 0) + // Assemble each of the results with their headers and retrieved data parts + var ( + failure error + useful bool + ) for i, header := range request.Headers { - // Short circuit block assembly if no more bodies are found - if i >= len(txLists) || i >= len(uncleLists) { + // Short circuit assembly if no more fetch results are found + if i >= results { break } - // Reconstruct the next block if contents match up - if types.DeriveSha(types.Transactions(txLists[i])) != header.TxHash || types.CalcUncleHash(uncleLists[i]) != header.UncleHash { - errs = []error{errInvalidBody} + // Reconstruct the next result if contents match up + index := int(header.Number.Int64() - int64(q.resultOffset)) + if index >= len(q.resultCache) || index < 0 || q.resultCache[index] == nil { + failure = errInvalidChain break } - block := types.NewBlockWithHeader(header).WithBody(txLists[i], uncleLists[i]) - - // Queue the block up for processing - if err := q.enqueue(id, block); err != nil { - errs = []error{err} + if err := reconstruct(header, i, q.resultCache[index]); err != nil { + failure = err break } + donePool[header.Hash()] = struct{}{} + q.resultCache[index].Pending-- + useful = true + + // Clean up a successful fetch request.Headers[i] = nil - delete(q.headerPool, header.Hash()) + delete(taskPool, header.Hash()) } // Return all failed or missing fetches to the queue for _, header := range request.Headers { if header != nil { - q.headerQueue.Push(header, -float32(header.Number.Uint64())) + taskQueue.Push(header, -float32(header.Number.Uint64())) } } - // If none of the blocks were good, it's a stale delivery + // If none of the data was good, it's a stale delivery switch { - case len(errs) == 0: - return nil + case failure == nil || failure == errInvalidChain: + return failure - case len(errs) == 1 && errs[0] == errInvalidBody: - return errInvalidBody + case useful: + return fmt.Errorf("partial failure: %v", failure) - case len(errs) == 1 && errs[0] == errInvalidChain: - return errInvalidChain + default: + return errStaleDelivery + } +} + +// DeliverNodeData injects a node state data retrieval response into the queue. +func (q *queue) DeliverNodeData(id string, data [][]byte, callback func(error, int)) error { + q.lock.Lock() + defer q.lock.Unlock() - case len(errs) == len(request.Headers): + // Short circuit if the data was never requested + request := q.statePendPool[id] + if request == nil { + return errNoFetchesPending + } + stateReqTimer.UpdateSince(request.Time) + delete(q.statePendPool, id) + + // If no data was retrieved, mark their hashes as unavailable for the origin peer + if len(data) == 0 { + for hash, _ := range request.Hashes { + request.Peer.ignored.Add(hash) + } + } + // Iterate over the downloaded data and verify each of them + errs := make([]error, 0) + process := []trie.SyncResult{} + for _, blob := range data { + // Skip any blocks that were not requested + hash := common.BytesToHash(crypto.Sha3(blob)) + if _, ok := request.Hashes[hash]; !ok { + errs = append(errs, fmt.Errorf("non-requested state data %x", hash)) + continue + } + // Inject the next state trie item into the processing queue + process = append(process, trie.SyncResult{hash, blob}) + + delete(request.Hashes, hash) + delete(q.stateTaskPool, hash) + } + // Start the asynchronous node state data injection + atomic.AddInt32(&q.stateProcessors, 1) + go func() { + defer atomic.AddInt32(&q.stateProcessors, -1) + q.deliverNodeData(process, callback) + }() + // Return all failed or missing fetches to the queue + for hash, index := range request.Hashes { + q.stateTaskQueue.Push(hash, float32(index)) + } + // If none of the data items were good, it's a stale delivery + switch { + case len(errs) == 0: + return nil + + case len(errs) == len(request.Hashes): return errStaleDelivery default: @@ -548,29 +986,40 @@ func (q *queue) Deliver(id string, txLists [][]*types.Transaction, uncleLists [] } } -// enqueue inserts a new block into the final delivery queue, waiting for pickup -// by the processor. -func (q *queue) enqueue(origin string, block *types.Block) error { - // If a requested block falls out of the range, the hash chain is invalid - index := int(int64(block.NumberU64()) - int64(q.blockOffset)) - if index >= len(q.blockCache) || index < 0 { - return errInvalidChain - } - // Otherwise merge the block and mark the hash done - q.blockCache[index] = &Block{ - RawBlock: block, - OriginPeer: origin, +// deliverNodeData is the asynchronous node data processor that injects a batch +// of sync results into the state scheduler. +func (q *queue) deliverNodeData(results []trie.SyncResult, callback func(error, int)) { + // Process results one by one to permit task fetches in between + for i, result := range results { + q.stateSchedLock.Lock() + + if q.stateScheduler == nil { + // Syncing aborted since this async delivery started, bail out + q.stateSchedLock.Unlock() + callback(errNoFetchesPending, i) + return + } + if _, err := q.stateScheduler.Process([]trie.SyncResult{result}); err != nil { + // Processing a state result failed, bail out + q.stateSchedLock.Unlock() + callback(err, i) + return + } + // Item processing succeeded, release the lock (temporarily) + q.stateSchedLock.Unlock() } - q.blockPool[block.Header().Hash()] = block.NumberU64() - return nil + callback(nil, len(results)) } -// Prepare configures the block cache offset to allow accepting inbound blocks. -func (q *queue) Prepare(offset uint64) { +// Prepare configures the result cache to allow accepting and caching inbound +// fetch results. +func (q *queue) Prepare(offset uint64, mode SyncMode, pivot uint64) { q.lock.Lock() defer q.lock.Unlock() - if q.blockOffset < offset { - q.blockOffset = offset + if q.resultOffset < offset { + q.resultOffset = offset } + q.fastSyncPivot = pivot + q.mode = mode } diff --git a/eth/downloader/types.go b/eth/downloader/types.go new file mode 100644 index 000000000..5937be606 --- /dev/null +++ b/eth/downloader/types.go @@ -0,0 +1,140 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. + +package downloader + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +// headerCheckFn is a callback type for verifying a header's presence in the local chain. +type headerCheckFn func(common.Hash) bool + +// blockCheckFn is a callback type for verifying a block's presence in the local chain. +type blockCheckFn func(common.Hash) bool + +// headerRetrievalFn is a callback type for retrieving a header from the local chain. +type headerRetrievalFn func(common.Hash) *types.Header + +// blockRetrievalFn is a callback type for retrieving a block from the local chain. +type blockRetrievalFn func(common.Hash) *types.Block + +// headHeaderRetrievalFn is a callback type for retrieving the head header from the local chain. +type headHeaderRetrievalFn func() *types.Header + +// headBlockRetrievalFn is a callback type for retrieving the head block from the local chain. +type headBlockRetrievalFn func() *types.Block + +// headFastBlockRetrievalFn is a callback type for retrieving the head fast block from the local chain. +type headFastBlockRetrievalFn func() *types.Block + +// headBlockCommitterFn is a callback for directly committing the head block to a certain entity. +type headBlockCommitterFn func(common.Hash) error + +// tdRetrievalFn is a callback type for retrieving the total difficulty of a local block. +type tdRetrievalFn func(common.Hash) *big.Int + +// headerChainInsertFn is a callback type to insert a batch of headers into the local chain. +type headerChainInsertFn func([]*types.Header, int) (int, error) + +// blockChainInsertFn is a callback type to insert a batch of blocks into the local chain. +type blockChainInsertFn func(types.Blocks) (int, error) + +// receiptChainInsertFn is a callback type to insert a batch of receipts into the local chain. +type receiptChainInsertFn func(types.Blocks, []types.Receipts) (int, error) + +// chainRollbackFn is a callback type to remove a few recently added elements from the local chain. +type chainRollbackFn func([]common.Hash) + +// peerDropFn is a callback type for dropping a peer detected as malicious. +type peerDropFn func(id string) + +// dataPack is a data message returned by a peer for some query. +type dataPack interface { + PeerId() string + Items() int + Stats() string +} + +// hashPack is a batch of block hashes returned by a peer (eth/61). +type hashPack struct { + peerId string + hashes []common.Hash +} + +func (p *hashPack) PeerId() string { return p.peerId } +func (p *hashPack) Items() int { return len(p.hashes) } +func (p *hashPack) Stats() string { return fmt.Sprintf("%d", len(p.hashes)) } + +// blockPack is a batch of blocks returned by a peer (eth/61). +type blockPack struct { + peerId string + blocks []*types.Block +} + +func (p *blockPack) PeerId() string { return p.peerId } +func (p *blockPack) Items() int { return len(p.blocks) } +func (p *blockPack) Stats() string { return fmt.Sprintf("%d", len(p.blocks)) } + +// headerPack is a batch of block headers returned by a peer. +type headerPack struct { + peerId string + headers []*types.Header +} + +func (p *headerPack) PeerId() string { return p.peerId } +func (p *headerPack) Items() int { return len(p.headers) } +func (p *headerPack) Stats() string { return fmt.Sprintf("%d", len(p.headers)) } + +// bodyPack is a batch of block bodies returned by a peer. +type bodyPack struct { + peerId string + transactions [][]*types.Transaction + uncles [][]*types.Header +} + +func (p *bodyPack) PeerId() string { return p.peerId } +func (p *bodyPack) Items() int { + if len(p.transactions) <= len(p.uncles) { + return len(p.transactions) + } + return len(p.uncles) +} +func (p *bodyPack) Stats() string { return fmt.Sprintf("%d:%d", len(p.transactions), len(p.uncles)) } + +// receiptPack is a batch of receipts returned by a peer. +type receiptPack struct { + peerId string + receipts [][]*types.Receipt +} + +func (p *receiptPack) PeerId() string { return p.peerId } +func (p *receiptPack) Items() int { return len(p.receipts) } +func (p *receiptPack) Stats() string { return fmt.Sprintf("%d", len(p.receipts)) } + +// statePack is a batch of states returned by a peer. +type statePack struct { + peerId string + states [][]byte +} + +func (p *statePack) PeerId() string { return p.peerId } +func (p *statePack) Items() int { return len(p.states) } +func (p *statePack) Stats() string { return fmt.Sprintf("%d", len(p.states)) } |