Python从头实现以太坊(六):Routing

Python从头实现以太坊系列索引:
一、Ping
二、Pinging引导节点
三、解码引导节点的响应
四、查找邻居节点
五、类-Kademlia协议
六、Routing

前几节讲到以太坊节点发现使用的是修改过的 Kademlia 会话协议。它的传输协议使用的是 UDP,UDP 跟 TCP 相比,因为不需要顺序发送和消息确认,所以可以做到低延迟,无阻塞,适合一些广播的场景,比如 DNS 或直播等。甚至 HTTP 未来也可能基于 UDP,HTTP-over-QUIC(QUIC 就是基于 UDP) 已经被 IETF 正式命名为 HTTP/3,UDP 的优点正在被越来越多的人发掘,未来可期。前面几节我们还讲到它的四种会话消息结构,分别是 Ping,Pong,FindNeighbor 和 Neighbors,这些消息在发送之前用 RLP(递归长度前缀)编码并用 ECDSA(椭圆曲线数字签名算法)进行签名,哈希算法使用的是 keccak256(即 sha3),这些代码也都实现了,现在还差路由部分,这就是我们这节课的主题。这节讲完,我们以太坊节点发现协议就完成了。

在开始之前,你可以到 https://github.com/HuangFJ/pyeth 查阅代码,或克隆到本地:

