Downloader synchronization of dieke Ethereum source code analysis

Time:2022-1-18

Downloader synchronization of dieke Ethereum source code analysis

Need to match the comment code to see:https://github.com/blockchain…

This article is long. What you can read is a man. I suggest collecting it

I hope readers will point out problems, pay attention to them and discuss them together in the reading process.

overview

downloaderThe code for the module is located ineth/downloaderDirectory. The main function codes are:

  • downloader.go: block synchronization logic is implemented
  • peer.goFor the assembly of each stage of each block, the following:FetchXXXIs very dependent on this module.
  • queue.go: Yeseth/peer.goPackaging of
  • statesync.go: synchronizationstateobject

Synchronous mode

### full sync

Full mode saves all block data in the database. During synchronization, the header and body data are synchronized from the remote node, while the state and receive data are calculated locally.

In the full mode, the downloader will synchronize the header and body data of the block to form a block, and then through the block chain moduleBlockChain.InsertChainInserts a block into the database. stayBlockChain.InsertChainIn, the values of each block are calculated and verified one by onestateandrecepitAnd other data. If everything is normal, the block data and their own calculated data will be usedstaterecepitThe data is written to the database together.

fast sync

fastIn mode,recepitIt is no longer calculated locally, but directly by, just like block datadownloaderSynchronize from other nodes;stateThe data will not be all calculated and downloaded, but a newer block (calledpivot)YesstateDownload. Take this block as the boundary. There are no previous blocksstateAfter the data, the block will look likefullIt is calculated locally in the same modestate。 So infastIn mode, the synchronized data is in addition toheaderAnd body, andreceipt, andpivotBlockstate

thereforefastPatterns ignore moststateData, and use the network to synchronize directlyreceiptThe data method replaces the local calculation in full mode, so it is faster.

light sync

Light mode is also called light mode. It only synchronizes the block header and does not synchronize other data.

SyncMode:

  • Fullsync: synchronize the entire blockchain history from the full blockchain
  • Fastsync: quickly download the title. It is fully synchronized only at the chain head
  • Lightsync: Download titles only and terminate

Block download process

The picture is just a general description. In fact, it should be combined with the code,Collection of all blockchain related articles,https://github.com/blockchain…

At the same time, if you want to meet more people in the blockchain circle, you can continue to update the projects above star

Downloader synchronization of dieke Ethereum source code analysis

First, according toSynchroniseStart block synchronization throughfindAncestorFind 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 blocks with a height of 100, you must firstheaderWake up after synchronization is successfulbodyandreceiptsSynchronization of.

The synchronization of each part is roughly controlled byFetchPartsTo complete, which contains variousChanMany callback functions will also be involved in the cooperation of. In a word, read it several times, and you will have a different understanding each time. Next, we will analyze these key contents step by step.


synchronise

① : make sure the other party’s TD 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.synchronise, take 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. Open separategoroutineRun the following functions:

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

The next article, and the wholeDownloaderThe main content of the module is to expand around these parts.


findAncestor

Synchronization first and foremostDetermine the interval of synchronization block: the top is the highest block of the remote node, and the bottom is the highest height of the same block owned by both nodes (ancestral block).findAncestorIt’s used to find ancestral blocks. 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 synchronized altitude interval and interval

from, count, skip, max := calculateRequestSpan(remoteHeight, localHeight) 
  • from:: indicates the height from which the block is obtained
  • count: indicates how many blocks are obtained from the remote node
  • skip: indicates interval, for example:skipIs 2, get the first height is 5, then the second is 8
  • max: indicates the maximum height

③ : send getheaderRequest for

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

④ : process the received request aboveheader :case packet := <-d.headerCh

  1. Discard content that is not from our request section
  2. Ensure returnedheaderQuantity cannot be empty
  3. Verify returnedheadersThe height is what we requested
  4. Check to find a common ancestor
//----①
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 whether the returned headers are the headers we requested above
                    p.log.Warn("Head headers broke chain ordering", "index", i, "requested", expectNumber, "received", number)
                    return 0, errInvalidChain
                }
            }
//-----④
//Check to find a common ancestor
            finished = true
            //Note that the search starts from 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 block in our local area. If so, it means finding a common ancestor,
                //And set the hash and height of the common ancestor in the number and hash variables.
                h := headers[i].Hash()
                n := headers[i].Number.Uint64()

