NEO looks at network communication from source code analysis

0x00 Preface

NEO is called the Chinese version of Ethereum, supports C# and java development, and has extended the SDK to js, ​​python and other programming environments with the efforts of the community, so there is no language barrier for NEO development. In addition to the important concept of blockchain, Bitcoin also introduced the ingenious solution of proof of work (PoW) to solve the problem of Byzantine error, which guarantees the creation of each block through mathematical problems. Calculations are required. However, practice has proved that it is too wasteful to provide proof of work through calculation: all full nodes in the world perform the same calculation, but the result calculated by only one node will be added to the blockchain, and the rest of the nodes will calculate All the electricity consumed is wasted. In particular, the proof of work has a 51% possible attack scheme, that is, as long as someone masters more than 50% of the computing power in the world, then he can attack the Bitcoin system and reset the blockchain. When Mr. Nakamoto invented this computing power proof-of-work method, he probably did not expect that someone would develop an ASIC mining machine specifically for mining. To solve these problems, NEO proposed a new sharing mechanism DBFT called Delegated Byzantine Fault Tolerant. NEO divides the nodes into two types, one is the ordinary node, which does not participate in the consensus, that is, the process of authenticating the transaction and signing the block is not performed. The other is the consensus node. As the name implies, it is a node that can participate in the consensus. For this part of the basic concepts, please refer to the official documentation . Next, I will use a series of blogs to analyze NEO from the source level. This article mainly analyzes the NEO network communication protocol at the source code level.

0x01 source code overview

The source code analyzed in this article is located here and downloaded to the local through the git command:

    git clone https://github.com/neo-project/neo.git

The compiler I am using is VS2017 Community Edition. After opening the neo project, you can see the project root directory file structure:

  • Consensus consensus protocol between nodes
  • Core neo core
  • Cryptography encryption method
  • Implementations Data storage and wallet implementation
  • IO class of IO NEO
  • Network method for p2p network communication
  • Related classes of SmartContract NEO smart contracts

The amount of code in the whole project is not very large, especially the project itself is written in the C# high-level language, so the code is easy to read.

0x02 message

In the NEO network, all messages are transmitted in units of Message. The definition of Message is in the Message.cs file, and its structure is as follows:Message message structure

  • The Magic field is used to determine whether the current node is running on the official network or the test network. If it is 0x00746e41, it is the official network, and if it is 0x74746e41, it is the test network.
  • The content of the _Command_ command is a directly used string, so it is not strictly defined, and it is a directly used string wherever it is used. The feeling here is that the dependency is very serious, and the command should be defined first and then called elsewhere. Although it is not stated which commands are there, we can find all the commands used in the code of message routing:

Source code location: neo/Network/RemoteNode.cs/OnMessageReceived

            switch (message.Command)
            {
                case "addr": 
                case "block": 
                case "consensus":
                case "filteradd":
                case "filterclear":
                case "filterload":
                case "getaddr":
                case "getblocks":
                case "getdata":
                case "getheaders":
                case "headers":
                case "inv":
                case "mempool":
                case "tx":
                case "verack":
                case "version":
                case "alert":
                case "merkleblock":
                case "notfound":
                case "ping":
                case "pong":
                case "reject":
            }

I have deleted the command processing part in the above source code, which is not the focus of this section. By analyzing the code, we can know that there are roughly 22 types of messages. The specific content of the message exists in the payload field in the Message after serialization.

Among all message types, there is a very special kind of message, which is three kinds of messages related to the ledger: account message (Block), consensus message (Consensus) and transaction message (Transaction). These three messages correspond to the three classes in the system:

  • neo/Core/Block
  • neo/Core/Transaction
  • neo/Network.Payloads/ConsensusPayload

