Python從頭實(shí)現(xiàn)以太坊系列索引:
一半等、Ping
二扁掸、Pinging引導(dǎo)節(jié)點(diǎn)
三、解碼引導(dǎo)節(jié)點(diǎn)的響應(yīng)
四帜羊、查找鄰居節(jié)點(diǎn)
五、類-Kademlia協(xié)議
六鸠天、Routing
這是我寫的從頭完整實(shí)現(xiàn)以太坊協(xié)議系列的第三部分(第一部分讼育,第二部分)。我們目前已經(jīng)實(shí)現(xiàn)了以太坊發(fā)現(xiàn)協(xié)議——用于查找其他可以與之進(jìn)行區(qū)塊鏈同步的節(jié)點(diǎn)的算法稠集。我們已經(jīng)能夠ping通引導(dǎo)節(jié)點(diǎn)——寫在源代碼里面的奶段,用于查找其他節(jié)點(diǎn)的起始端點(diǎn)。節(jié)點(diǎn)響應(yīng)了一條消息剥纷,但是我們還未對(duì)它進(jìn)行解碼痹籍。今天我們就要來解碼這個(gè)引導(dǎo)節(jié)點(diǎn)響應(yīng)的消息。
到https://github.com/HuangFJ/pyeth下載本項(xiàng)目代碼:
git clone https://github.com/HuangFJ/pyeth
數(shù)據(jù)解包
這一節(jié)晦鞋,我們將寫一些函數(shù)用來將RLP編碼的PingNode
和Pong
消息解包為Python對(duì)象蹲缠。這些方法將在哈希和簽名驗(yàn)證后,讀取消息數(shù)據(jù)悠垛。我們已經(jīng)定義了PingNode
類线定,現(xiàn)在要實(shí)現(xiàn)Pong
類。
在RLPx規(guī)范中确买,Pong
被定義成:
Pong packet-type: 0x02
struct Pong
{
Endpoint to;
h256 echo;
uint32_t timestamp;
};
讓我們?cè)?code>pyeth/discovery.py里寫一個(gè)此結(jié)構(gòu)相對(duì)應(yīng)的類:
class Pong(object):
packet_type = '\x02'
def __init__(self, to, echo, timestamp):
self.to = to
self.echo = echo
self.timestamp = timestamp
def __str__(self):
return "(Pong " + str(self.to) + " <echo hash=""> " + str(self.timestamp) + ")"
def pack(self):
return [
self.to.pack(),
self.echo,
struct.pack(">I", self.timestamp)]
@classmethod
def unpack(cls, packed):
to = EndPoint.unpack(packed[0])
echo = packed[1]
timestamp = struct.unpack(">I", packed[2])[0]
return cls(to, echo, timestamp)
這個(gè)類把包結(jié)構(gòu)編碼為Python對(duì)象斤讥,packet_type
作為類字段,并將to
湾趾,echo
和timestamp
傳遞給構(gòu)造函數(shù)芭商。我添加了一個(gè)__str__
方法用于打印對(duì)象,這里要用到to
撑帖,它是一個(gè)EndPoint
對(duì)象蓉坎,它也要一個(gè)__str__
方法。我們定義為:
class EndPoint(object):
...
def __str__(self):
return "(EP " + self.address.exploded + " " + str(self.udpPort) + " " + str(self.tcpPort) + ")"
...
回到Pong
類胡嘿。就如第一部分以及RLP規(guī)范所述蛉艾,pack
方法把對(duì)象轉(zhuǎn)為item
格式供rlp.encode
消費(fèi),item
是二進(jìn)制字符串或者多個(gè)item的列表衷敌。我們需要編碼to
勿侯,echo
和timestamp
。對(duì)于to
缴罗,我們可以用to.pack()
助琐,因?yàn)?code>item的定義是遞歸的。接著面氓,echo
已經(jīng)是字符串兵钮,因此它不需要再轉(zhuǎn)化蛆橡。最后,我用帶有>I
參數(shù)的struct.pack
將timestamp
編碼為大端無符號(hào)32位整型掘譬。
unpack
方法是pack
的逆過程泰演。對(duì)于to
,我們用下面定義的EndPoint
unpack
來解碼:
class EndPoint(object):
...
@classmethod
def unpack(cls, packed):
udpPort = struct.unpack(">H", packed[1])[0]
tcpPort = struct.unpack(">H", packed[2])[0]
return cls(packed[0], udpPort, tcpPort)
tcpPort
和udpPort
用struct.unpack
取回葱轩,IP地址以字符串格式傳遞睦焕,由ip_address
的構(gòu)造函數(shù)處理。
因?yàn)?code>echo被定義為h256靴拱,256字節(jié)的哈希垃喊,它可以解包成自己本身。timestamp
被用struct.unpack
解包成大端32位整型袜炕。
我們還需要定義PingNode
的__str__
和unpack
方法本谜,如下:
class PingNode(object):
packet_type = '\x01';
version = '\x03';
def __init__(self, endpoint_from, endpoint_to, timestamp):
self.endpoint_from = endpoint_from
self.endpoint_to = endpoint_to
self.timestamp = timestamp
def __str__(self):
return "(Ping " + str(ord(self.version)) + " " + str(self.endpoint_from) + " " + str(self.endpoint_to) + " " + str(self.timestamp) + ")"
def pack(self):
return [self.version,
self.endpoint_from.pack(),
self.endpoint_to.pack(),
struct.pack(">I", self.timestamp)]
@classmethod
def unpack(cls, packed):
assert(packed[0] == cls.version)
endpoint_from = EndPoint.unpack(packed[1])
endpoint_to = EndPoint.unpack(packed[2])
return cls(endpoint_from, endpoint_to)
對(duì)PingNode
構(gòu)造函數(shù)的修改會(huì)牽涉到PingServer
的ping
方法,此處也應(yīng)做相應(yīng)修改:
def ping(self, endpoint):
ping = PingNode(self.endpoint, endpoint, time.time() + 60)
message = self.wrap_packet(ping)
print "sending " + str(ping)
self.sock.sendto(message, (endpoint.address.exploded, endpoint.udpPort))
解碼響應(yīng)消息
回顧一下我們第一部分討論的在RLPx規(guī)范中的消息排列(||是連接符號(hào)):
hash || signature || packet-type || packet-data
在這一節(jié)偎窘,我們檢查消息的哈希和簽名耕突,如果都有效,才根據(jù)packet-type
解碼不同類型的消息评架。在pyeth/discovery.py
中,我們的包監(jiān)聽函數(shù)udp_listen
被定義在PingServer
里面:
def udp_listen(self):
def receive_ping():
print "listening..."
data, addr = self.sock.recvfrom(1024)
print "received message[", addr, "]"
return threading.Thread(target = receive_ping)
我將把receive_ping
拆分為更通用的函數(shù)炕泳,名稱叫做receive
纵诞,它將用于包解碼:
def udp_listen(self):
return threading.Thread(target = self.receive)
def receive(self):
print "listening..."
data, addr = self.sock.recvfrom(1024)
print "received message[", addr, "]"
## decode packet below
現(xiàn)在我們要填充## decode packet below
這一行以下的代碼,從解碼哈希開始培遵。我們把下面的代碼添加到receive
函數(shù):
## verify hash
msg_hash = data[:32]
if msg_hash != keccak256(data[32:]):
print " First 32 bytes are not keccak256 hash of the rest."
return
else:
print " Verified message hash."
第二步就是驗(yàn)證secp256k1
簽名浙芙。首先我們用secp256k1庫(kù)把簽名反系列化成對(duì)象,然后我們從簽名里面取回公鑰籽腕,然后我們用公鑰來驗(yàn)證簽名嗡呼。為了反系列化,我們使用服務(wù)器私鑰priv_key
的ecdsa_recoverable_deserialize
方法皇耗。我們之前用sig_serialized[0] + chr(sig_serialized[1])
編碼簽名南窗,第一項(xiàng)是64字節(jié),第二項(xiàng)是1字節(jié)郎楼。因此我們需要抓取哈希之后的65字節(jié)万伤,接著把簽名分成64字節(jié)和1字節(jié)的參數(shù)并對(duì)最后一個(gè)字節(jié)使用ord
函數(shù)——chr
的逆過程。
## verify signature
signature = data[32:97]
signed_data = data[97:]
deserialized_sig = self.priv_key.ecdsa_recoverable_deserialize(signature[:64],
ord(signature[64]))
為了取回公鑰呜袁,我們使用ecdsa_recover
敌买。還記得我們之前用raw = True
告訴算法我們使用自己的keccak256
哈希算法替代函數(shù)默認(rèn)的哈希算法嗎。
remote_pubkey = self.priv_key.ecdsa_recover(keccak256(signed_data),
deserialized_sig,
raw = True)
現(xiàn)在我們創(chuàng)建PublicKey
對(duì)象來存儲(chǔ)公鑰阶界。我們要在文件頭部引入這個(gè)類:
from secp256k1 import PrivateKey, PublicKey
回到receive
虹钮,我們繼續(xù)編寫:
pub = PublicKey()
pub.public_key = remote_pubkey
現(xiàn)在我們可以使用ecdsa_verify
驗(yàn)證signed_data
是否被deserialized_sig
簽名過:
verified = pub.ecdsa_verify(keccak256(signed_data),
pub.ecdsa_recoverable_convert(deserialized_sig),
raw = True)
if not verified:
print " Signature invalid"
return
else:
print " Verified signature."
最后一步就是解包消息聋庵。具體用什么函數(shù)由packet_type
字節(jié)確定。
response_types = {
PingNode.packet_type : self.receive_ping,
Pong.packet_type : self.receive_pong
}
try:
packet_type = data[97]
dispatch = response_types[packet_type]
except KeyError:
print " Unknown message type: " + data[97]
return
payload = data[98:]
dispatch(payload)
我們將receive_ping
和receive_pong
保持簡(jiǎn)單芙粱,就如PingServer
的其他函數(shù)祭玉。
def receive_pong(self, payload):
print " received Pong"
print "", Pong.unpack(rlp.decode(payload))
def receive_ping(self, payload):
print " received Ping"
print "", PingNode.unpack(rlp.decode(payload))
讓我們運(yùn)行python send_ping.py
檢查一下看看程序如何運(yùn)行。你應(yīng)該會(huì)看到:
sending (Ping 3 (EP 52.4.20.183 30303 30303) (EP 13.93.211.84 30303 30303) 1501093574.21)
listening...
received message[ ('13.93.211.84', 30303) ]:
Verified message hash.
Verified signature.
received Pong
(Pong (EP 52.4.20.183 30303 30303) <echo hash=""> 1501093534)
我們已經(jīng)成功驗(yàn)證和解碼了Pong
消息宅倒。下次攘宙,我們將實(shí)現(xiàn)FindNeighbors
消息和Neighbors
響應(yīng)消息。