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,30 @@
|
|
1
|
+
# -*- encoding : ascii-8bit -*-
|
2
|
+
|
3
|
+
module DEVp2p
|
4
|
+
module Kademlia
|
5
|
+
|
6
|
+
##
|
7
|
+
# defines the methods used by KademliaProtocol
|
8
|
+
#
|
9
|
+
class WireInterface
|
10
|
+
|
11
|
+
def send_ping(node)
|
12
|
+
raise NotImplementedError
|
13
|
+
end
|
14
|
+
|
15
|
+
def send_pong(node, id)
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
|
19
|
+
def send_find_node(nodeid)
|
20
|
+
raise NotImplementedError
|
21
|
+
end
|
22
|
+
|
23
|
+
def send_neighbours(node, neighbours)
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# -*- encoding : ascii-8bit -*-
|
2
|
+
|
3
|
+
module DEVp2p
|
4
|
+
|
5
|
+
class MultiplexedSession < Multiplexer
|
6
|
+
|
7
|
+
def initialize(privkey, hello_packet, remote_pubkey=nil)
|
8
|
+
@hello_packet = hello_packet
|
9
|
+
@remote_pubkey = remote_pubkey
|
10
|
+
|
11
|
+
@message_queue = SyncQueue.new # wire msg egress queue
|
12
|
+
@packet_queue = SyncQueue.new # packet ingress queue
|
13
|
+
|
14
|
+
@is_initiator = !!remote_pubkey
|
15
|
+
@handshake_finished = false
|
16
|
+
|
17
|
+
ecc = Crypto::ECCx.new privkey
|
18
|
+
@rlpx_session = RLPxSession.new ecc, @is_initiator
|
19
|
+
|
20
|
+
super(@rlpx_session)
|
21
|
+
|
22
|
+
send_init_msg if @is_initiator
|
23
|
+
end
|
24
|
+
|
25
|
+
def get_message
|
26
|
+
@message_queue.deq
|
27
|
+
end
|
28
|
+
|
29
|
+
def get_packet
|
30
|
+
@packet_queue.deq
|
31
|
+
end
|
32
|
+
|
33
|
+
def ready?
|
34
|
+
@rlpx_session.ready?
|
35
|
+
end
|
36
|
+
|
37
|
+
def initiator?
|
38
|
+
@is_initiator
|
39
|
+
end
|
40
|
+
|
41
|
+
def remote_pubkey
|
42
|
+
@remote_pubkey || @rlpx_session.remote_pubkey
|
43
|
+
end
|
44
|
+
|
45
|
+
def remote_pubkey=(key)
|
46
|
+
@remote_pubkey = key
|
47
|
+
end
|
48
|
+
|
49
|
+
def add_message(msg)
|
50
|
+
@handshake_finished ? add_message_post_handshake(msg) : add_message_during_handshake(msg)
|
51
|
+
end
|
52
|
+
|
53
|
+
##
|
54
|
+
# encodes a packet and adds the message(s) to the message queue.
|
55
|
+
#
|
56
|
+
def add_packet(packet)
|
57
|
+
raise MultiplexedSessionError, 'session is not ready' unless ready?
|
58
|
+
raise ArgumentError, 'packet must be instance of Packet' unless packet.is_a?(Packet)
|
59
|
+
|
60
|
+
super(packet)
|
61
|
+
|
62
|
+
pop_all_frames.each {|f| @message_queue.enq f.as_bytes }
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def send_init_msg
|
68
|
+
auth_msg = @rlpx_session.create_auth_message @remote_pubkey
|
69
|
+
auth_msg_ct = @rlpx_session.encrypt_auth_message auth_msg
|
70
|
+
|
71
|
+
@message_queue.enq auth_msg_ct
|
72
|
+
end
|
73
|
+
|
74
|
+
def add_message_during_handshake(msg)
|
75
|
+
raise MultiplexedSessionError, 'handshake after ready is not allowed' if ready?
|
76
|
+
|
77
|
+
if initiator?
|
78
|
+
# expecting auth ack message
|
79
|
+
rest = @rlpx_session.decode_auth_ack_message msg
|
80
|
+
@rlpx_session.setup_cipher
|
81
|
+
|
82
|
+
# add remains (hello) to queue
|
83
|
+
add_message_post_handshake(rest) unless rest.empty?
|
84
|
+
else
|
85
|
+
# expecting auth_init
|
86
|
+
rest = @rlpx_session.decode_authentication msg
|
87
|
+
auth_ack_msg = @rlpx_session.create_auth_ack_message
|
88
|
+
auth_ack_msg_ct = @rlpx_session.encrypt_auth_ack_message auth_ack_msg
|
89
|
+
|
90
|
+
@message_queue.enq auth_ack_msg_ct
|
91
|
+
|
92
|
+
@rlpx_session.setup_cipher
|
93
|
+
add_message_post_handshake(rest) unless rest.empty?
|
94
|
+
end
|
95
|
+
|
96
|
+
@handshake_finished = true
|
97
|
+
raise MultiplexedSessionError, 'session is not ready after handshake' unless @rlpx_session.ready?
|
98
|
+
|
99
|
+
add_packet @hello_packet
|
100
|
+
end
|
101
|
+
|
102
|
+
def add_message_post_handshake(msg)
|
103
|
+
decode(msg).each do |packet|
|
104
|
+
@packet_queue.enq packet
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
@@ -0,0 +1,358 @@
|
|
1
|
+
# -*- encoding : ascii-8bit -*-
|
2
|
+
|
3
|
+
module DEVp2p
|
4
|
+
|
5
|
+
##
|
6
|
+
# Multiplexing of protocols is performed via dynamic framing and fair
|
7
|
+
# queueing. Dequeuing packets is performed in a cycle which dequeues one or
|
8
|
+
# more packets from the queue(s) of each active protocol. The multiplexor
|
9
|
+
# determines the amount of bytes to send for each protocol prior to each
|
10
|
+
# round of dequeuing packets.
|
11
|
+
#
|
12
|
+
# If the size of an RLP-encoded packet is less than 1KB then the protocol may
|
13
|
+
# request that the network layer prioritize the delivery of the packet. This
|
14
|
+
# should be used if and only if the packet must be delivered before all other
|
15
|
+
# packets.
|
16
|
+
#
|
17
|
+
# The network layer maintains two queues and three buffers per protocol:
|
18
|
+
#
|
19
|
+
# * a queue for normal packets, a queue for priority packets
|
20
|
+
# * a chunked-frame buffer, a normal-frame buffer, and a priority-frame buffer
|
21
|
+
#
|
22
|
+
# Implemented Variant:
|
23
|
+
#
|
24
|
+
# each sub protocol has three queues: prio, normal, chunked
|
25
|
+
#
|
26
|
+
# protocols are queried round robin
|
27
|
+
#
|
28
|
+
class Multiplexer
|
29
|
+
|
30
|
+
extend Configurable
|
31
|
+
add_config(
|
32
|
+
max_window_size: 8 * 1024,
|
33
|
+
max_priority_frame_size: 1024,
|
34
|
+
max_payload_size: 10 * 1024**2,
|
35
|
+
frame_cipher: nil,
|
36
|
+
)
|
37
|
+
|
38
|
+
attr :decode_buffer
|
39
|
+
|
40
|
+
def initialize(frame_cipher=nil)
|
41
|
+
@frame_cipher = frame_cipher || self.class.frame_cipher
|
42
|
+
@last_protocol = nil
|
43
|
+
@decode_buffer = "" # byte array
|
44
|
+
|
45
|
+
# protocol_id: {normal: queue, chunked: queue, prio: queue}
|
46
|
+
@queues = {}
|
47
|
+
|
48
|
+
# protocol_id: counter
|
49
|
+
@sequence_id = {}
|
50
|
+
|
51
|
+
# decode: {protocol_id: {sequence_id: buffer}
|
52
|
+
@chunked_buffers = {}
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# A protocol is considered active if it's queue contains one or more
|
57
|
+
# packets.
|
58
|
+
#
|
59
|
+
def num_active_protocols
|
60
|
+
@queues.keys.select {|id| active_protocol?(id) }.size
|
61
|
+
end
|
62
|
+
|
63
|
+
def active_protocol?(id)
|
64
|
+
!@queues[id].values.all?(&:empty?)
|
65
|
+
end
|
66
|
+
|
67
|
+
# pws = protocol_window_size = window_size / active_protocol_count
|
68
|
+
def protocol_window_size(id=nil)
|
69
|
+
if id && !active_protocol?(id)
|
70
|
+
s = max_window_size / (1 + num_active_protocols)
|
71
|
+
else
|
72
|
+
s = max_window_size / [1, num_active_protocols].max
|
73
|
+
end
|
74
|
+
|
75
|
+
s - s % 16 # should be a multiple of padding size # FIXME: 16 should be constant
|
76
|
+
end
|
77
|
+
|
78
|
+
def add_protocol(id)
|
79
|
+
raise ArgumentError, 'protocol already added' if @queues.include?(id)
|
80
|
+
|
81
|
+
@queues[id] = {
|
82
|
+
normal: SyncQueue.new,
|
83
|
+
chunked: SyncQueue.new,
|
84
|
+
priority: SyncQueue.new
|
85
|
+
}
|
86
|
+
@sequence_id[id] = 0
|
87
|
+
@chunked_buffers[id] = {}
|
88
|
+
@last_protocol = id
|
89
|
+
end
|
90
|
+
|
91
|
+
def next_protocol
|
92
|
+
protocols = @queues.keys
|
93
|
+
if @last_protocol == protocols.last
|
94
|
+
proto = protocols.first
|
95
|
+
else
|
96
|
+
proto = protocols[protocols.index(@last_protocol) + 1]
|
97
|
+
end
|
98
|
+
|
99
|
+
@last_protocol = proto
|
100
|
+
proto
|
101
|
+
end
|
102
|
+
|
103
|
+
def add_packet(packet)
|
104
|
+
sid = @sequence_id[packet.protocol_id]
|
105
|
+
@sequence_id[packet.protocol_id] = (sid + 1) % TT16
|
106
|
+
|
107
|
+
frames = Frame.new(
|
108
|
+
packet.protocol_id, packet.cmd_id, packet.payload, sid,
|
109
|
+
protocol_window_size(packet.protocol_id),
|
110
|
+
false, nil, @frame_cipher
|
111
|
+
).frames
|
112
|
+
|
113
|
+
queues = @queues[packet.protocol_id]
|
114
|
+
|
115
|
+
if packet.prioritize
|
116
|
+
raise FrameError, "invalid priority packet frames" unless frames.size == 1
|
117
|
+
raise FrameError, "frame too large for priority packet" unless frames[0].frame_size <= max_priority_frame_size
|
118
|
+
|
119
|
+
queues[:priority].enq frames[0]
|
120
|
+
elsif frames.size == 1
|
121
|
+
queues[:normal].enq frames[0]
|
122
|
+
else
|
123
|
+
frames.each {|f| queues[:chunked].enq f }
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
##
|
128
|
+
# If priority packet and normal packet exist:
|
129
|
+
# send up to pws/2 bytes from each (priority first)
|
130
|
+
# else if priority packet and chunked-frame exist:
|
131
|
+
# send up to pws/2 bytes from each
|
132
|
+
# else if normal packet and chunked-frame exist:
|
133
|
+
# send up to pws/2 bytes from each
|
134
|
+
# else
|
135
|
+
# read pws bytes from active buffer
|
136
|
+
#
|
137
|
+
# If there are bytes leftover -- for example, if the bytes sent is < pws,
|
138
|
+
# then repeat the cycle.
|
139
|
+
#
|
140
|
+
def pop_frames_for_protocol(id)
|
141
|
+
pws = protocol_window_size
|
142
|
+
queues = @queues[id]
|
143
|
+
|
144
|
+
frames = []
|
145
|
+
size = 0
|
146
|
+
|
147
|
+
while size < pws
|
148
|
+
frames_added = 0
|
149
|
+
|
150
|
+
%i(priority normal chunked).each do |qn|
|
151
|
+
q = queues[qn]
|
152
|
+
|
153
|
+
if !q.empty?
|
154
|
+
fs = q.peek.frame_size
|
155
|
+
if size + fs <= pws
|
156
|
+
frames.push q.deq
|
157
|
+
size += fs
|
158
|
+
frames_added += 1
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# add no more than two in order to send normal and priority first
|
163
|
+
# i.e. next is 'priority' again
|
164
|
+
#
|
165
|
+
# FIXME: too weird
|
166
|
+
#
|
167
|
+
break if frames_added == 2
|
168
|
+
end
|
169
|
+
|
170
|
+
break if frames_added == 0 # empty queues
|
171
|
+
end
|
172
|
+
|
173
|
+
# the following can not be guaranteed, as pws might have been different
|
174
|
+
# at the time where packets were framed and added to the queues
|
175
|
+
#
|
176
|
+
# frames.map(&:frame_size).sum <= pws
|
177
|
+
return frames
|
178
|
+
end
|
179
|
+
|
180
|
+
##
|
181
|
+
# Returns the frames for the next protocol up to protocol window size bytes.
|
182
|
+
#
|
183
|
+
def pop_frames
|
184
|
+
protocols = @queues.keys
|
185
|
+
idx = protocols.index next_protocol
|
186
|
+
protocols = protocols[idx..-1] + protocols[0,idx]
|
187
|
+
|
188
|
+
protocols.each do |id|
|
189
|
+
frames = pop_frames_for_protocol id
|
190
|
+
return frames unless frames.empty?
|
191
|
+
end
|
192
|
+
|
193
|
+
[]
|
194
|
+
end
|
195
|
+
|
196
|
+
def pop_all_frames
|
197
|
+
frames = []
|
198
|
+
loop do
|
199
|
+
r = pop_frames
|
200
|
+
frames.concat r
|
201
|
+
break if r.empty?
|
202
|
+
end
|
203
|
+
frames
|
204
|
+
end
|
205
|
+
|
206
|
+
def pop_all_frames_as_bytes
|
207
|
+
pop_all_frames.map(&:as_bytes).join
|
208
|
+
end
|
209
|
+
|
210
|
+
def decode_header(buffer)
|
211
|
+
raise ArgumentError, "buffer too small" unless buffer.size >= 32
|
212
|
+
|
213
|
+
if @frame_cipher
|
214
|
+
header = @frame_cipher.decrypt_header(buffer[0, Frame.header_size + Frame.mac_size])
|
215
|
+
else
|
216
|
+
# header: frame-size || header-data || padding
|
217
|
+
header = buffer[0, Frame.header_size]
|
218
|
+
end
|
219
|
+
|
220
|
+
header
|
221
|
+
end
|
222
|
+
|
223
|
+
##
|
224
|
+
# w/o encryption
|
225
|
+
# peak info buffer for body_size
|
226
|
+
#
|
227
|
+
# return nil if buffer is not long enough to decode frame
|
228
|
+
#
|
229
|
+
def decode_body(buffer, header=nil)
|
230
|
+
return [nil, buffer] if buffer.size < Frame.header_size
|
231
|
+
|
232
|
+
header ||= decode_header buffer[0, Frame.header_size + Frame.mac_size]
|
233
|
+
body_size = Frame.decode_body_size header
|
234
|
+
|
235
|
+
if @frame_cipher
|
236
|
+
body = @frame_cipher.decrypt_body(buffer[(Frame.header_size+Frame.mac_size)..-1], body_size)
|
237
|
+
raise MultiplexerError, 'body length mismatch' unless body.size == body_size
|
238
|
+
|
239
|
+
bytes_read = Frame.header_size + Frame.mac_size + Utils.ceil16(body.size) + Frame.mac_size
|
240
|
+
else
|
241
|
+
header = buffer[0, Frame.header_size]
|
242
|
+
body_offset = Frame.header_size + Frame.mac_size
|
243
|
+
body = buffer[body_offset, body_size]
|
244
|
+
raise MultiplexerError, 'body length mismatch' unless body.size == body_size
|
245
|
+
|
246
|
+
bytes_read = Utils.ceil16(body_offset + body_size + Frame.mac_size)
|
247
|
+
end
|
248
|
+
raise MultiplexerError, "bytes not padded" unless bytes_read % Frame.padding == 0
|
249
|
+
|
250
|
+
# normal, chunked-n: RLP::List.new(protocol_type[, sequence_id])
|
251
|
+
# chunked-0: RLP::List.new(protocol_type, sequence_id, total_packet_size)
|
252
|
+
header_data = nil
|
253
|
+
begin
|
254
|
+
header_data = RLP.decode(header[3..-1], sedes: Frame.header_sedes, strict: false)
|
255
|
+
rescue RLP::Error::RLPException => e
|
256
|
+
logger.error(e)
|
257
|
+
raise MultiplexerError, 'invalid rlp data'
|
258
|
+
end
|
259
|
+
|
260
|
+
if header_data.size == 3
|
261
|
+
chunked_0 = true
|
262
|
+
total_payload_size = header_data[2]
|
263
|
+
raise MultiplexerError, "invalid total payload size" unless total_payload_size < 2**32
|
264
|
+
else
|
265
|
+
chunked_0 = false
|
266
|
+
total_payload_size = nil
|
267
|
+
end
|
268
|
+
|
269
|
+
protocol_id = header_data[0]
|
270
|
+
raise MultiplexerError, "invalid protocol id" unless protocol_id < TT16
|
271
|
+
|
272
|
+
if header_data.size > 1
|
273
|
+
sequence_id = header_data[1]
|
274
|
+
raise MultiplexerError, "invalid sequence id" unless sequence_id < TT16
|
275
|
+
else
|
276
|
+
sequence_id = nil
|
277
|
+
end
|
278
|
+
|
279
|
+
raise MultiplexerError, "unknown protocol id #{protocol_id}" unless @chunked_buffers.has_key?(protocol_id)
|
280
|
+
|
281
|
+
chunkbuf = @chunked_buffers[protocol_id]
|
282
|
+
if chunkbuf.has_key?(sequence_id)
|
283
|
+
packet = chunkbuf[sequence_id]
|
284
|
+
|
285
|
+
raise MultiplexerError, "received chunked_0 frame for existing buffer #{sequence_id} of protocol #{protocol_id}" if chunked_0
|
286
|
+
raise MultiplexerError, "too much data for chunked buffer #{sequence_id} of protocol #{protocol_id}" if body.size > (packet.total_payload_size - packet.payload.size)
|
287
|
+
|
288
|
+
packet.payload += body
|
289
|
+
if packet.total_payload_size == packet.payload.size
|
290
|
+
packet.total_payload_size = nil
|
291
|
+
chunkbuf.delete sequence_id
|
292
|
+
return packet
|
293
|
+
end
|
294
|
+
else
|
295
|
+
# body of normal, chunked_0: rlp(packet-type) [|| rlp(packet-data)] || padding
|
296
|
+
item, item_end = RLP.consume_item(body, 0)
|
297
|
+
cmd_id = RLP::Sedes.big_endian_int.deserialize item
|
298
|
+
|
299
|
+
if chunked_0
|
300
|
+
payload = body[item_end..-1]
|
301
|
+
total_payload_size -= item_end
|
302
|
+
else
|
303
|
+
payload = body[item_end..-1]
|
304
|
+
end
|
305
|
+
|
306
|
+
packet = Packet.new protocol_id, cmd_id, payload
|
307
|
+
if chunked_0
|
308
|
+
raise MultiplexerError, "total payload size smaller than initial chunk" if total_payload_size < payload.size
|
309
|
+
|
310
|
+
# shouldn't have been chunked, whatever
|
311
|
+
return packet if total_payload_size == payload.size
|
312
|
+
|
313
|
+
raise MultiplexerError, 'chunked_0 must have sequence id' if sequence_id.nil?
|
314
|
+
|
315
|
+
packet.total_payload_size = total_payload_size
|
316
|
+
chunkbuf[sequence_id] = packet
|
317
|
+
|
318
|
+
return nil
|
319
|
+
else
|
320
|
+
return packet # normal (non-chunked)
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
def decode(data='')
|
326
|
+
@decode_buffer.concat(data) unless data.empty?
|
327
|
+
|
328
|
+
unless @cached_decode_header
|
329
|
+
if @decode_buffer.size < Frame.header_size + Frame.mac_size
|
330
|
+
return []
|
331
|
+
else
|
332
|
+
@cached_decode_header = decode_header @decode_buffer
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
body_size = Frame.decode_body_size @cached_decode_header
|
337
|
+
required_len = Frame.header_size + Frame.mac_size + Utils.ceil16(body_size) + Frame.mac_size
|
338
|
+
|
339
|
+
if @decode_buffer.size >= required_len
|
340
|
+
packet = decode_body @decode_buffer, @cached_decode_header
|
341
|
+
@cached_decode_header = nil
|
342
|
+
@decode_buffer = @decode_buffer[required_len..-1]
|
343
|
+
|
344
|
+
return packet ? ([packet] + decode) : decode
|
345
|
+
end
|
346
|
+
|
347
|
+
[]
|
348
|
+
end
|
349
|
+
|
350
|
+
private
|
351
|
+
|
352
|
+
def logger
|
353
|
+
@logger ||= Logger.new('multiplexer')
|
354
|
+
end
|
355
|
+
|
356
|
+
end
|
357
|
+
|
358
|
+
end
|