These three classes all implement the interface IInventory. I translate the inventory into a ledger, and the class that implements the IInventory interface is a ledger class, and the message is called a ledger message. The IInventory interface defines the hash value Hash of the message to store the signature, the ledger message type InventoryType to save the message type, and a verification function verify to verify the message, that is to say, all ledger messages need to contain the signature and need to verify. The type of ledger message is defined in the InventoryType.cs file:

Source code location: neo/Network/InventoryType.cs

        /// 交易
        TX = 0x01,
        /// 区块
        Block = 0x02,
        /// 共识数据
        Consensus = 0xe0

If you are interested in the news of the consensus part, you can check my other blog NEO to see the consensus protocol from source code analysis . This article only focuses on transaction communication and block synchronization of ordinary nodes.

Each RemoteNode has two message queues, a high-priority queue and a low-priority queue. The high-priority queue is mainly responsible for:

  • "alert"
  • "consensus"
  • "filteradd"
  • "filterclear"
  • "filterload"
  • "getaddr"
  • "mempool"

These few commands and the rest are handled by the low-priority queue. The task of sending the command is responsible for the StartSendLoop method. There is a while loop in this method. In each round of the loop, it will first detect whether the high-priority queue is empty. If it is not empty, send the high-priority command first, otherwise send the low-priority command. level task, the core source code in the loop is as follows:

Source code location: neo/Netwotk/RemoteNode.cs/StartSendLoop

                Message message = null;
                lock (message_queue_high)
                {
                    //高优先级消息队列不为空
                    if (message_queue_high.Count > 0)
                    {
                        message = message_queue_high.Dequeue();
                    }
                }
                //若没有高优先级任务
                if (message == null)
                {
                    lock (message_queue_low)
                    {
                        if (message_queue_low.Count > 0)
                        {
                            //获取低优先级任务
                            message = message_queue_low.Dequeue();
                        }
                    }
                }

Since each RemoteNode object is only responsible for communicating with one corresponding remote node, there is no message buffer queue where the message is received. The loop for receiving the message is just below the position where StartSendLoop is called. Since StartSendLoop itself is an asynchronous method, it will not block the execution of the receiving message loop of the code. After each message is received, the OnMessageReceived method will be triggered, and the received message The message is passed as a parameter. As mentioned above, this OnMessageReceived method is actually a message router, and will call the response processing function according to the different message types.

0x03 New node networking

Nodes are the basic units that make up the NEO network, so everything starts with the local node connecting to the neo network. NEO has a LocalNode class under the Network folder. The main job of this class is to establish and manage the connection with the remote node with the p2p network, and communicate with the remote node through its internal RemoteNode object list. LocalNode creates a new thread in the Start method, requests the address information of the nodes in the network from the preset server in the new thread, and then sends the local server address and port to the remote server so that other nodes can find themselves.

Source code location: neo/Network/LocalNode.cs/Start

Task.Run(async () =>
                {
                    if ((port > 0 || ws_port > 0)
                        && UpnpEnabled
                        && LocalAddresses.All(p => !p.IsIPv4MappedToIPv6 || IsIntranetAddress(p))
                        && await UPnP.DiscoverAsync())
                    {
                        try
                        {
                            LocalAddresses.Add(await UPnP.GetExternalIPAsync());   //添加获取到的网络中节点信息
                            if (port > 0)
                                await UPnP.ForwardPortAsync(port, ProtocolType.Tcp, "NEO");  //向服务器注册本地节点
                            if (ws_port > 0)
                                await UPnP.ForwardPortAsync(ws_port, ProtocolType.Tcp, "NEO WebSocket");
                        }
                        catch { }
                    }
                    connectThread.Start();  //开启线程与网络中节点建立连接
                    poolThread?.Start();
                    if (port > 0)
                    {
                        listener = new TcpListener(IPAddress.Any, port); //开启服务,监听网络中的广播信息
                        listener.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1);
                        try
                        {
                            listener.Start();  //开启端口,监听连接请求
                            Port = (ushort)port;
                            AcceptPeers();  //处理p2p网络中的socket连接请求
                        }
                        catch (SocketException) { }
                    }
                    if (ws_port > 0)
                    {
                        ws_host = new WebHostBuilder().UseKestrel().UseUrls($"http://*:{ws_port}").Configure(app => app.UseWebSockets().Run(ProcessWebSocketAsync)).Build();
                        ws_host.Start();
                    }
                });

