devp2p 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +22 -0
  4. data/lib/devp2p.rb +57 -0
  5. data/lib/devp2p/app_helper.rb +85 -0
  6. data/lib/devp2p/base_app.rb +80 -0
  7. data/lib/devp2p/base_protocol.rb +136 -0
  8. data/lib/devp2p/base_service.rb +55 -0
  9. data/lib/devp2p/command.rb +82 -0
  10. data/lib/devp2p/configurable.rb +32 -0
  11. data/lib/devp2p/connection_monitor.rb +77 -0
  12. data/lib/devp2p/control.rb +32 -0
  13. data/lib/devp2p/crypto.rb +73 -0
  14. data/lib/devp2p/crypto/ecc_x.rb +133 -0
  15. data/lib/devp2p/crypto/ecies.rb +134 -0
  16. data/lib/devp2p/discovery.rb +118 -0
  17. data/lib/devp2p/discovery/address.rb +83 -0
  18. data/lib/devp2p/discovery/kademlia_protocol_adapter.rb +11 -0
  19. data/lib/devp2p/discovery/node.rb +32 -0
  20. data/lib/devp2p/discovery/protocol.rb +342 -0
  21. data/lib/devp2p/discovery/transport.rb +105 -0
  22. data/lib/devp2p/exception.rb +30 -0
  23. data/lib/devp2p/frame.rb +197 -0
  24. data/lib/devp2p/kademlia.rb +48 -0
  25. data/lib/devp2p/kademlia/k_bucket.rb +178 -0
  26. data/lib/devp2p/kademlia/node.rb +40 -0
  27. data/lib/devp2p/kademlia/protocol.rb +284 -0
  28. data/lib/devp2p/kademlia/routing_table.rb +131 -0
  29. data/lib/devp2p/kademlia/wire_interface.rb +30 -0
  30. data/lib/devp2p/multiplexed_session.rb +110 -0
  31. data/lib/devp2p/multiplexer.rb +358 -0
  32. data/lib/devp2p/p2p_protocol.rb +170 -0
  33. data/lib/devp2p/packet.rb +35 -0
  34. data/lib/devp2p/peer.rb +329 -0
  35. data/lib/devp2p/peer_errors.rb +35 -0
  36. data/lib/devp2p/peer_manager.rb +274 -0
  37. data/lib/devp2p/rlpx_session.rb +434 -0
  38. data/lib/devp2p/sync_queue.rb +76 -0
  39. data/lib/devp2p/utils.rb +106 -0
  40. data/lib/devp2p/version.rb +13 -0
  41. data/lib/devp2p/wired_service.rb +30 -0
  42. 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