⑤ : if the common ancestor is found through the fixed interval method, the ancestor will be returned and its height will be adjustedfloorVariables for validation,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 and synchronization is no longer allowed. If everything is normal, 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 the ancestor, find the ancestor through the dichotomy. The idea of this part is similar to the dichotomy algorithm. Those who are interested can take a closer look.


Queue details

queueObject andDownloaderObjects interact,DownloaderMany functions of are inseparable from it. Next, let’s introduce this part, but in this section,You can skip firstWait until you read the following aboutQueueCall some function parts, and then come back to read this part.

Queue structure

type queue struct {
  Mode syncmode // synchronization mode
  
  //Header processing related
  headerHead      common. Hash // the hash value of the last queued header in the verification order
  headerTaskPool  map[uint64]*types. Header // the header retrieval task to be processed maps the starting index to the frame header
  headerTaskQueue *prque. Prque // the priority queue of the skeleton index to get the padding header for
  Headerpeermiss map [string] map [Uint64] struct {} // known unavailable peer header batch sets
  Headerpendpool map [string] * fetchrequest // currently pending header retrieval operations
  headerResults []*types. Header // the completed header of the result cache accumulation
  Headerproced int // get the processed header from the result
  Headercontch Chan bool // the channel notified when the header download is completed
  
  blockTaskPool  map[common.Hash]*types. Header // retrieve the block (body) to be processed and map the hash to the header
  blockTaskQueue *prque. Prque // the priority queue of the header to get the blocks
  Blockpendpool map [string] * fetchrequest // the currently processing block (body) retrieval operation
  Blockdonepool map [common. Hash] struct {} // completed block (body)
  
    receiptTaskPool map[common.Hash]*types. Header // the receipt retrieval task to be processed maps the hash to the header
    receiptTaskQueue *prque. Prque // the priority queue of the header to get the receipt
    Receiptpendpool map [string] * fetchrequest // the current receipt retrieval operation in progress
    Receiptdonepool map [common. Hash] struct {} // completed receipt
    
    Resultcache [] * fetchresult // download but not deliver the obtained result
    Resultoffset Uint64 // the offset of the first cached result in the blockchain
    resultSize common. Storagesize // approximate size of the block

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

Main subdivision functions

Data download start scheduling task

  • ScheduleSkeleton:Put a batchheaderThe retrieval task is added to the queue to populate the retrievedheader skeleton
  • Schedule:To prepare for somebodyandreceiptData download

Various states in data download

  • pending

    pendingIndicates the number of XXX requests to be retrieved, including:PendingHeadersPendingBlocksPendingReceipts, respectively, are corresponding valuesXXXTaskQueueThe length of the.

  • InFlight

    InFlightIndicates whether there is a request for XXX, including:InFlightHeadersInFlightBlocksInFlightReceipts, all through judgmentlen(q.receiptPendPool) > 0To confirm.

  • ShouldThrottle

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

  • Reserve

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

  • Cancel

    CanceUsed to undo pairfetchRequestDownload of data in structure(queueThese data will be changed internally from “downloading” to “waiting for download”). include:CancelHeadersCancelBodiesCancelReceipts

  • expire

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

  • Deliver

    When data is downloaded successfully, the caller will usedeliverFunction to notifyqueueObject. include:DeliverHeadersDeliverBodiesDeliverReceipts

Block data acquisition after data download

  • RetrieveHeaders
    In fillskeletonWhen finished,queue.RetrieveHeadersUsed to get the entireskeletonAll inheader
  • Results
    queue.ResultsUsed to get the currentheaderbodyandreceipt(only infastIn mode), the blocks that have been downloaded successfully (and these blocks are downloaded fromqueueInternal removal)

Function implementation

ScheduleSkeleton

queue. Scheduleskeleton is mainly used to fill the skeleton. Its parameters are the starting height of the block to be downloaded and all parametersskeletonThe core content of the block header 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))
    }
}

Assuming that it is determined that the height range of the block to be downloaded is from 10 to 46,MaxHeaderFetchIf the value of is 10, the height block will be divided into three groups: 10 – 19, 20 – 29 and 30 – 39, while the skeleton is composed of block heads with heights of 19, 29 and 39 respectively. In loopindexThe variable is actually the height of the first block in each group of blocks (such as 10, 20, 30),queue.headerTaskPoolIt’s actually aThe mapping from the height of the first block to the header of the last block in each group of blocks

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

ReserveHeaders

reserveUsed to obtain 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 that the node failed to download data recorded by headerpeermiss 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 will be selected from the task queue and constructedfetchRequestPass tofetchGet data.


