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ò)竟秫。
這一部分不難,我們簡單地為FindNeighbors
和Neighbors
的數(shù)據(jù)包定義類結(jié)構(gòu)跷乐,并像我們之前發(fā)送PingNode
和Pong
那樣將它們發(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)建與PingNode
和Pong
數(shù)據(jù)包具有相同功能的FindNeighbors
和Neighbors
類谦秧。然后竟纳,我們需要在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)pong
和ping
揩悄,之后假設(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é),我們要以同樣的方式為FindNeighbors
和Neighbors
創(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é)的公鑰垃沦。這意味著我們可以在pack
和unpack
方法中存儲和提取它。對于__str__
用押,我將使用binascii.b2a_hex
把字節(jié)打印成16進制格式肢簿。除此以外,其他代碼跟我們在PingNode
和Pong
所見到的相似蜻拨。所以池充,我們在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))
在PingServer
的receive
方法里面,我們也要調(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
猜敢。這也要求我們將Server
的ping
方法修改成更通用的函數(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ù)需放進位于Server
的receive
方法里面的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)
對之前版本代碼的最后一個修改是扎筒,Pong
的pack
方法有一個拼寫錯誤嗜桌,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)建PingNode
和FindNeighbors
數(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ū)塊鏈贾陷。