ciri-p2p 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +15 -0
  5. data/.vscode/launch.json +90 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +6 -0
  8. data/Gemfile.lock +65 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +45 -0
  11. data/Rakefile +6 -0
  12. data/bin/bundle +105 -0
  13. data/bin/console +14 -0
  14. data/bin/htmldiff +29 -0
  15. data/bin/ldiff +29 -0
  16. data/bin/rake +29 -0
  17. data/bin/rspec +29 -0
  18. data/bin/setup +8 -0
  19. data/ciri-p2p.gemspec +37 -0
  20. data/lib/ciri/p2p.rb +7 -0
  21. data/lib/ciri/p2p/address.rb +51 -0
  22. data/lib/ciri/p2p/dial_scheduler.rb +73 -0
  23. data/lib/ciri/p2p/dialer.rb +55 -0
  24. data/lib/ciri/p2p/discovery/protocol.rb +237 -0
  25. data/lib/ciri/p2p/discovery/service.rb +255 -0
  26. data/lib/ciri/p2p/errors.rb +36 -0
  27. data/lib/ciri/p2p/kad.rb +301 -0
  28. data/lib/ciri/p2p/network_state.rb +223 -0
  29. data/lib/ciri/p2p/node.rb +96 -0
  30. data/lib/ciri/p2p/peer.rb +151 -0
  31. data/lib/ciri/p2p/peer_store.rb +183 -0
  32. data/lib/ciri/p2p/protocol.rb +62 -0
  33. data/lib/ciri/p2p/protocol_context.rb +54 -0
  34. data/lib/ciri/p2p/protocol_io.rb +65 -0
  35. data/lib/ciri/p2p/rlpx.rb +29 -0
  36. data/lib/ciri/p2p/rlpx/connection.rb +182 -0
  37. data/lib/ciri/p2p/rlpx/encryption_handshake.rb +143 -0
  38. data/lib/ciri/p2p/rlpx/errors.rb +34 -0
  39. data/lib/ciri/p2p/rlpx/frame_io.rb +229 -0
  40. data/lib/ciri/p2p/rlpx/message.rb +45 -0
  41. data/lib/ciri/p2p/rlpx/protocol_handshake.rb +56 -0
  42. data/lib/ciri/p2p/rlpx/protocol_messages.rb +71 -0
  43. data/lib/ciri/p2p/rlpx/secrets.rb +49 -0
  44. data/lib/ciri/p2p/server.rb +159 -0
  45. data/lib/ciri/p2p/version.rb +5 -0
  46. metadata +229 -0
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ # Copyright (c) 2018 by Jiang Jinyang <jjyruby@gmail.com>
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ # THE SOFTWARE.
23
+
24
+
25
+ # TODO Items:
26
+ # [x] implement k-buckets algorithm
27
+ # [x] implement peerstore(may use sqlite)
28
+ # [ ] implement a simple scoring system
29
+ # [ ] testing
30
+ require 'async'
31
+ require 'async/io/udp_socket'
32
+ require 'async/io/endpoint/each'
33
+ require 'ciri/utils/logger'
34
+ require 'ciri/core_ext'
35
+ require 'ciri/p2p/node'
36
+ require 'ciri/p2p/address'
37
+ require 'ciri/p2p/peer_store'
38
+ require 'ciri/p2p/kad'
39
+ require_relative 'protocol'
40
+
41
+ using Ciri::CoreExt
42
+
43
+ module Ciri
44
+ module P2P
45
+ module Discovery
46
+
47
+ # Implement the DiscV4 protocol
48
+ # https://github.com/ethereum/devp2p/blob/master/discv4.md
49
+ # notice difference between PeerStore and Kad,
50
+ # we use PeerStore to store all peers we known(upon 8192),
51
+ # and use Kad to store our neighbours for discovery query.
52
+ class Service
53
+ include Utils::Logger
54
+ # use message classes defined in Discovery
55
+ include Protocol
56
+
57
+ attr_reader :peer_store, :local_node_id, :host, :udp_port, :tcp_port
58
+
59
+ # we should consider search from peer_store instead connect to bootnodes everytime
60
+ def initialize(peer_store:, host:, udp_port:, tcp_port:, private_key:, discovery_interval_secs: 15)
61
+ @discovery_interval_secs = discovery_interval_secs
62
+ @cache = Set.new
63
+ @host = host
64
+ @udp_port = udp_port
65
+ @tcp_port = tcp_port
66
+ @peer_store = peer_store
67
+ @private_key = private_key
68
+ @local_node_id = NodeID.new(private_key)
69
+ @kad_table = Kad::RoutingTable.new(local_node: Kad::Node.new(@local_node_id.to_bytes))
70
+ setup_kad_table
71
+ end
72
+
73
+ def run(task: Async::Task.current)
74
+ # start listening
75
+ task.async do
76
+ start_listen
77
+ end
78
+ # search peers every x seconds
79
+ task.reactor.every(@discovery_interval_secs) do
80
+ task.async do
81
+ perform_discovery
82
+ end
83
+ end
84
+ end
85
+
86
+ private
87
+ def start_listen(task: Async::Task.current)
88
+ endpoint = Async::IO::Endpoint.udp(@host, @udp_port)
89
+ endpoint.bind do |socket|
90
+ @local_address = socket.local_address
91
+
92
+ # update port if port is zero
93
+ if @udp_port.zero?
94
+ @udp_port = @local_address.ip_port
95
+ end
96
+
97
+ debug "start discovery server on udp_port: #{@udp_port} tcp_port: #{@tcp_port}\nlocal_node_id: #{@local_node_id}"
98
+
99
+ loop do
100
+ # read discovery message
101
+ packet, address = socket.recvfrom(Discovery::Protocol::Message::MAX_LEN)
102
+ handle_request(packet, address)
103
+ end
104
+ end
105
+ end
106
+
107
+ MESSAGE_EXPIRATION_IN = 10 * 60 # set 10 minutes later to expiration message
108
+
109
+ def handle_request(raw_packet, address, now: Time.now.to_i)
110
+ msg = Message.decode_message(raw_packet)
111
+ msg.validate
112
+ if msg.packet.expiration < now
113
+ trace("ignore expired message, sender: #{msg.sender}, expired_at: #{msg.packet.expiration}")
114
+ return
115
+ end
116
+ raw_node_id = msg.sender.to_bytes
117
+ case msg.packet_type
118
+ when Ping::CODE
119
+ @kad_table.update(raw_node_id)
120
+ from = msg.packet.from
121
+ from_ip = IPAddr.new(from.sender_ip, Socket::AF_INET)
122
+ from_udp_port = from.sender_udp_port
123
+ from_tcp_port = from.sender_tcp_port
124
+ from_address = Address.new(
125
+ ip: from_ip,
126
+ udp_port: from_udp_port,
127
+ tcp_port: from_tcp_port)
128
+ debug("receive ping msg from #{from_address.inspect}")
129
+ # respond pong
130
+ pong = Pong.new(to: To.from_host_port(from_ip, from_udp_port),
131
+ ping_hash: msg.message_hash,
132
+ expiration: Time.now.to_i + MESSAGE_EXPIRATION_IN)
133
+ pong_msg = Message.pack(pong, private_key: @private_key).encode_message
134
+ send_msg(pong_msg, from_ip.to_s, from_udp_port)
135
+ @peer_store.add_node(Node.new(raw_node_id: raw_node_id, addresses: [from_address]))
136
+ when Pong::CODE
137
+ # check pong
138
+ if @peer_store.has_ping?(raw_node_id, msg.packet.ping_hash)
139
+ # update peer last seen
140
+ @peer_store.update_last_seen(msg.sender.to_bytes)
141
+ else
142
+ @peer_store.ban_peer(msg.sender.to_bytes)
143
+ end
144
+ when FindNode::CODE
145
+ unless @peer_store.has_seen?(raw_node_id)
146
+ # consider add to denylist
147
+ return
148
+ end
149
+ nodes = find_neighbours(msg.packet.target, 20).map do |raw_node_id, addr|
150
+ Neighbors::Node.new(ip: addr.ip.to_i, udp_port: addr.udp_port, tcp_port: addr.tcp_port, node_id: raw_node_id)
151
+ end
152
+ neighbors = Neighbors.new(nodes: nodes, expiration: Time.now.to_i + MESSAGE_EXPIRATION_IN)
153
+ send_msg_to_node(Message.pack(neighbors, private_key: @private_key).encode_message, raw_node_id)
154
+ @peer_store.update_last_seen(raw_node_id)
155
+ when Neighbors::CODE
156
+ unless @peer_store.has_seen?(raw_node_id)
157
+ # consider add to denylist
158
+ return
159
+ end
160
+ debug("receive neighours #{msg.packet.nodes.size} from #{raw_node_id.to_hex}")
161
+ msg.packet.nodes.each do |node|
162
+ raw_id = node.node_id
163
+ next if raw_id == raw_local_node_id
164
+ debug("receive neighour #{node} from #{raw_node_id.to_hex}")
165
+ ip = IPAddr.new(node.ip, Socket::AF_INET)
166
+ address = Address.new(ip: ip, udp_port: node.udp_port, tcp_port: node.tcp_port)
167
+ @peer_store.add_node(Node.new(raw_node_id: raw_id, addresses: [address]))
168
+ # add new discovered node_id
169
+ @kad_table.update(raw_id)
170
+ end
171
+ @kad_table.update(raw_node_id)
172
+ @peer_store.update_last_seen(raw_node_id)
173
+ else
174
+ @peer_store.ban_peer(msg.sender.to_bytes)
175
+ raise UnknownMessageCodeError.new("can't handle unknown code in discovery protocol, code: #{msg.packet_type}")
176
+ end
177
+ rescue StandardError => e
178
+ @peer_store.ban_peer(msg.sender.to_bytes)
179
+ error("discovery error: #{e} from address: #{address}\nbacktrace:#{e.backtrace.join("\n")}")
180
+ end
181
+
182
+ def send_ping_to_address(target_node_id, address)
183
+ send_ping(target_node_id, address[3], address[1])
184
+ end
185
+
186
+ # send discover ping to peer
187
+ def send_ping(target_node_id, host, port)
188
+ ping = Ping.new(to: To.from_host_port(host, port),
189
+ from: From.new(
190
+ sender_ip: IPAddr.new(@host).to_i,
191
+ sender_udp_port: @udp_port,
192
+ sender_tcp_port: @tcp_port),
193
+ expiration: Time.now.to_i + MESSAGE_EXPIRATION_IN)
194
+ ping_msg = Message.pack(ping, private_key: @private_key)
195
+ send_msg(ping_msg.encode_message, host, port)
196
+ @peer_store.update_ping(target_node_id, ping_msg.message_hash)
197
+ end
198
+
199
+ def send_msg_to_node(msg, raw_node_id)
200
+ address = @peer_store.get_node_addresses(raw_node_id)&.first
201
+ raise ArgumentsError.new("can't found peer address of #{raw_node_id.to_hex} from peer_store") unless address
202
+ send_msg(msg, address.ip.to_s, address.udp_port)
203
+ end
204
+
205
+ def send_msg(msg, host, port)
206
+ socket = Async::IO::UDPSocket.new(UDPSocket.new)
207
+ socket.send(msg, 0, host, port)
208
+ end
209
+
210
+ def raw_local_node_id
211
+ @raw_local_node_id ||= @local_node_id.to_bytes
212
+ end
213
+
214
+ # find nerly neighbours
215
+ def find_neighbours(raw_node_id, count)
216
+ @kad_table.find_neighbours(raw_node_id, k: count).map do |node|
217
+ [node.raw_node_id, @peer_store.get_node_addresses(node.raw_node_id)&.first]
218
+ end.delete_if do |_, addr|
219
+ addr.nil?
220
+ end
221
+ end
222
+
223
+ def setup_kad_table
224
+ if @kad_table.size.zero?
225
+ @peer_store.find_bootnodes(20).each do |node|
226
+ next if raw_local_node_id == node.raw_node_id
227
+ debug("setup kad_table with #{node}")
228
+ @kad_table.update(node.raw_node_id)
229
+ end
230
+ end
231
+ end
232
+
233
+ def perform_discovery(count_of_query_nodes=15, task: Async::Task.current)
234
+ query_node = NodeID.new(Key.random)
235
+ query_target = query_node.to_bytes
236
+ # randomly search
237
+ @kad_table.get_random_nodes(15).each do |node|
238
+ address = @peer_store.get_node_addresses(node.raw_node_id)&.first
239
+ next unless address
240
+ # start query node in async task
241
+ task.async do
242
+ debug("perform discovery #{address}")
243
+ send_ping(node.raw_node_id, address.ip.to_s, address.udp_port)
244
+ query = FindNode.new(target: query_target, expiration: Time.now.to_i + MESSAGE_EXPIRATION_IN)
245
+ query_msg = Message.pack(query, private_key: @private_key).encode_message
246
+ send_msg(query_msg, address.ip.to_s, address.udp_port)
247
+ end
248
+ end
249
+ end
250
+ end
251
+
252
+ end
253
+ end
254
+ end
255
+
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ # Copyright (c) 2018 by Jiang Jinyang <jjyruby@gmail.com>
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ # THE SOFTWARE.
23
+
24
+
25
+ module Ciri
26
+ module P2P
27
+
28
+ class Error < StandardError; end
29
+ class UselessPeerError < Error; end
30
+ class DisconnectError < Error; end
31
+ class UnknownMessageCodeError < Error; end
32
+ class InvalidMessageError < Error; end
33
+
34
+ end
35
+ end
36
+
@@ -0,0 +1,301 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ # Copyright (c) 2018 by Jiang Jinyang <jjyruby@gmail.com>
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ # THE SOFTWARE.
23
+
24
+
25
+ require 'ciri/utils/logger'
26
+ require 'ciri/utils'
27
+ require 'ciri/p2p/node'
28
+ require 'forwardable'
29
+
30
+ module Ciri
31
+ module P2P
32
+
33
+
34
+ # Kademlia algorithm
35
+ # modified from https://github.com/ethereum/py-evm/blob/master/p2p/kademlia.py
36
+ module Kad
37
+ K_BITS = 8
38
+ K_BUCKET_SIZE = 16
39
+ K_REQUEST_TIMEOUT = 0.9
40
+ K_IDLE_BUCKET_REFRESH_INTERVAL = 3600
41
+ K_PUBKEY_SIZE = 512
42
+ K_ID_SIZE = 256
43
+ K_MAX_NODE_ID = 2 ** K_ID_SIZE - 1
44
+
45
+ class Node
46
+ attr_reader :id, :raw_node_id
47
+
48
+ def initialize(raw_node_id)
49
+ @raw_node_id = raw_node_id
50
+ @id = Utils.big_endian_decode(Utils.keccak(raw_node_id))
51
+ end
52
+
53
+ def distance_to(id)
54
+ @id ^ id
55
+ end
56
+
57
+ def ==(other)
58
+ self.class == other.class && self.id == other.id
59
+ end
60
+
61
+ def <=>(other)
62
+ @id <=> other.id
63
+ end
64
+ end
65
+
66
+ class KBucket
67
+
68
+ attr_reader :k_size, :nodes, :start_id, :end_id, :last_updated, :replacement_cache
69
+
70
+ def initialize(start_id:, end_id:, k_size: K_BUCKET_SIZE)
71
+ @start_id = start_id
72
+ @end_id = end_id
73
+ @k_size = k_size
74
+ @nodes = []
75
+ @replacement_cache = []
76
+ @last_updated = Time.now.to_i
77
+ end
78
+
79
+ # use to compute node distance with kbucket
80
+ def midpoint
81
+ @start_id + (@end_id - @start_id) / 2
82
+ end
83
+
84
+ def distance_to(id)
85
+ midpoint ^ id
86
+ end
87
+
88
+ # find neighbour nodes
89
+ def nodes_by_distance_to(id)
90
+ @nodes.sort_by do |node|
91
+ node.distance_to(id)
92
+ end
93
+ end
94
+
95
+ # split to two kbucket by midpoint
96
+ def split
97
+ split_point = midpoint
98
+ lower = KBucket.new(start_id: @start_id, end_id: split_point)
99
+ upper = KBucket.new(start_id: split_point + 1, end_id: @end_id)
100
+ @nodes.each do |node|
101
+ if node.id <= split_point
102
+ lower.add(node)
103
+ else
104
+ upper.add(node)
105
+ end
106
+ end
107
+ @replacement_cache.each do |node|
108
+ if node.id <= split_point
109
+ lower.replacement_cache << node
110
+ else
111
+ upper.replacement_cache << node
112
+ end
113
+ end
114
+ [lower, upper]
115
+ end
116
+
117
+ def delete(node)
118
+ @nodes.delete(node)
119
+ end
120
+
121
+ def cover?(node)
122
+ @start_id <= node.id && node.id <= @end_id
123
+ end
124
+
125
+ def full?
126
+ @nodes.size == k_size
127
+ end
128
+
129
+ # Try add node into bucket
130
+ # if node is exists, it is moved to the tail
131
+ # if the node is node exists and bucket not full, it is added at tail
132
+ # if the bucket is full, node will added to replacement_cache, and return the head of the list, which should be evicted if it failed to respond to a ping.
133
+ def add(node)
134
+ @last_updated = Time.now.to_i
135
+ if @nodes.include?(node)
136
+ @nodes.delete(node)
137
+ @nodes << node
138
+ elsif @nodes.size < k_size
139
+ @nodes << node
140
+ else
141
+ @replacement_cache << node
142
+ return head
143
+ end
144
+ nil
145
+ end
146
+
147
+ def head
148
+ @nodes[0]
149
+ end
150
+
151
+ def include?(node)
152
+ @nodes.include?(node)
153
+ end
154
+
155
+ def size
156
+ @nodes.size
157
+ end
158
+ end
159
+
160
+ class RoutingTable
161
+ attr_reader :buckets, :local_node
162
+
163
+ def initialize(local_node:)
164
+ @local_node = local_node
165
+ @buckets = [KBucket.new(start_id: 0, end_id: K_MAX_NODE_ID)]
166
+ end
167
+
168
+ def get_random_nodes(count)
169
+ count = size if count > size
170
+ nodes = []
171
+ while nodes.size < count
172
+ bucket = @buckets.sample
173
+ next if bucket.nodes.empty?
174
+ node = bucket.nodes.sample
175
+ unless nodes.include?(node)
176
+ nodes << node
177
+ end
178
+ end
179
+ nodes
180
+ end
181
+
182
+ def idle_buckets
183
+ bucket_idled_at = Time.now.to_i - K_IDLE_BUCKET_REFRESH_INTERVAL
184
+ @buckets.select do |bucket|
185
+ bucket.last_updated < bucket_idled_at
186
+ end
187
+ end
188
+
189
+ def not_full_buckets
190
+ @buckets.select do |bucket|
191
+ !bucket.full?
192
+ end
193
+ end
194
+
195
+ def delete_node(node)
196
+ find_bucket_for_node(node).delete(node)
197
+ end
198
+
199
+ def update(raw_node_id)
200
+ add_node(Node.new(raw_node_id))
201
+ end
202
+
203
+ def add_node(node)
204
+ raise ArgumentError.new("can't add local_node") if @local_node == node
205
+ bucket = find_bucket_for_node(node)
206
+ eviction_candidate = bucket.add(node)
207
+ # bucket is full, otherwise will return nil
208
+ if eviction_candidate
209
+ depth = compute_shared_prefix_bits(bucket.nodes)
210
+ if bucket.cover?(@local_node) || (depth % K_BITS != 0 && depth != K_ID_SIZE)
211
+ split_bucket(@buckets.index(bucket))
212
+ return add_node(node)
213
+ end
214
+ return eviction_candidate
215
+ end
216
+ nil
217
+ end
218
+
219
+ def buckets_by_distance_to(id)
220
+ @buckets.sort_by do |bucket|
221
+ bucket.distance_to(id)
222
+ end
223
+ end
224
+
225
+ def include?(node)
226
+ find_bucket_for_node(node).include?(node)
227
+ end
228
+
229
+ def size
230
+ @buckets.map(&:size).sum
231
+ end
232
+
233
+ def each_node(&blk)
234
+ @buckets.each do |bucket|
235
+ bucket.nodes do |node|
236
+ blk.call(node)
237
+ end
238
+ end
239
+ end
240
+
241
+ def find_neighbours(id, k: K_BUCKET_SIZE)
242
+ # convert id to integer
243
+ unless id.is_a?(Integer)
244
+ id = Node.new(id).id
245
+ end
246
+ nodes = []
247
+ buckets_by_distance_to(id).each do |bucket|
248
+ bucket.nodes_by_distance_to(id).each do |node|
249
+ if node.id != id
250
+ nodes << node
251
+ # find 2 * k nodes to avoid edge cases
252
+ break if nodes.size == k * 2
253
+ end
254
+ end
255
+ end
256
+ sort_by_distance(nodes, id)[0...k]
257
+ end
258
+
259
+ # do binary search to find node
260
+ def find_bucket_for_node(node)
261
+ @buckets.bsearch do |bucket|
262
+ bucket.end_id >= node.id
263
+ end
264
+ end
265
+
266
+ private
267
+
268
+ def split_bucket(index)
269
+ bucket = @buckets[index]
270
+ a, b = bucket.split
271
+ @buckets[index] = a
272
+ @buckets.insert(index + 1, b)
273
+ end
274
+
275
+ def compute_shared_prefix_bits(nodes)
276
+ return K_ID_SIZE if nodes.size < 2
277
+ bits = nodes.map{|node| to_binary(node.id) }
278
+ (1..K_ID_SIZE).each do |i|
279
+ # check common prefix shared by nodes
280
+ if bits.map{|b| b[0..i]}.uniq.size != 1
281
+ return i - 1
282
+ end
283
+ end
284
+ end
285
+
286
+ def sort_by_distance(nodes, target_id)
287
+ nodes.sort_by do |node|
288
+ node.distance_to(target_id)
289
+ end
290
+ end
291
+
292
+ def to_binary(x)
293
+ x.to_s(2).b.rjust(K_ID_SIZE, "\x00".b)
294
+ end
295
+
296
+ end
297
+
298
+ end
299
+ end
300
+ end
301
+