主页 > imtoken换手机怎么登录 > Python从零开始实现以太坊(四):寻找邻居节点
Python从零开始实现以太坊(四):寻找邻居节点
这是我从零开始完整实现以太坊协议的第四部分(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 个邻居节点。
下一次,我们将构建一个过程来抓取这些邻居,直到我们有足够的对等点来同步区块链。