It can be seen from the code that after successfully obtaining the node information and registering it in the server, the node will open a thread and establish a connection with these nodes in the thread. The final interface to establish the connection in the LocalNode class is the ConnectToPeerAsync method. In the ConnectToPeerAsync method, a new object of the TcpRemoteNode class is created according to the received remote node address and port information:

Source code location: neo/Network/LocalNode.cs/ConnectToPeerAsync

            //新建远程节点对象
            TcpRemoteNode remoteNode = new TcpRemoteNode(this, remoteEndpoint);
            if (await remoteNode.ConnectAsync())
            {
                OnConnected(remoteNode);
            }

The TcpRemoteNode class inherits from RemoteNode. Each object represents a remote node that establishes a connection with itself. The relationship between RemoteNode and LocalNode can be roughly expressed as follows:Network topology

After receiving the remote node information, the constructor of TcpRemoteNode will establish a socket connection with the remote node and return a RemoteNode object. All remote node objects are stored in the remote node list in LocalNode.

In addition to obtaining network nodes from the NEO server, there is also an active way to obtain network nodes, which is to broadcast network node requests to all nodes that establish connections with local nodes, and obtain the list of nodes that establish connections with remote nodes in real time. Get node information in the entire network. This part of the code is in the thread that establishes the connection to the remote node:

Source code location: neo/Network/LocalNode.cs/ConnectToPeersLoop

                        lock (connectedPeers)
                        {
                            foreach (RemoteNode node in connectedPeers)
                                node.RequestPeers();
                        }

The RequestPeers method for requesting a list of nodes from a remote node is in the RemoteNode class. This method is obtained by sending the command "getaddr" to the remote node. Since the responsibility of the RemoteNode is to communicate with its corresponding remote node, the parsing and routing of the remote command "getaddr" is also performed in the RemoteNode class. After the RemoteNode receives the remote node information, the OnMessageReceived method is triggered to parse and route the received information:

Source code location: neo/Network/RemoteNode.cs

        /// <summary>
        /// 对接收信息进行路由
        /// </summary>
        /// <param name="message"></param>
        private void OnMessageReceived(Message message)
        {
            switch (message.Command)
            {
                case "getaddr":
                    OnGetAddrMessageReceived();
                    break;
                    //代码省略
            }
        }

I have deleted the analysis of other commands in switch, and only focus on the "getaddr" command here. After receiving the "getaddr" command, the corresponding handler function OnGetAddrMessageReceived is called:

Source code location: neo/Network/RemoteNode.cs/OnGetAddrMessageReceived

            AddrPayload payload;
            lock (localNode.connectedPeers)
            {
                const int MaxCountToSend = 200;
                //  获取本地连接节点
                IEnumerable<RemoteNode> peers = localNode.connectedPeers.Where(p => p.ListenerEndpoint != null && p.Version != null);
                if (localNode.connectedPeers.Count > MaxCountToSend)
                {
                    Random rand = new Random();
                    peers = peers.OrderBy(p => rand.Next());
                }
                peers = peers.Take(MaxCountToSend);
                payload = AddrPayload.Create(peers.Select(p => NetworkAddressWithTime.Create(p.ListenerEndpoint, p.Version.Services, p.Version.Timestamp)).ToArray());
            }
            EnqueueMessage("addr", payload);