DeliverHeaders

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

① : if the node where the data is downloaded is not found:queue.headerPendPoolIn, an error is returned directly; Otherwise, continue processing and record the node fromqueue.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 of verification:

  1. Check the height and hash of the starting block
  2. Check high connectivity
  3. Check hash connectivity
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 the height
            if want := request.From + 1 + uint64(i); header.Number.Uint64() != want {
                ...
            }
            if headers[i]. Hash() !=  header. Parenthash {// check hash connectivity
                ...
            }
        }
    }

③ : store invalid dataheaderPeerMissAnd 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 notifyheaderProcChHandle 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, notificationskeletonAll downloaded

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

DeliverHeadersIt will verify and save the data and send a channel message toDownloader.processHeadersandDownloader.fetchPartsofwakeChParameters.


Schedule

processHeadersIn processingheaderWhen data is, it will callqueue.ScheduleFor downloadbodyandreceiptPrepare.

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 is mainly to write information to the body and receive queues and wait for scheduling.


ReserveBody&Receipt

stayqueueAre you readybodyandreceiptRelevant data,processHeadersThe last paragraph is the key code to wake up and download Bodyies and receipts, which will be notifiedfetchBodiesandfetchReceiptsThe respective data can be downloaded.

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

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

Let’s analyze it firstreserveHeaders

① : if there is no task to handle, 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
    }

③ : calculate how many pieces of data the cache space in the queue object can hold

space := q.resultSlots(pendPool, donePool)

④ : take out tasks from “task queue” for processing

It mainly realizes the following functions:

  • Calculate the current header inqueue.resultCachePosition in, and then fillqueue.resultCacheElement at the corresponding position in the
  • Handle the 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 downloaded.

be careful:resultCacheField is used to record the processing results of all 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 here: body and receive,fullOnly need to download in modebodyData, andfastDownload 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)
        }
    }

The last is constructionfetchRequestStructure and return.


DeliverBodies&Receipts

bodyorreceiptThe data has passedreserveOperation constructedfetchRequestStructure and pass tofetchThe next step is to wait for the data to arrive. After the data is downloaded successfully, it will callqueueObjectdeliverMethod, 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]Corresponding fields in

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

③ : validationresultCacheThe data in the correspondingrequest.HeadersMediumheaderAll should be nil. If not, it means that the verification fails and you need to download it again in the task queue

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

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


Results

When the (header, body, and receipt) are downloaded, the blocks will be written to the database,queue.ResultsIt is used to return all data that has been downloaded. It is inDownloader.processFullSyncContentandDownloader.processFastSyncContentCalled in. The code is relatively simple, so I won’t say much.

only this and nothing morequeueThe object is almost analyzed.


Sync headers

fetchHeaders

synchronizationheadersBy functionfetchHeadersTo finish it.

fetchHeadersGeneral idea:

synchronizationheaderThe data will be filled inskeleton, the maximum 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 groups, each groupMaxHeaderFetch, the remaining less than 192 will not be filled inskeletonThe specific steps are shown in the figure below:

Downloader synchronization of dieke Ethereum source code analysis

This way canAvoid downloading too much wrong data from the same node, if we connect to a malicious node, it can create a long chain andTDBlockchain data with very high value. If our blocks are synchronized from 0, we will download some data that is not recognized by others. If I just sync from itMaxHeaderFetchBlocks, and then found that these blocks could not fill my previous blocks correctlyskeleton(probably)skeletonThe data is wrong or used to fill inskeletonIf your data is wrong, you will lose it.

Next, let’s see how the code implements:

① : initiate acquisitionheaderRequest for

If Downloadskeleton, from the heightfrom+MaxHeaderFetch-1Start (including), everyMaxHeaderFetch-1Height request oneheader, maximum requestsMaxSkeletonSizeOne. If not, obtain the completeheaders

② : wait and processheaderChMediumheaderdata

