devp2p 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +22 -0
- data/lib/devp2p.rb +57 -0
- data/lib/devp2p/app_helper.rb +85 -0
- data/lib/devp2p/base_app.rb +80 -0
- data/lib/devp2p/base_protocol.rb +136 -0
- data/lib/devp2p/base_service.rb +55 -0
- data/lib/devp2p/command.rb +82 -0
- data/lib/devp2p/configurable.rb +32 -0
- data/lib/devp2p/connection_monitor.rb +77 -0
- data/lib/devp2p/control.rb +32 -0
- data/lib/devp2p/crypto.rb +73 -0
- data/lib/devp2p/crypto/ecc_x.rb +133 -0
- data/lib/devp2p/crypto/ecies.rb +134 -0
- data/lib/devp2p/discovery.rb +118 -0
- data/lib/devp2p/discovery/address.rb +83 -0
- data/lib/devp2p/discovery/kademlia_protocol_adapter.rb +11 -0
- data/lib/devp2p/discovery/node.rb +32 -0
- data/lib/devp2p/discovery/protocol.rb +342 -0
- data/lib/devp2p/discovery/transport.rb +105 -0
- data/lib/devp2p/exception.rb +30 -0
- data/lib/devp2p/frame.rb +197 -0
- data/lib/devp2p/kademlia.rb +48 -0
- data/lib/devp2p/kademlia/k_bucket.rb +178 -0
- data/lib/devp2p/kademlia/node.rb +40 -0
- data/lib/devp2p/kademlia/protocol.rb +284 -0
- data/lib/devp2p/kademlia/routing_table.rb +131 -0
- data/lib/devp2p/kademlia/wire_interface.rb +30 -0
- data/lib/devp2p/multiplexed_session.rb +110 -0
- data/lib/devp2p/multiplexer.rb +358 -0
- data/lib/devp2p/p2p_protocol.rb +170 -0
- data/lib/devp2p/packet.rb +35 -0
- data/lib/devp2p/peer.rb +329 -0
- data/lib/devp2p/peer_errors.rb +35 -0
- data/lib/devp2p/peer_manager.rb +274 -0
- data/lib/devp2p/rlpx_session.rb +434 -0
- data/lib/devp2p/sync_queue.rb +76 -0
- data/lib/devp2p/utils.rb +106 -0
- data/lib/devp2p/version.rb +13 -0
- data/lib/devp2p/wired_service.rb +30 -0
- metadata +227 -0
@@ -0,0 +1,118 @@
|
|
1
|
+
##
|
2
|
+
# # Node Discovery Protocol
|
3
|
+
#
|
4
|
+
# * [Node] - an entity on the network
|
5
|
+
# * [Node] ID - 512 bit public key of node
|
6
|
+
#
|
7
|
+
# The Node Discovery protocol provides a way to find RLPx nodes that can be
|
8
|
+
# connected to. It uses a Kademlia-like protocol to maintain a distributed
|
9
|
+
# database of the IDs and endpoints of all listening nodes.
|
10
|
+
#
|
11
|
+
# Each node keeps a node table as described in the Kademlia paper (Maymounkov,
|
12
|
+
# Mazières 2002). The node table is configured with a bucket size of 16
|
13
|
+
# (denoted `k` in Kademlia), concurrency of 3 (denoted `α` in Kademlia), and 8
|
14
|
+
# bits per hop (denoted `b` in Kademlia) for routing. The eviction check
|
15
|
+
# interval is 75 milliseconds, and the idle bucket-refresh interval is 3600
|
16
|
+
# seconds.
|
17
|
+
#
|
18
|
+
# In order to maintain a well-formed network, RLPx nodes should try to connect
|
19
|
+
# to an unspecified number of close nodes. To increase resilience against Sybil
|
20
|
+
# attacks, nodes should also connect to randomly chosen, non-close nodes.
|
21
|
+
#
|
22
|
+
# Each node runs the UDP-based RPC protocol defined below. The `FIND_DATA` and
|
23
|
+
# `STORE` requests from the Kademlia paper are not part of the protocol since
|
24
|
+
# the Node Discovery Protocol does not provide DHT functionality.
|
25
|
+
#
|
26
|
+
# ## Joining the network
|
27
|
+
#
|
28
|
+
# When joining the network, fills its node table by performing a recursive Find
|
29
|
+
# Node operation with its own ID as the 'Target'. The initial Find Node request
|
30
|
+
# is sent to one or more bootstrap nodes.
|
31
|
+
#
|
32
|
+
# ## RPC Protocol
|
33
|
+
#
|
34
|
+
# RLPx nodes that want to accept incoming connections should listen on the same
|
35
|
+
# port number for UDP packets (Node Discovery Protocol) and TCP connections
|
36
|
+
# (RLPx protocol).
|
37
|
+
#
|
38
|
+
# All requests time out after 300ms. Requests are not re-sent.
|
39
|
+
#
|
40
|
+
# ## Packet Data
|
41
|
+
#
|
42
|
+
# All packets contain an `Expiration` date to guard against replay attacks. The
|
43
|
+
# date should be interpreted as a UNIX timestamp. The receiver should discard
|
44
|
+
# any packet whose `Expiration` value is in the past.
|
45
|
+
#
|
46
|
+
# ### Ping (type 0x01)
|
47
|
+
#
|
48
|
+
# Ping packets can be sent and received at any time. The receiver should reply
|
49
|
+
# with a Pong packet and update the IP/Port of the sender in its node table.
|
50
|
+
#
|
51
|
+
# PingNode packet-type: 0x01
|
52
|
+
#
|
53
|
+
# struct PingNode <= 59 bytes
|
54
|
+
# {
|
55
|
+
# h256 version = 0x3; <= 1
|
56
|
+
# Endpoint from; <= 23
|
57
|
+
# Endpoint to; <= 23
|
58
|
+
# unsigned expiration; <= 9
|
59
|
+
# }
|
60
|
+
#
|
61
|
+
# struct Endpoint <= 24 = [17,3,3]
|
62
|
+
# {
|
63
|
+
# unsigned address; // BE encoded 32-bit or 128-bit unsigned (layer3 address; size determins ipv4 vs ipv6)
|
64
|
+
# unsigned udpPort; // BE encoded 16-bit unsigned
|
65
|
+
# unsigned tcpPort; // BE encoded 16-bit unsigned
|
66
|
+
# }
|
67
|
+
#
|
68
|
+
# ### Pong (type 0x02)
|
69
|
+
#
|
70
|
+
# Pong is the reply to a Ping packet.
|
71
|
+
#
|
72
|
+
# Pong packet-type: 0x02
|
73
|
+
#
|
74
|
+
# struct Pong <= 66 bytes
|
75
|
+
# {
|
76
|
+
# Endpoint to;
|
77
|
+
# h256 echo;
|
78
|
+
# unsigned expiration;
|
79
|
+
# }
|
80
|
+
#
|
81
|
+
# ### Find Node (type 0x03)
|
82
|
+
#
|
83
|
+
# Find Node packets are sent to locate nodes close to a given target ID. The
|
84
|
+
# receiver should reply with a Neighbours packet containing the `k` nodes
|
85
|
+
# closest to target that it knows about.
|
86
|
+
#
|
87
|
+
# FindNode packet-type: 0x03
|
88
|
+
#
|
89
|
+
# struct FindNode <= 76 bytes
|
90
|
+
# {
|
91
|
+
# NodeId target; // Id of a node. The responding node will send back nodes closest to the target.
|
92
|
+
# unsigned expiration;
|
93
|
+
# }
|
94
|
+
#
|
95
|
+
# ### Neighbours (type 0x04)
|
96
|
+
#
|
97
|
+
# Neighbours is the reply to Find Node. It contains up to `k` nodes that the
|
98
|
+
# sender knows which are closest to the requested 'Target`.
|
99
|
+
#
|
100
|
+
# Neighbours packet-type: 0x04
|
101
|
+
#
|
102
|
+
# struct Neighbours <= 1423
|
103
|
+
# {
|
104
|
+
# list nodes: struct Neighbours <= 88...1411; 76...1219
|
105
|
+
# {
|
106
|
+
# inline Endpoint endpoint;
|
107
|
+
# NodeId node;
|
108
|
+
# };
|
109
|
+
# unsigned expiration;
|
110
|
+
# }
|
111
|
+
#
|
112
|
+
|
113
|
+
|
114
|
+
require 'devp2p/discovery/address'
|
115
|
+
require 'devp2p/discovery/node'
|
116
|
+
require 'devp2p/discovery/kademlia_protocol_adapter'
|
117
|
+
require 'devp2p/discovery/protocol'
|
118
|
+
require 'devp2p/discovery/transport'
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# -*- encoding : ascii-8bit -*-
|
2
|
+
|
3
|
+
require 'ipaddr'
|
4
|
+
require 'resolv'
|
5
|
+
|
6
|
+
module DEVp2p
|
7
|
+
module Discovery
|
8
|
+
|
9
|
+
class Address
|
10
|
+
|
11
|
+
attr :udp_port, :tcp_port
|
12
|
+
|
13
|
+
def self.from_endpoint(ip, udp_port, tcp_port="\x00\x00")
|
14
|
+
new(ip, udp_port, tcp_port, true)
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(ip, udp_port, tcp_port=nil, from_binary=false)
|
18
|
+
tcp_port ||= udp_port
|
19
|
+
|
20
|
+
if from_binary
|
21
|
+
raise ArgumentError, "invalid ip" unless [4,16].include?(ip.size)
|
22
|
+
|
23
|
+
@udp_port = dec_port udp_port
|
24
|
+
@tcp_port = dec_port tcp_port
|
25
|
+
else
|
26
|
+
raise ArgumentError, "udp_port must be Integer" unless udp_port.is_a?(Integer)
|
27
|
+
raise ArgumentError, "tcp_port must be Integer" unless tcp_port.is_a?(Integer)
|
28
|
+
|
29
|
+
@udp_port = udp_port
|
30
|
+
@tcp_port = tcp_port
|
31
|
+
end
|
32
|
+
|
33
|
+
begin
|
34
|
+
@ip = from_binary ? IPAddr.new_ntoh(ip) : IPAddr.new(ip)
|
35
|
+
rescue IPAddr::InvalidAddressError => e
|
36
|
+
ips = Resolv.getaddresses(ip).sort {|addr| addr =~ /:/ ? 1 : 0 } # use ipv4 first
|
37
|
+
raise e if ips.empty?
|
38
|
+
@ip = ips[0]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def ip
|
43
|
+
@ip.to_s
|
44
|
+
end
|
45
|
+
|
46
|
+
def update(addr)
|
47
|
+
@tcp_port = addr.tcp_port if @tcp_port.nil? || @tcp_port == 0
|
48
|
+
end
|
49
|
+
|
50
|
+
##
|
51
|
+
# addresses equal if they share ip and udp_port
|
52
|
+
#
|
53
|
+
def ==(other)
|
54
|
+
[ip, udp_port] == [other.ip, other.udp_port]
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_s
|
58
|
+
"Address(#{ip}:#{udp_port})"
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_h
|
62
|
+
{ip: ip, udp_port: udp_port, tcp_port: tcp_port}
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_b
|
66
|
+
[@ip.hton, enc_port(udp_port), enc_port(tcp_port)]
|
67
|
+
end
|
68
|
+
alias to_endpoint to_b
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def enc_port(p)
|
73
|
+
Utils.int_to_big_endian4(p)[-2..-1]
|
74
|
+
end
|
75
|
+
|
76
|
+
def dec_port(b)
|
77
|
+
Utils.big_endian_to_int(b)
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# -*- encoding : ascii-8bit -*-
|
2
|
+
|
3
|
+
module DEVp2p
|
4
|
+
module Discovery
|
5
|
+
|
6
|
+
class Node < Kademlia::Node
|
7
|
+
|
8
|
+
def self.from_uri(uri)
|
9
|
+
ip, port, pubkey = Utils.host_port_pubkey_from_uri(uri)
|
10
|
+
new(pubkey, Address.new(ip, port.to_i))
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_accessor :address
|
14
|
+
|
15
|
+
def initialize(pubkey, address=nil)
|
16
|
+
raise ArgumentError, 'invalid address' unless address.nil? || address.is_a?(Address)
|
17
|
+
|
18
|
+
super(pubkey)
|
19
|
+
|
20
|
+
self.address = address
|
21
|
+
@reputation = 0
|
22
|
+
@rlpx_version = 0
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_uri
|
26
|
+
Utils.host_port_pubkey_to_uri(address.ip, address.udp_port, pubkey)
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,342 @@
|
|
1
|
+
# -*- encoding : ascii-8bit -*-
|
2
|
+
|
3
|
+
module DEVp2p
|
4
|
+
module Discovery
|
5
|
+
|
6
|
+
class Protocol < Kademlia::WireInterface
|
7
|
+
|
8
|
+
VERSION = 4
|
9
|
+
|
10
|
+
EXPIRATION = 60 # let messages expire after N seconds
|
11
|
+
|
12
|
+
CMD_ID_MAP = {
|
13
|
+
ping: 1,
|
14
|
+
pong: 2,
|
15
|
+
find_node: 3,
|
16
|
+
neighbours: 4
|
17
|
+
}.freeze
|
18
|
+
REV_CMD_ID_MAP = CMD_ID_MAP.map {|k,v| [v,k] }.to_h.freeze
|
19
|
+
|
20
|
+
# number of required top-level list elements for each cmd_id.
|
21
|
+
# elements beyond this length are trimmed.
|
22
|
+
CMD_ELEM_COUNT_MAP = {
|
23
|
+
ping: 4,
|
24
|
+
poing: 3,
|
25
|
+
find_node: 2,
|
26
|
+
neighbours: 2
|
27
|
+
}
|
28
|
+
|
29
|
+
attr :pubkey, :kademlia
|
30
|
+
|
31
|
+
def initialize(app, transport)
|
32
|
+
@app = app
|
33
|
+
@transport = transport
|
34
|
+
|
35
|
+
@privkey = Utils.decode_hex app.config[:node][:privkey_hex]
|
36
|
+
@pubkey = Crypto.privtopub @privkey
|
37
|
+
|
38
|
+
@nodes = {} # nodeid => Node
|
39
|
+
@node = Node.new(pubkey, @transport.address)
|
40
|
+
|
41
|
+
@kademlia = KademliaProtocolAdapter.new @node, self
|
42
|
+
|
43
|
+
uri = Utils.host_port_pubkey_to_uri(ip, udp_port, pubkey)
|
44
|
+
logger.info "starting discovery proto", enode: uri
|
45
|
+
end
|
46
|
+
|
47
|
+
def bootstrap(nodes)
|
48
|
+
@kademlia.bootstrap(nodes) unless nodes.empty?
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# return node or create new, update address if supplied
|
53
|
+
#
|
54
|
+
def get_node(nodeid, address=nil)
|
55
|
+
raise ArgumentError, 'invalid nodeid' unless nodeid.size == Kademlia::PUBKEY_SIZE / 8
|
56
|
+
raise ArgumentError, 'must give either address or existing nodeid' unless address || @nodes.has_key?(nodeid)
|
57
|
+
|
58
|
+
@nodes[nodeid] = Node.new nodeid, address if !@nodes.has_key?(nodeid)
|
59
|
+
node = @nodes[nodeid]
|
60
|
+
|
61
|
+
if address
|
62
|
+
raise ArgumentError, 'address must be Address' unless address.instance_of?(Address)
|
63
|
+
node.address = address
|
64
|
+
end
|
65
|
+
|
66
|
+
node
|
67
|
+
end
|
68
|
+
|
69
|
+
def sign(msg)
|
70
|
+
msg = Crypto.keccak256 msg
|
71
|
+
Crypto.ecdsa_sign msg, @privkey
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
# UDP packets are structured as follows:
|
76
|
+
#
|
77
|
+
# hash || signature || packet-type || packet-data
|
78
|
+
#
|
79
|
+
# * packet-type: single byte < 2**7 // valid values are [1,4]
|
80
|
+
# * packet-data: RLP encoded list. Packet properties are serialized in
|
81
|
+
# the order in which they're defined. See packet-data below.
|
82
|
+
#
|
83
|
+
# Offset |
|
84
|
+
# 0 | MDC | Ensures integrity of packet.
|
85
|
+
# 65 | signature | Ensures authenticity of sender, `SIGN(sender-privkey, MDC)`
|
86
|
+
# 97 | type | Single byte in range [1, 4] that determines the structure of Data
|
87
|
+
# 98 | data | RLP encoded, see section Packet Data
|
88
|
+
#
|
89
|
+
# The packets are signed and authenticated. The sender's Node ID is
|
90
|
+
# determined by recovering the public key from the signature.
|
91
|
+
#
|
92
|
+
# sender-pubkey = ECRECOVER(Signature)
|
93
|
+
#
|
94
|
+
# The integrity of the packet can then be verified by computing the
|
95
|
+
# expected MDC of the packet as:
|
96
|
+
#
|
97
|
+
# MDC = keccak256(sender-pubkey || type || data)
|
98
|
+
#
|
99
|
+
# As an optimization, implementations may look up the public key by the
|
100
|
+
# UDP sending address and compute MDC before recovering the sender ID. If
|
101
|
+
# the MDC values do not match, the packet can be dropped.
|
102
|
+
#
|
103
|
+
def pack(cmd_id, payload)
|
104
|
+
raise ArgumentError, 'invalid cmd_id' unless REV_CMD_ID_MAP.has_key?(cmd_id)
|
105
|
+
raise ArgumentError, 'payload must be Array' unless payload.is_a?(Array)
|
106
|
+
|
107
|
+
cmd_id = encode_cmd_id cmd_id
|
108
|
+
expiration = encode_expiration Time.now.to_i + EXPIRATION
|
109
|
+
|
110
|
+
encoded_data = RLP.encode(payload + [expiration])
|
111
|
+
signed_data = Crypto.keccak256 "#{cmd_id}#{encoded_data}"
|
112
|
+
signature = Crypto.ecdsa_sign signed_data, @privkey
|
113
|
+
|
114
|
+
raise InvalidSignatureError unless signature.size == 65
|
115
|
+
|
116
|
+
mdc = Crypto.keccak256 "#{signature}#{cmd_id}#{encoded_data}"
|
117
|
+
raise InvalidMACError unless mdc.size == 32
|
118
|
+
|
119
|
+
"#{mdc}#{signature}#{cmd_id}#{encoded_data}"
|
120
|
+
end
|
121
|
+
|
122
|
+
##
|
123
|
+
# macSize = 256 / 8 = 32
|
124
|
+
# sigSize = 520 / 8 = 65
|
125
|
+
# headSize = macSize + sigSize = 97
|
126
|
+
#
|
127
|
+
def unpack(message)
|
128
|
+
mdc = message[0,32]
|
129
|
+
if mdc != Crypto.keccak256(message[32..-1])
|
130
|
+
logger.warn 'packet with wrong mcd'
|
131
|
+
raise InvalidMessageMAC
|
132
|
+
end
|
133
|
+
|
134
|
+
signature = message[32,65]
|
135
|
+
raise InvalidSignatureError unless signature.size == 65
|
136
|
+
|
137
|
+
signed_data = Crypto.keccak256(message[97..-1])
|
138
|
+
remote_pubkey = Crypto.ecdsa_recover(signed_data, signature)
|
139
|
+
raise InvalidKeyError unless remote_pubkey.size == Kademlia::PUBKEY_SIZE / 8
|
140
|
+
|
141
|
+
cmd_id = decode_cmd_id message[97]
|
142
|
+
cmd = REV_CMD_ID_MAP[cmd_id]
|
143
|
+
|
144
|
+
payload = RLP.decode message[98..-1], strict: false
|
145
|
+
raise InvalidPayloadError unless payload.instance_of?(Array)
|
146
|
+
|
147
|
+
# ignore excessive list elements as required by EIP-8
|
148
|
+
payload = payload[0, CMD_ELEM_COUNT_MAP[cmd]||payload.size]
|
149
|
+
|
150
|
+
return remote_pubkey, cmd_id, payload, mdc
|
151
|
+
end
|
152
|
+
|
153
|
+
def receive_message(address, message)
|
154
|
+
logger.debug "<<< message", address: address
|
155
|
+
raise ArgumentError, 'address must be Address' unless address.instance_of?(Address)
|
156
|
+
|
157
|
+
begin
|
158
|
+
remote_pubkey, cmd_id, payload, mdc = unpack message
|
159
|
+
|
160
|
+
# Note: as of discovery version 4, expiration is the last element for
|
161
|
+
# all packets. This might not be the case for a later version, but
|
162
|
+
# just popping the last element is good enough for now.
|
163
|
+
expiration = decode_expiration payload.pop
|
164
|
+
raise PacketExpired if Time.now.to_i > expiration
|
165
|
+
rescue DefectiveMessage
|
166
|
+
logger.debug $!
|
167
|
+
return
|
168
|
+
end
|
169
|
+
|
170
|
+
cmd = "recv_#{REV_CMD_ID_MAP[cmd_id]}"
|
171
|
+
nodeid = remote_pubkey
|
172
|
+
|
173
|
+
get_node(nodeid, address) unless @nodes.has_key?(nodeid)
|
174
|
+
send cmd, nodeid, payload, mdc
|
175
|
+
end
|
176
|
+
|
177
|
+
def send_message(node, message)
|
178
|
+
raise ArgumentError, 'node must have address' unless node.address
|
179
|
+
logger.debug ">>> message", address: node.address
|
180
|
+
@transport.send_message node.address, message
|
181
|
+
end
|
182
|
+
|
183
|
+
def send_ping(node)
|
184
|
+
raise ArgumentError, "node must be Node" unless node.is_a?(Node)
|
185
|
+
raise ArgumentError, "cannot ping self" if node == @node
|
186
|
+
|
187
|
+
logger.debug ">>> ping", remoteid: node
|
188
|
+
|
189
|
+
version = RLP::Sedes.big_endian_int.serialize VERSION
|
190
|
+
payload = [
|
191
|
+
version,
|
192
|
+
Address.new(ip, udp_port, tcp_port).to_endpoint,
|
193
|
+
node.address.to_endpoint
|
194
|
+
]
|
195
|
+
|
196
|
+
message = pack CMD_ID_MAP[:ping], payload
|
197
|
+
send_message node, message
|
198
|
+
|
199
|
+
message[0,32] # return the MDC to identify pongs
|
200
|
+
end
|
201
|
+
|
202
|
+
##
|
203
|
+
# Update ip, port in node table. Addresses can only be learned by ping
|
204
|
+
# messages.
|
205
|
+
#
|
206
|
+
def recv_ping(nodeid, payload, mdc)
|
207
|
+
if payload.size != 3
|
208
|
+
logger.error "invalid ping payload", payload: payload
|
209
|
+
return
|
210
|
+
end
|
211
|
+
|
212
|
+
node = get_node nodeid
|
213
|
+
logger.debug "<<< ping", node: node
|
214
|
+
|
215
|
+
remote_address = Address.from_endpoint(*payload[1]) # from
|
216
|
+
my_address = Address.from_endpoint(*payload[2]) # my address
|
217
|
+
|
218
|
+
get_node(nodeid).address.update remote_address
|
219
|
+
@kademlia.recv_ping node, mdc
|
220
|
+
end
|
221
|
+
|
222
|
+
def send_pong(node, token)
|
223
|
+
logger.debug ">>> pong", remoteid: node
|
224
|
+
|
225
|
+
payload = [node.address.to_endpoint, token]
|
226
|
+
raise InvalidPayloadError unless [4,16].include?(payload[0][0].size)
|
227
|
+
|
228
|
+
message = pack CMD_ID_MAP[:pong], payload
|
229
|
+
send_message node, message
|
230
|
+
end
|
231
|
+
|
232
|
+
def recv_pong(nodeid, payload, mdc)
|
233
|
+
if payload.size != 2
|
234
|
+
logger.error 'invalid pong payload', payload: payload
|
235
|
+
return
|
236
|
+
end
|
237
|
+
|
238
|
+
raise InvalidPayloadError unless payload[0].size == 3
|
239
|
+
raise InvalidPayloadError unless [4,16].include?(payload[0][0].size)
|
240
|
+
|
241
|
+
my_address = Address.from_endpoint *payload[0]
|
242
|
+
echoed = payload[1]
|
243
|
+
|
244
|
+
if @nodes.include?(nodeid)
|
245
|
+
node = get_node nodeid
|
246
|
+
@kademlia.recv_pong node, echoed
|
247
|
+
else
|
248
|
+
logger.debug "<<< unexpected pong from unknown node"
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
def send_find_node(node, target_node_id)
|
253
|
+
target_node_id = Utils.zpad_int target_node_id, Kademlia::PUBKEY_SIZE/8
|
254
|
+
logger.debug ">>> find_node", remoteid: node
|
255
|
+
|
256
|
+
message = pack CMD_ID_MAP[:find_node], [target_node_id]
|
257
|
+
send_message node, message
|
258
|
+
end
|
259
|
+
|
260
|
+
def recv_find_node(nodeid, payload, mdc)
|
261
|
+
node = get_node nodeid
|
262
|
+
|
263
|
+
logger.debug "<<< find_node", remoteid: node
|
264
|
+
raise InvalidPayloadError unless payload[0].size == Kademlia::PUBKEY_SIZE/8
|
265
|
+
|
266
|
+
target = Utils.big_endian_to_int payload[0]
|
267
|
+
@kademlia.recv_find_node node, target
|
268
|
+
end
|
269
|
+
|
270
|
+
def send_neighbours(node, neighbours)
|
271
|
+
raise ArgumentError, 'neighbours must be Array' unless neighbours.instance_of?(Array)
|
272
|
+
raise ArgumentError, 'neighbours must be Node' unless neighbours.all? {|n| n.is_a?(Node) }
|
273
|
+
|
274
|
+
nodes = neighbours.map {|n| n.address.to_endpoint + [n.pubkey] }
|
275
|
+
logger.debug ">>> neighbours", remoteid: node, count: nodes.size
|
276
|
+
|
277
|
+
message = pack CMD_ID_MAP[:neighbours], [nodes]
|
278
|
+
send_message node, message
|
279
|
+
end
|
280
|
+
|
281
|
+
def recv_neighbours(nodeid, payload, mdc)
|
282
|
+
node = get_node nodeid
|
283
|
+
raise InvalidPayloadError unless payload.size == 1
|
284
|
+
raise InvalidPayloadError unless payload[0].instance_of?(Array)
|
285
|
+
logger.debug "<<< neighbours", remoteid: node, count: payload[0].size
|
286
|
+
|
287
|
+
neighbours_set = payload[0].uniq
|
288
|
+
logger.warn "received duplicates" if neighbours_set.size < payload[0].size
|
289
|
+
|
290
|
+
neighbours = neighbours_set.map do |n|
|
291
|
+
if n.size != 4 || ![4,16].include?(n[0].size)
|
292
|
+
logger.error "invalid neighbours format", neighbours: n
|
293
|
+
return
|
294
|
+
end
|
295
|
+
|
296
|
+
n = n.dup
|
297
|
+
nodeid = n.pop
|
298
|
+
address = Address.from_endpoint *n
|
299
|
+
get_node nodeid, address
|
300
|
+
end
|
301
|
+
|
302
|
+
@kademlia.recv_neighbours node, neighbours
|
303
|
+
end
|
304
|
+
|
305
|
+
def ip
|
306
|
+
@app.config[:discovery][:listen_host]
|
307
|
+
end
|
308
|
+
|
309
|
+
def udp_port
|
310
|
+
@app.config[:discovery][:listen_port]
|
311
|
+
end
|
312
|
+
|
313
|
+
def tcp_port
|
314
|
+
@app.config[:p2p][:listen_port]
|
315
|
+
end
|
316
|
+
|
317
|
+
private
|
318
|
+
|
319
|
+
def logger
|
320
|
+
@logger ||= Logger.new("#{udp_port}.p2p.discovery").tap {|l| l.level = :info }
|
321
|
+
end
|
322
|
+
|
323
|
+
def encode_cmd_id(cmd_id)
|
324
|
+
cmd_id.chr
|
325
|
+
end
|
326
|
+
|
327
|
+
def decode_cmd_id(byte)
|
328
|
+
byte.ord
|
329
|
+
end
|
330
|
+
|
331
|
+
def encode_expiration(i)
|
332
|
+
RLP::Sedes.big_endian_int.serialize(i)
|
333
|
+
end
|
334
|
+
|
335
|
+
def decode_expiration(b)
|
336
|
+
RLP::Sedes.big_endian_int.deserialize(b)
|
337
|
+
end
|
338
|
+
|
339
|
+
end
|
340
|
+
|
341
|
+
end
|
342
|
+
end
|