主页 > imtoken钱包官方版下载 > Python从零开始实现以太坊(一):Ping

Python从零开始实现以太坊(一):Ping

imtoken钱包官方版下载 2023-10-16 05:10:18

以太坊是一种可以在区块链上执行代码的加密货币。 此功能允许人们编写可以自动运行的“智能合约”。 大约一年前,一个名为 The DAO 的智能合约爆炸了,有人找到了一种方法来操纵它来获取当时价值 4100 万美元的 ETH。 这导致网络分裂,人们决定分叉区块链以生成一条从未遭受过 DAO 攻击的链。 我一听说它就觉得“这听起来很有趣”,但直到现在我都没有时间去了解它是如何工作的。 本文是从初学者的角度完整实现以太坊协议系列的第一部分。 以后我打算把这个系列写成通俗易懂的小故事,陆续发布,让大家不用每天花太多时间阅读,但随着时间的积累,你会对以太坊有更深入的了解.

我假设读者对 Python、git 和网络概念(例如 TCP 和 UDP)有基本的了解(不一定是技术性的),并且不害怕使用原始字节。 除此之外,我将尝试尽可能详细地解释。 今天先从介绍加密货币的概念开始,然后搭建Python开发环境,最后在以太坊网络上实现ping。 开始吧。

加密货币的概念

加密货币是一种在没有中央票据交换所参与的情况下以电子方式存储和转移价值的方式。 中央票据交换所充当所有交易的可信第三方,它跟踪所有账户并为每笔交易进行更新。 在美国,联邦储备系统是中央结算机构。 所有的银行账户都在美联储,银行利用其权力来结算账户之间的交易。 如果没有集中解决,一方很难向另一方证明他们拥有他们声称的东西——他们可能在撒谎。

加密货币允许每个人都保留分类账的记录,从而在没有中央机构参与的情况下解决结算问题。 为了在交易发生后保持这些账本的一致性,将更新的信息和一个可解决的数学问题广播到整个网络,解决问题的人总是将信息更新到最长的账本上。 只要网络中超过 50% 的人都遵循这个规则,这个策略就是有效的,因为人越多,数学题就会越快解决,最终会产生最长的链。 当信息更新到区块链中每个人的共识时,交易被证明是有效的并且实际发生了。

因此区块链中以太坊的英文简称,为了实现加密货币,我们需要弄清楚一些事情,节点如何通信,交易如何存储,以及如何与其他人一起解决数学问题。

创建开发环境

(关于virtualenv的介绍从略,不知道的请谷歌,译者使用的操作系统是OSX+virtualenvwrapper)。

让我们为这个项目设置一个Python虚拟环境:

$ mkvirtualenv pyeth

注意Python的版本,我用的是2.7.13,不保证本项目的代码在其他版本也能运行。

(pyeth)$ python --version
Python 2.7.13

我最不想做的事情是使用名为 cookiecutter 的 pip 库构建包框架。

(pyeth)$ pip install cookiecutter

我将使用最小的框架来发布和测试 pip。

(pyeth)$ cookiecutter gh:wdm0006/cookiecutter-pipproject

执行时,系统会提示您回答几个问题。 比如项目名称,作者,版本等。我把这个项目命名为pyeth。 之后,我设置了 git 来跟踪我的项目代码。

让我们安装 nose 包进行单元测试。

(pyeth)$ pip install nose

我们可以使用包根目录下的nosetests命令来运行tests目录下的所有测试用例。

(pyeth)$ nosetests
.
----------------------------------------------------------------------
Ran 1 test in 0.003s
OK

好吧,我想我们可以开车了。

开始意识到

我们首先需要弄清楚如何与节点对话。 谷歌了一下,我找到了以太坊线路协议。 该文件说:

运行以太坊客户端的节点之间的点对点通信底层使用的是ÐΞVp2p线路协议。

基本链同步

所以,我查看了 devp2p 行协议文档:

ÐΞVp2p 节点通过使用 RLPx 发送消息进行通信,RLPx 是一种加密和经过身份验证的传输协议。 对等点可以自由地在他们想要的任何 TCP 端口上发布和接受连接,但是,我不敢在默认端口 30303 上创建和侦听连接。虽然 TCP 提供了一种面向连接的介质,但 ÐΞVp2p 节点以数据包为单位进行通信。 RLPx 提供发送和接收数据包的功能。 有关 RLPx 的更多信息,请参阅协议规范。

ÐΞVp2p 节点通过 RLPx 发现协议 DHT 找到其他对等点。 还可以通过向特定于客户端的 RPC API 提供对等端点来创建对等连接。

因此,我们使用RLPx协议,默认通过30303端口发送数据包。 devp2p协议有两种不同的模式:使用TCP的主协议和使用UDP的发现协议。 今天我只想弄清楚如何使用发现协议 DHT 找到对等点。 DHT是“分布式哈希表”的缩写。 您连接到称为引导节点的特定服务器(在 BitTorrent 中,这些服务器是 router.bittorrent.com 和 router.utorrent.com),它们会为您提供一小部分对等节点。 一旦你有了这些点,你就连接它们,他们与你共享它们的点,你连接这些点,等等,直到你拥有网络中所有点的完整清单。

