主页 > imtoken换手机怎么登录 > Python从零开始实现以太坊(四):寻找邻居节点

Python从零开始实现以太坊(四):寻找邻居节点

imtoken换手机怎么登录 2023-07-09 05:21:00

这是我从零开始完整实现以太坊协议的第四部分(part 1, part 2, part 3,如果你之前没有看过,我建议你从part 1开始)。 在本系列教程结束时,您将爬取以太坊网络以获取对等端点、同步和验证区块链、为以太坊虚拟机编写智能合约以及挖掘以太币。 我们现在正在实施它的发现协议部分。 完成后,我们可以以类似 torrent 的方式下载区块链。 我们最后完成了对 bootstrap 节点的 ping 并解码和验证其 Pong 响应。 今天我们将实现 FindNeighbors 请求和 Neighbors 响应,我们将使用它们来抓取以太坊网络。

这部分不难,我们简单的定义一下FindNeighbors和Neighbors的包的类结构,然后像之前发送PingNode和Pong一样发送。 但是,为了成功发送 FindNeighbors 数据包,需要满足一些先决条件。 我们在协议文档中看不到这些条件,因为文档比源代码旧。 go-ethereum源码发现协议采用v4版本。 但是RLPx协议(我们的实现)只到版本3,源码中甚至有一个叫discv5的模块,说明他们实现的是v5版本,但是通过检查发回的Ping报文的版本字段boot节点,我们发现他们运行的依然是v4版本。

该协议的 v4 版本要求,为了获得对 FindNeighbors 请求的响应,必须有 UDP“握手”。 我们可以在udp.go源文件中看到:

func (req *findnode) handle(t *udp, from *net.UDPAddr, fromID NodeID, mac []byte) error {    if expired(req.Expiration) {        return errExpired
    }    if t.db.node(fromID) == nil {        // No bond exists, we don't process the packet. This prevents
        // an attack vector where the discovery protocol could be used
        // to amplify traffic in a DDOS attack. A malicious actor
        // would send a findnode request with the IP address and UDP
        // port of the target as the source address. The recipient of
        // the findnode packet would then send a neighbors packet
        // (which is a much bigger packet than findnode) to the victim.
        return errUnknownNode
    }

为了处理一个 findnode 数据包(FindNeighbors 的 Go 实现),代码首先检查请求源 fromID 是否在其已知节点记录中。 如果没有,它会丢弃请求(难怪我以前的请求一直有问题,现在我想通了)。

为了成为已知节点,首先我们必须 ping bootstrap 节点。 当 bootstrap 节点收到一个 ping 时,它用一个 pong 响应,然后发送一个 ping,并等待我们用一个 pong 返回响应。 一旦我们响应了 pong以太坊确认查找,我们的 nodeID 就会进入引导程序节点的已知节点列表。

因此,为了能够发送 FindNeighbors 数据包,首先,我们需要创建具有与 PingNode 和 Pong 数据包相同功能的 FindNeighbors 和 Neighbors 类。 然后以太坊确认查找,我们需要向 receive_ping 添加一个 Pong 响应,以便与引导节点 UDP 握手。 接下来,我们需要调整 PingServer 以保持监听数据包。 最后,我们需要调整 send_ping.py 脚本:发送一个 ping,让引导节点有足够的时间依次响应 pong 和 ping,然后发送 FindNeighbors 数据包并接收 Neighbors 响应,假设我们实现了 pong正确响应。

下载项目代码:

混帐克隆

创建 FindNeighbors 和 Neighbors 类

在本系列的前一部分中,我们为 PingNode 和 Pong 创建了类,在这一部分中,我们将以相同的方式为 FindNeighbors 和 Neighbors 创建 Python 类。 我们为每个类创建__init__、__str__、pack、unpack方法,并为PingServer类添加receive_方法。

对于 FindNeighbors,规范描述的数据包结构为:

FindNeighbours packet-type: 0x03struct FindNeighbours{
    NodeId target; // Id of a node. The responding node will send back nodes closest to the target.
    uint32_t timestamp;
};

target是一个NodeId类型,是一个64字节的公钥。 这意味着我们可以在 pack 和 unpack 方法中存储和提取它。 对于 __str__,我将使用 binascii.b2a_hex 以十六进制格式打印字节。 除此之外,其余代码与我们在 PingNode 和 Pong 中看到的类似。 所以,我们在discovery.py中这样写:

class FindNeighbors(object):
    packet_type = '\x03'
    def __init__(self, target, timestamp):
        self.target = target
        self.timestamp = timestamp    def __str__(self):
        return "(FN " + binascii.b2a_hex(self.target)[:7] + "... " + str(self.ti\
mestamp) + ")"
    def pack(self):
        return [
            self.target,
            struct.pack(">I", self.timestamp)
        ]    @classmethod
    def unpack(cls, packed):
        timestamp = struct.unpack(">I", packed[1])[0]        return cls(packed[0], timestamp)

对于 Neighbors,数据包结构为:

Neighbors packet-type: 0x04struct Neighbours{
    list nodes: struct Neighbour
    {
        inline Endpoint endpoint;
        NodeId node;
    };    uint32_t timestamp;
};

这就需要我们先定义一个Neighbor类,后面我会定义它,称之为Node。 对于 Neighbors,唯一的新概念是节点是一个列表,因此我们将使用 map 来打包和解包数据:

class Neighbors(object):
    packet_type = '\x04'
    def __init__(self, nodes, timestamp):
        self.nodes = nodes
        self.timestamp = timestamp    def __str__(self):
        return "(Ns [" + ", ".join(map(str, self.nodes)) + "] " + str(self.times\
tamp) + ")"
    def pack(self):
        return [
            map(lambda x: x.pack(), self.nodes),
            struct.pack(">I", self.timestamp)
        ]    @classmethod
    def unpack(cls, packed):
        nodes = map(lambda x: Node.unpack(x), packed[0])
        timestamp = struct.unpack(">I", packed[1])[0]        return cls(nodes, timestamp)

对于 Node,唯一的新概念是端点是内联打包的,因此 endpoint.pack() 成为一个单独的列表项,但它不是必须的,它只是将 nodeID 附加到此列表的末尾。

class Node(object):
    def __init__(self, endpoint, node):
        self.endpoint = endpoint
        self.node = node    def __str__(self):
        return "(N " + binascii.b2a_hex(self.node)[:7] + "...)"
    def pack(self):
        packed  = self.endpoint.pack()
        packed.append(node)        return packed    @classmethod
    def unpack(cls, packed):
        endpoint = EndPoint.unpack(packed[0:3])        return cls(endpoint, packed[3])

对于新建的数据包类,我们定义一个新的PingServer方法来接收数据包,简单定义:

def receive_find_neighbors(self, payload):
    print " received FindNeighbors"
    print "", FindNeighbors.unpack(rlp.decode(payload))def receive_neighbors(self, payload):
    print " received Neighbors"
    print "", Neighbors.unpack(rlp.decode(payload))

在PingServer的receive方法中,我们还需要调整response_types的dispatch:

response_types = {
    PingNode.packet_type : self.receive_ping,
    Pong.packet_type : self.receive_pong,
    FindNeighbors.packet_type : self.receive_find_neighbors,
    Neighbors.packet_type : self.receive_neighbors
}

保持服务器监听

为了让服务继续侦听数据包,需要做一些事情:

最终对应的代码部分是这样的:

...import select
...class Server(object):
    def __init__(self, my_endpoint):
        ...        ## set socket non-blocking mode
        self.sock.setblocking(0)
    ...    def receive(self, data):
        ## verify hash
        msg_hash = data[:32]
        ...
    ...    def listen(self):
        print "listening..."
        while True:
            ready = select.select([self.sock], [], [], 1.0)            if ready[0]:
                data, addr = self.sock.recvfrom(2048)                print "received message[", addr, "]:"
                self.receive(data)
    ...    def listen_thread(self):
        thread = threading.Thread(target = self.listen)
        thread.daemon = True
        return thread

响应 ping

我们必须修改 Server 类的 receive_ping 方法来响应 Pong。 这也需要我们将Server的ping方法修改为更通用的函数send。 原来ping是创建了一个PingNode对象然后发送,但是现在变成了send接收一个新的packet参数,准备好发送再发送。

def receive_ping(self, payload, msg_hash):
    print " received Ping"
    ping = PingNode.unpack(rlp.decode(payload))
    pong = Pong(ping.endpoint_from, msg_hash, time.time() + 60)    print "  sending Pong response: " + str(pong)
    self.send(pong, pong.to)
...def send(self, packet, endpoint):
    message = self.wrap_packet(packet)    print "sending " + str(packet)
    self.sock.sendto(message, (endpoint.address.exploded, endpoint.udpPort))

请注意,receive_ping 有一个新的 msg_hash 参数。 这个参数需要放在Server的receive方法中的dispatch call中,其他所有以receive_开头的函数。

def receive_pong(self, payload, msg_hash):...def receive_find_neighbors(self, payload, msg_hash):...def receive_neighbors(self, payload, msg_hash):...def receive(self, data):
    ## verify hash
    msg_hash = data[:32]
    ...
    dispatch(payload, msg_hash)

其他修复

因为bootstrap节点使用了v4版本的RLPx协议。 但是规范文档和我们的实现使用的是v3,我们需要注释掉PingNode unpack方法的packed[0]==cls.version。 在找到基于新版本的集中文档之前,我不打算修改类的实际版本号。 在上一篇文章中,我忘记在 cls 参数中包含解包时间戳,所以你的 uppack 应该是这样的:

@classmethoddef unpack(cls, packed):
    ## assert(packed[0] == cls.version)
    endpoint_from = EndPoint.unpack(packed[1])
    endpoint_to = EndPoint.unpack(packed[2])
    timestamp = struct.unpack(">I", packed[3])[0]    return cls(endpoint_from, endpoint_to, timestamp)

v4的另一个变化是EndPoint编码的第二个参数是可选的,所以需要在unpack方法中指定。 如果不是,则必须将 tcpPort 设置为等于 udpPort。

@classmethoddef unpack(cls, packed):
    udpPort = struct.unpack(">H", packed[1])[0]    if packed[2] == '':
        tcpPort = udpPort    else:
        tcpPort = struct.unpack(">H", packed[2])[0]    return cls(packed[0], udpPort, tcpPort)

上一版代码的最后修改是Pong的pack方法有错别字,timestamp应该改成self.timestamp。 我们没有找到它的原因是因为我们从未发送过 Pong 消息:

def pack(self):
    return [
        self.to.pack(),
        self.echo,
        struct.pack(">I", self.timestamp)]

修改send_ping.py

我们需要重写 send_ping.py 来说明新的发送过程。

from discovery import EndPoint, PingNode, Server, FindNeighbors, Nodeimport timeimport binascii
bootnode_key = "3f1d12044546b76342d59d4a05532c14b85aa669704bfe1f864fe079415aa2c02d743e03218e57a33fb94523adb54032871a6c51b2cc5514cb7c7e35b3ed0a99"bootnode_endpoint = EndPoint(u'13.93.211.84',                    30303,                    30303)
bootnode = Node(bootnode_endpoint,
                binascii.a2b_hex(bootnode_key))
my_endpoint = EndPoint(u'52.4.20.183', 30303, 30303)    
server = Server(my_endpoint)
listen_thread = server.listen_thread()
listen_thread.start()
fn = FindNeighbors(bootnode.node, time.time() + 60)
ping = PingNode(my_endpoint, bootnode.endpoint, time.time() + 60)## introduce selfserver.send(ping, bootnode.endpoint)## wait for pong-ping-pongtime.sleep(3)## ask for neighborsserver.send(fn, bootnode.endpoint)## wait for responsetime.sleep(3)

首先,我们从 params/bootnodes.go 中获取一个引导节点密钥,并创建一个 Node 对象作为我们的第一个联系人对象。 然后我们创建一个服务器,启动监听线程,并创建 PingNode 和 FindNeighbors 数据包。 然后我们按照握手流程,ping bootstrap 节点,收到 pong 和 ping。 我们将响应一个 pong,使我们自己成为一个众所周知的节点。 最后我们可以发送 fn 数据包。 引导程序节点应响应 Neighbors。

执行 python send_ping.py 你应该看到:

$ python send_ping.py
sending (Ping 3 (EP 52.4.20.183 30303 30303) (EP 13.93.211.84 30303 30303) 1502819202.25)
listening...
received message[ ('13.93.211.84', 30303) ]:
 Verified message hash.
 Verified signature.
 received Pong
 (Pong (EP 52.4.20.183 30303 30303)  1502819162)
received message[ ('13.93.211.84', 30303) ]:
 Verified message hash.
 Verified signature.
 received Ping
   sending Pong response: (Pong (EP 13.93.211.84 30303 30303)  1502819202.34)
sending (Pong (EP 13.93.211.84 30303 30303)  1502819202.34)
sending (FN 3f1d120... 1502983026.6)
received message[ ('13.93.211.84', 30303) ]:
 Verified message hash.
 Verified signature.
 received Neighbors
 (Ns [(N 9e44f97...), (N 112917b...), (N ebf683d...), (N 2232e47...), (N f6ff826...), (N 7524431...), (N 804613e...), (N 78e5ce9...), (N c6dd88f...), (N 1dbf854...), (N 48a80a9...), (N 8b6c265...)] 1502982991)
received message[ ('13.93.211.84', 30303) ]:
 Verified message hash.
 Verified signature.
 received Neighbors
 (Ns [(N 8567bc4...), (N bf48f6a...), (N f8cb486...), (N 8e7e82e...)] 1502982991)

引导节点在两个数据包中响应 16 个邻居节点。

下一次,我们将构建一个过程来抓取这些邻居,直到我们有足够的对等点来同步区块链。