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
downloader
The code for the module is located ineth/downloader
Directory. The main function codes are:
downloader.go
: block synchronization logic is implementedpeer.go
For the assembly of each stage of each block, the following:FetchXXX
Is very dependent on this module.queue.go
: Yeseth/peer.go
Packaging ofstatesync.go
: synchronizationstate
object
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.InsertChain
Inserts a block into the database. stayBlockChain.InsertChain
In, the values of each block are calculated and verified one by onestate
andrecepit
And other data. If everything is normal, the block data and their own calculated data will be usedstate
、recepit
The data is written to the database together.
fast sync
fast
In mode,recepit
It is no longer calculated locally, but directly by, just like block datadownloader
Synchronize from other nodes;state
The data will not be all calculated and downloaded, but a newer block (calledpivot
)Yesstate
Download. Take this block as the boundary. There are no previous blocksstate
After the data, the block will look likefull
It is calculated locally in the same modestate
。 So infast
In mode, the synchronized data is in addition toheader
And body, andreceipt
, andpivot
Blockstate
。
thereforefast
Patterns ignore moststate
Data, and use the network to synchronize directlyreceipt
The 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
First, according toSynchronise
Start block synchronization throughfindAncestor
Find the common ancestor of the specified node, synchronize at this height, and turn on multiple nodes at the same timegoroutine
Synchronize different data:header
、receipt
、body
。 If you synchronize blocks with a height of 100, you must firstheader
Wake up after synchronization is successfulbody
andreceipts
Synchronization of.
The synchronization of each part is roughly controlled byFetchParts
To complete, which contains variousChan
Many 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
}
② : ondownloader
Synchronization of
pm.downloader.Synchronise(peer.id, pHead, pTd, mode)
Enter function: mainly do the following things:
d.synchronise(id, head, td, mode)
: synchronization process- Error log output and delete this
peer
。
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:
- Find ancestors
findAncestor
-
Open separate
goroutine
Run the following functions:- fetchHeaders
- processHeaders
- fetchbodies
- fetchReceipts
- processFastSyncContent
- processFullSyncContent
The next article, and the wholeDownloader
The 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).findAncestor
It’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 obtainedcount
: indicates how many blocks are obtained from the remote nodeskip
: indicates interval, for example:skip
Is 2, get the first height is 5, then the second is 8max
: indicates the maximum height
③ : send getheader
Request for
go p.peer.RequestHeadersByNumber(uint64(from), count, skip, false)
④ : process the received request aboveheader
:case packet := <-d.headerCh
- Discard content that is not from our request section
- Ensure returned
header
Quantity cannot be empty - Verify returned
headers
The height is what we requested - 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 adjustedfloor
Variables for validation,floor
The 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 foundnumber
Variable.
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
queue
Object andDownloader
Objects interact,Downloader
Many 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 aboutQueue
Call 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 batchheader
The retrieval task is added to the queue to populate the retrievedheader skeleton
Schedule
:To prepare for somebody
andreceipt
Data download
Various states in data download
pending
pending
Indicates the number of XXX requests to be retrieved, including:PendingHeaders
、PendingBlocks
、PendingReceipts
, respectively, are corresponding valuesXXXTaskQueue
The length of the.InFlight
InFlight
Indicates whether there is a request for XXX, including:InFlightHeaders
、InFlightBlocks
、InFlightReceipts
, all through judgmentlen(q.receiptPendPool) > 0
To confirm.ShouldThrottle
ShouldThrottle
Indicates whether the download of XXX should be restricted, including:ShouldThrottleBlocks
、ShouldThrottleReceipts
, mainly to prevent excessive local memory occupation during downloading.Reserve
Reserve
By constructing afetchRequest
Structure and returns information that provides the caller with a specified number of data to be downloaded(queue
Internally, these data will be marked as “downloading”). The caller uses the returnedfetchRequest
Data initiates a new request to the remote node to obtain data. include:ReserveHeaders
、ReserveBodies
、ReserveReceipts
。Cancel
Cance
Used to undo pairfetchRequest
Download of data in structure(queue
These data will be changed internally from “downloading” to “waiting for download”). include:CancelHeaders
、CancelBodies
、CancelReceipts
。expire
expire
Check whether the executing request exceeds the timeout limit, including:ExpireHeaders
、ExpireBodies
、ExpireReceipts
。Deliver
When data is downloaded successfully, the caller will use
deliver
Function to notifyqueue
Object. include:DeliverHeaders
、DeliverBodies
、DeliverReceipts
。
Block data acquisition after data download
RetrieveHeaders
In fillskeleton
When finished,queue.RetrieveHeaders
Used to get the entireskeleton
All inheader
。Results
queue.Results
Used to get the currentheader
、body
andreceipt
(only infast
In mode), the blocks that have been downloaded successfully (and these blocks are downloaded fromqueue
Internal 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 parametersskeleton
The 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,MaxHeaderFetch
If 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 loopindex
The variable is actually the height of the first block in each group of blocks (such as 10, 20, 30),queue.headerTaskPool
It’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
reserve
Used 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 toheaderPendPool
To determine whether the remote node is downloading data information.
② : fromheaderTaskQueue
Take the value as the starting height of this request and assign it tosend
Variable. In this process, the information that the node failed to download data recorded by headerpeermiss will be excluded.
③ : write the failed task backtask queue
④ : utilizationsend
Variable constructionfetchRequest
Structure, which is used asFetchHeaders
To use:
fetch = func(p *peerConnection, req *fetchRequest) error {
return p.FetchHeaders(req.From, MaxHeaderFetch)
}
So far,ReserveHeaders
The minimum starting height will be selected from the task queue and constructedfetchRequest
Pass tofetch
Get 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.headerPendPool
In, an error is returned directly; Otherwise, continue processing and record the node fromqueue.headerPendPool
Delete 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:
- Check the height and hash of the starting block
- Check high connectivity
- 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 dataheaderPeerMiss
And 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 notifyheaderProcCh
Handle 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
, notificationskeleton
All downloaded
if len(q.headerTaskPool) == 0 {
q.headerContCh <- false
}
DeliverHeaders
It will verify and save the data and send a channel message toDownloader.processHeaders
andDownloader.fetchParts
ofwakeCh
Parameters.
Schedule
processHeaders
In processingheader
When data is, it will callqueue.Schedule
For downloadbody
andreceipt
Prepare.
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
stayqueue
Are you readybodyandreceiptRelevant data,processHeaders
The last paragraph is the key code to wake up and download Bodyies and receipts, which will be notifiedfetchBodies
andfetchReceipts
The respective data can be downloaded.
for _, ch := range []chan bool{d.bodyWakeCh, d.receiptWakeCh} {
select {
case ch <- true:
default:
}
}
andfetchXXX
Will callfetchParts
The logic is similar to the above,reserve
Eventually, thereserveHeaders
,deliver
The 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 in
queue.resultCache
Position in, and then fillqueue.resultCache
Element 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:resultCache
Field is used to record the processing results of all data being processed. Its element type isfetchResult
。 itsPending
Field 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,full
Only need to download in modebody
Data, andfast
Download one more modereceipt
data
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 constructionfetchRequest
Structure and return.
DeliverBodies&Receipts
body
orreceipt
The data has passedreserve
Operation constructedfetchRequest
Structure and pass tofetch
The next step is to wait for the data to arrive. After the data is downloaded successfully, it will callqueue
Objectdeliver
Method, includingqueue.DeliverBodies
andqueue.DeliverReceipts
。 Both methods are called with different parametersqueue.deliver
method:
① : 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 callingreconstruct
fillresultCache[index]
Corresponding fields in
for i, header := range request.Headers {
...
if err := reconstruct(header, i, q.resultCache[index]); err != nil {
failure = err
break
}
}
③ : validationresultCache
The data in the correspondingrequest.Headers
Mediumheader
All 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.resultCache
Yes(accepted
>0), sendqueue.active
News.Results
Will wait for this signal.
Results
When the (header, body, and receipt) are downloaded, the blocks will be written to the database,queue.Results
It is used to return all data that has been downloaded. It is inDownloader.processFullSyncContent
andDownloader.processFastSyncContent
Called in. The code is relatively simple, so I won’t say much.
only this and nothing morequeue
The object is almost analyzed.
Sync headers
fetchHeaders
synchronizationheaders
By functionfetchHeaders
To finish it.
fetchHeaders
General idea:
synchronizationheader
The 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 inskeleton
The specific steps are shown in the figure below:
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 andTD
Blockchain 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 itMaxHeaderFetch
Blocks, and then found that these blocks could not fill my previous blocks correctlyskeleton
(probably)skeleton
The data is wrong or used to fill inskeleton
If your data is wrong, you will lose it.
Next, let’s see how the code implements:
① : initiate acquisitionheader
Request for
If Downloadskeleton
, from the heightfrom+MaxHeaderFetch-1
Start (including), everyMaxHeaderFetch-1
Height request oneheader
, maximum requestsMaxSkeletonSize
One. If not, obtain the completeheaders
。
② : wait and processheaderCh
Mediumheader
data
2.1 ensure that the remote node is returning. We need to fill inskeleton
Requiredheader
if packet.PeerId() != p.id {
log.Debug("Received skeleton from incorrect peer", "peer", packet.PeerId())
break
}
2.2 ifskeleton
After downloading, you need to continue fillingskeleton
if packet.Items() == 0 && skeleton {
skeleton = false
getHeaders(from)
continue
}
2.3 wholeskeleton
The population is complete and there are no to getheader
Yes, I want to inform youheaderProcCh
All 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 whenheader
There is data and is getting itskeleton
When, callfillHeaderSkeleton
fillskeleton
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 withinreorgProtThreshold
Within, if not, it will be the highestreorgProtHeaderDelay
Throw 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 anyheader
Not processed, sent toheaderProcCh
Processing,Downloader.processHeaders
It 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 waitingfsHeaderContCheck
Second, call againgetHeaders
Request 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 isreorgProtThreshold
andreorgProtHeaderDelay
The origin of this variable.
So far,Downloader.fetchHeaders
The method ends, and all block headers are synchronized. We mentioned filling aboveskeleton
When, byfillHeaderSkeleton
Function, and then we’ll talk about filling in detailskeleton
Details.
fillHeaderSkeleton
First, we know that when Ethereum synchronizes blocks, first determine the height interval of the block to be downloaded, and then press this intervalMaxHeaderFetch
It is divided into many groups, and the last block of each group forms a “skeleton” (the last group)MaxHeaderFetch
Blocks are not counted as a group). If you are not clear, you can see the figure above.
① : a batchheader
The 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) {}
② : callfetchParts
obtainheaders
data
fetchParts
Is a very core function, the followingFetchbodies
andFetchReceipts
Will be called. Let’s take a general look firstfetchParts
Structure 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 simplifiedchannel
In processing, the first fourchannel
Responsible for waiting for messages in a loop,update
Used to wait for the other fourchannel
Notice to process logic, first analyze one by onechannel
。
2.1 deliverych deliver downloaded data
deliveryCh
The function is to transfer the downloaded data. When the data is really downloaded, it will be sent to the userchannel
Send a message to pass the data. The corresponding channels are:d.headerCh
、d.bodyCh
、d.receiptCh
, and these threechannel
Data is written in the following three methods:DeliverHeaders
、DeliverBodies
、DeliverReceipts
。 look downdeliveryCh
How 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 passdeliver
Deliver the received download data. Notify if there are no errorsupdate
handle.
it is to be noted thatdeliver
Is a callback function that calls the deliver method of the queue object:queue.DeliverHeaders
、queue.DeliverBodies
、queue.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 asetIdle
Function, which is also a callback function. Its implementation is calledpeerConnection
Object related methods:SetHeadersIdle
、SetBodiesIdle
、SetReceiptsIdle
。 This function means that some nodes are idle for certain types of data, such asheader
、bodies
、receipts
, if you need to download these types of data, you can download these data from idle nodes.
2.2 wakeCh
awakenfetchParts
, 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,wakeCh
The value of is actuallyqueue.headerContCh
。 stayqueue.DeliverHeaders
When it is found that all headers to be worn are downloaded, false will be sent to this channel.fetchParts
When 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,body
andreceipt
then isbodyWakeCh
andreceiptWakeCh
, inprocessHeaders
If allheader
If the download is complete, send itfalse
Give these twochannel
, notify them that there are no new onesheader
Yes.body
andreceipt
Your download depends onheader
, needheader
Download can only be downloaded after downloading, so for the next wearbody
orreceipt
offetchParts
Come on, get thiswakeCh
It 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 activationupdate
Message 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.
expire
Is a callback function that returns all current timeout data information. The actual implementation of this function is calledqueue
ObjectExpire
method:ExpireHeaders
、ExpireBodies
、ExpireReceipts
, 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.requestTTL
Method 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,pending
They correspond to each otherqueue.PendingHeaders
、queue.PendingBlocks
、queue.PendingReceipts
, which is used to return the number of tasks to be downloaded.inFlight()
They correspond to each otherqueue.InFlightHeaders
、queue.InFlightBlocks
、queue.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, callfetch
Function to send a data request
Idle()
The callback function has been mentioned above,throttle()
Callback functionsqueue.ShouldThrottleBlocks
、queue.ShouldThrottleReceipts
, which indicates whether it should be downloadedbodies
perhapsreceipts
。
reserve
Functions correspond to each otherqueue.ReserveHeaders
、queue.ReserveBodies
、queue.ReserveReceipts
, which is used to select some downloadable tasks from the download tasks and construct afetchRequest
Structure. It also returns aprocess
Variable that indicates whether empty data is being processed. For example, it is possible that a block does not contain any transaction, so itsbody
andreceipt
All are empty. In fact, this kind of data does not need to be downloaded. stayqueue
ObjectReserve
This 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.
capacity
Callback functions correspond topeerConnection.HeaderCapacity
、peerConnection.BlockCapacity
、peerConnection.ReceiptCapacity
Used to determine the number of requested data to download.
fetch
Callback functions correspond topeer.FetchHeaders
、peer.Fetchbodies
、peer.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, returnerrPeersUnavailable
Wrong.
only this and nothing morefetchParts
The function analysis is almost complete. What’s involved in itqueue.go
Some related functions are inQueue detailsIt is introduced in the section.
processHeaders
adoptheaderProcCh
receiveheader
Data, and the process of processing is inprocessHeaders
Function. The whole process focuses on:Case headers: = < - d.headerprocch
:
① : ifheaders
If the length of is 0, the following operations will occur:
1.1 notify ownerheader
It 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 theirTD
Less than ours, or has passed oursfetcher
The 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 yesfast
perhapslight
Sync, 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
}
}
② : ifheaders
The length of is greater than 0
2.1 if it is fast or light synchronization, callightchain.InsertHeaderChain()write inheader
reachleveldb
database
if d.mode == FastSync || d.mode == LightSync {
....
d.lightchain.InsertHeaderChain(chunk, frequency);
....
}
2.2 if yesfast
perhapsfull sync
Mode, 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 hereHeaders
Your analysis is complete.
Synchronous bodies
synchronizationbodies
ByfetchBodies
Function completed.
fetchBodies
The process of synchronizing bodies is similar to that of synchronizing headers. The steps are as follows:
- call
fetchParts
ReserveBodies
() frombodyTaskPool
Remove the to be synchronized from thebody
;- call
fetch
, that is, call hereFetchBodies
Get from nodebody
, sendGetBlockBodiesMsg
Message; - received
bodyCh
After the data is invokeddeliver
Function that combines transactions andUncles
write inresultCache
。
Sync receipts
fetchReceipts
synchronizationreceipts
The process is synchronized withheader
Similarly, the following steps are roughly described:
- call
fetchParts
() ReserveBodies
() fromReceiptTaskPool
Remove the to be synchronized from theReceipt
- Call here
FetchReceipts
Get from nodereceipts
, sendGetReceiptsMsg
Message; - received
receiptCh
After the data is invokeddeliver
Function that willReceipts
write inresultCache
。
Synchronization status
Here we talk about state synchronization in two modes:
- fullSync:
processFullSyncContent
,full
ModeReceipts
Not cached toresultCache
First, it is directly extracted from the cachebody
Data, 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 fromresult
Get 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:
The specific code readers read by themselves is roughly such a simple process.
② : calculate pivot block
pivot
bylatestHeight - 64
, callsplitAroundPivot
() the method takes pivot as the centerresults
It is divided into three parts:beforeP
,P
,afterP
;
pivot := uint64(0)
if height := latest.Number.Uint64(); height > uint64(fsMinFullBlocks) {
pivot = height - uint64(fsMinFullBlocks)
}
P, beforeP, afterP := splitAroundPivot(pivot, results)
③ : YesbeforeP
Partial call tocommitFastSyncData
, willbody
andreceipt
All are written to the blockchain
d.commitFastSyncData(beforeP, sync);
④ : YesPSome of the update status information isP block
State, 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
}
⑤ : YesafterP
calld.importBlockResults
, willbody
Insert blockchain instead ofreceipt
。 Because it is the last 64 blocks, there are onlyheader
andbody
, Noreceipt
And status, to passfullSync
Mode 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…