Peel the original View Code 05: How to get the chunk data from the original node?

Source: Internet
Author: User
Tags netsync

Author: freewind

Compared to the original project warehouse:

GitHub Address: Https://github.com/Bytom/bytom

Gitee Address: Https://gitee.com/BytomBlockc ...

In the previous article, we already know how to connect to a peer port on the original node and authenticate with each other. At this point, the two nodes have been established trust, and the connection will not be broken, the next step, the two can continue to exchange data.

So, the first thing I think about is, how can I get each other to send me all the chunk data that I have?

This can actually be divided into three questions:

    1. What kind of data do I need to send it?
    2. How does it respond internally?
    3. What should I do when I get the data?

Since this piece of logic is still relatively complex, so in this article we first answer the first question:

What kind of data request do we have to send to the original node to let me have the chunk data it holds?

Find the code that sent the request

First of all we need to locate in the code, than when the original is to send the request to the other node.

In the previous article on how to establish a connection and verify the identity, the operation that sent the data request must be after the last code. According to this idea, after we start in the class, we SyncManager Switch find a BlockKeeper class called, and the related operation is done in it.

The following is the usual, or starting from the start, but it will be more streamlined:

Cmd/bytomd/main.go#l54

func main() {    cmd := cli.PrepareBaseCmd(commands.RootCmd, "TM", os.ExpandEnv(config.DefaultDataDir()))    cmd.Execute()}

Cmd/bytomd/commands/run_node.go#l41

