Python從頭實現以太坊(六):Routing

local0發表於2021-09-09
$ git clone 
$ cd pyeth
$ git checkout partfive

原始碼檔案包含:

├── app.py
├── priv_key
├── pyeth
│   ├── __init__.py
│   ├── constants.py
│   ├── crypto.py
│   ├── discovery.py
│   ├── packets.py
│   └── table.py
├── requirements.txt

跟上一次的程式碼版本(使用 git checkout fartfour 檢視)比較:

├── priv_key
├── pyeth
│   ├── __init__.py
│   ├── crypto.py
│   └── discovery.py
├── requirements.txt
├── send_ping.py

可以看到我已經將 send_ping.py 改名為 app.py,因為它的作用不再只是收發一次訊息的程式碼而是一個完整應用程式入口了。在 pyeth/ 目錄中新增了 constants.pypackets.pytable.py 三個原始檔。constants.py 是我們協議使用的一些常量,另外我們將原來 discovery.py 的 ,, 和 四種訊息結構移到了 packets.py 裡,最後在 table.py 實現路由表結構。

程式碼變化還是蠻大的,而且相對於前面的幾節,比較幹,考驗大家對 Python 這門語言是否熟練以及程式設計能力,至少會涉及以下一些知識:

  • gevent 協程

  • Actor 併發模型

  • 巢狀函式

  • 訊息佇列

  • 非同步/回撥

請求和響應

如果是 TCP 的話,客戶端要先 connect 到服務端,服務端要 accept 之後才建立到客戶端的連線,之後雙方透過這個連線建立會話。但是 UDP 是沒有連線狀態的,收發訊息全部透過一個 socket,而且是非同步的,為了建立會話必須確定訊息來源並將 request 和 response 訊息對應起來,這樣在傳送 request 訊息之後,方可知道對端響應的確切 response。

因此,我用了 Actor 併發模型來實現這樣邏輯。你可以把 Actor 理解為一個具體物件,這個物件跟外界是隔離的,它與外界聯絡的唯一途徑就是透過訊息,它內部有一個訊息佇列,一旦接收到外部訊號,就會併發地執行過程並儲存狀態。

discovery.py 有一個 Pending 類,它相當於一個 Actor:

class Pending(Greenlet):
    def __init__(self, node, packet_type, callback, timeout=K_REQUEST_TIMEOUT):
        Greenlet.__init__(self)

        self._node = node
        self._packet_type = packet_type
        self._callback = callback
        self._timeout = timeout

        self._box = Queue()    @property
    def is_alive(self):
        return self._box is not None    @property
    def from_id(self):
        return self._node.node_id    @property
    def packet_type(self):
        return self._packet_type    @property
    def ep(self):
        return self._node.endpoint.address.exploded, self._node.endpoint.udpPort    def emit(self, packet):
        self._box.put(packet)    def _run(self):
        chunks = []        while self._box is not None:            try:
                packet = self._box.get(timeout=self._timeout)
                chunks.append(packet)            except Empty:                # timeout
                self._box = None
                return None
            except:                # die
                self._box = None
                raise

            try:                if self._callback(chunks):                    # job done
                    self._box = None
                    return chunks            except:                # die
                self._box = None
                raise

Pending 類繼承自 gevent.Greenlet 協程類,有五個欄位,_node 是響應節點,_packet_type 是響應包型別,_callback 是回撥函式,_timeout 是超時時間,_box 是訊息佇列。它透過 emit 方法獲取外部訊號,併發執行 _run 方法內的過程。

我們將在傳送 PingFindNeighbors 請求的時候用到這個類。因為傳送 PingFindNeighbors 請求後,需要在 _timeout 時間內等待對端返回 PongNeighbors 響應並執行後續過程;但是如果超過這個時間沒有回應,我們認為請求超時無效。所以在請求的地方,我們用 Pending(node, self, node, packet_type, callback).start() 非同步啟動了一個Actor,當 UDP Socket 接收到相應的訊息的之後,我們就用 pending.emit(response) 把訊息傳給它處理,以響應之前的請求。

