NEO從源碼分析看網絡通訊

0x00 前言

NEO被稱爲中國版的Ethereum,支持C#和java開發,而且在社區的努力下已經把SDK拓展到了js,python等編程環境,因此進行NEO開發的話是沒有太大語言障礙的。 比特幣在解決拜占庭錯誤這個問題時除了引入了區塊鏈這個重要的概念以外,還引入了工做量證實(PoW)這個機智的解決方案,經過數學意義上的難題來保證每一個區塊建立都須要付出計算量。然而實踐已經證實,經過計算來提供工做量證實,實在是太浪費:全世界全部的徹底節點都進行一樣的計算,然而只有一個節點計算出的結果會被添加到區塊鏈中,其他節點計算消耗的電力就都白白浪費了。尤爲,工做量證實存在一個51%的可能攻擊方案,就是說只要有人掌握了世界上超過50%的算力,那麼他就能夠對比特幣這個系統進行攻擊,重置區塊鏈。中本聰先生髮明這個算力工做量證實方法的時候大概沒有料到會有人專門爲了挖礦開發出ASIC礦機。 NEO在解決這些問題的時候提出了一個新的共享機制DBFT 全稱爲 Delegated Byzantine Fault Tolerant。NEO將節點分爲兩種,一種爲普通節點,不參與共識,也就是不進行認證交易簽名區塊的過程。另外一種是則是共識節點。顧名思義,就是能夠參與共識的節點,這部分基礎概念能夠參考官方文檔。 接下來我將會以一系列的博客來從源碼層面上對NEO進行分析。 而本文主要進行的是源碼層級的NEO網絡通訊協議分析。html

0x01 源碼概覽

本文分析的源碼位於這裏,經過git命令下載到本地:java

git clone https://github.com/neo-project/neo.git
複製代碼

我是用的編譯器是VS2017社區版。打開neo項目以後能夠看到項目根目錄文件結構:node

  • Consensus 共識節點間共識協議
  • Core neo核心
  • Cryptography 加密方法
  • Implementations 數據存儲以及錢包的實現
  • IO NEO的io類
  • Network 用於p2p網絡通訊的方法
  • SmartContract NEO智能合約的相關類

整個項目代碼量不算很大,尤爲是項目自己是C#高級語言編寫,因此代碼很容易讀懂。python

0x02 消息

在NEO網絡中,全部的消息都以Message爲單位進行傳輸,Message的定義在Message.cs文件中,其結構以下: Message消息結構git

  • Magic
    字段用來肯定當前節點是運行在正式網絡仍是在測試網絡,若是是0x00746e41則爲正式網,若是是0x74746e41則爲測試網。
  • _Command_命令的內容是直接使用的字符串,因此沒有進行嚴格定義,在全部使用到的地方都是直接使用的字符串。這裏給個人感受是依賴特別嚴重,應該先定義好命令再在別的地方調用。雖然沒有明說都有哪些命令,可是對消息路由的代碼裏咱們能夠找到全部使用到的命令:

源碼位置:neo/Network/RemoteNode.cs/OnMessageReceivedgithub

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":
            }
複製代碼

以上源碼中的對命令的處理部分我都刪掉了,這個不是本小節討論重點。經過分析代碼能夠知道,消息種類大體22種。 消息的具體內容在序列化以後存在在Message裏的payload字段中。編程

在全部的消息類型中有一類消息很是特殊,這就是與帳本相關的三種消息:帳目消息(Block),共識消息(Consensus)以及交易消息(Transaction)。這三中消息分別對應系統中的三個類:緩存

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

這三個類都實現了接口IInventory,我把inventory翻譯爲帳本,把實現了IInventory接口的類成爲帳本類,消息稱爲帳本消息。IInventory接口定義了消息的哈希值Hash用來存放簽名、帳本消息類型InventoryType用來保存消息類型以及一個驗證函數verify用來對消息進行驗證,也就是說全部的帳本消息都須要包含簽名,而且須要驗證。 帳本消息的類型定義在InventoryType.cs文件中:bash

