Downloader synchronization in the analysis of Ethereum source code

Time:2021-5-13

Downloader synchronization in the analysis of Ethereum source code

Need to cooperate with comment code to seehttps://github.com/blockchain…

This article is a long one. It’s a man who can read it. I suggest you collect it

I hope readers can point out the problems, pay attention to them and discuss them together.

overview

downloaderThe code for the module is located ineth/downloaderUnder the directory. The main function codes are as follows:

  • downloader.goThe block synchronization logic is implemented
  • peer.go: for the assembly of each stage of the block, the following steps are as followsFetchXXXIt depends on this module.
  • queue.go: Yeseth/peer.goEncapsulation of
  • statesync.go: synchronizationstateobject

Synchronization mode

### full sync

Full mode saves all block data in the database, synchronizes header and body data from remote nodes, while state and receive data are calculated locally.

In full mode, the downloader synchronizes the header and body data of the block to form a block, and then uses theBlockChain.InsertChainInsert a chunk into the database. stayBlockChain.InsertChainIn, the values of each block are calculated and verified one by onestateandrecepitAnd so on. If everything is normal, the block data and the data calculated by yourself will be usedstaterecepitThe data is written to the database together.

fast sync

fastIn this mode,recepitIt is no longer calculated locally, but directly calculated by thedownloaderSynchronization from other nodes;stateInstead of all the data being calculated and downloaded, a newer block (called thepivot)OfstateDownload, with this block as the boundary, the previous block is not availablestateData, and subsequent blocks will look likefullIn this mode, it is calculated locallystate. So infastIn this mode, the synchronized dataheaderAnd the body, andreceipt, andpivotBlockstate

thereforefastThe pattern ignores most of themstateData, and use the network to synchronize directlyreceiptData mode replaces local calculation in full mode, so it is faster.

light sync

Light mode is also called light mode. It only synchronizes the block header, but not other data.

SyncMode:

  • Fullsync: synchronizes the entire blockchain history from a complete block
  • Fastsync: download the title quickly and synchronize completely at the head of the chain
  • Lightsync: Download title only, then terminate

Block download process

The picture is just a general description, but actually it should be combined with the code,Collection of all blockchain related articleshttps://github.com/blockchain…

At the same time, I hope to get to know more people in the blockchain circle. I can star the above projects and keep them updated

Downloader synchronization in the analysis of Ethereum source code

First, according toSynchroniseStart block synchronization byfindAncestorFind the common ancestor of the specified node, synchronize at this height, and turn on multiple nodes at the same timegoroutineSynchronize different data:headerreceiptbody. If you synchronize a block with a height of 100, you must firstheaderThe synchronization is successful. You can wake up only after the synchronization is completedbodyandreceiptsSynchronization of.

And the synchronization of each part is roughly controlled byFetchPartsTo complete, which contains the variousChanIn addition, many callback functions will be involved. In a word, if you read more than once, you will have a different understanding each time. Next, we will analyze these key contents step by step.


synchronise

① : make sure the TD of the other party is higher than our own

currentBlock := pm.blockchain.CurrentBlock()
    td := pm.blockchain.GetTd(currentBlock.Hash(), currentBlock.NumberU64())
    pHead, pTd := peer.Head()
    if pTd.Cmp(td) <= 0 {
        return
    }

② : ondownloaderSynchronization of

pm.downloader.Synchronise(peer.id, pHead, pTd, mode)

Enter function: mainly do the following things:

  1. d.synchronise(id, head, td, mode): synchronization process
  2. Error log output and delete thispeer

Enter intod.synchroniseTo the last stepd.syncWithPeer(p, hash, td)Really turn on synchronization.

func (d *Downloader) synchronise(id string, hash common.Hash, td *big.Int, mode SyncMode) error {
  ...
  return d.syncWithPeer(p, hash, td)
}

Syncwithpeer does the following things:

  1. Find ancestorsfindAncestor
  2. Turn on separategoroutineRun the following functions respectively:

    • fetchHeaders
    • processHeaders
    • fetchbodies
    • fetchReceipts
    • processFastSyncContent
    • processFullSyncContent

The next article, and the whole storyDownloaderThe main content of the module is around these parts.


findAncestor

Synchronization is, first and foremostDetermine the interval of synchronous block: the top is the highest block of the remote node, and the bottom is the highest height (ancestor block) of the same block owned by both nodes.findAncestorIt’s used to find ancestors. The function analysis is as follows

① : determine the local height and the maximum height of the remote node

var (
        Floor = Int64 (- 1) // bottom
        Localheight Uint64 // local maximum height
        Remoteheight = remoteheader. Number. Uint64() // maximum height of remote node
    )
switch d.mode {
    case FullSync:
        localHeight = d.blockchain.CurrentBlock().NumberU64()
    case FastSync:
        localHeight = d.blockchain.CurrentFastBlock().NumberU64()
    default:
        localHeight = d.lightchain.CurrentHeader().Number.Uint64()
    }

② : calculate the height interval and interval of synchronization

from, count, skip, max := calculateRequestSpan(remoteHeight, localHeight) 
  • from:: indicates the height from which to get the block
  • count: indicates how many blocks are obtained from the remote node
  • skip: denotes an interval, such asskipIf the first height is 5, then the second height is 8
  • max: indicates the maximum height