訊息處理完之後 Actor 是結束退出還是繼續等待是由回撥函式 _callback 的返回值決定的,這個函式在請求的時候定義,如果返回 True 表示這次請求成功,Actor 可以結束退出了;如果返回 False 說明還得繼續等待響應。之所以這樣做是因為 Neighbors 訊息大小有可能超過協議規定的最大包的大小限制,而必須拆成多個訊息返回。在傳送一個 FindNeighbors 請求之後可能會有得到多個 Neighbors 訊息做為回應,我們必須在請求建立的時候對這個流程加以控制。

節點 Key 和 ID

Node 節點類在 packets.py 檔案裡面:

class Node(object):
    def __init__(self, endpoint, node_key):
        self.endpoint = endpoint
        self.node_key = None
        self.node_id = None
        self.added_time = Node

        self.set_pubkey(node_key)    def set_pubkey(self, pubkey):
        self.node_key = pubkey
        self.node_id = keccak256(self.node_key)

主要變化是將原來的 node 欄位名稱改成了 node_key 表示節點公鑰,從變數名就可以認出來。新增了 node_id 表示節點 ID,它由 node_key 進行 keccak256 雜湊運算得來,它是一個 256 bits 的大整數。node_keynode_id 都是 raw bytes 的形式。節點 ID 作為節點的指紋,一個地方用在計算節點的相近度,另一個地方用在訊息請求與響應的來源節點的對應關係上。

伺服器

discovery.py 的伺服器類 Server 做了很大的變動:

class Server(object):
    def __init__(self, boot_nodes):        # hold all of pending
        self.pending_hold = []        # last pong received time of the special node id
        self.last_pong_received = {}        # last ping received time of the special node id
        self.last_ping_received = {}        # routing table
        self.table = RoutingTable(Node(self.endpoint, pubkey_format(self.priv_key.pubkey)[1:]), self)
        ...    def add_table(self, node):        self.table.add_node(node)    def add_pending(self, pending):
        pending.start()        self.pending_hold.append(pending)        return pending    def run(self):
        gevent.spawn(self.clean_pending)
        gevent.spawn(self.listen)        # wait forever
        evt = Event()
        evt.wait()    def clean_pending(self):        while True:
            for pending in list(self.pending_hold):                if not pending.is_alive:
                    self.pending_hold.remove(pending)
            time.sleep(K_REQUEST_TIMEOUT)    def listen(self):
        LOGGER.info("{:5} listening...".format(''))        while True:
            ready = select([self.sock], [], [], 1.0)            if ready[0]:
                data, addr = self.sock.recvfrom(2048)                # non-block data reading
                gevent.spawn(self.receive, data, addr)    def receive(self, data, addr):...

它新增了幾個欄位,pending_hold 是用來儲存請求時建立的 Pending 物件的列表,當伺服器接收到訊息後會從這個列表裡過濾相應的 Pending 物件。last_pong_received 記錄每個對端節點最後發來的 pong 訊息的時間。last_ping_received 記錄每個對端節點最後發來的 ping 訊息的時間。table 就是路由表 RoutingTable 物件。

原來的 listen_thread 方法改成了 run,作為伺服器的啟動入口,它建立並執行 self.clean_pendingself.listen 協程,然後讓主程式陷入等待。self.clean_pending 定時將已經結束或超時的 Pending 物件從 pending_hold 列表裡面清除掉。self.listen 的變動是將訊息的接收後處理用 gevent.spawn 改成了並行。此外還新增了 add_tableadd_pending 兩個方法,前者是在接收到 Neighbors 訊息的時候將返回的節點新增到路由表;後者是將請求後建立的 Pending 物件新增到 pending_hold 列表。

伺服器的四個訊息接收處理方法已經全部實現,他們會執行一個共同的過程 handle_reply,這個方法就是用於從 pending_hold 裡過濾查詢相應的 Pending 物件的,並把響應的訊息傳給它讓原來的請求邏輯繼續執行。這裡特別需要強調的一個地方是打 LOGGER.warning 的那個地方,它反映一個問題,Neighbors 訊息裡面提取的一些節點,其自帶的 key 和同一個端點(IP 地址和埠一樣)返回的訊息簽名用的真實的 key 不一樣,這點一直讓我百思不得其解。

    def handle_reply(self, addr, pubkey, packet_type, packet, match_callback=None):
        remote_id = keccak256(pubkey)
        is_match = False
        for pending in self.pending_hold:            if pending.is_alive and packet_type == pending.packet_type:                if remote_id == pending.from_id:
                    is_match = True
                    pending.emit(packet)
                    match_callback and match_callback()                elif pending.ep is not None and pending.ep == addr:
                    LOGGER.warning('{:5} {}@{}:{} mismatch request {}'.format(                        '',
                        binascii.hexlify(remote_id)[:8],
                        addr[0],
                        addr[1],
                        binascii.hexlify(pending.from_id)[:8]
                    ))