源碼位置:neo/Network/InventoryType.cs服務器

/// 交易
        TX = 0x01,
        /// 區塊
        Block = 0x02,
        /// 共識數據
        Consensus = 0xe0
複製代碼

對共識部分的消息感興趣的能夠查看個人另外一篇博客NEO從源碼分析看共識協議,本文僅僅關注於交易通訊和普通節點的區塊同步。

每一個RemoteNode內部都有兩個消息隊列,一個高優先級隊列和一個低優先級隊列,高優先級隊列主要負責:

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

這幾個命令,其他的命令都由低優先級隊列負責。 發送命令的任務由StartSendLoop方法負責,在這個方法中有一個while循環,在每一輪循環中都會首先檢測高優先級隊列是否爲空,若是不爲空則先發送高優先命令,不然發送低優先級任務,循環中的核心源碼以下:

源碼位置: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();
                        }
                    }
                }
複製代碼

因爲每一個RemoteNode對象都只負責和一個相對應的遠程節點通訊,因此接收消息的地方沒有設置消息緩存隊列。接收消息的循環就在調用StartSendLoop位置的下面,因爲StartSendLoop自己是個異步方法,因此不會阻塞代碼的接收消息循環的執行,在每次收到消息後,都會觸發OnMessageReceived方法,並將收到的message消息做爲參數傳遞過去。在上文中也講了,這個OnMessageReceived方法實際上是個消息的路由器來着,會根據消息類型的不一樣調用響應的處理函數。

0x03 新節點組網

節點是組成NEO網絡的基本單位,因此一切都從本地節點接入neo網絡開始講起。 NEO在Network文件夾下有一個LocalNode的類,這個類的主要工做是與p2p網絡創建並管理與遠程節點鏈接,經過其內部的RemoteNode對象列表與遠程節點進行通訊。 LocalNode在Start方法中建立了新的線程,在新線程中向預設的服務器請求網絡中節點的地址信息,以後將本地的服務器地址及端口發送到遠程服務器去以便別的節點能夠找到本身。

源碼位置: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();
                    }
                });
複製代碼

經過代碼能夠看到,在成功獲取到節點信息並在服務器中註冊過以後,節點會開啓一個線程,並在線程中與這些節點創建鏈接,創建鏈接在LocalNode類中最終的接口是ConnectToPeerAsync方法,在ConnectToPeerAsync方法中根據接收到的遠程節點地址和端口信息新建一個TcpRemoteNode類的對象:

源碼位置:neo/Network/LocalNode.cs/ConnectToPeerAsync

//新建遠程節點對象
            TcpRemoteNode remoteNode = new TcpRemoteNode(this, remoteEndpoint);
            if (await remoteNode.ConnectAsync())
            {
                OnConnected(remoteNode);
            }
複製代碼

TcpRemoteNode類繼承自RemoteNode,每一個對象都表明着一個與本身創建鏈接的遠程節點,RemoteNode和LocalNode的關係大體能夠這樣表示: 網絡拓撲

TcpRemoteNode的構造函數在接收到遠程節點信息以後會與遠程節點創建socket鏈接並返回一個RemoteNode對象,全部的遠程節點對象都被保存在LocalNode中的遠程節點列表裏。

獲取網絡節點的方式除了從NEO服務器獲取以外還有一個主動獲取的方式,那就是向全部的與本地節點創建鏈接的節點廣播網絡節點請求,經過獲取這些與遠程節點創建鏈接的節點列表來實時獲取整個網絡中的節點信息。這部分代碼在與遠程節點創建鏈接的線程中:

源碼位置:neo/Network/LocalNode.cs/ConnectToPeersLoop

lock (connectedPeers)
                        {
                            foreach (RemoteNode node in connectedPeers)
                                node.RequestPeers();
                        }
複製代碼

