devp2p 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|