接收 Pong 響應

    def receive_pong(self, addr, pubkey, pong):
        remote_id = keccak256(pubkey)        # response to ping
        last_pong_received = self.last_pong_received        def match_callback():            # solicited reply
            last_pong_received[remote_id] = time.time()        self.handle_reply(addr, pubkey, Pong.packet_type, pong, match_callback)

接收到 Pong 訊息的時候,如果這個 Pong 有效(確實是我方節點請求的,且沒有超時),我們會更新對端節點最後 Pong 響應時間。因為 Python 只有 lambda 表示式,沒有匿名函式的概念,我們只能在這個函式里面定義一個巢狀函式 match_callback 當成回撥物件傳遞。

接收 Ping 請求

    def receive_ping(self, addr, pubkey, ping, msg_hash):
        remote_id = keccak256(pubkey)
        endpoint_to = EndPoint(addr[0], ping.endpoint_from.udpPort, ping.endpoint_from.tcpPort)
        pong = Pong(endpoint_to, msg_hash, time.time() + K_EXPIRATION)
        node_to = Node(pong.to, pubkey)        # sending Pong response
        self.send_sock(pong, node_to)        self.handle_reply(addr, pubkey, PingNode.packet_type, ping)

        node = Node(endpoint_to, pubkey)        if time.time() - self.last_pong_received.get(remote_id, 0) > K_BOND_EXPIRATION:
            self.ping(node, lambda: self.add_table(node))        else:
            self.add_table(node)        self.last_ping_received[remote_id] = time.time()

接收到 Ping 訊息的時候,除了回應 Pong 訊息之外,還會判斷對端節點最後一次響應 Pong 回來的時間是否在 K_BOND_EXPIRATION 時間之前,是的話說明它要麼不在我方節點的路由表裡面要麼它無法連線了,此時我們需要重新傳送 Ping 跟它握手,如果 Ping 通的話,將它更新到我方節點的路由表;否則直接將它更新到我方節點的路由表。最後更新對端節點最後的響應 Ping 的時間。

接收 FindNeighbors 請求

    def receive_find_neighbors(self, addr, pubkey, fn):
        remote_id = keccak256(pubkey)        if time.time() - self.last_pong_received.get(remote_id, 0) > K_BOND_EXPIRATION:
            # lost origin or origin is off
            return

        target_id = keccak256(fn.target)
        closest = self.table.closest(target_id, BUCKET_SIZE)        # sent neighbours in chunks
        ns = Neighbors([], time.time() + K_EXPIRATION)
        sent = False
        node_to = Node(EndPoint(addr[0], addr[1], addr[1]), pubkey)        for c in closest:
            ns.nodes.append(c)            if len(ns.nodes) == K_MAX_NEIGHBORS:
                self.send_sock(ns, node_to)

                ns.nodes = []
                sent = True        if len(ns.nodes) > 0 or not sent:
            self.send_sock(ns, node_to)

接收 FindNeighbors 訊息的時候,先判斷對端節點最後 Pong 響應的時間是否在 K_BOND_EXPIRATION 之前,是的話直接丟棄不理,因為避免攻擊,我方不能接受不在我方的節點路由表內的節點請求。否則呼叫 self.table.closest(target_id, BUCKET_SIZE) 方法獲取和 target_id 相近的 BUCKET_SIZE 個節點,分批返回給請求節點。

接收 Neighbors 響應

    def receive_neighbors(self, addr, pubkey, neighbours):        # response to find neighbours
        self.handle_reply(addr, pubkey, Neighbors.packet_type, neighbours)

接收 Neighbors 訊息的方法比較簡單,因為主要的邏輯在請求的方法 find_neighbors 裡面。

