Python從頭實現(xiàn)以太坊(四):查找鄰居節(jié)點

Python從頭實現(xiàn)以太坊系列索引:
一奠伪、Ping
二翰苫、Pinging引導(dǎo)節(jié)點
三、解碼引導(dǎo)節(jié)點的響應(yīng)
四拗盒、查找鄰居節(jié)點
五派继、類-Kademlia協(xié)議
六、Routing

這是我寫的從頭完整實現(xiàn)以太坊協(xié)議系列的第四部分(第一部分踩身,第二部分第三部分社露,如果你以前沒看過挟阻,建議你從第一部分開始看)。等這個系列教程結(jié)束的時候峭弟,你就可以爬取以太坊網(wǎng)絡(luò)獲取對等端點附鸽,同步和驗證區(qū)塊鏈,為以太坊虛擬機編寫智能合約瞒瘸,以及挖以太幣坷备。我們現(xiàn)在正在實現(xiàn)其發(fā)現(xiàn)協(xié)議部分。一旦完成情臭,我們就可以用一種類似torrent的方式下載區(qū)塊鏈省撑。我們上一次完成了Ping引導(dǎo)節(jié)點并解碼和驗證其Pong響應(yīng)。今天我們要實現(xiàn)FindNeighbors請求和Neighbors響應(yīng)俯在,我們將用它們爬取以太坊網(wǎng)絡(luò)竟秫。

這一部分不難,我們簡單地為FindNeighborsNeighbors的數(shù)據(jù)包定義類結(jié)構(gòu)跷乐,并像我們之前發(fā)送PingNodePong那樣將它們發(fā)送即可肥败。但是,想要成功發(fā)送FindNeighbors數(shù)據(jù)包愕提,還需要滿足一些必備條件馒稍。我們并沒有在協(xié)議文檔中看到這些條件,是因為文檔比源代碼舊浅侨。go-ethereum源代碼的發(fā)現(xiàn)協(xié)議采用v4版本纽谒。但是RLPx協(xié)議(我們的實現(xiàn))卻只到v3版本。源代碼里甚至還有一個叫discv5的模塊如输,表明它們正在實現(xiàn)v5版本鼓黔,不過央勒,通過檢查引導(dǎo)節(jié)點發(fā)回的Ping消息的version字段,我們發(fā)現(xiàn)它們跑的依然是v4版本请祖。

v4版本的協(xié)議要求,為了獲取FindNeighbors請求的響應(yīng)脖祈,必須先要有一次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數(shù)據(jù)包(FindNeighbors的Go實現(xiàn)),代碼首先檢查請求來源fromID是否在它的已知節(jié)點記錄里面盖高。如果不在慎陵,它就丟棄這個請求(難怪我之前的請求一直出問題,現(xiàn)在弄明白了)喻奥。

為了成為已知節(jié)點席纽,首先我們必須ping引導(dǎo)節(jié)點。當引導(dǎo)節(jié)點接收到ping撞蚕,它會先響應(yīng)一個pong润梯,然后再發(fā)一個ping,并等待我們響應(yīng)一個pong回去甥厦。一旦我們響應(yīng)了pong纺铭,我們的nodeID就會進入引導(dǎo)節(jié)點的已知節(jié)點列表。

因此刀疙,為了能夠發(fā)送FindNeighbors數(shù)據(jù)包舶赔,首先,我們需要創(chuàng)建與PingNodePong數(shù)據(jù)包具有相同功能的FindNeighborsNeighbors類谦秧。然后竟纳,我們需要在receive_ping中加一個Pong的響應(yīng)以便跟引導(dǎo)節(jié)點UDP握手。接著疚鲤,我們需要調(diào)整PingServer使之能持續(xù)監(jiān)聽數(shù)據(jù)包锥累。最后,我們需要調(diào)整send_ping.py腳本:發(fā)送一個ping集歇,留足夠的時間等待引導(dǎo)節(jié)點依次響應(yīng)pongping揩悄,之后假設(shè)我們正確實現(xiàn)了pong響應(yīng)的話,就可以發(fā)送FindNeighbors數(shù)據(jù)包并接收Neighbors響應(yīng)了鬼悠。

https://github.com/HuangFJ/pyeth下載本項目代碼:
git clone https://github.com/HuangFJ/pyeth

