devp2p 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/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,105 @@
|
|
1
|
+
# -*- encoding : ascii-8bit -*-
|
2
|
+
|
3
|
+
module DEVp2p
|
4
|
+
module Discovery
|
5
|
+
|
6
|
+
##
|
7
|
+
# Persist the list of known nodes with their reputation.
|
8
|
+
#
|
9
|
+
class Transport < BaseService
|
10
|
+
include Celluloid::IO
|
11
|
+
|
12
|
+
name 'discovery'
|
13
|
+
|
14
|
+
default_config(
|
15
|
+
discovery: {
|
16
|
+
listen_port: 30303,
|
17
|
+
listen_host: '0.0.0.0'
|
18
|
+
},
|
19
|
+
node: {
|
20
|
+
privkey_hex: ''
|
21
|
+
}
|
22
|
+
)
|
23
|
+
|
24
|
+
attr :protocol
|
25
|
+
|
26
|
+
def initialize(app)
|
27
|
+
super(app)
|
28
|
+
logger.info "Discovery service init"
|
29
|
+
|
30
|
+
@server = nil # will be UDPSocket
|
31
|
+
@protocol = Protocol.new app, Actor.current
|
32
|
+
end
|
33
|
+
|
34
|
+
def address
|
35
|
+
ip = @app.config[:discovery][:listen_host]
|
36
|
+
port = @app.config[:discovery][:listen_port]
|
37
|
+
Address.new ip, port
|
38
|
+
end
|
39
|
+
|
40
|
+
def send_message(address, message)
|
41
|
+
raise ArgumentError, 'address must be Address' unless address.instance_of?(Address)
|
42
|
+
logger.debug "sending", size: message.size, to: address
|
43
|
+
|
44
|
+
begin
|
45
|
+
@server.send message, 0, address.ip, address.udp_port
|
46
|
+
rescue
|
47
|
+
# should never reach here? udp has no connection!
|
48
|
+
logger.error "udp write error", error: $!
|
49
|
+
logger.error "waiting for recovery"
|
50
|
+
sleep 5
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def receive_message(address, message)
|
55
|
+
raise ArgumentError, 'address must be Address' unless address.instance_of?(Address)
|
56
|
+
@protocol.receive_message address, message
|
57
|
+
end
|
58
|
+
|
59
|
+
def start
|
60
|
+
logger.info 'starting discovery'
|
61
|
+
|
62
|
+
ip = @app.config[:discovery][:listen_host]
|
63
|
+
port = @app.config[:discovery][:listen_port]
|
64
|
+
|
65
|
+
logger.info "starting udp listener", port: port, host: ip
|
66
|
+
|
67
|
+
@server = UDPSocket.new
|
68
|
+
@server.bind ip, port
|
69
|
+
|
70
|
+
super
|
71
|
+
|
72
|
+
@protocol.bootstrap(
|
73
|
+
@app.config[:discovery][:bootstrap_nodes].map {|x| Node.from_uri(x) }
|
74
|
+
)
|
75
|
+
end
|
76
|
+
|
77
|
+
def stop
|
78
|
+
logger.info "stopping discovery"
|
79
|
+
super
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def logger
|
85
|
+
@logger ||= Logger.new "#{@app.config[:discovery][:listen_port]}.p2p.discovery"
|
86
|
+
end
|
87
|
+
|
88
|
+
def _run
|
89
|
+
maxlen = Multiplexer.max_window_size * 2
|
90
|
+
loop do
|
91
|
+
break if stopped?
|
92
|
+
message, info = @server.recvfrom maxlen
|
93
|
+
async.handle_packet message, info[3], info[1]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def handle_packet(message, ip, port)
|
98
|
+
logger.debug "handling packet", ip: ip, port: port, size: message.size
|
99
|
+
receive_message Address.new(ip, port), message
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding : ascii-8bit -*-
|
2
|
+
|
3
|
+
module DEVp2p
|
4
|
+
|
5
|
+
class MissingRequiredServiceError < StandardError; end
|
6
|
+
class InvalidCommandStructure < StandardError; end
|
7
|
+
class DuplicatedCommand < StandardError; end
|
8
|
+
class UnknownCommandError < StandardError; end
|
9
|
+
class FrameError < StandardError; end
|
10
|
+
class MultiplexerError < StandardError; end
|
11
|
+
class RLPxSessionError < StandardError; end
|
12
|
+
class MultiplexedSessionError < StandardError; end
|
13
|
+
class AuthenticationError < StandardError; end
|
14
|
+
class FormatError < StandardError; end
|
15
|
+
class InvalidKeyError < StandardError; end
|
16
|
+
class InvalidSignatureError < StandardError; end
|
17
|
+
class InvalidMACError < StandardError; end
|
18
|
+
class InvalidPayloadError < StandardError; end
|
19
|
+
class EncryptionError < StandardError; end
|
20
|
+
class DecryptionError < StandardError; end
|
21
|
+
class KademliaRoutingError < StandardError; end
|
22
|
+
class KademliaNodeNotFound < StandardError; end
|
23
|
+
class PeerError < StandardError; end
|
24
|
+
class ProtocolError < StandardError; end
|
25
|
+
|
26
|
+
class DefectiveMessage < StandardError; end
|
27
|
+
class PacketExpired < DefectiveMessage; end
|
28
|
+
class InvalidMessageMAC < DefectiveMessage; end
|
29
|
+
|
30
|
+
end
|
data/lib/devp2p/frame.rb
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
# -*- encoding : ascii-8bit -*-
|
2
|
+
|
3
|
+
module DEVp2p
|
4
|
+
|
5
|
+
##
|
6
|
+
# When sending a packet over RLPx, the packet will be framed. The frame
|
7
|
+
# provides information about the size of the packet and the packet's source
|
8
|
+
# protocol. There are three slightly different frames, depending on whether
|
9
|
+
# or not the frame is delivering a multi-frame packet. A multi-frame packet
|
10
|
+
# is a packet which is split (aka chunked) into multiple frames because it's
|
11
|
+
# size is larger than the protocol window size (pws, see Multiplexing). When
|
12
|
+
# a packet is chunked into multiple frames, there is an implicit difference
|
13
|
+
# between the first frame and all subsequent frames.
|
14
|
+
#
|
15
|
+
# Thus, the three frame types are normal, chunked-0 (first frame of a
|
16
|
+
# multi-frame packet), and chunked-n (subsequent frames of a multi-frame
|
17
|
+
# packet).
|
18
|
+
#
|
19
|
+
# * Single-frame packet:
|
20
|
+
#
|
21
|
+
# header || header-mac || frame || mac
|
22
|
+
#
|
23
|
+
# * Multi-frame packet:
|
24
|
+
#
|
25
|
+
# header || header-mac || frame-0 ||
|
26
|
+
# [ header || header-mac || frame-n || ... || ]
|
27
|
+
# header || header-mac || frame-last || mac
|
28
|
+
#
|
29
|
+
class Frame
|
30
|
+
|
31
|
+
extend Configurable
|
32
|
+
add_config(
|
33
|
+
header_size: 16,
|
34
|
+
mac_size: 16,
|
35
|
+
padding: 16,
|
36
|
+
header_sedes: RLP::Sedes::List.new(elements: [RLP::Sedes.big_endian_int]*3, strict: false)
|
37
|
+
)
|
38
|
+
|
39
|
+
class <<self
|
40
|
+
def encode_body_size(size)
|
41
|
+
[size].pack('I>')[1..-1]
|
42
|
+
end
|
43
|
+
|
44
|
+
def decode_body_size(header)
|
45
|
+
"\x00#{header[0,3]}".unpack('I>').first
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
attr :protocol_id, :cmd_id, :sequence_id, :payload, :is_chunked_n, :total_payload_size, :frames
|
50
|
+
|
51
|
+
def initialize(protocol_id, cmd_id, payload, sequence_id, window_size, is_chunked_n=false, frames=nil, frame_cipher=nil)
|
52
|
+
raise ArgumentError, 'invalid protocol_id' unless protocol_id < TT16
|
53
|
+
raise ArgumentError, 'invalid sequence_id' unless sequence_id.nil? || sequence_id < TT16
|
54
|
+
raise ArgumentError, 'invalid window_size' unless window_size % padding == 0
|
55
|
+
raise ArgumentError, 'invalid cmd_id' unless cmd_id < 256
|
56
|
+
|
57
|
+
@protocol_id = protocol_id
|
58
|
+
@cmd_id = cmd_id
|
59
|
+
@payload = payload
|
60
|
+
@sequence_id = sequence_id
|
61
|
+
@is_chunked_n = is_chunked_n
|
62
|
+
@frame_cipher = frame_cipher
|
63
|
+
|
64
|
+
@frames = frames || []
|
65
|
+
@frames.push self
|
66
|
+
|
67
|
+
# chunk payloads resulting in frames exceeing window_size
|
68
|
+
fs = frame_size
|
69
|
+
if fs > window_size
|
70
|
+
unless is_chunked_n
|
71
|
+
@is_chunked_0 = true
|
72
|
+
@total_payload_size = body_size
|
73
|
+
end
|
74
|
+
|
75
|
+
# chunk payload
|
76
|
+
@payload = payload[0...(window_size-fs)]
|
77
|
+
raise FrameError, "invalid frame size" unless frame_size <= window_size
|
78
|
+
|
79
|
+
remain = payload[@payload.size..-1]
|
80
|
+
raise FrameError, "invalid remain size" unless (remain.size + @payload.size) == payload.size
|
81
|
+
|
82
|
+
Frame.new(protocol_id, cmd_id, remain, sequence_id, window_size, true, @frames, frame_cipher)
|
83
|
+
end
|
84
|
+
|
85
|
+
raise FrameError, "invalid frame size" unless frame_size <= window_size
|
86
|
+
end
|
87
|
+
|
88
|
+
def frame_type
|
89
|
+
return :normal if normal?
|
90
|
+
@is_chunked_n ? :chunked_n : :chunked_0
|
91
|
+
end
|
92
|
+
|
93
|
+
def frame_size
|
94
|
+
# header16 || mac16 || dataN + [padding] || mac16
|
95
|
+
header_size + mac_size + body_size(true) + mac_size
|
96
|
+
end
|
97
|
+
|
98
|
+
##
|
99
|
+
# frame-size: 3-byte integer, size of frame, big endian encoded (excludes
|
100
|
+
# padding)
|
101
|
+
#
|
102
|
+
def body_size(padded=false)
|
103
|
+
l = enc_cmd_id.size + payload.size
|
104
|
+
padded ? Utils.ceil16(l) : l
|
105
|
+
end
|
106
|
+
|
107
|
+
def normal?
|
108
|
+
!@is_chunked_n && !@is_chunked_0
|
109
|
+
end
|
110
|
+
|
111
|
+
##
|
112
|
+
# header: frame-size || header-data || padding
|
113
|
+
#
|
114
|
+
# frame-size: 3-byte integer, size of frame, big endian encoded
|
115
|
+
# header-data:
|
116
|
+
# normal: RLP::Sedes::List.new(protocol_type[, sequence_id])
|
117
|
+
# chunked_0: RLP::Sedes::List.new(protocol_type, sequence_id, total_packet_size)
|
118
|
+
# chunked_n: RLP::Sedes::List.new(protocol_type, sequence_id)
|
119
|
+
# normal, chunked_n: RLP::Sedes::List.new(protocol_type[, sequence_id])
|
120
|
+
# values:
|
121
|
+
# protocol_type: < 2**16
|
122
|
+
# sequence_id: < 2**16 (this value is optional for normal frames)
|
123
|
+
# total_packet_size: < 2**32
|
124
|
+
# padding: zero-fill to 16-byte boundary
|
125
|
+
#
|
126
|
+
def header
|
127
|
+
raise FrameError, "invalid protocol id" unless protocol_id < 2**16
|
128
|
+
raise FrameError, "invalid sequence id" unless sequence_id.nil? || sequence_id < TT16
|
129
|
+
|
130
|
+
l = [protocol_id]
|
131
|
+
if @is_chunked_0
|
132
|
+
raise FrameError, 'chunked_0 must have sequence_id' if sequence_id.nil?
|
133
|
+
l.push sequence_id
|
134
|
+
l.push total_payload_size
|
135
|
+
elsif sequence_id
|
136
|
+
l.push sequence_id
|
137
|
+
end
|
138
|
+
|
139
|
+
header_data = RLP.encode l, sedes: header_sedes
|
140
|
+
raise FrameError, 'invalid rlp' unless l == RLP.decode(header_data, sedes: header_sedes, strict: false)
|
141
|
+
|
142
|
+
bs = body_size
|
143
|
+
raise FrameError, 'invalid body size' unless bs < 256**3
|
144
|
+
|
145
|
+
header = Frame.encode_body_size(body_size) + header_data
|
146
|
+
header = Utils.rzpad16 header
|
147
|
+
raise FrameError, 'invalid header' unless header.size == header_size
|
148
|
+
|
149
|
+
header
|
150
|
+
end
|
151
|
+
|
152
|
+
def enc_cmd_id
|
153
|
+
@is_chunked_n ? '' : RLP.encode(cmd_id, sedes: RLP::Sedes.big_endian_int)
|
154
|
+
end
|
155
|
+
|
156
|
+
##
|
157
|
+
# frame:
|
158
|
+
# normal: rlp(packet_type) [|| rlp(packet_data)] || padding
|
159
|
+
# chunked_0: rlp(packet_type) || rlp(packet_data ...)
|
160
|
+
# chunked_n: rlp(...packet_data) || padding
|
161
|
+
# padding: zero-fill to 16-byte boundary (only necessary for last frame)
|
162
|
+
#
|
163
|
+
def body
|
164
|
+
Utils.rzpad16 "#{enc_cmd_id}#{payload}"
|
165
|
+
end
|
166
|
+
|
167
|
+
def as_bytes
|
168
|
+
raise FrameError, 'can only be called once' if @cipher_called
|
169
|
+
|
170
|
+
if @frame_cipher
|
171
|
+
@cipher_called = true
|
172
|
+
e = @frame_cipher.encrypt(header, body)
|
173
|
+
raise FrameError, 'invalid frame size of encrypted frame' unless e.size == frame_size
|
174
|
+
e
|
175
|
+
else
|
176
|
+
h = header
|
177
|
+
raise FrameError, 'invalid header size' unless h.size == header_size
|
178
|
+
|
179
|
+
b = body
|
180
|
+
raise FrameError, 'invalid body size' unless b.size == body_size(true)
|
181
|
+
|
182
|
+
dummy_mac = "\x00" * mac_size
|
183
|
+
r = h + dummy_mac + b + dummy_mac
|
184
|
+
raise FrameError, 'invalid frame' unless r.size == frame_size
|
185
|
+
|
186
|
+
r
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def to_s
|
191
|
+
"<Frame(#{frame_type}, len=#{frame_size}, protocol=#{protocol_id} sid=#{sequence_id})"
|
192
|
+
end
|
193
|
+
alias :inspect :to_s
|
194
|
+
|
195
|
+
end
|
196
|
+
|
197
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# -*- encoding : ascii-8bit -*-
|
2
|
+
|
3
|
+
module DEVp2p
|
4
|
+
|
5
|
+
##
|
6
|
+
# Node discovery and network formation are implemented via a Kademlia-like
|
7
|
+
# protocol. The major differences are that packets are signed, node ids are
|
8
|
+
# the public keys, and DHT-related features are excluded. The FIND_VALUE and
|
9
|
+
# STORE packets are not implemented.
|
10
|
+
#
|
11
|
+
# The parameters necessary to implement the protocol are:
|
12
|
+
#
|
13
|
+
# * bucket size of 16 (denoted k in Kademlia)
|
14
|
+
# * concurrency of 3 (denoted alpha)
|
15
|
+
# * 8 bits per hop (denoted b) for routing
|
16
|
+
# * The eviction check interval is 75 milliseconds
|
17
|
+
# * request timeouts are 300ms
|
18
|
+
# * idle bucket-refresh interval is 3600 seconds
|
19
|
+
#
|
20
|
+
# Aside from the previously described exclusions, node discovery closely
|
21
|
+
# follows system and protocol described by Maymounkov and Mazieres.
|
22
|
+
#
|
23
|
+
module Kademlia
|
24
|
+
B = 8 # bits per hop for routing
|
25
|
+
K = 16 # bucket size
|
26
|
+
A = 3 # alpha, parallel find node lookups
|
27
|
+
|
28
|
+
REQUEST_TIMEOUT = 3 * 300 / 1000.0 # timeout of message round trips
|
29
|
+
IDLE_BUCKET_REFRESH_INTERVAL = 3600 # ping all nodes in bucket if bucket was idle
|
30
|
+
PUBKEY_SIZE = 512
|
31
|
+
ID_SIZE = 256
|
32
|
+
MAX_NODE_ID = 2 ** ID_SIZE - 1
|
33
|
+
|
34
|
+
class <<self
|
35
|
+
def random_nodeid
|
36
|
+
SecureRandom.random_number(MAX_NODE_ID+1)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
require 'devp2p/kademlia/node'
|
43
|
+
require 'devp2p/kademlia/k_bucket'
|
44
|
+
require 'devp2p/kademlia/routing_table'
|
45
|
+
require 'devp2p/kademlia/wire_interface'
|
46
|
+
require 'devp2p/kademlia/protocol'
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# -*- encoding : ascii-8bit -*-
|
2
|
+
|
3
|
+
module DEVp2p
|
4
|
+
module Kademlia
|
5
|
+
|
6
|
+
##
|
7
|
+
# Each k-bucket is kept sorted by time last seen - least-recently seen node
|
8
|
+
# at the head, most-recently seen at the tail. For small values of i, the
|
9
|
+
# k-buckets will generally be empty (as no appropriate nodes will exist).
|
10
|
+
# For large values of i, the lists can grow up to size k, where k is a
|
11
|
+
# system-wide replication parameter.
|
12
|
+
#
|
13
|
+
# k is chosen such that any given k nodes are very unlikely to fail within
|
14
|
+
# an hour of each other (for example k = 20).
|
15
|
+
#
|
16
|
+
class KBucket
|
17
|
+
|
18
|
+
attr :left, :right, :last_updated
|
19
|
+
|
20
|
+
def initialize(left, right)
|
21
|
+
@left, @right = left, right
|
22
|
+
@nodes = []
|
23
|
+
@replacement_cache = []
|
24
|
+
@last_updated = Time.now
|
25
|
+
end
|
26
|
+
|
27
|
+
include Enumerable
|
28
|
+
def each(&block)
|
29
|
+
@nodes.each(&block)
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# If the sending node already exists in the recipient's k-bucket, the
|
34
|
+
# recipient moves it to the tail of the list.
|
35
|
+
#
|
36
|
+
# If the node is not already in the appropriate k-bucket and the bucket
|
37
|
+
# has fewer than k entries, then the recipient just inserts the new
|
38
|
+
# sender at the tail of the list.
|
39
|
+
#
|
40
|
+
# If the appropriate k-bucket is full, however, then the recipient pings
|
41
|
+
# the k-bucket's least-recently seen node to decide what to do:
|
42
|
+
#
|
43
|
+
# * on success: return nil
|
44
|
+
# * on bucket full: return least recently seen node for eviction check
|
45
|
+
#
|
46
|
+
def add(node)
|
47
|
+
@last_updated = Time.now
|
48
|
+
|
49
|
+
if include?(node) # already exists
|
50
|
+
delete node
|
51
|
+
@nodes.push node
|
52
|
+
nil
|
53
|
+
elsif size < K # add if fewer than k entries
|
54
|
+
@nodes.push node
|
55
|
+
nil
|
56
|
+
else # bucket is full
|
57
|
+
head
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def add_replacement(node)
|
62
|
+
@replacement_cache.push node
|
63
|
+
end
|
64
|
+
|
65
|
+
def delete(node)
|
66
|
+
return unless include?(node)
|
67
|
+
@nodes.delete node
|
68
|
+
end
|
69
|
+
|
70
|
+
##
|
71
|
+
# least recently seen
|
72
|
+
#
|
73
|
+
def head
|
74
|
+
@nodes.first
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# last recently seen
|
79
|
+
#
|
80
|
+
def tail
|
81
|
+
@nodes.last
|
82
|
+
end
|
83
|
+
|
84
|
+
def range
|
85
|
+
[left, right]
|
86
|
+
end
|
87
|
+
|
88
|
+
def midpoint
|
89
|
+
left + (right - left) / 2
|
90
|
+
end
|
91
|
+
|
92
|
+
def distance(node)
|
93
|
+
midpoint ^ node.id
|
94
|
+
end
|
95
|
+
|
96
|
+
def id_distance(id)
|
97
|
+
midpoint ^ id
|
98
|
+
end
|
99
|
+
|
100
|
+
def nodes_by_id_distance(id)
|
101
|
+
raise ArgumentError, 'invalid id' unless id.is_a?(Integer)
|
102
|
+
@nodes.sort_by {|n| n.id_distance(id) }
|
103
|
+
end
|
104
|
+
|
105
|
+
def should_split?
|
106
|
+
full? && splitable?
|
107
|
+
end
|
108
|
+
|
109
|
+
def splitable?
|
110
|
+
d = depth
|
111
|
+
d % B != 0 && d != ID_SIZE
|
112
|
+
end
|
113
|
+
|
114
|
+
##
|
115
|
+
# split at the median id
|
116
|
+
#
|
117
|
+
def split
|
118
|
+
split_id = midpoint
|
119
|
+
|
120
|
+
lower = self.class.new left, split_id
|
121
|
+
upper = self.class.new split_id + 1, right
|
122
|
+
|
123
|
+
# distribute nodes
|
124
|
+
@nodes.each do |node|
|
125
|
+
bucket = node.id <= split_id ? lower : upper
|
126
|
+
bucket.add node
|
127
|
+
end
|
128
|
+
|
129
|
+
# distribute replacement nodes
|
130
|
+
@replacement_cache.each do |node|
|
131
|
+
bucket = node.id <= split_id ? lower : upper
|
132
|
+
bucket.add_replacement node
|
133
|
+
end
|
134
|
+
|
135
|
+
return lower, upper
|
136
|
+
end
|
137
|
+
|
138
|
+
##
|
139
|
+
# depth is the prefix shared by all nodes in bucket. i.e. the number of
|
140
|
+
# shared leading bits.
|
141
|
+
#
|
142
|
+
def depth
|
143
|
+
return ID_SIZE if size < 2
|
144
|
+
|
145
|
+
bits = @nodes.map {|n| Utils.bpad(n.id, ID_SIZE) }
|
146
|
+
ID_SIZE.times do |i|
|
147
|
+
if bits.map {|b| b[0,i] }.uniq.size != 1
|
148
|
+
return i - 1
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
raise "should never be here"
|
153
|
+
end
|
154
|
+
|
155
|
+
def in_range?(node)
|
156
|
+
left <= node.id && node.id <= right
|
157
|
+
end
|
158
|
+
|
159
|
+
def full?
|
160
|
+
size == K
|
161
|
+
end
|
162
|
+
|
163
|
+
def size
|
164
|
+
@nodes.size
|
165
|
+
end
|
166
|
+
|
167
|
+
def include?(node)
|
168
|
+
@nodes.include?(node)
|
169
|
+
end
|
170
|
+
|
171
|
+
def empty?
|
172
|
+
@nodes.empty?
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
176
|
+
|
177
|
+
end
|
178
|
+
end
|