向遠程節點請求節點列表的RequestPeers方法在RemoteNode類中,這個方法經過向遠程節點發送指令「getaddr」來獲取。 因爲RemoteNode的責任是與其對應的遠程節點進行通訊,因此對「getaddr」這個遠程命令的解析和路由也是在RemoteNode類中進行。在RemoteNode接收到遠程節點信息後會觸發OnMessageReceived方法對收到的信息進行解析和路由:

源碼位置:neo/Network/RemoteNode.cs

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

switch中對於別的命令的解析我都刪掉了,這裏只關注「getaddr」命令。在收到「getaddr」命令後,會調用相應的處理函數OnGetAddrMessageReceived:

源碼位置: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);
複製代碼

因爲直接與遠程節點進行通訊的是與其對應的本地的RemoteNode對象,而這些對象有須要獲取LocalNode中保存的信息,NEO源碼的處理方式是直接在建立RemoteNode對象的時候傳入LocalNode的引用,這裏我感受很不舒服,由於明顯有循環引用,儘管在這裏功能上不會有什麼問題。 由於每一個節點既作爲客戶端,又做爲服務端,與本節點創建的網絡鏈接裏,即存在本身主動發起的socket鏈接,也存在遠程節點將本節點做爲服務端而創建的socket鏈接。 監聽socket鏈接的任務在線程中不斷的執行,每當接收到一個新的socket鏈接,當前節點會根據這個socket來建立一個新的TcpRemoteNode對象並保存在LocalNode的遠程節點列表中:

源碼位置:neo/Network/LocalNode.cs/AcceptPeers

TcpRemoteNode remoteNode = new TcpRemoteNode(this, socket);
 OnConnected(remoteNode);
複製代碼

最後以三個節點的網絡拓撲爲例: 三節點網絡結構拓撲

0x04 區塊同步

新區快的生成與同步主要依靠共識完成後的廣播,可是對於新組網的節點應該如何獲取完整的區塊鏈呢?本小節將針對這個問題進行源碼的分析。

當一個新的RemoteNode對象建立以後,會開啓這個對象的protocal: 源碼位置: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();//開啓通訊協議
        }
複製代碼

在協議開始執行後,會向遠程節點發送一個 "version" 命令。在查詢這個 "version" 命令的響應方法的時候簡直把我嚇了一大跳,竟然調用的是Disconnect並且傳的參數是true。本着「新鏈接創建以後的第一件事確定不會是斷開鏈接」這個惟物主義價值觀,我又對代碼進行了一番研究,終於發現這個發送 「version」 的命令是直接由ReceiveMessageAsync方法獲取的,也就是不通過那個消息路由。因爲在兩個節點創建鏈接後。二者作的第一件事都是發送 「version」 命令和本身的VersionPayload過去,因此在這個socket鏈接中節點接收到的第一條消息也都是「version」類型的消息。

源碼位置: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);
複製代碼

這裏須要對這個VersionPayload進行下講解,這個VersionPayload裏包含當前節點的狀態信息: VersionPayload

也就是說在鏈接創建後,當前節點就能夠知道遠程節點當前的區塊鏈高度,若是本身當前的區塊鏈高度低於遠程節點,就會向遠程節點發送 "getblocks" 命令請求區塊鏈同步: 源碼位置:neo/Network/RemoteNode.cs/StartProtocol

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

由於區塊鏈有很是大的數據量,區塊鏈同步不可能直接一次完成,每次收到 「getblocks」的命令以後,每次發送500個區塊的哈希值:

源碼位置: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()));
複製代碼

以後在每次接收到遠程節點的消息以後,若是當前節點區塊高度依然小於遠程節點,本地節點會繼續發送區塊鏈同步請求,直到與遠程節點的區塊鏈同步。

捐贈地址(NEO)

:ASCjW4xpfr8kyVHY1J2PgvcgFbPYa1qX7F


羣交流:795681763

原文:https://my.oschina.net/u/2276921/blog/1622015

相關文章
相關標籤/搜索