創(chuàng)建FindNeighbors和Neighbors類

在此系列前一部分我們?yōu)?code>PingNode和Pong創(chuàng)建了類删性,這一節(jié),我們要以同樣的方式為FindNeighborsNeighbors創(chuàng)建Python類焕窝。我們?yōu)槊總€類都創(chuàng)建__init__蹬挺,__str__pack它掂,unpack方法巴帮,并為PingServer類添加receive_的方法溯泣。

對于FindNeighbors規(guī)范描述的數(shù)據(jù)包結(jié)構(gòu)是:

FindNeighbours packet-type: 0x03
struct FindNeighbours
{
    NodeId target; // Id of a node. The responding node will send back nodes closest to the target.
    uint32_t timestamp;
};

target是一個NodeId類型榕茧,它是一個64字節(jié)的公鑰垃沦。這意味著我們可以在packunpack方法中存儲和提取它。對于__str__用押,我將使用binascii.b2a_hex把字節(jié)打印成16進制格式肢簿。除此以外,其他代碼跟我們在PingNodePong所見到的相似蜻拨。所以池充,我們在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,數(shù)據(jù)包結(jié)構(gòu)為:

Neighbors packet-type: 0x04
struct Neighbours
{
    list nodes: struct Neighbour
    {
        inline Endpoint endpoint;
        NodeId node;
    };

    uint32_t timestamp;
};

這要求我們先定義一個Neighbor類缎讼,我將在之后定義并取名為Node收夸。對于Neighbors,唯一新概念是nodes是一個列表血崭,所以我們將使用map來打包和解包數(shù)據(jù):

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是內(nèi)聯(lián)打包,所以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])

對于新建的數(shù)據(jù)包類捷凄,讓我們定義新的PingServer方法來接收數(shù)據(jù)包忱详,先簡單地定義:

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))

PingServerreceive方法里面,我們也要調(diào)整response_types派發(fā)表:

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
}

讓服務(wù)器持續(xù)監(jiān)聽

為了讓服務(wù)可以持續(xù)監(jiān)聽數(shù)據(jù)包跺涤,還有幾個事項需要處理:

  • PingServer的功能變得更通用匈睁,因此我們將它改名為Server桶错。
  • 我們通過設(shè)置self.sock.setblocking(0)讓服務(wù)器的套接字不再阻塞。
  • 讓我們把receive方法中#verify hash上面的代碼移到新的listen方法中院刁,并給receive添加一個新的參數(shù)data。這個新的listen函數(shù)循環(huán)以select等待數(shù)據(jù)包的到達并以receive響應(yīng)退腥。select函數(shù)的作用是在可選的超時時間內(nèi)等待直至資源可用任岸。
  • 我們把從套接字讀取的字節(jié)數(shù)增加到2048狡刘,因為一些以太數(shù)據(jù)包大小超過1024字節(jié)長享潜。
  • 我們將udp_listen更改為listen_thread嗅蔬,并將線程對象返回剑按,我們把線程的daemon字段設(shè)置為True疾就,這意味著即便監(jiān)聽線程依然在運行,進程也將終止猬腰。(之前進程是掛起的)

最終相應(yīng)的代碼部分是這樣的:

...
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

響應(yīng)pings

我們必須修改Server類的receive_ping方法以響應(yīng)一個Pong猜敢。這也要求我們將Serverping方法修改成更通用的函數(shù)send。原來ping創(chuàng)建一個PingNode對象并發(fā)送厢拭,現(xiàn)在變成了send接收一個新的packet自變量撇叁,做發(fā)送前準備并發(fā)送畦贸。

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參數(shù)。這個參數(shù)需放進位于Serverreceive方法里面的dispatch調(diào)用中趋厉,以及所有其他receive_開頭的函數(shù)胶坠。

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)

其他修復(fù)

因為引導(dǎo)節(jié)點使用v4版本的RLPx協(xié)議。但是規(guī)范文檔和我們的實現(xiàn)使用的是v3乡数,我們需要把PingNode unpack方法的packed[0]==cls.version注釋掉闻牡。在我可以找到基于新版本的集中文檔之前,我不打算修改類的實際版本號罩润。在前一篇文章里面,我忘記了把解包的timestamp包含到cls的參數(shù)里面割以,所以你的uppack看上去要像下面這樣:

