Python從頭實(shí)現(xiàn)以太坊(三):解碼引導(dǎo)節(jié)點(diǎn)的響應(yīng)

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編碼的PingNodePong消息解包為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湾趾,echotimestamp傳遞給構(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勿侯,echotimestamp。對(duì)于to缴罗,我們可以用to.pack()助琐,因?yàn)?code>item的定義是遞歸的。接著面氓,echo已經(jīng)是字符串兵钮,因此它不需要再轉(zhuǎn)化蛆橡。最后,我用帶有>I參數(shù)的struct.packtimestamp編碼為大端無符號(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)

tcpPortudpPortstruct.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ì)牽涉到PingServerping方法,此處也應(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_keyecdsa_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_pingreceive_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)消息。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末拐迁,一起剝皮案震驚了整個(gè)濱河市蹭劈,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌线召,老刑警劉巖铺韧,帶你破解...
    沈念sama閱讀 212,884評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異缓淹,居然都是意外死亡哈打,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門讯壶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來料仗,“玉大人,你說我怎么就攤上這事伏蚊×⒃” “怎么了?”我有些...
    開封第一講書人閱讀 158,369評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵躏吊,是天一觀的道長(zhǎng)氛改。 經(jīng)常有香客問我,道長(zhǎng)比伏,這世上最難降的妖魔是什么胜卤? 我笑而不...
    開封第一講書人閱讀 56,799評(píng)論 1 285
  • 正文 為了忘掉前任,我火速辦了婚禮赁项,結(jié)果婚禮上葛躏,老公的妹妹穿的比我還像新娘。我一直安慰自己悠菜,他們只是感情好紫新,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,910評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著李剖,像睡著了一般芒率。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上篙顺,一...
    開封第一講書人閱讀 50,096評(píng)論 1 291
  • 那天偶芍,我揣著相機(jī)與錄音充择,去河邊找鬼。 笑死匪蟀,一個(gè)胖子當(dāng)著我的面吹牛椎麦,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播材彪,決...
    沈念sama閱讀 39,159評(píng)論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼观挎,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了段化?” 一聲冷哼從身側(cè)響起嘁捷,我...
    開封第一講書人閱讀 37,917評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎显熏,沒想到半個(gè)月后雄嚣,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,360評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡喘蟆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,673評(píng)論 2 327
  • 正文 我和宋清朗相戀三年缓升,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蕴轨。...
    茶點(diǎn)故事閱讀 38,814評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡港谊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出橙弱,到底是詐尸還是另有隱情封锉,我是刑警寧澤,帶...
    沈念sama閱讀 34,509評(píng)論 4 334
  • 正文 年R本政府宣布膘螟,位于F島的核電站,受9級(jí)特大地震影響碾局,放射性物質(zhì)發(fā)生泄漏荆残。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,156評(píng)論 3 317
  • 文/蒙蒙 一净当、第九天 我趴在偏房一處隱蔽的房頂上張望内斯。 院中可真熱鬧,春花似錦像啼、人聲如沸俘闯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽真朗。三九已至,卻和暖如春僧诚,著一層夾襖步出監(jiān)牢的瞬間遮婶,已是汗流浹背蝗碎。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評(píng)論 1 267
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留旗扑,地道東北人蹦骑。 一個(gè)月前我還...
    沈念sama閱讀 46,641評(píng)論 2 362
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像臀防,于是被迫代替她去往敵國(guó)和親眠菇。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,728評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容