Python从头实现以太坊(一):Ping

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

以太坊是一种可以在区块链上执行代码的加密货币。这个功能允许人们编写可以自动运行的“智能合约”。大概一年前,一个叫做DAO的智能合约炸锅了,有人找到方法操纵它去获取当时价值4100万美元的ETH。从而导致了网络的分裂,人们决定分叉区块链,生成一条从未发生DAO攻击的链。我一听说这件事,就寻思“这听上去真是太有趣了”,但却没时间深入了解其运行机制,直到现在。本文是以初学者角度完整实现以太坊协议系列的第一部分。后面,我计划把这个系列写成易消化的小短篇,陆续发布,这样你就不用每天花太多时间去阅读,但是随着时间积累,你会对以太坊有更深入地理解。

我假设读者对Python、git以及诸如TCP和UDP这样的网络概念知识(不必很专业)有基本的了解,并且不怕使用原始字节。除此之外,我会尽量做详细解释。今天,我从介绍加密货币的概念开始,然后搭建Python开发环境,最后在以太坊网络上实现ping。让我们开始吧。

加密货币的概念

加密货币是一种无需中央结算机构参与的,以电子方式存储和转移价值的方式。中央结算机构扮演所有交易可信的第三方,它跟踪所有账户,并为每笔交易做更新。在美国,联邦储备系统就是中央结算机构。所有银行账户都在美联储,银行利用其权威来结算账户之间的交易。如果没有中心化的结算,一方难以向另一方证明他们拥有自己宣称的东西——他们有可能撒谎。

加密货币让每个人都保存一份账本记录,以此解决没有中央权威机构参与的结算问题。为了让这些账本在发生交易之后保持一致,更新信息和一个可解的数学问题会广播给整个网络,求得解的人始终把信息更新到最长的账本上。只要网络超过50%的人按照这个规则来,这个策略就有效,因为人越多数学问题解得越快,最终会生成一条最长的链。当信息更新到区块链被所有人共识后,交易就被证明有效且真正发生。

因此,为了实现加密货币,我们需要搞清楚几件事,节点是如何对话的,交易是如何存储的,以及,如何与其他人一起解数学问题。

建立开发环境

(略去了virtualenv的介绍,不知道的话请自行 Google。译者用的操作系统是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线路协议

基本链同步

  • 两个对等端连接打招呼并发送状态消息。状态包含总难度(TD)和最佳区块的哈希。

于是,我去看了devp2p线路协议文档

ÐΞVp2p节点通过发送使用了RLPx(一种加密和认证的传输协议)的消息进行通讯。对等端可以在他们想要的任意TCP端口上自由发布通告和接受连接,但是,恐怕得在一个默认的30303端口上创建和监听连接。虽然TCP提供了面向连接的介质,但是ÐΞVp2p节点以包(packets)为单位通讯。RLPx提供发送和接收数据包的设施。了解RLPx的更多信息,请参考协议规范

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

所以,我们使用RLPx协议默认通过30303端口发送数据包。devp2p协议有两种不同的模式:使用TCP的主协议和使用UDP的发现协议。今天我只想要搞明白怎样用发现协议DHT找到对等端。DHT是“分布式哈希表(Distributed Hash Table)”的缩写。你连接到被称为引导节点(在BitTorrent中,这些服务器是router.bittorrent.comrouter.utorrent.com)的特定服务器,它们会给你一个对等端的小清单。一旦有了这些对等端,你就可以连接它们,它们又会和你共享它们的对等端,你再连接这些对等端,如此延展,直到你拥有网络中所有对等端的完整清单。

听上去已经足够简单,但是我们还要让它再简单一点。在RLPx规范最后一个块引用中有一节称为节点发现(Node Discovery)的提示。它介绍了如何通过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: 0x01
struct PingNode
{
    h256 version = 0x3;
    Endpoint from;
    Endpoint to;
    uint32_t timestamp;
};

Pong packet-type: 0x02
struct Pong
{
    Endpoint to;
    h256 echo;
    uint32_t timestamp;
};

FindNeighbours packet-type: 0x03
struct FindNeighbours
{
    NodeId target; //一个节点的Id。响应节点将会发回离目标最近的那些节点。
    uint32_t timestamp;
};

Neighbors packet-type: 0x04
struct 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,它由一个version,两个EndPoint对象和一个timestamp组成。EndPoint对象由一个IP地址,分别用两个整数表示的UDP和TCP端口组成。

为了把这些结构体发送到线路上,我们把它们放进RLP,即递归长度前缀编码(recursive length prefix)。详情请查看RLP编码原理RLP

在任何东西被转为RLP编码之前,我们首先需要把结构体转化为“item”:字符串或多个item的列表(item的定义是递归的)。编码后输出形式是<LENGTH><BYTES>,因此叫做“递归长度前缀”。就如文档所说,RLP只编码结构体,把BYTES的解释留给更高阶的协议。

因为我更愿意实现协议本身,所以我将使用rlp库,用它的encodedecode函数来做RLP编码。使用pip install rlp将它包含到本地的软件包中。

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

pyeth/discovery.py

# -*- coding: utf8 -*-
import socket
import threading
import time
import struct
import rlp
from crypto import keccak256
from secp256k1 import PrivateKey
from ipaddress import ip_address

class 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的规范中,地址要求是大端编码的4字节数据,由self.address.packed输出。对于端口,EndPoint规范把他们的数据类型列为uint16_t。所以我使用struct.pack方法,并用了格式字符串>H,意思是大端无符号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_typeversion当做常量字段,填入原始字节值,后面就不需要再转化了。在构造函数中你必须传入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对象,在网络空间中指代它自己。发送数据包的时候,服务器用这个对象作为from地址。服务器对象创建的时候,它的私钥也会被加载——我们需要事先生成。

以太坊使用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方法将包编码为:

hash || signature || packet-type || packet-data

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

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

pyeth/crypto.py, 我们定义keccak256

# -*- coding: utf8 -*-
import hashlib
import sha3

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

这个函数很简单。

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

最后的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) ]

我已经成功的和自己打招呼。我还没有连接任何的引导节点,那是下一篇帖子计划做的。请继续关注本系列的第二部分

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,265评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,274评论 1 288
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,087评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,479评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,782评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,218评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,594评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,316评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,955评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,274评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,803评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,177评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,732评论 3 229
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,953评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,687评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,263评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,189评论 2 258

推荐阅读更多精彩内容