Ping 請求

    def ping(self, node, callback=None):
        ping = PingNode(self.endpoint, node.endpoint, time.time() + K_EXPIRATION)
        message = self.wrap_packet(ping)
        msg_hash = message[:32]        def reply_call(chunks):            if chunks.pop().echo == msg_hash:
                if callback is not None:
                    callback()                return True

        ep = (node.endpoint.address.exploded, node.endpoint.udpPort)        self.sock.sendto(message, ep)        return self.add_pending(Pending(node, Pong.packet_type, reply_call))

ping 方法是非同步的。它建立了 PingNode 的訊息包併傳送出去之後,建立了一個 Pending,把回撥函式 reply_call 傳給它,非同步等待響應。

FindNeighbors 請求

    def find_neighbors(self, node, target_key):
        node_id = node.node_id        if time.time() - self.last_ping_received.get(node_id, 0) > K_BOND_EXPIRATION:
            # send a ping and wait for a pong
            self.ping(node).join()            # wait for a ping
            self.add_pending(Pending(node, PingNode.packet_type, lambda _: True)).join()

        fn = FindNeighbors(target_key, time.time() + K_EXPIRATION)        def reply_call(chunks):
            num_received = 0
            for neighbors in chunks:
                num_received += len(neighbors.nodes)            if num_received >= BUCKET_SIZE:
                return True        self.send_sock(fn, node)
        ep = (node.endpoint.address.exploded, node.endpoint.udpPort)        # block to wait for neighbours
        ret = self.add_pending(Pending(node, Neighbors.packet_type, reply_call, timeout=3)).get()        if ret:
            neighbor_nodes = []            for chunk in ret:
                for n in chunk.nodes:
                    neighbor_nodes.append(n)            return neighbor_nodes

find_neighbors 方法是同步的。它首先要做避免流量放大攻擊,判斷對端節點最後一次 Ping 請求的時間是否在 K_BOND_EXPIRATION 之前,是的話說明雙方節點可能已經互相不在對方的節點路由表裡面了,必須重新建立 ping-pong-ping 握手,避免訊息被雙方互相丟棄。這裡 self.ping(node).join() 可以看到,傳送 Ping 請求之後用 join() 阻塞等待這個 Pending Greenlet 協程的結束。等待對端節點發來 Ping 請求的過程也是一樣。傳送 FindNeighbors 訊息之後等待 Neighbors 響應的過程也是同步,這裡用了 Greenlet 協程的 get() 阻塞等待協程結束並返回結果——和 target_key 相鄰的節點。回撥函式 reply_call 控制此次的 FindNeighbors 請求何時可以結束,這裡是收集到 BUCKET_SIZE 個節點後結束。

路由表

table.py 檔案裡面定義了兩個類——RoutingTableBucketRoutingTable 是路由表,Bucket 是儲存節點的 k-桶