听起来很简单,但让我们让它更简单一点。 在 RLPx 规范的最后一个 block reference 中有一个叫做 Node Discovery(节点发现)Hints 的部分。 它描述了如何通过 UDP 端口 30303 发送消息,并指定了以下数据包结构:

hash || signature || packet-type || packet-data    hash: sha3(signature || packet-type || packet-data) 
    signature: sign(privkey, sha3(packet-type || packet-data))
    signature: sign(privkey, sha3(pubkey || packet-type || packet-data))
    packet-type: single byte < 2**7 // 可用值 [1,4]
    packet-data: RLP编码的列表。包属性按它们被定义的顺序序列化。见后面的packet-data。

和不同类型的数据包:

所有的数据结构都是RLP编码。
包(除了IP头)的数据体大小不能超过1280字节。
NodeId: 节点的公钥。inline: 属性被追加到当前列表而不是编码成列表。
包的最大字节大小仅标记为参考。
timestamp: 包何时创建(UNIX时间戳)。
PingNode packet-type: 0x01struct PingNode{
    h256 version = 0x3;
    Endpoint from;
    Endpoint to;    uint32_t timestamp;
};
Pong packet-type: 0x02struct Pong{
    Endpoint to;
    h256 echo;    uint32_t timestamp;
};
FindNeighbours packet-type: 0x03struct FindNeighbours{
    NodeId target; //一个节点的Id。响应节点将会发回离目标最近的那些节点。
    uint32_t timestamp;
};
Neighbors packet-type: 0x04struct Neighbours{
    list nodes: struct Neighbour
    {
        inline Endpoint endpoint;
        NodeId node;
    };    uint32_t timestamp;
};struct Endpoint{
    bytes address; // 大端编码的4字节或16字节地址 (大小取决于ipv4 vs ipv6)
    uint16_t udpPort; // 大端编码的16位无符号整型
    uint16_t tcpPort; // 大端编码的16位无符号整型}

消息类型用类似于C语言的数据结构来表示。 今天,我们能做的最简单的事情就是实现 PingNode,它由一个版本、两个 EndPoint 对象和一个时间戳组成。 EndPoint 对象由一个 IP 地址、两个整数表示的 UDP 和 TCP 端口组成。

为了通过网络发送这些结构,我们将它们放入递归长度前缀 RLP 中。 有关详细信息,请参阅 RLP 编码原则或 RLP。

在将任何内容转换为 RLP 编码之前,我们首先需要将结构转换为“项目”:一个字符串或多个项目的列表(项目是递归定义的)。 编码后的输出形式因此得名“递归长度前缀”。 正如文档所说,RLP 仅对结构进行编码,将 BYTES 的解释留给更高级别的协议。

由于我宁愿自己实现协议,我将使用 rlp 库及其编码和解码函数来进行 RLP 编码。 使用 pip install rlp 将其包含在本地包中。

我们已经拥有发送 PingNode 数据包所需的一切。 在下面的Python程序中,我们将创建一个PingNode类,打包,发送给我们自己。 为了打包数据,我们将从结构的 RLP 编码值开始,添加一个指示结构类型的字节,添加一个加密签名,最后添加一个哈希值以验证数据包的完整性。

pyeth/discovery.py:

# -*- coding: utf8 -*-import socketimport threadingimport timeimport structimport rlpfrom crypto import keccak256from secp256k1 import PrivateKeyfrom ipaddress import ip_addressclass EndPoint(object):
    def __init__(self, address, udpPort, tcpPort):
        self.address = ip_address(address)
        self.udpPort = udpPort
        self.tcpPort = tcpPort    def pack(self):
        return [self.address.packed,
                struct.pack(">H", self.udpPort),
                struct.pack(">H", self.tcpPort)]

根据规范,第一类是 EndPoint 类。 端口为整数,地址为带“.”的格式,如“127.0.0.1”。 我们将地址传递给 ipaddress 库,以使用其实用函数将地址转换为二进制格式,就像我在 pack 方法中所做的那样。 使用 pip install ipaddress 安装这个包。 pack 方法将对象转换为字符串列表,供 rlp.encode 稍后使用。 在EndPoint的规范中,地址要求是big-endian编码的4字节数据,由self.address.packed输出。 对于端口,EndPoint 规范将其数据类型列为 uint16_t。 所以我使用 struct.pack 方法,并使用格式字符串 >H,这意味着 big-endian 无符号 16 位整数,如 Python 文档所述。

class PingNode(object):
    packet_type = '\x01';
    version = '\x03';    def __init__(self, endpoint_from, endpoint_to):
        self.endpoint_from = endpoint_from
        self.endpoint_to = endpoint_to    def pack(self):
        return [self.version,
                self.endpoint_from.pack(),
                self.endpoint_to.pack(),
                struct.pack(">I", time.time() + 60)]

第二类是 PingNode 结构。 我决定把packet_type和version当作常量字段,填入原来的byte值,后面就不用转换了。 在构造函数中,您必须传递 from 和 to 端点对象,如规范中所列。 在 pack 方法中,我在时间戳上加上 60,让数据包有额外的 60 秒到达目的地(规范说过去时间收到的数据包将被丢弃,以防止重放攻击)。

class PingServer(object):
    def __init__(self, my_endpoint):
        self.endpoint = my_endpoint        ## 获取私钥
        priv_key_file = open('priv_key', 'r')
        priv_key_serialized = priv_key_file.read()
        priv_key_file.close()
        self.priv_key = PrivateKey()
        self.priv_key.deserialize(priv_key_serialized)    def wrap_packet(self, packet):
        payload = packet.packet_type + rlp.encode(packet.pack())
        sig = self.priv_key.ecdsa_sign_recoverable(keccak256(payload), raw = True)
        sig_serialized = self.priv_key.ecdsa_recoverable_serialize(sig)
        payload = sig_serialized[0] + chr(sig_serialized[1]) + payload
        payload_hash = keccak256(payload)        return payload_hash + payload    def udp_listen(self):
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sock.bind(('0.0.0.0', self.endpoint.udpPort))        def receive_ping():
            print "listening..."
            data, addr = sock.recvfrom(1024)            print "received message[", addr, "]"
        return threading.Thread(target = receive_ping)    def ping(self, endpoint):
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        ping = PingNode(self.endpoint, endpoint)
        message = self.wrap_packet(ping)        print "sending ping."
        sock.sendto(message, (endpoint.address.exploded, endpoint.udpPort))

最后一个类是 PingServer。 此类打开一个网络套接字,对消息进行签名和哈希处理,然后将消息发送到其他服务器。 构造函数接收一个 EndPoint 对象,该对象在网络空间中引用自身。 发送数据包时,服务器使用此对象作为发件人地址。 当创建服务器对象时,它的私钥也会被加载——我们需要事先生成它。

以太坊使用 secp256k1,一种用于非对称加密的椭圆曲线。 实现的 Python 库是 secp256k1-py。 您可以使用 pip install secp256k1 安装它。

为了生成私钥,需要以None为参数调用PrivateKey的构造函数,然后将其serialize()的输出写入文件。

>>> from secp256k1 import PrivateKey>>> k = PrivateKey(None)>>> f = open("priv_key", 'w')>>> f.write(k.serialize())>>> f.close()

我把它和源文件放在一起。 如果你使用 git区块链中以太坊的英文简称,记得把它添加到你的 .gitignore 文件中,以免不小心发布。

wrap_packet 方法将数据包编码为:

散列 || 签名|| 数据包类型 || 包数据

首先要做的是将数据包类型添加到 RLP 编码的数据包数据中。 然后用私钥的 ecdsa_sign_recoverable 函数对散列后的数据体进行签名。 raw 参数设置为 True 是因为我们自己进行了散列。 然后我们序列化签名并将其添加到之前的数据主体中。 签名序列化后是一个元组对象,它的第二个元素需要用chr转换成字符串。 最后,对整个数据体进行哈希处理,将得到的哈希值添加到前面,就可以发送数据包了。

您可能已经注意到我们还没有定义 keccak256 函数。 以太坊使用一种称为 keccak-256 的非标准 sha3 算法。 已经实现的Python库是pysha3。 使用 pip install pysha3 安装。

pyeth/crypto.py,我们定义keccak256:

# -*- coding: utf8 -*-import hashlibimport sha3## 以太坊使用keccak-256哈希算法def keccak256(s):
    k = sha3.keccak_256()
    k.update(s)    return k.digest()

这个功能非常简单。

返回 PingServer。 第二个函数 udp_listen 监听传入的传输。 它创建一个套接字对象并将其绑定到服务器端点的 UDP 端口。 然后我在函数中定义了receive_ping函数。 它的作用是监听这个socket上传入的数据,打印传输的证书地址并返回。 函数最后返回一个Thread线程对象,receive_ping会在这个线程中运行,这样我们就可以同时监听接收和发送ping。

最后的 ping 方法接收一个目的端点,为它创建一个 PingNode 对象,使用 wrap_packert 将这个对象转换成消息,最后使用 UDP 协议发送消息。

send_ping.py,现在我们可以启动一个脚本来发送一些数据包。

# -*- coding: utf8 -*-from pyeth.discovery import EndPoint, PingNode, PingServer
my_endpoint = EndPoint(u'52.4.20.183', 30303, 30303)
their_endpoint = EndPoint(u'127.0.0.1', 30303, 30303)
server = PingServer(my_endpoint)
listen_thread = server.udp_listen()
listen_thread.start()
server.ping(their_endpoint)

当我们执行这段代码时,我们可以看到:

(pyeth)$ python send_ping.py
sending ping
listening...
received message[ ('127.0.0.1', 58974) ]

我已经成功地向自己问好。 我还没有连接任何引导程序节点,这就是下一篇文章计划做的事情。 请继续关注本系列的第二部分。