2.1 ensure that 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 wholeskeletonThe population is complete and there are no to getheaderYes, I want to inform youheaderProcChAll 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
                    }
                }
                //If the pivot operation (or no quick synchronization) is completed and there is 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 is data and is getting itskeletonWhen, callfillHeaderSkeletonfillskeleton

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 notskeleton, indicating 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 withinreorgProtThresholdWithin, if not, it will be the highestreorgProtHeaderDelayThrow away 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 anyheaderNot processed, sent toheaderProcChProcessing,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 headers are sent or all headers are waitingfsHeaderContCheckSecond, 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. The commit record is here, and the “pull request” is here. We can understand the logic and function of this code from the explanation of the author in “pull request”: this modification is mainly to solve the frequent “invalid hash chain” error. The reason for this error is that we last obtained some blocks from the remote node and added them to the local main chain, The remote node has reorg operation (see the introduction of “main chain and side chain” in this article); When we request a new block according to the height again, the other party returns to us the block on its new main chain, and we do not have a historical block on this chain. Therefore, an “invalid hash chain” error will be returned when writing a block locally.

If you want to “reorg” the operation, you need to add new blocks. On the Ethereum main network, the interval of a new block is about 10 seconds to 20 seconds. Generally, if it is only block data, its synchronization speed is still very fast, and there is a maximum number limit for each download. Therefore, during the period when a new block is generated, it is enough to complete a group of block data synchronously without the “reorg” operation of the opposite node. But note that the synchronization of “just block data” is fast,The synchronization of state data is very slow。 In short, there may be multiple “pivot” blocks before synchronization is completed, and the state data of these blocks will be downloaded from the network, which greatly slows down the synchronization speed of the whole block, and greatly increases the probability of “reorg” operation of the other party while synchronizing a group of blocks locally.

The author believes 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 during synchronization is relatively long) (here, “the number of blocks synchronized this time” shows that the difference between the maximum height of the newly received block and the maximum height in the local database is greater thanreorgProtThreshold), you can avoid synchronizing the latest block during synchronization, which isreorgProtThresholdandreorgProtHeaderDelayThe origin of this variable.

So far,Downloader.fetchHeadersThe method ends, and all block headers are synchronized. We mentioned filling aboveskeletonWhen, byfillHeaderSkeletonFunction, and then we’ll talk about filling in detailskeletonDetails.


fillHeaderSkeleton

First, we know that when Ethereum synchronizes blocks, first determine the height interval of the block to be downloaded, and then press this intervalMaxHeaderFetchIt is divided into many groups, and the last block of each group forms a “skeleton” (the last group)MaxHeaderFetchBlocks are not counted as a group). If you are not clear, you can see the figure above.

① : a batchheaderThe retrieval task is added to the queue to populateskeleton

This function refers to the aboveQueue detailsAnalysis of

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

② : callfetchPartsobtainheadersdata

fetchPartsIs a very core function, the followingFetchbodiesandFetchReceiptsWill be called. Let’s take a general look firstfetchPartsStructure of:

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

These five are simplifiedchannelIn processing, the first fourchannelResponsible for waiting for messages in a loop,updateUsed to wait for the other fourchannelNotice to process logic, first analyze one by onechannel

2.1 deliverych deliver downloaded data

deliveryChThe function is to transfer the downloaded data. When the data is really downloaded, it will be sent to the userchannelSend a message to pass the data. The corresponding channels are:d.headerChd.bodyChd.receiptCh, and 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 downloaded data, judge whether the node is valid. If the node is not removed, it will passdeliverDeliver the received download data. Notify if there are no errorsupdatehandle.