Since the direct communication with the remote node is the corresponding local RemoteNode object, and these objects need to obtain the information stored in the LocalNode, the processing method of the NEO source code is to directly pass in the reference of the LocalNode when creating the RemoteNode object, here I It's uncomfortable because there are obviously circular references, although there won't be any problem functionally here. Because each node acts as both a client and a server, in the network connection established with this node, there is a socket connection initiated by itself, and a socket connection established by a remote node using this node as a server. The task of monitoring socket connections is continuously executed in the thread. Whenever a new socket connection is received, the current node will create a new TcpRemoteNode object according to the socket and save it in the remote node list of LocalNode:

Source code location: neo/Network/LocalNode.cs/AcceptPeers

 TcpRemoteNode remoteNode = new TcpRemoteNode(this, socket);
 OnConnected(remoteNode);

Finally, take the network topology of three nodes as an example:Three-node network structure topology

0x04 Block synchronization

The generation and synchronization of the new area mainly depends on the broadcast after the consensus is completed, but how should the nodes of the new network obtain the complete blockchain? This section will analyze the source code for this problem.

When a new RemoteNode object is created, the protocal of the object will be opened: Source code location: neo/Network/LocalNode.cs

 private void OnConnected(RemoteNode remoteNode)
        {
            lock (connectedPeers)
            {
                connectedPeers.Add(remoteNode);
            }
            remoteNode.Disconnected += RemoteNode_Disconnected;//断开连接通知
            remoteNode.InventoryReceived += RemoteNode_InventoryReceived;//账单消息通知
            remoteNode.PeersReceived += RemoteNode_PeersReceived;//节点列表信息通知
            remoteNode.StartProtocol();//开启通信协议
        }

After the protocol starts executing, a "version" command is sent to the remote node. When querying the response method of the "version" command, I was shocked. The call was Disconnect and the parameter passed was true. In line with the materialistic value of "the first thing after a new connection is established is definitely not to disconnect", I did some research on the code and finally found that the command to send "version" is directly obtained by the ReceiveMessageAsync method Yes, that is, without going through that message routing. As after the connection is established between the two nodes. The first thing both do is to send the "version" command and its own VersionPayload, so the first message received by the node in this socket connection is also a "version" type of message.

Source code location: neo/Network/RemoteNode.cs/StartProtocol

if (!await SendMessageAsync(Message.Create("version", VersionPayload.Create(localNode.Port, localNode.Nonce, localNode.UserAgent))))
                return;
Message message = await ReceiveMessageAsync(HalfMinute);

Here we need to explain the VersionPayload, which contains the status information of the current node:VersionPayload

That is to say, after the connection is established, the current node can know the current blockchain height of the remote node. If its current blockchain height is lower than that of the remote node, it will send the "getblocks" command to the remote node to request blockchain synchronization. : Source location: neo/Network/RemoteNode.cs/StartProtocol

if (missions.Count == 0 && Blockchain.Default.Height < Version.StartHeight)
{
        EnqueueMessage("getblocks", GetBlocksPayload.Create(Blockchain.Default.CurrentBlockHash));
}

Because the blockchain has a very large amount of data, the synchronization of the blockchain cannot be completed directly at one time. After receiving the "getblocks" command, the hash value of 500 blocks is sent each time:

Source code location: neo/Network/RemoteNode.cs/OnGetBlocksMessageReceived

            List<UInt256> hashes = new List<UInt256>();
            do
            {
                hash = Blockchain.Default.GetNextBlockHash(hash);
                if (hash == null) break;
                hashes.Add(hash);
            } while (hash != payload.HashStop && hashes.Count < 500);
            EnqueueMessage("inv", InvPayload.Create(InventoryType.Block, hashes.ToArray()));

After each time it receives a message from the remote node, if the block height of the current node is still smaller than that of the remote node, the local node will continue to send blockchain synchronization requests until it is synchronized with the remote node's blockchain.

Donation address (NEO) : ASCjW4xpfr8kyVHY1J2PgvcgFbPYa1qX7F

{{o.name}}
{{m.name}}

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=324120015&siteId=291194637