Python從頭實現以太坊(四):查找鄰居節點

Python從頭實現以太坊系列索引:
一、Ping
二、Pinging引導節點
三、解碼引導節點的響應
四、查找鄰居節點
五、類-Kademlia協議
六、Routing

這是我寫的從頭完整實現以太坊協議系列的第四部分(第一部分第二部分第三部分,如果你以前沒看過,建議你從第一部分開始看)。等這個系列教程結束的時候,你就可以爬取以太坊網絡獲取對等端點,同步和驗證區塊鏈,爲以太坊虛擬機編寫智能合約,以及挖以太幣。我們現在正在實現其發現協議部分。一旦完成,我們就可以用一種類似torrent的方式下載區塊鏈。我們上一次完成了Ping引導節點並解碼和驗證其Pong響應。今天我們要實現FindNeighbors請求和Neighbors響應,我們將用它們爬取以太坊網絡。

這一部分不難,我們簡單地爲FindNeighborsNeighbors的數據包定義類結構,並像我們之前發送PingNodePong那樣將它們發送即可。但是,想要成功發送FindNeighbors數據包,還需要滿足一些必備條件。我們並沒有在協議文檔中看到這些條件,是因爲文檔比源代碼舊。go-ethereum源代碼的發現協議採用v4版本。但是RLPx協議(我們的實現)卻只到v3版本。源代碼裏甚至還有一個叫discv5的模塊,表明它們正在實現v5版本,不過,通過檢查引導節點發回的Ping消息的version字段,我們發現它們跑的依然是v4版本。

v4版本的協議要求,爲了獲取FindNeighbors請求的響應,必須先要有一次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數據包(FindNeighbors的Go實現),代碼首先檢查請求來源fromID是否在它的已知節點記錄裏面。如果不在,它就丟棄這個請求(難怪我之前的請求一直出問題,現在弄明白了)。

爲了成爲已知節點,首先我們必須ping引導節點。當引導節點接收到ping,它會先響應一個pong,然後再發一個ping,並等待我們響應一個pong回去。一旦我們響應了pong,我們的nodeID就會進入引導節點的已知節點列表。

因此,爲了能夠發送FindNeighbors數據包,首先,我們需要創建與PingNodePong數據包具有相同功能的FindNeighborsNeighbors類。然後,我們需要在receive_ping中加一個Pong的響應以便跟引導節點UDP握手。接着,我們需要調整PingServer使之能持續監聽數據包。最後,我們需要調整send_ping.py腳本:發送一個ping,留足夠的時間等待引導節點依次響應pongping,之後假設我們正確實現了pong響應的話,就可以發送FindNeighbors數據包並接收Neighbors響應了。

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

創建FindNeighbors和Neighbors類

在此係列前一部分我們爲PingNodePong創建了類,這一節,我們要以同樣的方式爲FindNeighborsNeighbors創建Python類。我們爲每個類都創建__init____str__packunpack方法,併爲PingServer類添加receive_的方法。

對於FindNeighbors規範描述的數據包結構是:

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字節的公鑰。這意味着我們可以在packunpack方法中存儲和提取它。對於__str__,我將使用binascii.b2a_hex把字節打印成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,數據包結構爲:

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

    uint32_t timestamp;
};

這要求我們先定義一個Neighbor類,我將在之後定義並取名爲Node。對於Neighbors,唯一新概念是nodes是一個列表,所以我們將使用map來打包和解包數據:

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是內聯打包,所以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])

對於新建的數據包類,讓我們定義新的PingServer方法來接收數據包,先簡單地定義:

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方法裏面,我們也要調整response_types派發表:

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
}

讓服務器持續監聽

爲了讓服務可以持續監聽數據包,還有幾個事項需要處理:

  • PingServer的功能變得更通用,因此我們將它改名爲Server
  • 我們通過設置self.sock.setblocking(0)讓服務器的套接字不再阻塞。
  • 讓我們把receive方法中#verify hash上面的代碼移到新的listen方法中,並給receive添加一個新的參數data。這個新的listen函數循環以select等待數據包的到達並以receive響應。select函數的作用是在可選的超時時間內等待直至資源可用。
  • 我們把從套接字讀取的字節數增加到2048,因爲一些以太數據包大小超過1024字節長。
  • 我們將udp_listen更改爲listen_thread,並將線程對象返回,我們把線程的daemon字段設置爲True,這意味着即便監聽線程依然在運行,進程也將終止。(之前進程是掛起的)

最終相應的代碼部分是這樣的:

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

響應pings

我們必須修改Server類的receive_ping方法以響應一個Pong。這也要求我們將Serverping方法修改成更通用的函數send。原來ping創建一個PingNode對象併發送,現在變成了send接收一個新的packet自變量,做發送前準備併發送。

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參數。這個參數需放進位於Serverreceive方法裏面的dispatch調用中,以及所有其他receive_開頭的函數。

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)

其他修復

因爲引導節點使用v4版本的RLPx協議。但是規範文檔和我們的實現使用的是v3,我們需要把PingNode unpack方法的packed[0]==cls.version註釋掉。在我可以找到基於新版本的集中文檔之前,我不打算修改類的實際版本號。在前一篇文章裏面,我忘記了把解包的timestamp包含到cls的參數裏面,所以你的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方法中闡釋。如果沒有的話,你要設置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應該改爲self.timestamp。之所以沒發現是因爲我們從未發送過Pong消息:

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

修改send_ping.py

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

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扒一個引導節點的key,創建一個Node對象,作爲我們的第一個聯繫對象。然後我們創建一個服務器,啓動監聽線程,並創建PingNodeFindNeighbors數據包。接着我們按照握手流程,ping引導節點,接收一個pong和一個ping。我們將響應一個pong以使自己成爲一個公認已知節點。最後我們就可以發送fn數據包。引導節點應該會以Neighbors響應。

執行python send_ping.py你應該可以看到:

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

引導節點分兩個數據包響應了16個鄰居節點。

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

參考:https://ocalog.com/post/21/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章