class Bucket(object):
    def __init__(self):        self.nodes = []        self.replace_cache = []class RoutingTable(object):
    def __init__(self, self_node, server):        self.buckets = [Bucket() for _ in range(BUCKET_NUMBER)]        self.self_node = self_node        self.server = server        # add seed nodes
        for bn in self.server.boot_nodes:
            self.add_node(bn)

        gevent.spawn(self.re_validate)
        gevent.spawn(self.refresh)    def lookup(self, target_key):
        target_id = keccak256(target_key)
        closest = []        while not closest:
            closest = self.closest(target_id, BUCKET_SIZE)            if not closest:
                # add seed nodes
                for bn in self.server.boot_nodes:
                    self.add_node(bn)

        asked = [self.self_node.node_id]
        pending_queries = 0
        reply_queue = Queue()        while True:
            for n in closest:
                if pending_queries >= KAD_ALPHA:
                    break

                if n.node_id not in asked:
                    asked.append(n.node_id)
                    pending_queries += 1
                    gevent.spawn(self.find_neighbours, n, target_key, reply_queue)            if pending_queries == 0:                break

            ns = reply_queue.get()
            pending_queries -= 1

            if ns:
                for node in ns:
                    farther = find_farther_to_target_than(closest, target_id, node)                    if farther:
                        closest.remove(farther)                    if len(closest) < BUCKET_SIZE:
                        closest.append(node)    def refresh(self):
        assert self.server.boot_nodes, "no boot nodes"

        while True:
            # self lookup to discover neighbours
            self.lookup(self.self_node.node_key)            for i in range(3):
                random_int = random.randint(0, K_MAX_KEY_VALUE)
                node_key = int_to_big_endian(random_int).rjust(K_PUBKEY_SIZE / 8, b'x00')                self.lookup(node_key)

            time.sleep(REFRESH_INTERVAL)    def re_validate(self):        while True:
            time.sleep(RE_VALIDATE_INTERVAL)            # the last node in a random, non-empty bucket
            bi = 0
            last = None
            idx_arr = [i for i in range(len(self.buckets))]
            random.shuffle(idx_arr)            for bi in idx_arr:
                bucket = self.buckets[bi]                if len(bucket.nodes) > 0:
                    last = bucket.nodes.pop()                    break
            if last is not None:
                LOGGER.debug('{:5} revalidate {}'.format('', last))                # wait for a pong
                ret = self.server.ping(last).get()
                bucket = self.buckets[bi]                if ret:
                    # bump node
                    bucket.nodes.insert(0, last)                else:
                    # pick a replacement
                    if len(bucket.replace_cache) > 0:
                        r = bucket.replace_cache.pop(random.randint(0, len(bucket.replace_cache) - 1))                        if r:
                            bucket.nodes.append(r)    def add_node(self, node):...    def get_bucket(self, node):...    def closest(self, target_id, num):...    def find_neighbours(self, node, target_key, reply_queue):...

從路由表的建構函式可以看出,它一開始就建立了 BUCKET_NUMBER 個的 k-桶,接著把啟動節點加進去,然後啟動 re_validaterefresh 協程。re_validate 做的是持續隨機挑選 k-桶,從 k-桶裡挑出最少互動的節點重新 Ping,檢視節點是否線上。線上則將它重新更新到路由表,否則從 k-桶的 replace_cache 裡面挑出一個節點替換它,如果有的話。refresh 做的是不斷發現節點並填充路由表,它首先查詢跟自己相近的節點,然後查詢三個隨機節點的相鄰節點。

查詢跟某個 target_key 相近的節點用 lookup 方法,這個方法叫做遞迴查詢(Recursive Lookup)。它首先從路由表裡面獲取和 target_key 相鄰最近的 BUCKET_SIZE 個節點放在 closest 列表裡面,如果路由裡面一個節點都沒有,重新把啟動節點加進去,最後 closest 總會有一些節點。接著遍歷 closest 裡面的節點,並向每個節點索取其和 target_key 更相近的節點;將返回結果的節點新增到路由表,然後遍歷返回結果的節點,將它和 closest 裡面的節點做對比,看誰離 target_key 更近,更近的留在 closest 裡面。這個過程迴圈進行直到 closest 列表裡面的所有節點都被問過了。

路由表是不直接儲存節點的,它必須存在路由表相應的 k-桶裡面。如何選擇 k-桶?我們將某個節點的 ID 和當前伺服器的節點 ID 做一下異或距離運算,然後看這個距離有多少個“前導零”。最後將 k-桶個數減去“前導零”的個數的結果做為這個節點所在 k-桶的索引編號,如果結果小於 0,取 0。這個邏輯在 get_bucket 方法裡。

找到 k-桶後,如何將節點新增進去就按照 Kademlia 協議的規則:k-桶滿了,把它加到 k-桶的 replace_cache;k-桶沒滿,但是k-桶已經包含此節點,把它調到最前面,否則把它直接插到最前面。

啟動伺服器

如果你執行 python app.py 可以看到的輸出:

2018-12-14 20:37:39.778       push (N 930cf49c) to bucket #142018-12-14 20:37:39.778       push (N 674085f6) to bucket #162018-12-14 20:37:39.778       push (N 009be51d) to bucket #162018-12-14 20:37:39.778       push (N 816ee7e3) to bucket #142018-12-14 20:37:39.778       push (N 3d1edcb0) to bucket #162018-12-14 20:37:39.778       push (N 29cca67d) to bucket #162018-12-14 20:37:39.786       listening...2018-12-14 20:37:39.795 ----> 816ee7e3@13.75.154.138:30303 (Ping)2018-12-14 20:37:39.796 ----> 930cf49c@52.16.188.185:30303 (Ping)2018-12-14 20:37:39.796 ----> 29cca67d@5.1.83.226:30303 (Ping)2018-12-14 20:37:40.142 <---- 816ee7e3@13.75.154.138:30303 (Pong)2018-12-14 20:37:40.798 <-//- 930cf49c@52.16.188.185:30303 (Pong) timeout2018-12-14 20:37:40.799 <-//- 29cca67d@5.1.83.226:30303 (Pong) timeout2018-12-14 20:37:41.143 <-//- 816ee7e3@13.75.154.138:30303 (Ping) timeout2018-12-14 20:37:41.145 ----> 816ee7e3@13.75.154.138:30303 (FN a3d334fa)2018-12-14 20:37:41.507 <---- 816ee7e3@13.75.154.138:30303 (Ns [(N a3ba0512), (N a35e82d2), (N a1ae51f6), (N a153f900), (N a14a9593), (N a7b31894), (N a5d7d971), (N a5268243), (N af477601), (N adda7a78), (N b6ed28e7), (N b62a7422)] 1544791081)2018-12-14 20:37:41.515 <---- 816ee7e3@13.75.154.138:30303 (Ns [(N b4114787), (N b8fb3e88), (N b83a8eb9), (N bf834266)] 1544791081)2018-12-14 20:37:41.515       push (N a3ba0512) to bucket #72018-12-14 20:37:41.515       push (N a35e82d2) to bucket #82018-12-14 20:37:41.515       push (N a1ae51f6) to bucket #102018-12-14 20:37:41.516       push (N a5268243) to bucket #112018-12-14 20:37:41.516       push (N af477601) to bucket #122018-12-14 20:37:41.516       push (N adda7a78) to bucket #122018-12-14 20:37:41.517       push (N bf834266) to bucket #132018-12-14 20:37:41.518 ----> a3ba0512@35.236.159.118:30303 (Ping)2018-12-14 20:37:41.607 <---- a3ba0512@35.236.159.118:30303 (Pong)2018-12-14 20:37:41.615 <---- a3ba0512@35.236.159.118:30303 (Ping)2018-12-14 20:37:41.615 ----> a3ba0512@35.236.159.118:30303 (Pong)2018-12-14 20:37:41.615       a3ba0512@35.236.159.118:30303 unsolicited response Ping2018-12-14 20:37:41.616       bump (N a3ba0512) in bucket #72018-12-14 20:37:41.800 <-//- 930cf49c@52.16.188.185:30303 (Ping) timeout2018-12-14 20:37:41.801 <-//- 29cca67d@5.1.83.226:30303 (Ping) timeout2018-12-14 20:37:41.801 ----> 930cf49c@52.16.188.185:30303 (FN a3d334fa)2018-12-14 20:37:41.802 ----> 29cca67d@5.1.83.226:30303 (FN a3d334fa)2018-12-14 20:37:42.617 <-//- a3ba0512@35.236.159.118:30303 (Ping) timeout2018-12-14 20:37:42.618 ----> a3ba0512@35.236.159.118:30303 (FN a3d334fa)2018-12-14 20:37:42.695 <---- a3ba0512@35.236.159.118:30303 (Ns [(N a3d1c129), (N a3d6aca8), (N a3c5cbb3), (N a3c42eff), (N a3cb0acd), (N a3ca35e8), (N a3c8dd80), (N a3cf1b5b), (N a3ceb7fb), (N a3f0ba70), (N a3f5b977), (N a3fbe63b)] 1544791082)2018-12-14 20:37:42.703 <---- a3ba0512@35.236.159.118:30303 (Ns [(N a3e3f5f9), (N a3e82fb3), (N a3e864b4), (N a3ed88d1)] 1544791082)2018-12-14 20:37:42.703       push (N a3d1c129) to bucket #22018-12-14 20:37:42.703       push (N a3d6aca8) to bucket #32018-12-14 20:37:42.704       push (N a3e864b4) to bucket #62018-12-14 20:37:42.705       push (N a3ed88d1) to bucket #62018-12-14 20:37:42.705 ----> a3d1c129@79.142.21.222:30303 (Ping)2018-12-14 20:37:43.144 <---- a3d1c129@79.142.21.222:30303 (Pong)2018-12-14 20:37:43.152 <---- a3d1c129@79.142.21.222:30303 (Ping)2018-12-14 20:37:43.153 ----> a3d1c129@79.142.21.222:30303 (Pong)2018-12-14 20:37:43.153       a3d1c129@79.142.21.222:30303 unsolicited response Ping2018-12-14 20:37:43.153       bump (N a3d1c129) in bucket #22018-12-14 20:37:44.155 <-//- a3d1c129@79.142.21.222:30303 (Ping) timeout2018-12-14 20:37:44.156 ----> a3d1c129@79.142.21.222:30303 (FN a3d334fa)2018-12-14 20:37:44.577 <---- a3d1c129@79.142.21.222:30303 (Ns [(N a3d334fa), (N a3d3d431), (N a3d6aca8), (N a3d681d2), (N a3d571d4), (N a3d91e85), (N a3c1bd3f), (N a3c1ad80), (N a3c5cbb3), (N a3c42eff), (N a3cb0acd), (N a3ca35e8)] 1544791084)2018-12-14 20:37:44.585 <---- a3d1c129@79.142.21.222:30303 (Ns [(N a3cae3f7), (N a3c80d3f), (N a3c8dd80), (N a3cf1b5b)] 1544791084)2018-12-14 20:37:44.585       push (N a3d3d431) to bucket #02018-12-14 20:37:44.585       bump (N a3d6aca8) in bucket #32018-12-14 20:37:44.586       push (N a3d681d2) to bucket #32018-12-14 20:37:44.586       push (N a3d571d4) to bucket #32018-12-14 20:37:44.586       push (N a3d91e85) to bucket #42018-12-14 20:37:44.587       bump (N a3cf1b5b) in bucket #52018-12-14 20:37:44.588 ----> a3d3d431@27.10.110.49:30303 (Ping)2018-12-14 20:37:44.803 <-//- 930cf49c@52.16.188.185:30303 (Ns) timeout2018-12-14 20:37:44.803 <-//- 29cca67d@5.1.83.226:30303 (Ns) timeout2018-12-14 20:37:44.805 ----> a3d6aca8@172.104.229.232:30303 (Ping)2018-12-14 20:37:44.805 ----> a3d681d2@172.104.180.194:30303 (Ping)2018-12-14 20:37:45.107 <---- a3d6aca8@172.104.229.232:30303 (Pong)2018-12-14 20:37:45.115 <---- a3d6aca8@172.104.229.232:30303 (Ping)2018-12-14 20:37:45.115 ----> a3d6aca8@172.104.229.232:30303 (Pong)2018-12-14 20:37:45.115       a3d6aca8@172.104.229.232:30303 unsolicited response Ping2018-12-14 20:37:45.115       bump (N a3d6aca8) in bucket #32018-12-14 20:37:45.590 <-//- a3d3d431@27.10.110.49:30303 (Pong) timeout2018-12-14 20:37:45.806 <-//- a3d681d2@172.104.180.194:30303 (Pong) timeout2018-12-14 20:37:46.117 <-//- a3d6aca8@172.104.229.232:30303 (Ping) timeout2018-12-14 20:37:46.118 ----> a3d6aca8@172.104.229.232:30303 (FN a3d334fa)2018-12-14 20:37:46.399 <---- a3d6aca8@172.104.229.232:30303 (Ns [(N a3d01efc), (N a3d05035), (N a3d08c94), (N a3d08bcf)] 1544791086)2018-12-14 20:37:46.407 <---- a3d6aca8@172.104.229.232:30303 (Ns [(N a3d33fe5), (N a3d3bdc2), (N a3d3bea3), (N a3d3a940), (N a3d39ef5), (N a3d3d431), (N a3d264f5), (N a3d2e0fd), (N a3d2e293), (N a3d2cf5f), (N a3d1b2a3), (N a3d1a970)] 1544791086)2018-12-14 20:37:46.407       push (N a3d01efc) to bucket #22018-12-14 20:37:46.408       push (N a3d05035) to bucket #2...



作者:JonHuang
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/132/viewspace-2818810/,如需轉載,請註明出處,否則將追究法律責任。

相關文章