③ : send getheaderRequest for

go p.peer.RequestHeadersByNumber(uint64(from), count, skip, false)

④ : process the data received by the above requestheader :case packet := <-d.headerCh

  1. Discard content that does not come from our request section
  2. Make sure that you returnheaderQuantity is not empty
  3. Verify the returnedheadersIt’s the height we asked for
  4. Check if a common ancestor is found
//----①
if packet.PeerId() != p.id {
                log.Debug("Received headers from incorrect peer", "peer", packet.PeerId())
                break
            }
//-----②
headers := packet.(*headerPack).headers
            if len(headers) == 0 {
                p.log.Warn("Empty head header set")
        return 0
      }
//-----③
for i, header := range headers {
                expectNumber := from + int64(i)*int64(skip+1)
                if number := header.Number.Int64();  number !=  Expectnumber {// verify that the returned headers are the ones we requested above
                    p.log.Warn("Head headers broke chain ordering", "index", i, "requested", expectNumber, "received", number)
                    return 0, errInvalidChain
                }
            }
//-----④
//Check if a common ancestor is found
            finished = true
            //Note that the search starts with the last element of headers, that is, the block with the highest height.
            for i := len(headers) - 1; i >= 0; i-- {
                //Skip blocks that are not within the height range we requested
                if headers[i].Number.Int64() < from || headers[i].Number.Uint64() > max {
                    continue
                }
                //// check whether there is a local block. If there is, it means that we have found a common ancestor,
                //The hash and height of the common ancestor are set in the number and hash variables.
                h := headers[i].Hash()
                n := headers[i].Number.Uint64()

⑤ : if the common ancestor is found by the fixed interval method, return to the ancestor and compare its height with the common ancestorfloorVariable,floorThe variable represents the minimum height of the common ancestor. If the height of the common ancestor is smaller than this value, it is considered that the bifurcation between the two nodes is too large to allow synchronization. If all is well, return to the height of the common ancestor foundnumberVariable.

if hash != (common.Hash{}) {
        if int64(number) <= floor {
            return 0, errInvalidAncestor
        }
        return number, nil
    }

⑥ : if the fixed interval method does not find ancestors, we can use dichotomy to find ancestors. This part is similar to dichotomy algorithm, and you can take a closer look if you are interested.


Details of queue

queueObjects andDownloaderObjects interact,DownloaderWe can’t do without it. Let’s introduce this part,You can skip it firstWait until you read aboutQueueCall some of the functions, and then go back to this section.

Queue structure

type queue struct {
  Mode syncmode // synchronization mode
  
  //Header processing related
  Headerhead common. Hash // the hash value of the last queued header to verify the order
  Headertaskpool map [Uint64] * types. Header // the header retrieval task to be processed, mapping the starting index to the frame header
  Headertaskqueue * prque. Prque // the priority queue of the skeleton index to get the padding header used for
  Headerpeermiss map [string] map [Uint64] struct {} // known unavailable peer batch set
  Headerpendpool map [string] * fetchrequest // the currently pending header retrieval operation
  Headerresults [] * types. Header // the result cache accumulates the completed headers
  Headerproced int // gets the processed header from the result
  Headercontch Chan bool // the channel to be notified when the download is completed
  
  Blocktaskpool map [common. Hash] * types. Header // the block (body) retrieval task to be processed, and map the hash to the header
  Blocktaskqueue * prque. Prque // the priority queue of the header, which is used to obtain the blocks (bodies)
  Blockpendpool map [string] * fetchrequest // the current block (body) retrieval operation in progress
  Blockdonepool map [common. Hash] struct {} // completed block (body)
  
    Receipttaskpool map [common. Hash] * types. Header // the receipt retrieval task to be processed, and map the hash to the header
    The priority queue of the receipttaskqueue * prque. Prque // header to obtain the receipt
    Receiptpendpool map [string] * fetchrequest // current receipt retrieval operation in progress
    Receiptdonepool map [common. Hash] struct {} // completed receipt
    
    Resultcache [] fetchresult // downloaded but not delivered yet
    Resultoffset Uint64 // the offset of the first cached result in the blockchain
    Resultsize common.storagesize // approximate block size

    lock   *sync.Mutex
    active *sync.Cond
    closed bool
  
}

Main subdivision functions

Data download starts to schedule tasks

  • ScheduleSkeleton:A batch ofheaderThe retrieval task is added to the queue to fill in the retrieved tasksheader skeleton
  • Schedule:To prepare for somebodyandreceiptData download

Various states in data download

  • pending

    pendingIndicates the number of XXX requests to be retrieved, including:PendingHeadersPendingBlocksPendingReceipts, respectivelyXXXTaskQueueThe length of the.

  • InFlight

    InFlightIndicates whether there is a request to obtain XXX, including:InFlightHeadersInFlightBlocksInFlightReceiptsIt’s all through judgmentlen(q.receiptPendPool) > 0To confirm.

  • ShouldThrottle

    ShouldThrottleIndicates whether the download of XXX should be restricted, including:ShouldThrottleBlocksShouldThrottleReceipts, mainly to prevent the local memory occupation during the download process.

  • Reserve

    ReserveBy constructing afetchRequestStructure and return to provide the caller with the information of a specified number of data to be downloaded(queueInternally, the data will be marked as “downloading”). The caller uses the returnedfetchRequestData to the remote node to initiate a new request to obtain data. include:ReserveHeadersReserveBodiesReserveReceipts

  • Cancel

    CanceUsed to undo a pair offetchRequestData download in structure(queueInternally, the data will be changed from “downloading” to “waiting to download”). include:CancelHeadersCancelBodiesCancelReceipts

  • expire

    expireCheck whether the executing request exceeds the timeout limit, including:ExpireHeadersExpireBodiesExpireReceipts

  • Deliver

    When the data is downloaded successfully, the caller will use thedeliverThe function is used to notifyqueueObject. include:DeliverHeadersDeliverBodiesDeliverReceipts

Data download is completed to obtain block data

  • RetrieveHeaders
    Fill inskeletonWhen it’s done,queue.RetrieveHeadersUsed to get the entireskeletonAll inheader
  • Results
    queue.ResultsUsed to get the currentheaderbodyandreceipt(only infastThe successful blocks have been downloaded (and removed from thequeueInternal removal)

Function implementation

ScheduleSkeleton

The main purpose of queue. Scheduleskeleton is to fill the skeleton. Its parameters are the starting height of the block to be downloaded and all parametersskeletonThe core content of the block is the following cycle:

func (q *queue) ScheduleSkeleton(from uint64, skeleton []*types.Header) {
    ......
    for i, header := range skeleton {
        index := from + uint64(i*y)
        q.headerTaskPool[index] = header
        q.headerTaskQueue.Push(index, -int64(index))
    }
}

Suppose that the height range of blocks to be downloaded is determined to be from 10 to 46,MaxHeaderFetchThe height block is divided into three groups: 10-19, 20-29, and 30-39, while the skeleton is composed of blocks with height of 19, 29, and 39. In the cycleindexThe variable is actually the height of the first block in each group (for example, 10, 20, 30),queue.headerTaskPoolIt’s actually aThe mapping from the height of the first block in each group to the header of the last block

headerTaskPool = {
  10: headerOf_19,
    20: headerOf_20,
    30: headerOf_39,
}

ReserveHeaders

reserveUsed to get downloadable data.

reserve  = func(p *peerConnection, count int) (*fetchRequest, bool, error) {
            return d.queue.ReserveHeaders(p, count), false, nil
        }
func (q *queue) ReserveHeaders(p *peerConnection, count int) *fetchRequest {
  if _, ok := q.headerPendPool[p.id]; ok {
        return nil
    } //①
  ...
  send, skip := uint64(0), []uint64{}
    for send == 0 && !q.headerTaskQueue.Empty() {
        from, _ := q.headerTaskQueue.Pop()
        if q.headerPeerMiss[p.id] != nil {
            if _, ok := q.headerPeerMiss[p.id][from.(uint64)]; ok {
                skip = append(skip, from.(uint64))
                continue
            }
        }
        send = from.(uint64) // ②
    }
  
 ...
  for _, from := range skip {
        q.headerTaskQueue.Push(from, -int64(from))
    } // ③
  ...
  request := &fetchRequest{
        Peer: p,
        From: send,
        Time: time.Now(),
    }
    q.headerPendPool[p.id] = request // ④
  
}

① : according toheaderPendPoolTo determine whether the remote node is downloading data information.

② : fromheaderTaskQueueTake the value as the starting height of this request and assign it tosendVariable. In this process, the information recorded by headerpeermiss that the node failed to download data will be excluded.

③ : write the failed task backtask queue

④ : utilizationsendVariable constructionfetchRequestStructure, which is used asFetchHeadersTo use:

fetch = func(p *peerConnection, req *fetchRequest) error { 
    return p.FetchHeaders(req.From, MaxHeaderFetch) 
}

So far,ReserveHeadersThe minimum starting height is selected from the task queue and constructedfetchRequestPass on tofetchGet the data.


DeliverHeaders

deliver = func(packet dataPack) (int, error) {
            pack := packet.(*headerPack)
            return d.queue.DeliverHeaders(pack.peerID, pack.headers, d.headerProcCh)
        }

① : if it is found that the node downloading the data is not in thequeue.headerPendPoolIn this case, the error is returned directly; Otherwise, the processing continues and the node record is removed from thequeue.headerPendPoolDelete from.

request := q.headerPendPool[id]
    if request == nil {
        return 0, errNoFetchesPending
    }
    headerReqTimer.UpdateSince(request.Time)
    delete(q.headerPendPool, id)

② : validationheaders

It includes three aspects

  1. Check the height and hash of the starting block
  2. Check the connectivity of the height
  3. Check the connectivity of the hash
if accepted {
        //Check the height and hash of the starting block
        if headers[0].Number.Uint64() != request.From {
            ...
            accepted = false
        } else if headers[len(headers)-1].Hash() != target {
            ...
            accepted = false
        }
    }
    if accepted {
        for i, header := range headers[1:] {
            Hash: = header. Hash() // check the connectivity of height
            if want := request.From + 1 + uint64(i); header.Number.Uint64() != want {
                ...
            }
            if headers[i].Hash() !=  Header. Parenthash {// check hash connectivity
                ...
            }
        }
    }

③ : save invalid data toheaderPeerMissAnd put the starting height of this group of blocks back inheaderTaskQueue

if !accepted {
    ...
        miss := q.headerPeerMiss[id]
        if miss == nil {
            q.headerPeerMiss[id] = make(map[uint64]struct{})
            miss = q.headerPeerMiss[id]
        }
        miss[request.From] = struct{}{}
        q.headerTaskQueue.Push(request.From, -int64(request.From))
        return 0, errors.New("delivery not accepted")
    }

④ : save data and notifyheaderProcChDeal with newheader

if ready > 0 {
        process := make([]*types.Header, ready)
        copy(process, q.headerResults[q.headerProced:q.headerProced+ready])
        select {
        case headerProcCh <- process:
            q.headerProced += len(process)
        default:
        }
    }

⑤ : Send a message toheaderContCh, noticeskeletonIt’s all downloaded

if len(q.headerTaskPool) == 0 {
        q.headerContCh <- false
    }

DeliverHeadersThe data will be checked and saved, and the channel message will be sent to theDownloader.processHeadersandDownloader.fetchPartsOfwakeChParameters.


Schedule

processHeadersIn processheaderData, it will callqueue.ScheduleFor downloadbodyandreceiptGet ready.

inserts := d.queue.Schedule(chunk, origin)
func (q *queue) Schedule(headers []*types.Header, from uint64) []*types.Header {
    inserts := make([]*types.Header, 0, len(headers))
    for _, header := range headers {
    //Check
    ...
        q.blockTaskPool[hash] = header
        q.blockTaskQueue.Push(header, -int64(header.Number.Uint64()))

        if q.mode == FastSync {
            q.receiptTaskPool[hash] = header
            q.receiptTaskQueue.Push(header, -int64(header.Number.Uint64()))
        }
        inserts = append(inserts, header)
        q.headerHead = hash
        from++
    }
    return inserts
}

This function mainly writes information to the body and receive queues, waiting for scheduling.


ReserveBody&Receipt

stayqueueWe’re ready in the middlebodyandreceiptRelevant data,processHeadersThe last section is the key code to wake up and download Bodyies and receivers, which will be notifiedfetchBodiesandfetchReceiptsYou can download your own data.

for _, ch := range []chan bool{d.bodyWakeCh, d.receiptWakeCh} {
                select {
                case ch <- true:
                default:
                }
            }

andfetchXXXWill callfetchPartsThe logic is similar to the above,reserveIn the end, thereserveHeadersdeliverThe final call isqueue.deliver.

Let’s analyze firstreserveHeaders

① : if there is no task to process, return directly

if taskQueue.Empty() {
        return nil, false, nil
    }

② : if the node given by the parameter is downloading data, return

 if _, ok := pendPool[p.id]; ok {
        return nil, false, nil
    }

③ : calculates how many more pieces of data can be held in the cache space of the queue object

space := q.resultSlots(pendPool, donePool)

④ : extract tasks from task queue in turn for processing

The main functions are as follows:

  • Calculate the current header in thequeue.resultCacheAnd then fill inqueue.resultCacheThe element in the corresponding position in the
  • Handle the situation of empty block. If it is empty, it will not be downloaded.
  • When the remote node lacks the current block data, if it is found that the node has failed to download the current data, it will not be allowed to download any more.

be careful:resultCacheField is used to record the processing results of all the data being processed. Its element type isfetchResult. itsPendingField represents the current block. There are several types of data to download. There are at most two types of data to download: body and receive,fullOnly need to download in modebodyData, not datafastDownload one more modereceiptData.

for proc := 0; proc < space && len(send) < count && !taskQueue.Empty(); proc++ {
        header := taskQueue.PopItem().(*types.Header)
        hash := header.Hash()
        index := int(header.Number.Int64() - int64(q.resultOffset))
        if index >= len(q.resultCache) || index < 0 {
            ....
        }
        if q.resultCache[index] == nil {
            components := 1
            if q.mode == FastSync {
                components = 2
            }
            q.resultCache[index] = &fetchResult{
                Pending: components,
                Hash:    hash,
                Header:  header,
            }
        }
  
        if isNoop(header) {
            donePool[hash] = struct{}{}
            delete(taskPool, hash)

            space, proc = space-1, proc-1
            q.resultCache[index].Pending--
            progress = true
            continue
        }
        if p.Lacks(hash) {
            skip = append(skip, header)
        } else {
            send = append(send, header)
        }
    }

Finally, constructionfetchRequestStructure and return.


DeliverBodies&Receipts

bodyorreceiptThe data has been passedreserveOperation constructsfetchRequestStructure and transmit tofetchThe next step is to wait for the data to arrive. After the data is downloaded successfully, thequeueObjectdeliverMethods, includingqueue.DeliverBodiesandqueue.DeliverReceipts. Both methods are called with different parametersqueue.delivermethod:

① : if the number of downloaded data is 0, all the downloaded data of this node will be marked as “missing”

if results == 0 {
        for _, header := range request.Headers {
            request.Peer.MarkLacking(header.Hash())
        }
    }

② : loop processing data by callingreconstructfillresultCache[index]The corresponding field in

for i, header := range request.Headers {
  ...
  if err := reconstruct(header, i, q.resultCache[index]); err != nil {
            failure = err
            break
        }
}

③ : validationresultCacheThe correspondingrequest.HeadersInheaderAll should be nil, if not, it means that the verification has not passed, and it needs to be downloaded again in task queue

for _, header := range request.Headers {
        if header != nil {
            taskQueue.Push(header, -int64(header.Number.Uint64()))
        }
    }

④ : if data is verified and writtenqueue.resultCacheYes(accepted>0), sendqueue.activeNews.ResultsWill wait for this signal.


Results

When (header, body, receive) are all downloaded, the block will be written to the database,queue.ResultsIt is used to return all the data that has been downloaded at present. It is used in theDownloader.processFullSyncContentandDownloader.processFastSyncContentIs called in. The code is relatively simple.

only this and nothing morequeueThe object is almost analyzed.


Synchronization headers

fetchHeaders

synchronizationheadersIt’s a functionfetchHeadersTo do it.

fetchHeadersThe main idea of this paper is as follows

synchronizationheaderThe data will be filled in theskeletonThe maximum value of the block data obtained from the remote node each time isMaxHeaderFetch(192), so if the block data to be obtained is greater than 192, it will be divided into groupsMaxHeaderFetchThe remaining 192 will not be filled inskeletonThe specific steps are as follows:

Downloader synchronization in the analysis of Ethereum source code

This way canAvoid downloading too much wrong data from the same nodeIf we’re connected to a malicious node, it can create a chain that’s long and complexTDBlockchain data with very high value. If our blocks are synchronized from 0, we will download some data that is not recognized by others at all. If I only sync from itMaxHeaderFetchThen I found that these blocks couldn’t fill my previous ones correctlyskeleton(it could beskeletonWrong data, or used to fill inskeletonIf the data is wrong, it will be lost.

Next, let’s see how the code is implemented

① : initiate acquisitionheaderRequest for

If it’s a downloadskeletonFrom the heightfrom+MaxHeaderFetch-1Start (including), everyMaxHeaderFetch-1The height of the request oneheader, maximum requestsMaxSkeletonSizeOne. If not, get the completeheaders

② : wait and processheaderChInheaderdata

2.1 make sure the remote node is returning, we need to fill inskeletonRequiredheader

if packet.PeerId() != p.id {
                log.Debug("Received skeleton from incorrect peer", "peer", packet.PeerId())
                break
            }

2.2 ifskeletonAfter downloading, you need to continue fillingskeleton

if packet.Items() == 0 && skeleton {
                skeleton = false
                getHeaders(from)
                continue
            }

2.3 overallskeletonThe fill is complete and there is no data to getheaderIt’s time to informheaderProcChAll done

if packet.Items() == 0 {
                //Do not abort header extraction when downloading pivot
                if atomic.LoadInt32(&d.committed) == 0 && pivot <= from {
                    p.log.Debug("No headers, waiting for pivot commit")
                    select {
                    case <-time.After(fsHeaderContCheck):
                        getHeaders(from)
                        continue
                    case <-d.cancelCh:
                        return errCanceled
                    }
                }
                //Complete the pivot operation (or no fast synchronization), and no header file, terminate the process
                p.log.Debug("No more headers available")
                select {
                case d.headerProcCh <- nil:
                    return nil
                case <-d.cancelCh:
                    return errCanceled
                }
            }

2.4 whenheaderThere’s data and it’s gettingskeletonCall thefillHeaderSkeletonfillskeleton

if skeleton {
                filled, proced, err := d.fillHeaderSkeleton(from, headers)
                if err != nil {
                    p.log.Debug("Skeleton chain invalid", "err", err)
                    return errInvalidChain
                }
                headers = filled[proced:]
                from += uint64(proced)
            }

2.5 if the current processing is notskeletonIt indicates that the blocks are almost synchronized, and some blocks at the end are processed

Judge whether the height difference between the local main chain height and the highest height of the newly received header is within the rangereorgProtThresholdIf not, it will be the highestreorgProtHeaderDelayLose a header.

if head+uint64(reorgProtThreshold) < headers[n-1].Number.Uint64() {
                        delay := reorgProtHeaderDelay
                        if delay > n {
                            delay = n
                        }
                        headers = headers[:n-delay]
                    }

2.6 if anyheaderUnprocessed, sent toheaderProcChFor treatment,Downloader.processHeadersIt will wait for the message of this channel and process it;

if len(headers) > 0 {
                ...
                select {
                case d.headerProcCh <- headers:
                case <-d.cancelCh:
                    return errCanceled
                }
                from += uint64(len(headers))
  getHeaders(from)
}

2.7 if no header is sent, or all headers waitfsHeaderContCheckSecond, call againgetHeadersRequest block

p.log.Trace("All headers delayed, waiting")
                select {
                case <-time.After(fsHeaderContCheck):
                    getHeaders(from)
                    continue
                case <-d.cancelCh:
                    return errCanceled
                }

This code was added later, and its commit is recorded inhere, and “pull request” inhere. From the author’s explanation in “pull request”, we can understand the logic and function of this Code: this modification is mainly to solve the frequent “invalid hash chain” error. The reason for this error is that in the last time we obtained some blocks from a remote node and added them to the local main chain, The remote node has a reorg operation (seeThis articleThe introduction of “main chain and side chain” in; When we request a new block according to the height again, the other party returns us the block on its new main chain, but we do not have the historical block on this chain. Therefore, when we write the block locally, we will return an “invalid hash chain” error.

If you want to “reorg”, you need to add new blocks. On the Ethereum main network, the interval between new blocks is about 10 seconds to 20 seconds. In general, if it’s just block data, its synchronization speed is very fast, and there is a maximum limit for each download. Therefore, in the period of time when a new block is generated, it is enough to synchronize a set of block data without “reorg” operation on the other node. But notice that the synchronization of “just block data” is faster,The synchronization of state data is very slow. In short, there may be multiple “pivot” blocks before the completion of synchronization. The state data of these blocks will be downloaded from the network, which greatly slows down the synchronization speed of the whole block, and increases the probability of “reorg” operation of the other party while synchronizing a group of blocks locally.

The author thinks that the “reorg” operation in this case is caused by the competition of newly generated blocks, so the latest blocks are “unstable”, If the number of blocks synchronized this time is large (that is, the time consumed for synchronization is relatively long), the difference between the maximum height of the newly received blocks and the maximum height in the local database is greater than 0reorgProtThreshold)In this case, the synchronization of the latest block can be avoided firstreorgProtThresholdandreorgProtHeaderDelayThe origin of this variable.

So far,Downloader.fetchHeadersThe method is over, and all the blocks are synchronized. We mentioned filling aboveskeletonWhen I was young, it was byfillHeaderSkeletonFunction to complete, next we will talk about fillingskeletonDetails of the project.


fillHeaderSkeleton

First of all, we know that when Ethereum synchronizes a block, it first determines the height range of the block to be downloaded, and then presses this rangeMaxHeaderFetchIt is divided into many groups, and the last block of each group forms “skeleton”MaxHeaderFetchBlocks are not counted as a group). If you are not clear, you can see the figure above.

① : transfer a batch ofheaderThe retrieval task is added to the queue to fill in theskeleton

This function refers to the aboveDetails of queueAnalysis of

func (q queue) ScheduleSkeleton(from uint64, skeleton []types.Header) {}

② : callfetchPartsobtainheadersdata

fetchPartsIs a very core function, the followingFetchbodiesandFetchReceiptsWill be called. Let’s have a general look firstfetchPartsThe structure of the system is as follows

func (d *Downloader) fetchParts(...) error {
  ...
  for {
        select {
        case <-d.cancelCh:
        case packet := <-deliveryCh:
        case cont := <-wakeCh:
        case <-ticker.C:
        case <-update:
        ...
    }
}

These are the fivechannelIn the process, the first fourchannelResponsible for loop waiting for messages,updateTo wait for the other fourchannelTo deal with the logic, we first analyze one by onechannel

2.1 deliverych delivers the downloaded data

deliveryChThe function is to transfer the downloaded data. When the data is actually downloaded, it will be sent to the userchannelSend a message to deliver the data. The corresponding channels are:d.headerChd.bodyChd.receiptChAnd these threechannelData is written in the following three methods:DeliverHeadersDeliverBodiesDeliverReceipts. look downdeliveryChHow to process data:

case packet := <-deliveryCh:
            if peer := d.peers.Peer(packet.PeerId()); peer != nil {
                Accepted, err: = deliver (packet) // pass the received data block and check the validity of the chain
                if err == errInvalidChain {
                    return err
        }
                if err != errStaleDelivery {
                    setIdle(peer, accepted)
                }
                switch {
                case err == nil && packet.Items() == 0:
                    ...
                case err == nil:
                ...
                }
            }
            select {
            case update <- struct{}{}:
            default:
            }

After receiving the download data, judge whether the node is valid. If the node has not been removed, it will pass thedeliverDeliver the received download data. If there are no errors, notifyupdatehandle.

it is to be noted thatdeliverIs a callback function that calls the deliver method of the queue object:queue.DeliverHeadersqueue.DeliverBodiesqueue.DeliverReceiptsAfter receiving the download data, this callback function will be called(For the analysis of queue correlation function, please refer to the detailed explanation of queue)。

In the error handling section above, there is asetIdleFunction, which is also a callback function, its implementation is calledpeerConnectionObject related methods:SetHeadersIdleSetBodiesIdleSetReceiptsIdle. This function means that some nodes are idle for certain types of data, such asheaderbodiesreceiptsIf you need to download these kinds of data, you can download them from the idle nodes.

2.2 wakeChawakenfetchParts, download new data or download completed

case cont := <-wakeCh:
            if !cont {
                finished = true
            }
            select {
            case update <- struct{}{}:
            default:
            }

First, we know by calling the parameters passed by fetchparts,wakeChThe value of is actuallyqueue.headerContCh. stayqueue.DeliverHeadersIf you find that all the headers you need to wear are downloaded, you will send false to this channel.fetchPartsWhen you receive this message, you will know that there is no header to download. The code is as follows:

func (q *queue) DeliverHeaders(......) (int, error) {
    ......
    if len(q.headerTaskPool) == 0 {
        q.headerContCh <- false
    }
    ......
}

The same is true,bodyandreceiptthen isbodyWakeChandreceiptWakeCh, inprocessHeadersIf allheaderThe download is finished, so send itfalseHere are twochannelTo inform them that there are no new onesheaderIt’s too late.bodyandreceiptThe download of depends onheader, needheaderFirst download to complete the download, so for the next wearbodyorreceiptOffetchPartsCome on, get thiswakeChIt means that there will be no more notice to download data

func (d *Downloader) processHeaders(origin uint64, pivot uint64, td *big.Int) error {
    for {
        select {
        case headers := <-d.headerProcCh:
            if len(headers) == 0 {
                for _, ch := range []chan bool{d.bodyWakeCh, d.receiptWakeCh} {
                    select {
                    case ch <- false:
                    case <-d.cancelCh:
                    }
                }
                        ...
            }
            ...
            for _, ch := range []chan bool{d.bodyWakeCh, d.receiptWakeCh} {
                select {
                case ch <- true:
                default:
                }
            }
        }
    }
}

2.3 ticker is responsible for periodic activationupdateMessage processing

case <-ticker.C:
            select {
            case update <- struct{}{}:
            default:
            }

2.4 update(dealing with the previous several problems)channelData of(important)

2.4.1 judge whether the node is valid, and obtain the information of timeout data

Get the node ID and data quantity of timeout data. If it is more than two, set the node to idle state(setIdle)If it is less than two, disconnect the node directly.

expireIs a callback function, will return all the current timeout data information. The actual implementation of this function is calledqueueObjectExpiremethod:ExpireHeadersExpireBodiesExpireReceipts, this function will count the difference between the start time and the current time of the data being downloaded, which exceeds the given threshold(downloader.requestTTLMethod) and return it.

if d.peers.Len() == 0 {
                return errNoPeers
            }
for pid, fails := range expire() {
  if peer := d.peers.Peer(pid); peer != nil {
    if fails > 2 {
                        ...
                        setIdle(peer, 0)
                    } else {
                    ...
                        if d.dropPeer == nil {
                        } else {
                            d.dropPeer(pid)
                            ....
                        }
                    }
  }

2.4.2 after processing the timeout data, judge whether there is any downloaded data

If there is no other downloadable content, please wait or terminate herepending()andinFlight()They are all callback functions,pendingThey correspond to each otherqueue.PendingHeadersqueue.PendingBlocksqueue.PendingReceiptsTo return the number of tasks to be downloaded.inFlight()They correspond to each otherqueue.InFlightHeadersqueue.InFlightBlocksqueue.InFlightReceiptsTo return the amount of data being downloaded.

if pending() == 0 {
                if !inFlight() && finished {
                ...
                    return nil
                }
                break
            }

2.4.3 use idle node, callfetchFunction to send a data request

Idle()The callback function has been mentioned above,throttle()The callback functionqueue.ShouldThrottleBlocksqueue.ShouldThrottleReceipts, used to indicate whether it should be downloadedbodiesperhapsreceipts

reserveFunctions correspond to each otherqueue.ReserveHeadersqueue.ReserveBodiesqueue.ReserveReceipts, which is used to select some downloadable tasks from the download tasks and construct afetchRequestStructure. It also returns aprocessVariable, indicating whether there is empty data being processed. For example, it is possible that a block does not contain any transactions, so it is not easy to usebodyandreceiptAll of them are empty. In fact, this kind of data does not need to be downloaded. stayqueueObjectReserveMethod, this situation will be identified. If empty data is encountered, the data will be directly marked as successful download. When the method returns, it will return whether the “direct mark as download succeeded” has occurred.

capacityCallback functions correspond topeerConnection.HeaderCapacitypeerConnection.BlockCapacitypeerConnection.ReceiptCapacityTo determine the number of requested data to download.

fetchCallback functions correspond topeer.FetchHeaderspeer.Fetchbodiespeer.FetchReceipts, which is used to send requests to obtain various kinds of data.

progressed, throttled, running := false, false, inFlight()
            idles, total := idle()
            for _, peer := range idles {
                if throttle() {
                    ...
        }
                if pending() == 0 {
                    break
                }
                request, progress, err := reserve(peer, capacity(peer))
                if err != nil {
                    return err
                }
                if progress {
                    progressed = true
                }
        if request == nil {
                    continue
                }
                if request.From > 0 {
                ...
                }
                ...
                if err := fetch(peer, request); err != nil {
                ...
            }
            if !progressed && !throttled && !running && len(idles) == total && pending() > 0 {
                return errPeersUnavailable
            }

To sum up, this code is: use idle nodes to download data, judge whether it needs to be suspended, or whether the data has been downloaded; Then select the data to download; Finally, if there is no empty block to download, and the download is not suspended, and all valid nodes are idle, and there is data to download, but the download is not running, the system will returnerrPeersUnavailableWrong.

only this and nothing morefetchPartsFunction analysis of almost. What’s involved in itqueue.goSome of the related functions are in theDetails of queueThis is described in the section.


processHeaders

adoptheaderProcChreceiveheaderData, and the process of processing is inprocessHeadersFunction. The whole process focuses on the following aspectsCase headers: = < - d.headerprocch:

① : ifheadersIf the length of is 0, there will be the following operations:

1.1 notice to ownerheaderIt’s finished

for _, ch := range []chan bool{d.bodyWakeCh, d.receiptWakeCh} {
                    select {
                    case ch <- false:
                    case <-d.cancelCh:
                    }
                }

1.2 if noheaderTo explain theirTDSmaller than ours, or already through oursfetcherThe modules are synchronized.

if d.mode != LightSync {
                    head := d.blockchain.CurrentBlock()
                    if !gotHeaders && td.Cmp(d.blockchain.GetTd(head.Hash(), head.NumberU64())) > 0 {
                        return errStallingPeer
                    }
                }

1.3 if yesfastperhapslightSynchronize to ensure deliveryheader

if d.mode == FastSync || d.mode == LightSync {
                    head := d.lightchain.CurrentHeader()
                    if td.Cmp(d.lightchain.GetTd(head.Hash(), head.Number.Uint64())) > 0 {
                        return errStallingPeer
                    }
                }

② : ifheadersThe length of is greater than 0

2.1 in case of fast or light synchronization, callightchain.InsertHeaderChain()write inheaderreachleveldbDatabase;

if d.mode == FastSync || d.mode == LightSync {
  ....
  d.lightchain.InsertHeaderChain(chunk, frequency);
  ....
}

2.2 if yesfastperhapsfull syncMode, d.queue.schedule is called to retrieve the content (body and receive).

if d.mode == FullSync || d.mode == FastSync {
  ...
  inserts := d.queue.Schedule(chunk, origin)
  ...
}

③ : if an updated block number is found, the new task is signaled

if d.syncStatsChainHeight < origin {
                d.syncStatsChainHeight = origin - 1
            }
for _, ch := range []chan bool{d.bodyWakeCh, d.receiptWakeCh} {
                select {
                case ch <- true:
                default:
                }
            }

Here we goHeadersThe analysis is complete.


Synchronous bodies

synchronizationbodiesThis is due tofetchBodiesFunction.

fetchBodies

The process of synchronizing bodies is similar to that of synchronizing headers. The steps are as follows:

  1. callfetchParts
  2. ReserveBodies() frombodyTaskPoolTo synchronizebody
  3. callfetchThat is to call theFetchBodiesGet from nodebody, sendGetBlockBodiesMsgNews;
  4. receivedbodyChCall thedeliverFunction, the transactions andUncleswrite inresultCache

Synchronous receivers

fetchReceipts

synchronizationreceiptsThe process of synchronizationheaderSimilarly, the steps are as follows:

  1. callfetchParts()
  2. ReserveBodies() fromReceiptTaskPoolTo synchronizeReceipt
  3. Call theFetchReceiptsGet from nodereceipts, sendGetReceiptsMsgNews;
  4. receivedreceiptChCall thedeliverFunction, willReceiptswrite inresultCache

Synchronization status

Here we talk about state synchronization in two modes:

  • fullSync: processFullSyncContentfullModeReceiptsNo cache toresultCacheTo retrieve the data from the cachebodyData, and then execute the transaction generation status, and finally write to the blockchain.
  • fastSync:processFastSyncContent: the receipts, transactions and uncles of fast mode are all in the resultcache, so you need to download “state” for verification, and then write to the blockchain.

Next, we will discuss these two ways.

processFullSyncContent

func (d *Downloader) processFullSyncContent() error {
    for {
        results := d.queue.Results(true)
        ...
        if err := d.importBlockResults(results); err != nil ...
    }
}
func (d *Downloader) importBlockResults(results []*fetchResult) error {
    ...
    select {
...
    blocks := make([]*types.Block, len(results))
    for i, result := range results {
        blocks[i] = types.NewBlockWithHeader(result.Header).WithBody(result.Transactions, result.Uncles)
    }
    if index, err := d.blockchain.InsertChain(blocks); err != nil {
        ....
}

Directly fromresultGet data from and generateblock, insert directly into the blockchain, and it’s over.


processFastSyncContent

The content of fast mode synchronization state is quite large, which is roughly as follows. Let’s start with a brief analysis of the following.

① : download the latest block status

sync := d.syncState(latest.Root)

We use a graph to show the whole process

Downloader synchronization in the analysis of Ethereum source code

The specific code is a simple process for readers to read by themselves.

② : calculate the pivot block

pivotbylatestHeight - 64, callsplitAroundPivot() method is based on pivotresultsIt is divided into three partsbeforePPafterP

pivot := uint64(0)
    if height := latest.Number.Uint64(); height > uint64(fsMinFullBlocks) {
        pivot = height - uint64(fsMinFullBlocks)
    }
P, beforeP, afterP := splitAroundPivot(pivot, results)

③ : YesbeforePPartial call tocommitFastSyncData, willbodyandreceiptWrite to blockchain

d.commitFastSyncData(beforeP, sync); 

④ : YesPThe partial update status information of isP blockThe state ofPCorrespondingresult(includingbodyandreceipt)CallcommitPivotBlockInsert into the local blockchain and callFastSyncCommitHeadRecord thispivotOfhashValue, existingdownloaderThe last block marked for fast synchronizationhashValue;

if err := d.commitPivotBlock(P); err != nil {
                    return err
                }

⑤ : YesafterPcalld.importBlockResults, willbodyInsert blockchain instead ofreceipt. Because it is the last 64 blocks, there are only 64 blocks in the database at this timeheaderandbody, NoreceiptAnd status, to passfullSyncMode for the final synchronization.

if err := d.importBlockResults(afterP); err != nil {
            return err
        }

So far, the whole downloader synchronization is complete.

reference resources

https://mindcarver.cn

https://github.com/ethereum/g…

https://yangzhe.me/2019/05/09…