$ git clone https://github.com/HuangFJ/pyeth
$ 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 的 Ping,Pong,FindNeighbor 和 Neighbors 四种消息结构移到了 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 方法是同步的。它首先要做端点证明(Endpoint Proof)避免流量放大攻击,判断对端节点最后一次 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 #14
2018-12-14 20:37:39.778       push (N 674085f6) to bucket #16
2018-12-14 20:37:39.778       push (N 009be51d) to bucket #16
2018-12-14 20:37:39.778       push (N 816ee7e3) to bucket #14
2018-12-14 20:37:39.778       push (N 3d1edcb0) to bucket #16
2018-12-14 20:37:39.778       push (N 29cca67d) to bucket #16
2018-12-14 20:37:39.786       listening...
2018-12-14 20:37:39.795 ----> [email protected]:30303 (Ping)
2018-12-14 20:37:39.796 ----> [email protected]:30303 (Ping)
2018-12-14 20:37:39.796 ----> [email protected]:30303 (Ping)
2018-12-14 20:37:40.142 <---- [email protected]:30303 (Pong)
2018-12-14 20:37:40.798 <-//- [email protected]:30303 (Pong) timeout
2018-12-14 20:37:40.799 <-//- [email protected]:30303 (Pong) timeout
2018-12-14 20:37:41.143 <-//- [email protected]:30303 (Ping) timeout
2018-12-14 20:37:41.145 ----> [email protected]:30303 (FN a3d334fa)
2018-12-14 20:37:41.507 <---- [email protected]: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 <---- [email protected]:30303 (Ns [(N b4114787), (N b8fb3e88), (N b83a8eb9), (N bf834266)] 1544791081)
2018-12-14 20:37:41.515       push (N a3ba0512) to bucket #7
2018-12-14 20:37:41.515       push (N a35e82d2) to bucket #8
2018-12-14 20:37:41.515       push (N a1ae51f6) to bucket #10
2018-12-14 20:37:41.516       push (N a5268243) to bucket #11
2018-12-14 20:37:41.516       push (N af477601) to bucket #12
2018-12-14 20:37:41.516       push (N adda7a78) to bucket #12
2018-12-14 20:37:41.517       push (N bf834266) to bucket #13
2018-12-14 20:37:41.518 ----> [email protected]:30303 (Ping)
2018-12-14 20:37:41.607 <---- [email protected]:30303 (Pong)
2018-12-14 20:37:41.615 <---- [email protected]:30303 (Ping)
2018-12-14 20:37:41.615 ----> [email protected]:30303 (Pong)
2018-12-14 20:37:41.615       [email protected]:30303 unsolicited response Ping
2018-12-14 20:37:41.616       bump (N a3ba0512) in bucket #7
2018-12-14 20:37:41.800 <-//- [email protected]:30303 (Ping) timeout
2018-12-14 20:37:41.801 <-//- [email protected]:30303 (Ping) timeout
2018-12-14 20:37:41.801 ----> [email protected]:30303 (FN a3d334fa)
2018-12-14 20:37:41.802 ----> [email protected]:30303 (FN a3d334fa)
2018-12-14 20:37:42.617 <-//- [email protected]:30303 (Ping) timeout
2018-12-14 20:37:42.618 ----> [email protected]:30303 (FN a3d334fa)
2018-12-14 20:37:42.695 <---- [email protected]: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 <---- [email protected]:30303 (Ns [(N a3e3f5f9), (N a3e82fb3), (N a3e864b4), (N a3ed88d1)] 1544791082)
2018-12-14 20:37:42.703       push (N a3d1c129) to bucket #2
2018-12-14 20:37:42.703       push (N a3d6aca8) to bucket #3
2018-12-14 20:37:42.704       push (N a3e864b4) to bucket #6
2018-12-14 20:37:42.705       push (N a3ed88d1) to bucket #6
2018-12-14 20:37:42.705 ----> [email protected]:30303 (Ping)
2018-12-14 20:37:43.144 <---- [email protected]:30303 (Pong)
2018-12-14 20:37:43.152 <---- [email protected]:30303 (Ping)
2018-12-14 20:37:43.153 ----> [email protected]:30303 (Pong)
2018-12-14 20:37:43.153       [email protected]:30303 unsolicited response Ping
2018-12-14 20:37:43.153       bump (N a3d1c129) in bucket #2
2018-12-14 20:37:44.155 <-//- [email protected]:30303 (Ping) timeout
2018-12-14 20:37:44.156 ----> [email protected]:30303 (FN a3d334fa)
2018-12-14 20:37:44.577 <---- [email protected]: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 <---- [email protected]:30303 (Ns [(N a3cae3f7), (N a3c80d3f), (N a3c8dd80), (N a3cf1b5b)] 1544791084)
2018-12-14 20:37:44.585       push (N a3d3d431) to bucket #0
2018-12-14 20:37:44.585       bump (N a3d6aca8) in bucket #3
2018-12-14 20:37:44.586       push (N a3d681d2) to bucket #3
2018-12-14 20:37:44.586       push (N a3d571d4) to bucket #3
2018-12-14 20:37:44.586       push (N a3d91e85) to bucket #4
2018-12-14 20:37:44.587       bump (N a3cf1b5b) in bucket #5
2018-12-14 20:37:44.588 ----> [email protected]:30303 (Ping)
2018-12-14 20:37:44.803 <-//- [email protected]:30303 (Ns) timeout
2018-12-14 20:37:44.803 <-//- [email protected]:30303 (Ns) timeout
2018-12-14 20:37:44.805 ----> [email protected]:30303 (Ping)
2018-12-14 20:37:44.805 ----> [email protected]:30303 (Ping)
2018-12-14 20:37:45.107 <---- [email protected]:30303 (Pong)
2018-12-14 20:37:45.115 <---- [email protected]:30303 (Ping)
2018-12-14 20:37:45.115 ----> [email protected]:30303 (Pong)
2018-12-14 20:37:45.115       [email protected]:30303 unsolicited response Ping
2018-12-14 20:37:45.115       bump (N a3d6aca8) in bucket #3
2018-12-14 20:37:45.590 <-//- [email protected]:30303 (Pong) timeout
2018-12-14 20:37:45.806 <-//- [email protected]:30303 (Pong) timeout
2018-12-14 20:37:46.117 <-//- [email protected]:30303 (Ping) timeout
2018-12-14 20:37:46.118 ----> [email protected]:30303 (FN a3d334fa)
2018-12-14 20:37:46.399 <---- [email protected]:30303 (Ns [(N a3d01efc), (N a3d05035), (N a3d08c94), (N a3d08bcf)] 1544791086)
2018-12-14 20:37:46.407 <---- [email protected]: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 #2
2018-12-14 20:37:46.408       push (N a3d05035) to bucket #2
...

最后

至此以太坊的节点发现协议部分已经实现,接下来的就是同步区块数据了,敬请关注后续教程。

你可能感兴趣的:(Python从头实现以太坊(六):Routing)