@classmethod
def 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方法中闡釋严沥。如果沒有的話,你要設(shè)置tcpPort等于udpPort祝峻。

@classmethod
def 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)

對之前版本代碼的最后一個修改是扎筒,Pongpack方法有一個拼寫錯誤嗜桌,timestamp應(yīng)該改為self.timestamp辞色。之所以沒發(fā)現(xiàn)是因為我們從未發(fā)送過Pong消息:

def pack(self):
    return [
        self.to.pack(),
        self.echo,
        struct.pack(">I", self.timestamp)]

修改send_ping.py

我們需要重寫send_ping.py以闡釋新的發(fā)送流程。

from discovery import EndPoint, PingNode, Server, FindNeighbors, Node
import time
import 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 self
server.send(ping, bootnode.endpoint)
## wait for pong-ping-pong
time.sleep(3)
## ask for neighbors
server.send(fn, bootnode.endpoint)
## wait for response
time.sleep(3)

首先层亿,我們從params/bootnodes.go扒一個引導(dǎo)節(jié)點的key立美,創(chuàng)建一個Node對象,作為我們的第一個聯(lián)系對象碌更。然后我們創(chuàng)建一個服務(wù)器洞慎,啟動監(jiān)聽線程,并創(chuàng)建PingNodeFindNeighbors數(shù)據(jù)包旭绒。接著我們按照握手流程焦人,ping引導(dǎo)節(jié)點,接收一個pong和一個ping垃瞧。我們將響應(yīng)一個pong以使自己成為一個公認已知節(jié)點个从。最后我們就可以發(fā)送fn數(shù)據(jù)包。引導(dǎo)節(jié)點應(yīng)該會以Neighbors響應(yīng)嗦锐。

執(zhí)行python send_ping.py你應(yīng)該可以看到:

$ 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) <echo hash=""> 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) <echo hash=""> 1502819202.34)
sending (Pong (EP 13.93.211.84 30303 30303) <echo hash=""> 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)

引導(dǎo)節(jié)點分兩個數(shù)據(jù)包響應(yīng)了16個鄰居節(jié)點奕污。

下一次,我們將構(gòu)建一個流程來爬取這些鄰居直到我們有足夠的對等端點可以同步區(qū)塊鏈贾陷。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市巷懈,隨后出現(xiàn)的幾起案子慌洪,更是在濱河造成了極大的恐慌,老刑警劉巖涌攻,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件频伤,死亡現(xiàn)場離奇詭異,居然都是意外死亡惠爽,警方通過查閱死者的電腦和手機瞬哼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門坐慰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來用僧,“玉大人,你說我怎么就攤上這事责循。” “怎么了秸抚?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵歹垫,是天一觀的道長。 經(jīng)常有香客問我吭敢,道長暮芭,這世上最難降的妖魔是什么欲低? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任伸头,我火速辦了婚禮舷蟀,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘野宜。我一直安慰自己,他們只是感情好河胎,可當我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布游岳。 她就那樣靜靜地躺著其徙,像睡著了一般。 火紅的嫁衣襯著肌膚如雪唾那。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天期犬,我揣著相機與錄音避诽,去河邊找鬼。 笑死鲤妥,一個胖子當著我的面吹牛旭斥,可吹牛的內(nèi)容都是我干的古涧。 我是一名探鬼主播,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼菇爪,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了熙揍?” 一聲冷哼從身側(cè)響起氏涩,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎意系,沒想到半個月后饺汹,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡迎瞧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年逸吵,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片咏尝。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡啸罢,死狀恐怖胎食,靈堂內(nèi)的尸體忽然破棺而出厕怜,到底是詐尸還是另有隱情衩匣,我是刑警寧澤粥航,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布递雀,位于F島的核電站,受9級特大地震影響缀程,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜滤奈,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一蜒程、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧昭躺,春花似錦、人聲如沸汉规。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽啄枕。三九已至族沃,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間脆淹,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工漓糙, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留烘嘱,地道東北人。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓醉鳖,卻偏偏與公主長得像哮内,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子漾根,可洞房花燭夜當晚...
    茶點故事閱讀 45,512評論 2 359

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