devp2p 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/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
|