eth: improve shutdown synchronization (#20695)

* eth: improve shutdown synchronization

Most goroutines started by eth.Ethereum didn't have any shutdown sync at
all, which lead to weird error messages when quitting the client.

This change improves the clean shutdown path by stopping all internal
components in dependency order and waiting for them to actually be
stopped before shutdown is considered done. In particular, we now stop
everything related to peers before stopping 'resident' parts such as
core.BlockChain.

* eth: rewrite sync controller

* eth: remove sync start debug message

* eth: notify chainSyncer about new peers after handshake

* eth: move downloader.Cancel call into chainSyncer

* eth: make post-sync block broadcast synchronous

* eth: add comments

* core: change blockchain stop message

* eth: change closeBloomHandler channel type
This commit is contained in:
Felix Lange 2020-03-27 14:03:20 +01:00 committed by GitHub
parent d7851e6359
commit d6c5f2417c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 213 additions and 138 deletions

@ -897,7 +897,7 @@ func (bc *BlockChain) Stop() {
log.Error("Dangling trie nodes after full cleanup") log.Error("Dangling trie nodes after full cleanup")
} }
} }
log.Info("Blockchain manager stopped") log.Info("Blockchain stopped")
} }
func (bc *BlockChain) procFutureBlocks() { func (bc *BlockChain) procFutureBlocks() {

@ -67,9 +67,6 @@ type LesServer interface {
type Ethereum struct { type Ethereum struct {
config *Config config *Config
// Channel for shutting down the service
shutdownChan chan bool
// Handlers // Handlers
txPool *core.TxPool txPool *core.TxPool
blockchain *core.BlockChain blockchain *core.BlockChain
@ -84,8 +81,9 @@ type Ethereum struct {
engine consensus.Engine engine consensus.Engine
accountManager *accounts.Manager accountManager *accounts.Manager
bloomRequests chan chan *bloombits.Retrieval // Channel receiving bloom data retrieval requests bloomRequests chan chan *bloombits.Retrieval // Channel receiving bloom data retrieval requests
bloomIndexer *core.ChainIndexer // Bloom indexer operating during block imports bloomIndexer *core.ChainIndexer // Bloom indexer operating during block imports
closeBloomHandler chan struct{}
APIBackend *EthAPIBackend APIBackend *EthAPIBackend
@ -145,17 +143,17 @@ func New(ctx *node.ServiceContext, config *Config) (*Ethereum, error) {
log.Info("Initialised chain configuration", "config", chainConfig) log.Info("Initialised chain configuration", "config", chainConfig)
eth := &Ethereum{ eth := &Ethereum{
config: config, config: config,
chainDb: chainDb, chainDb: chainDb,
eventMux: ctx.EventMux, eventMux: ctx.EventMux,
accountManager: ctx.AccountManager, accountManager: ctx.AccountManager,
engine: CreateConsensusEngine(ctx, chainConfig, &config.Ethash, config.Miner.Notify, config.Miner.Noverify, chainDb), engine: CreateConsensusEngine(ctx, chainConfig, &config.Ethash, config.Miner.Notify, config.Miner.Noverify, chainDb),
shutdownChan: make(chan bool), closeBloomHandler: make(chan struct{}),
networkID: config.NetworkId, networkID: config.NetworkId,
gasPrice: config.Miner.GasPrice, gasPrice: config.Miner.GasPrice,
etherbase: config.Miner.Etherbase, etherbase: config.Miner.Etherbase,
bloomRequests: make(chan chan *bloombits.Retrieval), bloomRequests: make(chan chan *bloombits.Retrieval),
bloomIndexer: NewBloomIndexer(chainDb, params.BloomBitsBlocks, params.BloomConfirms), bloomIndexer: NewBloomIndexer(chainDb, params.BloomBitsBlocks, params.BloomConfirms),
} }
bcVersion := rawdb.ReadDatabaseVersion(chainDb) bcVersion := rawdb.ReadDatabaseVersion(chainDb)
@ -557,18 +555,20 @@ func (s *Ethereum) Start(srvr *p2p.Server) error {
// Stop implements node.Service, terminating all internal goroutines used by the // Stop implements node.Service, terminating all internal goroutines used by the
// Ethereum protocol. // Ethereum protocol.
func (s *Ethereum) Stop() error { func (s *Ethereum) Stop() error {
s.bloomIndexer.Close() // Stop all the peer-related stuff first.
s.blockchain.Stop()
s.engine.Close()
s.protocolManager.Stop() s.protocolManager.Stop()
if s.lesServer != nil { if s.lesServer != nil {
s.lesServer.Stop() s.lesServer.Stop()
} }
// Then stop everything else.
s.bloomIndexer.Close()
close(s.closeBloomHandler)
s.txPool.Stop() s.txPool.Stop()
s.miner.Stop() s.miner.Stop()
s.eventMux.Stop() s.blockchain.Stop()
s.engine.Close()
s.chainDb.Close() s.chainDb.Close()
close(s.shutdownChan) s.eventMux.Stop()
return nil return nil
} }

@ -54,7 +54,7 @@ func (eth *Ethereum) startBloomHandlers(sectionSize uint64) {
go func() { go func() {
for { for {
select { select {
case <-eth.shutdownChan: case <-eth.closeBloomHandler:
return return
case request := <-eth.bloomRequests: case request := <-eth.bloomRequests:

@ -87,14 +87,12 @@ type ProtocolManager struct {
whitelist map[uint64]common.Hash whitelist map[uint64]common.Hash
// channels for fetcher, syncer, txsyncLoop // channels for fetcher, syncer, txsyncLoop
newPeerCh chan *peer txsyncCh chan *txsync
txsyncCh chan *txsync quitSync chan struct{}
quitSync chan struct{}
noMorePeers chan struct{}
// wait group is used for graceful shutdowns during downloading chainSync *chainSyncer
// and processing wg sync.WaitGroup
wg sync.WaitGroup peerWG sync.WaitGroup
// Test fields or hooks // Test fields or hooks
broadcastTxAnnouncesOnly bool // Testing field, disable transaction propagation broadcastTxAnnouncesOnly bool // Testing field, disable transaction propagation
@ -105,18 +103,17 @@ type ProtocolManager struct {
func NewProtocolManager(config *params.ChainConfig, checkpoint *params.TrustedCheckpoint, mode downloader.SyncMode, networkID uint64, mux *event.TypeMux, txpool txPool, engine consensus.Engine, blockchain *core.BlockChain, chaindb ethdb.Database, cacheLimit int, whitelist map[uint64]common.Hash) (*ProtocolManager, error) { func NewProtocolManager(config *params.ChainConfig, checkpoint *params.TrustedCheckpoint, mode downloader.SyncMode, networkID uint64, mux *event.TypeMux, txpool txPool, engine consensus.Engine, blockchain *core.BlockChain, chaindb ethdb.Database, cacheLimit int, whitelist map[uint64]common.Hash) (*ProtocolManager, error) {
// Create the protocol manager with the base fields // Create the protocol manager with the base fields
manager := &ProtocolManager{ manager := &ProtocolManager{
networkID: networkID, networkID: networkID,
forkFilter: forkid.NewFilter(blockchain), forkFilter: forkid.NewFilter(blockchain),
eventMux: mux, eventMux: mux,
txpool: txpool, txpool: txpool,
blockchain: blockchain, blockchain: blockchain,
peers: newPeerSet(), peers: newPeerSet(),
whitelist: whitelist, whitelist: whitelist,
newPeerCh: make(chan *peer), txsyncCh: make(chan *txsync),
noMorePeers: make(chan struct{}), quitSync: make(chan struct{}),
txsyncCh: make(chan *txsync),
quitSync: make(chan struct{}),
} }
if mode == downloader.FullSync { if mode == downloader.FullSync {
// The database seems empty as the current block is the genesis. Yet the fast // The database seems empty as the current block is the genesis. Yet the fast
// block is ahead, so fast sync was enabled for this node at a certain point. // block is ahead, so fast sync was enabled for this node at a certain point.
@ -140,6 +137,7 @@ func NewProtocolManager(config *params.ChainConfig, checkpoint *params.TrustedCh
manager.fastSync = uint32(1) manager.fastSync = uint32(1)
} }
} }
// If we have trusted checkpoints, enforce them on the chain // If we have trusted checkpoints, enforce them on the chain
if checkpoint != nil { if checkpoint != nil {
manager.checkpointNumber = (checkpoint.SectionIndex+1)*params.CHTFrequency - 1 manager.checkpointNumber = (checkpoint.SectionIndex+1)*params.CHTFrequency - 1
@ -199,6 +197,8 @@ func NewProtocolManager(config *params.ChainConfig, checkpoint *params.TrustedCh
} }
manager.txFetcher = fetcher.NewTxFetcher(txpool.Has, txpool.AddRemotes, fetchTx) manager.txFetcher = fetcher.NewTxFetcher(txpool.Has, txpool.AddRemotes, fetchTx)
manager.chainSync = newChainSyncer(manager)
return manager, nil return manager, nil
} }
@ -213,15 +213,7 @@ func (pm *ProtocolManager) makeProtocol(version uint) p2p.Protocol {
Version: version, Version: version,
Length: length, Length: length,
Run: func(p *p2p.Peer, rw p2p.MsgReadWriter) error { Run: func(p *p2p.Peer, rw p2p.MsgReadWriter) error {
peer := pm.newPeer(int(version), p, rw, pm.txpool.Get) return pm.runPeer(pm.newPeer(int(version), p, rw, pm.txpool.Get))
select {
case pm.newPeerCh <- peer:
pm.wg.Add(1)
defer pm.wg.Done()
return pm.handle(peer)
case <-pm.quitSync:
return p2p.DiscQuitting
}
}, },
NodeInfo: func() interface{} { NodeInfo: func() interface{} {
return pm.NodeInfo() return pm.NodeInfo()
@ -260,40 +252,37 @@ func (pm *ProtocolManager) Start(maxPeers int) {
pm.maxPeers = maxPeers pm.maxPeers = maxPeers
// broadcast transactions // broadcast transactions
pm.wg.Add(1)
pm.txsCh = make(chan core.NewTxsEvent, txChanSize) pm.txsCh = make(chan core.NewTxsEvent, txChanSize)
pm.txsSub = pm.txpool.SubscribeNewTxsEvent(pm.txsCh) pm.txsSub = pm.txpool.SubscribeNewTxsEvent(pm.txsCh)
go pm.txBroadcastLoop() go pm.txBroadcastLoop()
// broadcast mined blocks // broadcast mined blocks
pm.wg.Add(1)
pm.minedBlockSub = pm.eventMux.Subscribe(core.NewMinedBlockEvent{}) pm.minedBlockSub = pm.eventMux.Subscribe(core.NewMinedBlockEvent{})
go pm.minedBroadcastLoop() go pm.minedBroadcastLoop()
// start sync handlers // start sync handlers
go pm.syncer() pm.wg.Add(2)
go pm.chainSync.loop()
go pm.txsyncLoop64() // TODO(karalabe): Legacy initial tx echange, drop with eth/64. go pm.txsyncLoop64() // TODO(karalabe): Legacy initial tx echange, drop with eth/64.
} }
func (pm *ProtocolManager) Stop() { func (pm *ProtocolManager) Stop() {
log.Info("Stopping Ethereum protocol")
pm.txsSub.Unsubscribe() // quits txBroadcastLoop pm.txsSub.Unsubscribe() // quits txBroadcastLoop
pm.minedBlockSub.Unsubscribe() // quits blockBroadcastLoop pm.minedBlockSub.Unsubscribe() // quits blockBroadcastLoop
// Quit the sync loop. // Quit chainSync and txsync64.
// After this send has completed, no new peers will be accepted. // After this is done, no new peers will be accepted.
pm.noMorePeers <- struct{}{}
// Quit fetcher, txsyncLoop.
close(pm.quitSync) close(pm.quitSync)
pm.wg.Wait()
// Disconnect existing sessions. // Disconnect existing sessions.
// This also closes the gate for any new registrations on the peer set. // This also closes the gate for any new registrations on the peer set.
// sessions which are already established but not added to pm.peers yet // sessions which are already established but not added to pm.peers yet
// will exit when they try to register. // will exit when they try to register.
pm.peers.Close() pm.peers.Close()
pm.peerWG.Wait()
// Wait for all peer handler goroutines and the loops to come down.
pm.wg.Wait()
log.Info("Ethereum protocol stopped") log.Info("Ethereum protocol stopped")
} }
@ -302,6 +291,15 @@ func (pm *ProtocolManager) newPeer(pv int, p *p2p.Peer, rw p2p.MsgReadWriter, ge
return newPeer(pv, p, rw, getPooledTx) return newPeer(pv, p, rw, getPooledTx)
} }
func (pm *ProtocolManager) runPeer(p *peer) error {
if !pm.chainSync.handlePeerEvent(p) {
return p2p.DiscQuitting
}
pm.peerWG.Add(1)
defer pm.peerWG.Done()
return pm.handle(p)
}
// handle is the callback invoked to manage the life cycle of an eth peer. When // handle is the callback invoked to manage the life cycle of an eth peer. When
// this function terminates, the peer is disconnected. // this function terminates, the peer is disconnected.
func (pm *ProtocolManager) handle(p *peer) error { func (pm *ProtocolManager) handle(p *peer) error {
@ -323,6 +321,7 @@ func (pm *ProtocolManager) handle(p *peer) error {
p.Log().Debug("Ethereum handshake failed", "err", err) p.Log().Debug("Ethereum handshake failed", "err", err)
return err return err
} }
// Register the peer locally // Register the peer locally
if err := pm.peers.Register(p); err != nil { if err := pm.peers.Register(p); err != nil {
p.Log().Error("Ethereum peer registration failed", "err", err) p.Log().Error("Ethereum peer registration failed", "err", err)
@ -334,6 +333,8 @@ func (pm *ProtocolManager) handle(p *peer) error {
if err := pm.downloader.RegisterPeer(p.id, p.version, p); err != nil { if err := pm.downloader.RegisterPeer(p.id, p.version, p); err != nil {
return err return err
} }
pm.chainSync.handlePeerEvent(p)
// Propagate existing transactions. new transactions appearing // Propagate existing transactions. new transactions appearing
// after this will be sent via broadcasts. // after this will be sent via broadcasts.
pm.syncTransactions(p) pm.syncTransactions(p)
@ -723,14 +724,7 @@ func (pm *ProtocolManager) handleMsg(p *peer) error {
// Update the peer's total difficulty if better than the previous // Update the peer's total difficulty if better than the previous
if _, td := p.Head(); trueTD.Cmp(td) > 0 { if _, td := p.Head(); trueTD.Cmp(td) > 0 {
p.SetHead(trueHead, trueTD) p.SetHead(trueHead, trueTD)
pm.chainSync.handlePeerEvent(p)
// Schedule a sync if above ours. Note, this will not fire a sync for a gap of
// a single block (as the true TD is below the propagated block), however this
// scenario should easily be covered by the fetcher.
currentHeader := pm.blockchain.CurrentHeader()
if trueTD.Cmp(pm.blockchain.GetTd(currentHeader.Hash(), currentHeader.Number.Uint64())) > 0 {
go pm.synchronise(p)
}
} }
case msg.Code == NewPooledTransactionHashesMsg && p.version >= eth65: case msg.Code == NewPooledTransactionHashesMsg && p.version >= eth65:
@ -883,9 +877,10 @@ func (pm *ProtocolManager) BroadcastTransactions(txs types.Transactions, propaga
} }
} }
// Mined broadcast loop // minedBroadcastLoop sends mined blocks to connected peers.
func (pm *ProtocolManager) minedBroadcastLoop() { func (pm *ProtocolManager) minedBroadcastLoop() {
// automatically stops if unsubscribe defer pm.wg.Done()
for obj := range pm.minedBlockSub.Chan() { for obj := range pm.minedBlockSub.Chan() {
if ev, ok := obj.Data.(core.NewMinedBlockEvent); ok { if ev, ok := obj.Data.(core.NewMinedBlockEvent); ok {
pm.BroadcastBlock(ev.Block, true) // First propagate block to peers pm.BroadcastBlock(ev.Block, true) // First propagate block to peers
@ -894,7 +889,10 @@ func (pm *ProtocolManager) minedBroadcastLoop() {
} }
} }
// txBroadcastLoop announces new transactions to connected peers.
func (pm *ProtocolManager) txBroadcastLoop() { func (pm *ProtocolManager) txBroadcastLoop() {
defer pm.wg.Done()
for { for {
select { select {
case event := <-pm.txsCh: case event := <-pm.txsCh:
@ -906,7 +904,6 @@ func (pm *ProtocolManager) txBroadcastLoop() {
pm.BroadcastTransactions(event.Txs, true) // First propagate transactions to peers pm.BroadcastTransactions(event.Txs, true) // First propagate transactions to peers
pm.BroadcastTransactions(event.Txs, false) // Only then announce to the rest pm.BroadcastTransactions(event.Txs, false) // Only then announce to the rest
// Err() channel will be closed when unsubscribing.
case <-pm.txsSub.Err(): case <-pm.txsSub.Err():
return return
} }

@ -170,23 +170,14 @@ func newTestPeer(name string, version int, pm *ProtocolManager, shake bool) (*te
// Create a message pipe to communicate through // Create a message pipe to communicate through
app, net := p2p.MsgPipe() app, net := p2p.MsgPipe()
// Generate a random id and create the peer // Start the peer on a new thread
var id enode.ID var id enode.ID
rand.Read(id[:]) rand.Read(id[:])
peer := pm.newPeer(version, p2p.NewPeer(id, name, nil), net, pm.txpool.Get) peer := pm.newPeer(version, p2p.NewPeer(id, name, nil), net, pm.txpool.Get)
// Start the peer on a new thread
errc := make(chan error, 1) errc := make(chan error, 1)
go func() { go func() { errc <- pm.runPeer(peer) }()
select {
case pm.newPeerCh <- peer:
errc <- pm.handle(peer)
case <-pm.quitSync:
errc <- p2p.DiscQuitting
}
}()
tp := &testPeer{app: app, net: net, peer: peer} tp := &testPeer{app: app, net: net, peer: peer}
// Execute any implicitly requested handshakes and return // Execute any implicitly requested handshakes and return
if shake { if shake {
var ( var (

@ -385,7 +385,7 @@ func testSyncTransaction(t *testing.T, propagtion bool) {
go pmFetcher.handle(pmFetcher.newPeer(65, p2p.NewPeer(enode.ID{}, "fetcher", nil), io1, pmFetcher.txpool.Get)) go pmFetcher.handle(pmFetcher.newPeer(65, p2p.NewPeer(enode.ID{}, "fetcher", nil), io1, pmFetcher.txpool.Get))
time.Sleep(250 * time.Millisecond) time.Sleep(250 * time.Millisecond)
pmFetcher.synchronise(pmFetcher.peers.BestPeer()) pmFetcher.doSync(peerToSyncOp(downloader.FullSync, pmFetcher.peers.BestPeer()))
atomic.StoreUint32(&pmFetcher.acceptTxs, 1) atomic.StoreUint32(&pmFetcher.acceptTxs, 1)
newTxs := make(chan core.NewTxsEvent, 1024) newTxs := make(chan core.NewTxsEvent, 1024)

@ -17,6 +17,7 @@
package eth package eth
import ( import (
"math/big"
"math/rand" "math/rand"
"sync/atomic" "sync/atomic"
"time" "time"
@ -30,9 +31,9 @@ import (
const ( const (
forceSyncCycle = 10 * time.Second // Time interval to force syncs, even if few peers are available forceSyncCycle = 10 * time.Second // Time interval to force syncs, even if few peers are available
minDesiredPeerCount = 5 // Amount of peers desired to start syncing defaultMinSyncPeers = 5 // Amount of peers desired to start syncing
// This is the target size for the packs of transactions sent by txsyncLoop. // This is the target size for the packs of transactions sent by txsyncLoop64.
// A pack can get larger than this if a single transactions exceeds this size. // A pack can get larger than this if a single transactions exceeds this size.
txsyncPackSize = 100 * 1024 txsyncPackSize = 100 * 1024
) )
@ -81,12 +82,15 @@ func (pm *ProtocolManager) syncTransactions(p *peer) {
// transactions. In order to minimise egress bandwidth usage, we send // transactions. In order to minimise egress bandwidth usage, we send
// the transactions in small packs to one peer at a time. // the transactions in small packs to one peer at a time.
func (pm *ProtocolManager) txsyncLoop64() { func (pm *ProtocolManager) txsyncLoop64() {
defer pm.wg.Done()
var ( var (
pending = make(map[enode.ID]*txsync) pending = make(map[enode.ID]*txsync)
sending = false // whether a send is active sending = false // whether a send is active
pack = new(txsync) // the pack that is being sent pack = new(txsync) // the pack that is being sent
done = make(chan error, 1) // result of the send done = make(chan error, 1) // result of the send
) )
// send starts a sending a pack of transactions from the sync. // send starts a sending a pack of transactions from the sync.
send := func(s *txsync) { send := func(s *txsync) {
if s.p.version >= eth65 { if s.p.version >= eth65 {
@ -149,73 +153,148 @@ func (pm *ProtocolManager) txsyncLoop64() {
} }
} }
// syncer is responsible for periodically synchronising with the network, both // chainSyncer coordinates blockchain sync components.
// downloading hashes and blocks as well as handling the announcement handler. type chainSyncer struct {
func (pm *ProtocolManager) syncer() { pm *ProtocolManager
// Start and ensure cleanup of sync mechanisms force *time.Timer
pm.blockFetcher.Start() forced bool // true when force timer fired
pm.txFetcher.Start() peerEventCh chan struct{}
defer pm.blockFetcher.Stop() doneCh chan error // non-nil when sync is running
defer pm.txFetcher.Stop() }
defer pm.downloader.Terminate()
// Wait for different events to fire synchronisation operations // chainSyncOp is a scheduled sync operation.
forceSync := time.NewTicker(forceSyncCycle) type chainSyncOp struct {
defer forceSync.Stop() mode downloader.SyncMode
peer *peer
td *big.Int
head common.Hash
}
// newChainSyncer creates a chainSyncer.
func newChainSyncer(pm *ProtocolManager) *chainSyncer {
return &chainSyncer{
pm: pm,
peerEventCh: make(chan struct{}),
}
}
// handlePeerEvent notifies the syncer about a change in the peer set.
// This is called for new peers and every time a peer announces a new
// chain head.
func (cs *chainSyncer) handlePeerEvent(p *peer) bool {
select {
case cs.peerEventCh <- struct{}{}:
return true
case <-cs.pm.quitSync:
return false
}
}
// loop runs in its own goroutine and launches the sync when necessary.
func (cs *chainSyncer) loop() {
defer cs.pm.wg.Done()
cs.pm.blockFetcher.Start()
cs.pm.txFetcher.Start()
defer cs.pm.blockFetcher.Stop()
defer cs.pm.txFetcher.Stop()
defer cs.pm.downloader.Terminate()
// The force timer lowers the peer count threshold down to one when it fires.
// This ensures we'll always start sync even if there aren't enough peers.
cs.force = time.NewTimer(forceSyncCycle)
defer cs.force.Stop()
for { for {
if op := cs.nextSyncOp(); op != nil {
cs.startSync(op)
}
select { select {
case <-pm.newPeerCh: case <-cs.peerEventCh:
// Make sure we have peers to select from, then sync // Peer information changed, recheck.
if pm.peers.Len() < minDesiredPeerCount { case <-cs.doneCh:
break cs.doneCh = nil
cs.force.Reset(forceSyncCycle)
cs.forced = false
case <-cs.force.C:
cs.forced = true
case <-cs.pm.quitSync:
if cs.doneCh != nil {
cs.pm.downloader.Cancel()
<-cs.doneCh
} }
go pm.synchronise(pm.peers.BestPeer())
case <-forceSync.C:
// Force a sync even if not enough peers are present
go pm.synchronise(pm.peers.BestPeer())
case <-pm.noMorePeers:
return return
} }
} }
} }
// synchronise tries to sync up our local block chain with a remote peer. // nextSyncOp determines whether sync is required at this time.
func (pm *ProtocolManager) synchronise(peer *peer) { func (cs *chainSyncer) nextSyncOp() *chainSyncOp {
// Short circuit if no peers are available if cs.doneCh != nil {
if peer == nil { return nil // Sync already running.
return
} }
// Make sure the peer's TD is higher than our own
currentHeader := pm.blockchain.CurrentHeader()
td := pm.blockchain.GetTd(currentHeader.Hash(), currentHeader.Number.Uint64())
pHead, pTd := peer.Head() // Ensure we're at mininum peer count.
if pTd.Cmp(td) <= 0 { minPeers := defaultMinSyncPeers
return if cs.forced {
minPeers = 1
} else if minPeers > cs.pm.maxPeers {
minPeers = cs.pm.maxPeers
} }
// Otherwise try to sync with the downloader if cs.pm.peers.Len() < minPeers {
mode := downloader.FullSync return nil
if atomic.LoadUint32(&pm.fastSync) == 1 {
// Fast sync was explicitly requested, and explicitly granted
mode = downloader.FastSync
} }
if mode == downloader.FastSync {
// Make sure the peer's total difficulty we are synchronizing is higher. // We have enough peers, check TD.
if pm.blockchain.GetTdByHash(pm.blockchain.CurrentFastBlock().Hash()).Cmp(pTd) >= 0 { peer := cs.pm.peers.BestPeer()
return if peer == nil {
} return nil
} }
// Run the sync cycle, and disable fast sync if we've went past the pivot block mode, ourTD := cs.modeAndLocalHead()
if err := pm.downloader.Synchronise(peer.id, pHead, pTd, mode); err != nil { op := peerToSyncOp(mode, peer)
return if op.td.Cmp(ourTD) <= 0 {
return nil // We're in sync.
}
return op
}
func peerToSyncOp(mode downloader.SyncMode, p *peer) *chainSyncOp {
peerHead, peerTD := p.Head()
return &chainSyncOp{mode: mode, peer: p, td: peerTD, head: peerHead}
}
func (cs *chainSyncer) modeAndLocalHead() (downloader.SyncMode, *big.Int) {
if atomic.LoadUint32(&cs.pm.fastSync) == 1 {
block := cs.pm.blockchain.CurrentFastBlock()
td := cs.pm.blockchain.GetTdByHash(block.Hash())
return downloader.FastSync, td
} else {
head := cs.pm.blockchain.CurrentHeader()
td := cs.pm.blockchain.GetTd(head.Hash(), head.Number.Uint64())
return downloader.FullSync, td
}
}
// startSync launches doSync in a new goroutine.
func (cs *chainSyncer) startSync(op *chainSyncOp) {
cs.doneCh = make(chan error, 1)
go func() { cs.doneCh <- cs.pm.doSync(op) }()
}
// doSync synchronizes the local blockchain with a remote peer.
func (pm *ProtocolManager) doSync(op *chainSyncOp) error {
// Run the sync cycle, and disable fast sync if we're past the pivot block
err := pm.downloader.Synchronise(op.peer.id, op.head, op.td, op.mode)
if err != nil {
return err
} }
if atomic.LoadUint32(&pm.fastSync) == 1 { if atomic.LoadUint32(&pm.fastSync) == 1 {
log.Info("Fast sync complete, auto disabling") log.Info("Fast sync complete, auto disabling")
atomic.StoreUint32(&pm.fastSync, 0) atomic.StoreUint32(&pm.fastSync, 0)
} }
// If we've successfully finished a sync cycle and passed any required checkpoint, // If we've successfully finished a sync cycle and passed any required checkpoint,
// enable accepting transactions from the network. // enable accepting transactions from the network.
head := pm.blockchain.CurrentBlock() head := pm.blockchain.CurrentBlock()
@ -226,6 +305,7 @@ func (pm *ProtocolManager) synchronise(peer *peer) {
atomic.StoreUint32(&pm.acceptTxs, 1) atomic.StoreUint32(&pm.acceptTxs, 1)
} }
} }
if head.NumberU64() > 0 { if head.NumberU64() > 0 {
// We've completed a sync cycle, notify all peers of new state. This path is // We've completed a sync cycle, notify all peers of new state. This path is
// essential in star-topology networks where a gateway node needs to notify // essential in star-topology networks where a gateway node needs to notify
@ -233,6 +313,8 @@ func (pm *ProtocolManager) synchronise(peer *peer) {
// scenario will most often crop up in private and hackathon networks with // scenario will most often crop up in private and hackathon networks with
// degenerate connectivity, but it should be healthy for the mainnet too to // degenerate connectivity, but it should be healthy for the mainnet too to
// more reliably update peers or the local TD state. // more reliably update peers or the local TD state.
go pm.BroadcastBlock(head, false) pm.BroadcastBlock(head, false)
} }
return nil
} }

@ -33,6 +33,8 @@ func TestFastSyncDisabling65(t *testing.T) { testFastSyncDisabling(t, 65) }
// Tests that fast sync gets disabled as soon as a real block is successfully // Tests that fast sync gets disabled as soon as a real block is successfully
// imported into the blockchain. // imported into the blockchain.
func testFastSyncDisabling(t *testing.T, protocol int) { func testFastSyncDisabling(t *testing.T, protocol int) {
t.Parallel()
// Create a pristine protocol manager, check that fast sync is left enabled // Create a pristine protocol manager, check that fast sync is left enabled
pmEmpty, _ := newTestProtocolManagerMust(t, downloader.FastSync, 0, nil, nil) pmEmpty, _ := newTestProtocolManagerMust(t, downloader.FastSync, 0, nil, nil)
if atomic.LoadUint32(&pmEmpty.fastSync) == 0 { if atomic.LoadUint32(&pmEmpty.fastSync) == 0 {
@ -43,14 +45,17 @@ func testFastSyncDisabling(t *testing.T, protocol int) {
if atomic.LoadUint32(&pmFull.fastSync) == 1 { if atomic.LoadUint32(&pmFull.fastSync) == 1 {
t.Fatalf("fast sync not disabled on non-empty blockchain") t.Fatalf("fast sync not disabled on non-empty blockchain")
} }
// Sync up the two peers // Sync up the two peers
io1, io2 := p2p.MsgPipe() io1, io2 := p2p.MsgPipe()
go pmFull.handle(pmFull.newPeer(protocol, p2p.NewPeer(enode.ID{}, "empty", nil), io2, pmFull.txpool.Get)) go pmFull.handle(pmFull.newPeer(protocol, p2p.NewPeer(enode.ID{}, "empty", nil), io2, pmFull.txpool.Get))
go pmEmpty.handle(pmEmpty.newPeer(protocol, p2p.NewPeer(enode.ID{}, "full", nil), io1, pmEmpty.txpool.Get)) go pmEmpty.handle(pmEmpty.newPeer(protocol, p2p.NewPeer(enode.ID{}, "full", nil), io1, pmEmpty.txpool.Get))
time.Sleep(250 * time.Millisecond) time.Sleep(250 * time.Millisecond)
pmEmpty.synchronise(pmEmpty.peers.BestPeer()) op := peerToSyncOp(downloader.FastSync, pmEmpty.peers.BestPeer())
if err := pmEmpty.doSync(op); err != nil {
t.Fatal("sync failed:", err)
}
// Check that fast sync was disabled // Check that fast sync was disabled
if atomic.LoadUint32(&pmEmpty.fastSync) == 1 { if atomic.LoadUint32(&pmEmpty.fastSync) == 1 {