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,40 @@
|
|
1
|
+
# -*- encoding : ascii-8bit -*-
|
2
|
+
|
3
|
+
module DEVp2p
|
4
|
+
module Kademlia
|
5
|
+
|
6
|
+
class Node
|
7
|
+
|
8
|
+
attr :id, :pubkey
|
9
|
+
attr_accessor :address
|
10
|
+
|
11
|
+
def initialize(pubkey)
|
12
|
+
raise ArgumentError, "invalid pubkey" unless pubkey.size == 64
|
13
|
+
|
14
|
+
@id = Crypto.keccak256(pubkey)
|
15
|
+
raise "invalid node id" unless @id.size * 8 == ID_SIZE
|
16
|
+
|
17
|
+
@id = Utils.big_endian_to_int @id
|
18
|
+
@pubkey = pubkey
|
19
|
+
end
|
20
|
+
|
21
|
+
def distance(other)
|
22
|
+
id ^ other.id
|
23
|
+
end
|
24
|
+
|
25
|
+
def id_distance(_id)
|
26
|
+
id ^ _id
|
27
|
+
end
|
28
|
+
|
29
|
+
def ==(other)
|
30
|
+
other.instance_of?(self.class) && pubkey == other.pubkey
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_s
|
34
|
+
"<Node(#{Utils.encode_hex pubkey[0,8]})>"
|
35
|
+
end
|
36
|
+
alias inspect to_s
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,284 @@
|
|
1
|
+
# -*- encoding : ascii-8bit -*-
|
2
|
+
|
3
|
+
module DEVp2p
|
4
|
+
module Kademlia
|
5
|
+
|
6
|
+
class Protocol
|
7
|
+
|
8
|
+
attr :node, :wire, :routing
|
9
|
+
|
10
|
+
def initialize(node, wire)
|
11
|
+
raise ArgumentError, 'node must be Node' unless node.is_a?(Node)
|
12
|
+
raise ArgumentError, 'wire must be WireInterface' unless wire.is_a?(WireInterface)
|
13
|
+
|
14
|
+
@node = node
|
15
|
+
@wire = wire
|
16
|
+
|
17
|
+
@routing = RoutingTable.new node
|
18
|
+
|
19
|
+
@expected_pongs = {} # pingid => [timeout, node, replacement_node]
|
20
|
+
@find_requests = {} # nodeid => timeout
|
21
|
+
@deleted_pingids = {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def bootstrap(nodes)
|
25
|
+
nodes.each do |node|
|
26
|
+
next if node == @node
|
27
|
+
|
28
|
+
@routing.add node
|
29
|
+
find_node @node.id, node # add self to boot node's routing table
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# When a Kademlia node receives any message (request or reply) from
|
35
|
+
# another node, it updates the appropriate k-bucket for the sender's node
|
36
|
+
# ID.
|
37
|
+
#
|
38
|
+
# If the sending node already exists in the recipient's k-bucket, the
|
39
|
+
# recipient moves it to the tail of the list.
|
40
|
+
#
|
41
|
+
# If the node is not already in the appropriate k-bucket and the bucket
|
42
|
+
# has fewer than k entries, then the recipient just inserts the new
|
43
|
+
# sender at the tail of the list.
|
44
|
+
#
|
45
|
+
# If the appropriate k-bucket is full, however, then the recipient pings
|
46
|
+
# the k-bucket's least-recently seen node to decide what to do.
|
47
|
+
#
|
48
|
+
# If the least-recently seen node fails to respond, it is evicted from
|
49
|
+
# the k-bucket and the new sender inserted at the tail.
|
50
|
+
#
|
51
|
+
# Otherwise, if the least-recently seen node responds, it is moved to the
|
52
|
+
# tail of the list, and the new sender's contact is discarded.
|
53
|
+
#
|
54
|
+
# k-buckets effectively implement a least-recently seen eviction policy,
|
55
|
+
# except the live nodes are never removed from the list.
|
56
|
+
#
|
57
|
+
def update(node, pingid=nil)
|
58
|
+
raise ArgumentError, 'node must be Node' unless node.is_a?(Node)
|
59
|
+
|
60
|
+
if node == @node
|
61
|
+
logger.debug 'node is self', remoteid: node
|
62
|
+
return
|
63
|
+
end
|
64
|
+
|
65
|
+
if pingid && !@expected_pongs.has_key?(pingid)
|
66
|
+
pong_nodes = @expected_pongs.values.map {|v| v[1] }.uniq
|
67
|
+
logger.debug "surprising pong", remoteid: node, expected: pong_nodes, pingid: Utils.encode_hex(pingid)[0,8]
|
68
|
+
|
69
|
+
if @deleted_pingids.has_key?(pingid)
|
70
|
+
logger.debug "surprising pong was deleted"
|
71
|
+
else
|
72
|
+
@expected_pongs.each_key do |key|
|
73
|
+
if key.end_with?(node.pubkey)
|
74
|
+
logger.debug "waiting for ping from node, but echo mismatch", node: node, expected_echo: Utils.encode_hex(key[0,8]), received_echo: Utils.encode_hex(pingid[0,8])
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
return
|
80
|
+
end
|
81
|
+
|
82
|
+
# check for timed out pings and eventually evict them
|
83
|
+
@expected_pongs.each do |_pingid, (timeout, _node, replacement)|
|
84
|
+
if Time.now > timeout
|
85
|
+
logger.debug "deleting timeout node", remoteid: _node, pingid: Utils.encode_hex(_pingid)[0,8]
|
86
|
+
|
87
|
+
@deleted_pingids[_pingid] = true
|
88
|
+
@expected_pongs.delete _pingid
|
89
|
+
|
90
|
+
@routing.delete _node
|
91
|
+
|
92
|
+
if replacement
|
93
|
+
logger.debug "adding replacement", remoteid: replacement
|
94
|
+
update replacement
|
95
|
+
return
|
96
|
+
end
|
97
|
+
|
98
|
+
# prevent node from being added later
|
99
|
+
return if _node == node
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# if we had registered this node for eviction test
|
104
|
+
if @expected_pongs.has_key?(pingid)
|
105
|
+
timeout, _node, replacement = @expected_pongs[pingid]
|
106
|
+
logger.debug "received expected pong", remoteid: node
|
107
|
+
|
108
|
+
if replacement
|
109
|
+
logger.debug "adding replacement to cache", remoteid: replacement
|
110
|
+
@routing.bucket_by_node(replacement).add_replacement(replacement)
|
111
|
+
end
|
112
|
+
|
113
|
+
@expected_pongs.delete pingid
|
114
|
+
end
|
115
|
+
|
116
|
+
# add node
|
117
|
+
eviction_candidate = @routing.add node
|
118
|
+
if eviction_candidate
|
119
|
+
logger.debug "could not add", remoteid: node, pinging: eviction_candidate
|
120
|
+
ping eviction_candidate, node
|
121
|
+
else
|
122
|
+
logger.debug "added", remoteid: node
|
123
|
+
end
|
124
|
+
|
125
|
+
# check idle buckets
|
126
|
+
# idle bucket refresh:
|
127
|
+
# for each bucket which hasn't been touched in 3600 seconds
|
128
|
+
# pick a random value in the range of the bucket and perform
|
129
|
+
# discovery for that value
|
130
|
+
@routing.idle_buckets.each do |bucket|
|
131
|
+
rid = SecureRandom.random_number bucket.left, bucket.right+1
|
132
|
+
find_node rid
|
133
|
+
end
|
134
|
+
|
135
|
+
# check and removed timeout find requests
|
136
|
+
@find_requests.keys.each do |nodeid|
|
137
|
+
timeout = @find_requests[nodeid]
|
138
|
+
@find_requests.delete(nodeid) if Time.now > timeout
|
139
|
+
end
|
140
|
+
|
141
|
+
logger.debug "updated", num_nodes: @routing.size, num_buckets: @routing.buckets_count
|
142
|
+
end
|
143
|
+
|
144
|
+
# FIXME: amplification attack (need to ping pong ping pong first)
|
145
|
+
def find_node(targetid, via_node=nil)
|
146
|
+
raise ArgumentError, 'targetid must be Integer' unless targetid.is_a?(Integer)
|
147
|
+
raise ArgumentError, 'via_node must be nil or Node' unless via_node.nil? || via_node.is_a?(Node)
|
148
|
+
|
149
|
+
@find_requests[targetid] = Time.now + REQUEST_TIMEOUT
|
150
|
+
|
151
|
+
if via_node
|
152
|
+
@wire.send_find_node via_node, targetid
|
153
|
+
else
|
154
|
+
query_neighbours targetid
|
155
|
+
end
|
156
|
+
|
157
|
+
# FIXME: should we return the closest node (allow callbacks on find_request)
|
158
|
+
end
|
159
|
+
|
160
|
+
##
|
161
|
+
# successful pings should lead to an update
|
162
|
+
# if bucket is not full
|
163
|
+
# elsif least recently seen, does ont respond in time
|
164
|
+
#
|
165
|
+
def ping(node, replacement=nil)
|
166
|
+
raise ArgumentError, 'node must be Node' unless node.is_a?(Node)
|
167
|
+
raise ArgumentError, 'cannot ping self' if node == @node
|
168
|
+
logger.debug "pinging", remote: node, local: @node
|
169
|
+
|
170
|
+
echoed = @wire.send_ping node
|
171
|
+
pingid = mkpingid echoed, node
|
172
|
+
timeout = Time.now + REQUEST_TIMEOUT
|
173
|
+
logger.debug "set wait for pong from", remote: node, local: @node, pingid: Utils.encode_hex(pingid)[0,8]
|
174
|
+
|
175
|
+
@expected_pongs[pingid] = [timeout, node, replacement]
|
176
|
+
end
|
177
|
+
|
178
|
+
##
|
179
|
+
# udp addresses determined by socket address of received Ping packets # ok
|
180
|
+
# tcp addresses determined by contents of Ping packet # not yet
|
181
|
+
def recv_ping(remote, echo)
|
182
|
+
raise ArgumentError, 'remote must be Node' unless remote.is_a?(Node)
|
183
|
+
logger.debug "recv ping", remote: remote, local: @node
|
184
|
+
|
185
|
+
if remote == @node
|
186
|
+
logger.warn "recv ping from self?!"
|
187
|
+
return
|
188
|
+
end
|
189
|
+
|
190
|
+
update remote
|
191
|
+
@wire.send_pong remote, echo
|
192
|
+
end
|
193
|
+
|
194
|
+
##
|
195
|
+
# tcp addresses are only updated upon receipt of Pong packet
|
196
|
+
#
|
197
|
+
def recv_pong(remote, echoed)
|
198
|
+
raise ArgumentError, 'remote must be Node' unless remote.is_a?(Node)
|
199
|
+
raise ArgumentError, 'cannot pong self' if remote == @node
|
200
|
+
|
201
|
+
pingid = mkpingid echoed, remote
|
202
|
+
logger.debug 'recv pong', remote: remote, pingid: Utils.encode_hex(pingid)[0,8], local: @node
|
203
|
+
|
204
|
+
# FIXME: but neighbours will NEVER include remote
|
205
|
+
#neighbours = @routing.neighbours remote
|
206
|
+
#if !neighbours.empty? && neighbours[0] == remote
|
207
|
+
# neighbours[0].address = remote.address # update tcp address
|
208
|
+
#end
|
209
|
+
|
210
|
+
update remote, pingid
|
211
|
+
end
|
212
|
+
|
213
|
+
##
|
214
|
+
# if one of the neighbours is closer than the closest known neighbours
|
215
|
+
# if not timed out
|
216
|
+
# query closest node for neighbours
|
217
|
+
# add all nodes to the list
|
218
|
+
#
|
219
|
+
def recv_neighbours(remote, neighbours)
|
220
|
+
logger.debug "recv neighbours", remoteid: remote, num: neighbours.size, local: @node, neighbours: neighbours
|
221
|
+
|
222
|
+
neighbours = neighbours.select {|n| n != @node && !@routing.include?(n) }
|
223
|
+
|
224
|
+
# FIXME: we don't map requests to responses, thus forwarding to all
|
225
|
+
@find_requests.each do |nodeid, timeout|
|
226
|
+
closest = neighbours.sort_by {|n| n.id_distance(nodeid) }
|
227
|
+
|
228
|
+
if Time.now < timeout
|
229
|
+
closest_known = @routing.neighbours(nodeid)[0]
|
230
|
+
raise KademliaRoutingError if closest_known == @node
|
231
|
+
|
232
|
+
# send find_node requests to A closests
|
233
|
+
closest[0, A].each do |close_node|
|
234
|
+
if !closest_known || close_node.id_distance(nodeid) < closest_known.id_distance(nodeid)
|
235
|
+
logger.debug "forwarding find request", closest: close_node, closest_known: closest_known
|
236
|
+
@wire.send_find_node close_node, nodeid
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
# add all nodes to the list
|
243
|
+
neighbours.each do |node|
|
244
|
+
ping node if node != @node
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# FIXME: amplification attack (need to ping pong ping pong first)
|
249
|
+
def recv_find_node(remote, targetid)
|
250
|
+
raise ArgumentError, 'remote must be Node' unless remote.is_a?(Node)
|
251
|
+
|
252
|
+
update remote
|
253
|
+
|
254
|
+
found = @routing.neighbours(targetid)
|
255
|
+
logger.debug "recv find_node", remoteid: remote, found: found.size
|
256
|
+
|
257
|
+
@wire.send_neighbours remote, found
|
258
|
+
end
|
259
|
+
|
260
|
+
private
|
261
|
+
|
262
|
+
def logger
|
263
|
+
@logger ||= Logger.new('p2p.discovery.kademlia').tap {|l| l.level = :info }
|
264
|
+
end
|
265
|
+
|
266
|
+
def query_neighbours(targetid)
|
267
|
+
@routing.neighbours(targetid)[0, A].each do |n|
|
268
|
+
@wire.send_find_node n, targetid
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
def mkpingid(echoed, node)
|
273
|
+
raise ArgumentError, 'node has no pubkey' if node.pubkey.nil? || node.pubkey.empty?
|
274
|
+
|
275
|
+
pid = echoed + node.pubkey
|
276
|
+
logger.debug "mkpingid", echoed: Utils.encode_hex(echoed), node: Utils.encode_hex(node.pubkey)
|
277
|
+
|
278
|
+
pid
|
279
|
+
end
|
280
|
+
|
281
|
+
end
|
282
|
+
|
283
|
+
end
|
284
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# -*- encoding : ascii-8bit -*-
|
2
|
+
|
3
|
+
module DEVp2p
|
4
|
+
module Kademlia
|
5
|
+
|
6
|
+
class RoutingTable
|
7
|
+
|
8
|
+
attr :node
|
9
|
+
|
10
|
+
def initialize(node)
|
11
|
+
@node = node
|
12
|
+
@buckets = [KBucket.new(0, MAX_NODE_ID)]
|
13
|
+
end
|
14
|
+
|
15
|
+
include Enumerable
|
16
|
+
def each(&block)
|
17
|
+
@buckets.each do |b|
|
18
|
+
b.each(&block)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def split_bucket(bucket)
|
23
|
+
index = @buckets.index bucket
|
24
|
+
@buckets[index..index] = bucket.split
|
25
|
+
end
|
26
|
+
|
27
|
+
def idle_buckets
|
28
|
+
t_idle = Time.now - IDLE_BUCKET_REFRESH_INTERVAL
|
29
|
+
@buckets.select {|b| b.last_updated < t_idle }
|
30
|
+
end
|
31
|
+
|
32
|
+
def not_full_buckets
|
33
|
+
@buckets.select {|b| b.size < K }
|
34
|
+
end
|
35
|
+
|
36
|
+
def add(node)
|
37
|
+
raise ArgumentError, 'cannot add self' if node == @node
|
38
|
+
|
39
|
+
bucket = bucket_by_node node
|
40
|
+
eviction_candidate = bucket.add node
|
41
|
+
|
42
|
+
if eviction_candidate # bucket is full
|
43
|
+
# split if the bucket has the local node in its range or if the depth
|
44
|
+
# is not congruent to 0 mod B
|
45
|
+
if bucket.in_range?(@node) || bucket.splitable?
|
46
|
+
split_bucket bucket
|
47
|
+
return add(node) # retry
|
48
|
+
end
|
49
|
+
|
50
|
+
# nothing added, ping eviction_candidate
|
51
|
+
return eviction_candidate
|
52
|
+
end
|
53
|
+
|
54
|
+
nil # successfully added to not full bucket
|
55
|
+
end
|
56
|
+
|
57
|
+
def delete(node)
|
58
|
+
bucket_by_node(node).delete node
|
59
|
+
end
|
60
|
+
|
61
|
+
def bucket_by_node(node)
|
62
|
+
@buckets.each do |bucket|
|
63
|
+
if node.id < bucket.right
|
64
|
+
raise KademliaRoutingError, "mal-formed routing table" unless node.id >= bucket.left
|
65
|
+
return bucket
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
raise KademliaNodeNotFound
|
70
|
+
end
|
71
|
+
|
72
|
+
def buckets_by_id_distance(id)
|
73
|
+
raise ArgumentError, 'id must be integer' unless id.is_a?(Integer)
|
74
|
+
@buckets.sort_by {|b| b.id_distance(id) }
|
75
|
+
end
|
76
|
+
|
77
|
+
def buckets_by_distance(node)
|
78
|
+
raise ArgumentError, 'node must be Node' unless node.is_a?(Node)
|
79
|
+
buckets_by_id_distance(node.id)
|
80
|
+
end
|
81
|
+
|
82
|
+
def include?(node)
|
83
|
+
bucket_by_node(node).include?(node)
|
84
|
+
end
|
85
|
+
|
86
|
+
def size
|
87
|
+
@buckets.map(&:size).reduce(0, &:+)
|
88
|
+
end
|
89
|
+
|
90
|
+
def buckets_count
|
91
|
+
@buckets.size
|
92
|
+
end
|
93
|
+
|
94
|
+
##
|
95
|
+
# sorting by bucket.midpoint does not work in edge cases, buld a short
|
96
|
+
# list of `k * 2` nodes and sort and shorten it.
|
97
|
+
#
|
98
|
+
# TODO: can we do better?
|
99
|
+
#
|
100
|
+
def neighbours(node, k=K)
|
101
|
+
raise ArgumentError, 'node must be Node or node id' unless node.instance_of?(Node) || node.is_a?(Integer)
|
102
|
+
|
103
|
+
node = node.id if node.instance_of?(Node)
|
104
|
+
|
105
|
+
nodes = []
|
106
|
+
buckets_by_id_distance(node).each do |bucket|
|
107
|
+
bucket.nodes_by_id_distance(node).each do |n|
|
108
|
+
if n != node
|
109
|
+
nodes.push n
|
110
|
+
break if nodes.size == k * 2
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
nodes.sort_by {|n| n.id_distance(node) }[0,k]
|
116
|
+
end
|
117
|
+
|
118
|
+
##
|
119
|
+
# naive correct version simply compares all nodes
|
120
|
+
#
|
121
|
+
def neighbours_within_distance(id, distance)
|
122
|
+
raise ArgumentError, 'invalid id' unless id.is_a?(Integer)
|
123
|
+
|
124
|
+
select {|n| n.id_distance(id) <= distance }
|
125
|
+
.sort_by {|n| n.id_distance(id) }
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
end
|