ciri-p2p 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/.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
|
+
|