it is to be noted thatdeliverIs a callback function that calls the deliver method of the queue object:queue.DeliverHeadersqueue.DeliverBodiesqueue.DeliverReceipts, this callback function will be called when the download data is received(For queue correlation function analysis, 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 asheaderbodiesreceipts, if you need to download these types of data, you can download these data from idle nodes.

2.2 wakeChawakenfetchParts, download new data or download is complete

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.DeliverHeadersWhen it is found that all headers to be worn are downloaded, false will be sent to this channel.fetchPartsWhen you receive this message, you 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 allheaderIf the download is complete, send itfalseGive these twochannel, notify them that there are no new onesheaderYes.bodyandreceiptYour download depends onheader, needheaderDownload can only be downloaded after downloading, 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 previous issues)channel(data for)(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 greater than two, set the node to idle state(setIdle), if less than two, disconnect the node directly.

expireIs a callback function that returns all current timeout data information. The actual implementation of this function is calledqueueObjectExpiremethod:ExpireHeadersExpireBodiesExpireReceipts, this function will count that the gap between the start time and the current time in the data currently being downloaded exceeds the given threshold(downloader.requestTTLMethod and returns 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 still downloaded data

If there is no other content to download, please wait or terminate herepending()andinFlight()Are callback functions,pendingThey correspond to each otherqueue.PendingHeadersqueue.PendingBlocksqueue.PendingReceipts, which is used to return the number of tasks to be downloaded.inFlight()They correspond to each otherqueue.InFlightHeadersqueue.InFlightBlocksqueue.InFlightReceipts, which is used to return the amount of data being downloaded.

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

2.4.3 using idle nodes, callfetchFunction to send a data request

Idle()The callback function has been mentioned above,throttle()Callback functionsqueue.ShouldThrottleBlocksqueue.ShouldThrottleReceipts, which indicates 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 that indicates whether empty data is being processed. For example, it is possible that a block does not contain any transaction, so itsbodyandreceiptAll are empty. In fact, this kind of data does not need to be downloaded. stayqueueObjectReserveThis situation is identified in the method. If empty data is encountered, it will be directly marked as successful download. When the method returns, it will return whether “directly mark as download successful” has occurred.

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

fetchCallback functions correspond topeer.FetchHeaderspeer.Fetchbodiespeer.FetchReceipts, which is used to send requests to obtain various types 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 to use idle nodes to download data to 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, the download is not suspended, all valid nodes are idle and there is data to download, but the download is not running, returnerrPeersUnavailableWrong.

only this and nothing morefetchPartsThe function analysis is almost complete. What’s involved in itqueue.goSome related functions are inQueue detailsIt is introduced in the section.


processHeaders

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

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

1.1 notify ownerheaderIt has been processed

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

1.2 if no information is retrievedheader, explain theirTDLess than ours, or has passed oursfetcherThe module is 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 yesfastperhapslightSync, make sure you passheader

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 if it is 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, call d.queue Schedule retrieves the contents (body and receipt).

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

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

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

Process hereHeadersYour analysis is complete.


Synchronous bodies

synchronizationbodiesByfetchBodiesFunction completed.

fetchBodies

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

  1. callfetchParts
  2. ReserveBodies() frombodyTaskPoolRemove the to be synchronized from thebody
  3. callfetch, that is, call hereFetchBodiesGet from nodebody, sendGetBlockBodiesMsgMessage;
  4. receivedbodyChAfter the data is invokeddeliverFunction that combines transactions andUncleswrite inresultCache

Sync receipts

fetchReceipts

synchronizationreceiptsThe process is synchronized withheaderSimilarly, the following steps are roughly described:

  1. callfetchParts()
  2. ReserveBodies() fromReceiptTaskPoolRemove the to be synchronized from theReceipt
  3. Call hereFetchReceiptsGet from nodereceipts, sendGetReceiptsMsgMessage;
  4. receivedreceiptChAfter the data is invokeddeliverFunction that willReceiptswrite inresultCache

Synchronization status

Here we talk about state synchronization in two modes:

  • fullSync: processFullSyncContentfullModeReceiptsNot cached toresultCacheFirst, it is directly extracted from the cachebodyData, then execute the transaction generation status, and finally write it to the blockchain.
  • fastSync:processFastSyncContent: the receipts, transactions and uncles of the fast mode are all in the resultcache, so you also need to download the “state” for verification, and then write it to the blockchain.

Next, we will discuss these two methods roughly.

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 and generateblock, insert it directly into the blockchain and it’s over.


processFastSyncContent

There are many contents of fast mode synchronization status, which are roughly as follows. Let’s start with a brief analysis.

① : download the latest block status

sync := d.syncState(latest.Root)

We directly use a diagram to represent the whole general process:

Downloader synchronization of dieke Ethereum source code analysis

The specific code readers read by themselves is roughly such a simple process.

② : calculate pivot block

pivotbylatestHeight - 64, callsplitAroundPivot() the method takes pivot as the centerresultsIt is divided into three parts:beforePPafterP

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

③ : YesbeforePPartial call tocommitFastSyncData, willbodyandreceiptAll are written to the blockchain

d.commitFastSyncData(beforeP, sync); 

④ : YesPSome of the update status information isP blockState, putPCorrespondingresult(includingbodyandreceipt)CallcommitPivotBlockInsert into the local blockchain and callFastSyncCommitHeadRecord thispivotofhashValue, existsdownloaderIn, the last block marked as fast synchashValue;

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

⑤ : YesafterPcalld.importBlockResults, willbodyInsert blockchain instead ofreceipt。 Because it is the last 64 blocks, there are onlyheaderandbody, NoreceiptAnd status, to passfullSyncMode for final synchronization.

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

So far, the entire downloader synchronization is complete.

reference resources

https://mindcarver.cn

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

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