ciri-p2p 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/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +15 -0
- data/.vscode/launch.json +90 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +65 -0
- data/LICENSE.txt +21 -0
- data/README.md +45 -0
- data/Rakefile +6 -0
- data/bin/bundle +105 -0
- data/bin/console +14 -0
- data/bin/htmldiff +29 -0
- data/bin/ldiff +29 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/bin/setup +8 -0
- data/ciri-p2p.gemspec +37 -0
- data/lib/ciri/p2p.rb +7 -0
- data/lib/ciri/p2p/address.rb +51 -0
- data/lib/ciri/p2p/dial_scheduler.rb +73 -0
- data/lib/ciri/p2p/dialer.rb +55 -0
- data/lib/ciri/p2p/discovery/protocol.rb +237 -0
- data/lib/ciri/p2p/discovery/service.rb +255 -0
- data/lib/ciri/p2p/errors.rb +36 -0
- data/lib/ciri/p2p/kad.rb +301 -0
- data/lib/ciri/p2p/network_state.rb +223 -0
- data/lib/ciri/p2p/node.rb +96 -0
- data/lib/ciri/p2p/peer.rb +151 -0
- data/lib/ciri/p2p/peer_store.rb +183 -0
- data/lib/ciri/p2p/protocol.rb +62 -0
- data/lib/ciri/p2p/protocol_context.rb +54 -0
- data/lib/ciri/p2p/protocol_io.rb +65 -0
- data/lib/ciri/p2p/rlpx.rb +29 -0
- data/lib/ciri/p2p/rlpx/connection.rb +182 -0
- data/lib/ciri/p2p/rlpx/encryption_handshake.rb +143 -0
- data/lib/ciri/p2p/rlpx/errors.rb +34 -0
- data/lib/ciri/p2p/rlpx/frame_io.rb +229 -0
- data/lib/ciri/p2p/rlpx/message.rb +45 -0
- data/lib/ciri/p2p/rlpx/protocol_handshake.rb +56 -0
- data/lib/ciri/p2p/rlpx/protocol_messages.rb +71 -0
- data/lib/ciri/p2p/rlpx/secrets.rb +49 -0
- data/lib/ciri/p2p/server.rb +159 -0
- data/lib/ciri/p2p/version.rb +5 -0
- 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
|
+
|
data/lib/ciri/p2p/kad.rb
ADDED
@@ -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
|
+
|