func runNode(cmd *cobra.Command, args []string) error {    n := node.NewNode(config)    if _, err := n.Start(); err != nil {        // ...}

node/node.go#l169

func (n *Node) OnStart() error {    // ...    n.syncManager.Start()    // ...}

netsync/handle.go#l141

func (sm *SyncManager) Start() {    go sm.netStart()    // ...    go sm.syncer()}

Note sm.netStart() that the operation in which we establish the connection and verify the identity in an article is done within it. And this time, this problem is done in the following sm.syncer() .

Also note that because both function calls use Goroutine, they are performed concurrently.

sm.syncer()The code is as follows:

Netsync/sync.go#l46

func (sm *SyncManager) syncer() {    sm.fetcher.Start()    defer sm.fetcher.Stop()    // ...    for {        select {        case <-sm.newPeerCh:            log.Info("New peer connected.")            // Make sure we have peers to select from, then sync            if sm.sw.Peers().Size() < minDesiredPeerCount {                break            }            go sm.synchronise()            // ..    }}

Here fetcher is a strange thing called, the name seems to be specifically to crawl the data, we are looking for it?

Unfortunately, fetcher the function is to get the chunk data from multiple peers, organize the data, and put useful on the local chain. We will study it later, so there is no discussion here.

Then a for loop, when the Discovery Channel newPeerCh has new data (that is, with a new node connection), will determine whether the current node is more than enough (greater than or equal minDesiredPeerCount to the value 5 ), enough, will enter sm.synchronise() , data synchronization.

Why wait a few more nodes here instead of synchronizing at once? I think this is the chance to have more choices, to find a node with enough data.

sm.synchronise()Still belong SyncManager to the method. Before actually invoking BlockKeeper the method, it does some things like cleaning up the disconnected peer, finding the peer that best fits the synchronization data. The "clean peer" work involves synchronization between peer collections held by different objects, which is somewhat cumbersome, but not helpful for current problems, so I'm going to put them in a later question (such as "when a node is disconnected, than what it would have done"), this is omitted.

sm.synchronise()The code is as follows:

Netsync/sync.go#l77

func (sm *SyncManager) synchronise() {    log.Info("bk peer num:", sm.blockKeeper.peers.Len(), " sw peer num:", sm.sw.Peers().Size(), " ", sm.sw.Peers().List())    // ...    peer, bestHeight := sm.peers.BestPeer()    // ...    if bestHeight > sm.chain.BestBlockHeight() {        // ...        sm.blockKeeper.BlockRequestWorker(peer.Key, bestHeight)    }}

As you can see, the first is to find the most suitable one from the numerous peers. What do you mean best? Look at BestPeer() the definition:

netsync/peer.go#l266

func (ps *peerSet) BestPeer() (*p2p.Peer, uint64) {    // ...    for _, p := range ps.peers {        if bestPeer == nil || p.height > bestHeight {            bestPeer, bestHeight = p.swPeer, p.height        }    }    return bestPeer, bestHeight}

is actually the longest one that holds the blockchain data.

After finding the Bestpeer, we call the sm.blockKeeper.BlockRequestWorker(peer.Key, bestHeight) method, from here, formally into the BlockKeeper world of the protagonist of this article.

Blockkeeper

blockKeeper.BlockRequestWorkeris more complex, it contains:

    1. Calculate the data that needs to be synchronized based on the chunk data you hold
    2. Send data request to the best node found earlier
    3. Get the chunk data from each other.
    4. Processing of data
    5. Broadcast new status
    6. Handle all kinds of error situations, etc.

Since this article focuses only on "sending requests", some logic that has little to do with it will be overlooked and left to be told later.

In the "Send Request" here, there are actually two scenarios, a simple, a complex:

    1. Simple: Assuming there is no fork, directly check the local highest-height chunk and then request the next chunk
    2. Complex: Consider the case of bifurcation, the current local block may exist bifurcation, then in the end should request which block, need to carefully consider

Because the 2nd case is too complex for this article (because it requires a deep understanding of the processing logic than the fork in the original chain), the problem is simplified in this article and only the 1th is considered. and the processing of the fork, will be put in the future to explain.

The following is a blockKeeper.BlockRequestWorker simplified code that contains only the 1th case:

netsync/block_keeper.go#l72

func (bk *blockKeeper) BlockRequestWorker(peerID string, maxPeerHeight uint64) error {    num := bk.chain.BestBlockHeight() + 1    reqNum := uint64(0)    reqNum = num    // ...    bkPeer, ok := bk.peers.Peer(peerID)    swPeer := bkPeer.getPeer()    // ...    block, err := bk.BlockRequest(peerID, reqNum)    // ...}

In this case, we can think of the bk.chain.BestBlockHeight() Best one that is locally held with the highest height of the blockchain without forking. (It is necessary to be reminded that if there is a fork, it is not Best necessarily the highest-level one.)

Then we can request the next high block directly to the best peer, which is implemented by bk.BlockRequest(peerID, reqNum) :

netsync/block_keeper.go#l152

func (bk *blockKeeper) BlockRequest(peerID string, height uint64) (*types.Block, error) {    var block *types.Block    if err := bk.blockRequest(peerID, height); err != nil {        return nil, errReqBlock    }    // ...    for {        select {        case pendingResponse := <-bk.pendingProcessCh:            block = pendingResponse.block            // ...            return block, nil        // ...        }    }}

In the simplified code above, it is divided into two main parts. One is to send the request bk.blockRequest(peerID, height) , this is the focus of this article for-select , it is already waiting for and processing the return data of the other node, this part of our first skip today.

bk.blockRequest(peerID, height)This method, logically, can be divided into two parts:

    1. Constructs the requested information
    2. Send the message to the other node

Constructs the requested information

bk.blockRequest(peerID, height)After a series of method calls, height an object is constructed using the BlockRequestMessage following code:

netsync/block_keeper.go#l148

func (bk *blockKeeper) blockRequest(peerID string, height uint64) error {    return bk.peers.requestBlockByHeight(peerID, height)}

netsync/peer.go#l332

func (ps *peerSet) requestBlockByHeight(peerID string, height uint64) error {    peer, ok := ps.Peer(peerID)    // ...    return peer.requestBlockByHeight(height)}

netsync/peer.go#l73

func (p *peer) requestBlockByHeight(height uint64) error {    msg := &BlockRequestMessage{Height: height}    p.swPeer.TrySend(BlockchainChannel, struct{ BlockchainMessage }{msg})    return nil}

Here, finally constructs the need BlockRequestMessage , actually basically is to height tell the peer.

The information is Peer then TrySend() sent out.

Send Request

In TrySend , the main is through the github.com/tendermint/go-wire library to serialize it, and then sent to the other side. It should be a very simple operation, the first police, or quite around.

When we enter TrySend() after:

p2p/peer.go#l242

func (p *Peer) TrySend(chID byte, msg interface{}) bool {    if !p.IsRunning() {        return false    }    return p.mconn.TrySend(chID, msg)}

Find it throws the pot to the p.mconn.TrySend method, then mconn what is it? chIDWhat is it again?

mconnis an MConnection example of where it came from? It should have been initialized somewhere before, or we wouldn't be able to call it directly. So let's start by finding out where it's initialized.

After a search, it was found that after the previous, that is, after the original node and another node to complete the authentication, the specific location in the Switch class start place.

This time we directly from Swtich the OnStart starting point:

p2p/switch.go#l186

func (sw *Switch) OnStart() error {    //...    // Start listeners    for _, listener := range sw.listeners {        go sw.listenerRoutine(listener)    }    return nil}

p2p/switch.go#l498

func (sw *Switch) listenerRoutine(l Listener) {    for {        inConn, ok := <-l.Connections()        // ...        err := sw.addPeerWithConnectionAndConfig(inConn, sw.peerConfig)        // ...    }}

p2p/switch.go#l645

func (sw *Switch) addPeerWithConnectionAndConfig(conn net.Conn, config *PeerConfig) error {    // ...    peer, err := newInboundPeerWithConfig(conn, sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodePrivKey, config)    // ...}

P2p/peer.go#l87

func newInboundPeerWithConfig(conn net.Conn, reactorsByCh map[byte]Reactor, chDescs []*ChannelDescriptor, onPeerError func(*Peer, interface{}), ourNodePrivKey crypto.PrivKeyEd25519, config *PeerConfig) (*Peer, error) {    return newPeerFromConnAndConfig(conn, false, reactorsByCh, chDescs, onPeerError, ourNodePrivKey, config)}

P2p/peer.go#l91

func newPeerFromConnAndConfig(rawConn net.Conn, outbound bool, reactorsByCh map[byte]Reactor, chDescs []*ChannelDescriptor, onPeerError func(*Peer, interface{}), ourNodePrivKey crypto.PrivKeyEd25519, config *PeerConfig) (*Peer, error) {    conn := rawConn    // ...    if config.AuthEnc {        // ...        conn, err = MakeSecretConnection(conn, ourNodePrivKey)        // ...    }    // Key and NodeInfo are set after Handshake    p := &Peer{        outbound: outbound,        conn:     conn,        config:   config,        Data:     cmn.NewCMap(),    }    p.mconn = createMConnection(conn, p, reactorsByCh, chDescs, onPeerError, config.MConfig)    p.BaseService = *cmn.NewBaseService(nil, "Peer", p)    return p, nil}

Finally found it. The above method is the place where the MakeSecretConnection public key is exchanged for authentication with the other node, and the following is where it is p.mconn = createMConnection(...) created mconn .

Keep going in:

p2p/peer.go#l292

func createMConnection(conn net.Conn, p *Peer, reactorsByCh map[byte]Reactor, chDescs []*ChannelDescriptor, onPeerError func(*Peer, interface{}), config *MConnConfig) *MConnection {    onReceive := func(chID byte, msgBytes []byte) {        reactor := reactorsByCh[chID]        if reactor == nil {            if chID == PexChannel {                return            } else {                cmn.PanicSanity(cmn.Fmt("Unknown channel %X", chID))            }        }        reactor.Receive(chID, p, msgBytes)    }    onError := func(r interface{}) {        onPeerError(p, r)    }    return NewMConnectionWithConfig(conn, chDescs, onReceive, onError, config)}

The original mconn is MConnection the instance that it was NewMConnectionWithConfig created by.

Look at the above code, found that this is MConnectionWithConfig net.Conn not very much the same as the ordinary, but only when the data received from the other side, according to the specified chID invocation of the Reactor corresponding Receive method to deal with. So it plays a Reactor role in distributing the data.

Why do you need such a distribution operation? This is because there are several different ways to exchange data between nodes in the original:

    1. One is to specify a detailed data interaction protocol (such as what kind of information, what is the meaning, what is issued, how to answer, etc.), in the ProtocolReactor implementation, it chID corresponds BlockchainChannel to the value ofbyte(0x40)
    2. Another uses a file sharing protocol similar to BitTorrent, called PEX, PEXReactor implemented in, which corresponds chID to a PexChannel value ofbyte(0x00)

So when sending information between nodes, you need to know which way the data is sent, and then transfer it to the corresponding Reactor processing.

In the original, the former is the main way, the latter play a supplementary role. Our current article deals with the former, which will be studied later.

p.mconn.TrySend

When we know p.mconn.TrySend what's in it and when it's mconn initialized, here's how to get into it TrySend .

p2p/connection.go#l243

func (c *MConnection) TrySend(chID byte, msg interface{}) bool {    // ...    channel, ok := c.channelsIdx[chID]    // ...    ok = channel.trySendBytes(wire.BinaryBytes(msg))    if ok {        // Wake up sendRoutine if necessary        select {        case c.send <- struct{}{}:        default:        }    }    return ok}

As you can see, it finds the corresponding channel (where it should be ProtocolReactor the corresponding channel) and calls the channel trySendBytes method. When the data is sent, the library is used and serialized into a binary github.com/tendermint/go-wire msg array.

p2p/connection.go#l602

func (ch *Channel) trySendBytes(bytes []byte) bool {    select {    case ch.sendQueue <- bytes:        atomic.AddInt32(&ch.sendQueueSize, 1)        return true    default:        return false    }}

Originally it is to send the data, put it in the corresponding channel sendQueue , handed over to others to send. Specifically who will send it, we will find it immediately.

Careful classmates will find that, in Channel addition trySendBytes to methods, there is one sendBytes (not used in this article):

p2p/connection.go#l589

func (ch *Channel) sendBytes(bytes []byte) bool {    select {    case ch.sendQueue <- bytes:        atomic.AddInt32(&ch.sendQueueSize, 1)        return true    case <-time.After(defaultSendTimeout):        return false    }}

The two difference is that the former attempts to put the data to be sent in, bytes ch.sendQueue if it can be put in, then return true , or immediately fail, return false , so it is non-blocking. The latter, if not put in ( sendQueue full, there has not finished processing), then wait defaultSendTimeout (the value of 10 seconds), and then will fail. In addition, sendQueue the capacity defaults to 1 .

In fact, we already know how to request chunk data from other nodes and when to send the information.

In this article, I would like to put the actual code to send the data together, but found that its logic is also quite complex, so you can open another story.

The

goes back to this article, and again, as we said earlier, there are two scenarios for requesting chunk data from a peer: one is simply not considering forking, and the other is a complex consideration of bifurcation. In this article, only simple cases are considered, in which case the so-called Bestheight refers to the height of the highest chunk, and in complex cases it is not necessarily. This is left to be discussed in detail in the future, the question of this article is the answer is complete.

Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.