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:
- What kind of data do I need to send it?
- How does it respond internally?
- 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.BlockRequestWorker
is more complex, it contains:
- Calculate the data that needs to be synchronized based on the chunk data you hold
- Send data request to the best node found earlier
- Get the chunk data from each other.
- Processing of data
- Broadcast new status
- 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:
- Simple: Assuming there is no fork, directly check the local highest-height chunk and then request the next chunk
- 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:
- Constructs the requested information
- 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? chID
What is it again?
mconn
is 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:
- 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)
- 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.