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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +22 -0
  4. data/lib/devp2p.rb +57 -0
  5. data/lib/devp2p/app_helper.rb +85 -0
  6. data/lib/devp2p/base_app.rb +80 -0
  7. data/lib/devp2p/base_protocol.rb +136 -0
  8. data/lib/devp2p/base_service.rb +55 -0
  9. data/lib/devp2p/command.rb +82 -0
  10. data/lib/devp2p/configurable.rb +32 -0
  11. data/lib/devp2p/connection_monitor.rb +77 -0
  12. data/lib/devp2p/control.rb +32 -0
  13. data/lib/devp2p/crypto.rb +73 -0
  14. data/lib/devp2p/crypto/ecc_x.rb +133 -0
  15. data/lib/devp2p/crypto/ecies.rb +134 -0
  16. data/lib/devp2p/discovery.rb +118 -0
  17. data/lib/devp2p/discovery/address.rb +83 -0
  18. data/lib/devp2p/discovery/kademlia_protocol_adapter.rb +11 -0
  19. data/lib/devp2p/discovery/node.rb +32 -0
  20. data/lib/devp2p/discovery/protocol.rb +342 -0
  21. data/lib/devp2p/discovery/transport.rb +105 -0
  22. data/lib/devp2p/exception.rb +30 -0
  23. data/lib/devp2p/frame.rb +197 -0
  24. data/lib/devp2p/kademlia.rb +48 -0
  25. data/lib/devp2p/kademlia/k_bucket.rb +178 -0
  26. data/lib/devp2p/kademlia/node.rb +40 -0
  27. data/lib/devp2p/kademlia/protocol.rb +284 -0
  28. data/lib/devp2p/kademlia/routing_table.rb +131 -0
  29. data/lib/devp2p/kademlia/wire_interface.rb +30 -0
  30. data/lib/devp2p/multiplexed_session.rb +110 -0
  31. data/lib/devp2p/multiplexer.rb +358 -0
  32. data/lib/devp2p/p2p_protocol.rb +170 -0
  33. data/lib/devp2p/packet.rb +35 -0
  34. data/lib/devp2p/peer.rb +329 -0
  35. data/lib/devp2p/peer_errors.rb +35 -0
  36. data/lib/devp2p/peer_manager.rb +274 -0
  37. data/lib/devp2p/rlpx_session.rb +434 -0
  38. data/lib/devp2p/sync_queue.rb +76 -0
  39. data/lib/devp2p/utils.rb +106 -0
  40. data/lib/devp2p/version.rb +13 -0
  41. data/lib/devp2p/wired_service.rb +30 -0
  42. metadata +227 -0
@@ -0,0 +1,170 @@
1
+ # -*- encoding : ascii-8bit -*-
2
+
3
+ module DEVp2p
4
+
5
+ ##
6
+ # DEV P2P Wire Protocol
7
+ #
8
+ # @see https://github.com/ethereum/wiki/wiki/%C3%90%CE%9EVp2p-Wire-Protocol
9
+ #
10
+ class P2PProtocol < BaseProtocol
11
+
12
+ class Ping < Command
13
+ cmd_id 2
14
+
15
+ def receive(proto, data)
16
+ proto.send_pong
17
+ end
18
+ end
19
+
20
+ class Pong < Command
21
+ cmd_id 3
22
+ end
23
+
24
+ class Hello < Command
25
+ cmd_id 0
26
+ decode_strict false # don't throw for additional list elements as mandated by EIP-8
27
+
28
+ structure(
29
+ version: RLP::Sedes.big_endian_int,
30
+ client_version_string: RLP::Sedes.binary,
31
+ capabilities: RLP::Sedes::CountableList.new(
32
+ RLP::Sedes::List.new(elements: [RLP::Sedes.binary, RLP::Sedes.big_endian_int])
33
+ ),
34
+ listen_port: RLP::Sedes.big_endian_int,
35
+ remote_pubkey: RLP::Sedes.binary
36
+ )
37
+
38
+ def create(proto)
39
+ { version: proto.class.version,
40
+ client_version_string: proto.config[:client_version_string],
41
+ capabilities: proto.peer.capabilities,
42
+ listen_port: proto.config[:p2p][:listen_port],
43
+ remote_pubkey: proto.config[:node][:id] }
44
+ end
45
+
46
+ def receive(proto, data)
47
+ logger.debug 'receive_hello', peer: proto.peer, version: data[:version]
48
+
49
+ reasons = proto.class::Disconnect::Reason
50
+ if data[:remote_pubkey] == proto.config[:node][:id]
51
+ logger.debug 'connected myself'
52
+ return proto.send_disconnect(reason: reasons[:connected_to_self])
53
+ end
54
+
55
+ proto.peer.receive_hello proto, data
56
+ super(proto, data)
57
+ end
58
+
59
+ private
60
+
61
+ def logger
62
+ @logger = Logger.new "#{::Celluloid::Actor.current.peer.config[:p2p][:listen_port]}.p2p.protocol"
63
+ end
64
+
65
+ end
66
+
67
+ class Disconnect < Command
68
+ cmd_id 1
69
+
70
+ structure reason: RLP::Sedes.big_endian_int
71
+
72
+ Reason = {
73
+ disconnect_requested: 0,
74
+ tcp_sub_system_error: 1,
75
+ bad_protocol: 2, # e.g. a malformed message, bad RLP, incorrect magic number
76
+ useless_peer: 3,
77
+ too_many_peers: 4,
78
+ already_connected: 5,
79
+ incompatible_p2p_version: 6,
80
+ null_node_identity_received: 7,
81
+ client_quitting: 8,
82
+ unexpected_identity: 9,
83
+ connected_to_self: 10,
84
+ timeout: 11,
85
+ subprotocol_error: 12,
86
+ other: 16
87
+ }.freeze
88
+
89
+ def reason_key(id)
90
+ Reason.invert[id]
91
+ end
92
+
93
+ def reason_name(id)
94
+ key = reason_key id
95
+ key ? key.to_s : "unknown (id:#{id})"
96
+ end
97
+
98
+ def create(proto, reason=Reason[:client_quitting])
99
+ raise ArgumentError, "unknown reason" unless reason_key(reason)
100
+ logger.debug "send_disconnect", peer: proto.peer, reason: reason_name(reason)
101
+
102
+ proto.peer.report_error "sending disconnect #{reason_name(reason)}"
103
+
104
+ Celluloid::Actor.current.after(0.5) { proto.peer.stop }
105
+
106
+ {reason: reason}
107
+ end
108
+
109
+ def receive(proto, data)
110
+ logger.debug "receive_disconnect", peer: proto.peer, reason: reason_name(data[:reason])
111
+ proto.peer.report_error "disconnected #{reason_name(data[:reason])}"
112
+ proto.peer.stop
113
+ end
114
+
115
+ private
116
+
117
+ def logger
118
+ @logger = Logger.new "#{::Celluloid::Actor.current.peer.config[:p2p][:listen_port]}.p2p.protocol"
119
+ end
120
+
121
+ end
122
+
123
+ class <<self
124
+ # special: we need this packet before the protocol can be initialized
125
+ def get_hello_packet(peer)
126
+ res = {
127
+ version: 55,
128
+ client_version_string: peer.config[:client_version_string],
129
+ capabilities: peer.capabilities,
130
+ listen_port: peer.config[:p2p][:listen_port],
131
+ remote_pubkey: peer.config[:node][:id]
132
+ }
133
+
134
+ payload = Hello.encode_payload(res)
135
+ Packet.new protocol_id, Hello.cmd_id, payload
136
+ end
137
+ end
138
+
139
+ name 'p2p'
140
+ protocol_id 0
141
+ version 4
142
+ max_cmd_id 15
143
+
144
+ attr :config
145
+
146
+ def initialize(peer, service)
147
+ raise ArgumentError, "invalid peer" unless peer.respond_to?(:capabilities)
148
+ raise ArgumentError, "invalid peer" unless peer.respond_to?(:stop)
149
+ raise ArgumentError, "invalid peer" unless peer.respond_to?(:receive_hello)
150
+
151
+ @config = peer.config
152
+ super(peer, service)
153
+
154
+ @monitor = ConnectionMonitor.new self
155
+ end
156
+
157
+ def stop
158
+ @monitor.stop
159
+ super
160
+ end
161
+
162
+ private
163
+
164
+ def logger
165
+ @logger = Logger.new "#{@config[:p2p][:listen_port]}.p2p.protocol"
166
+ end
167
+
168
+ end
169
+
170
+ end
@@ -0,0 +1,35 @@
1
+ # -*- encoding : ascii-8bit -*-
2
+
3
+ module DEVp2p
4
+
5
+ ##
6
+ # Packets are emitted and received by subprotocols.
7
+ #
8
+ class Packet
9
+
10
+ attr_accessor :protocol_id, :cmd_id, :prioritize, :payload, :total_payload_size
11
+
12
+ def initialize(protocol_id, cmd_id, payload, prioritize=false)
13
+ @protocol_id = protocol_id
14
+ @cmd_id = cmd_id
15
+ @payload = payload
16
+ @prioritize = prioritize
17
+ end
18
+
19
+ def to_s
20
+ "Packet(protocol_id=#{protocol_id} cmd_id=#{cmd_id} payload_size=#{payload.size} prioritize=#{prioritize})"
21
+ end
22
+
23
+ def ==(other)
24
+ protocol_id == other.protocol_id &&
25
+ cmd_id == other.cmd_id &&
26
+ payload == other.payload
27
+ end
28
+
29
+ def size
30
+ payload.size
31
+ end
32
+ alias :length :size
33
+ end
34
+
35
+ end
@@ -0,0 +1,329 @@
1
+ # -*- encoding : ascii-8bit -*-
2
+
3
+ module DEVp2p
4
+
5
+ class Peer
6
+ include Celluloid::IO
7
+
8
+ DUMB_REMOTE_TIMEOUT = 10.0
9
+
10
+ attr :config, :protocols, :safe_to_read
11
+
12
+ def initialize(peermanager, socket, remote_pubkey=nil)
13
+ @peermanager = peermanager
14
+ @socket = socket
15
+ @config = peermanager.config
16
+
17
+ @protocols = {}
18
+
19
+ @stopped = true
20
+ @hello_received = false
21
+
22
+ @remote_client_version = ''
23
+ logger.debug "peer init", peer: Actor.current
24
+
25
+ privkey = Utils.decode_hex @config[:node][:privkey_hex]
26
+ hello_packet = P2PProtocol.get_hello_packet Actor.current
27
+
28
+ @mux = MultiplexedSession.new privkey, hello_packet, remote_pubkey
29
+ @remote_pubkey = remote_pubkey
30
+
31
+ connect_service @peermanager
32
+
33
+ # assure, we don't get messages while replies are not read
34
+ # TODO: is it safe to use flag + cond to mimic gevent.event.Event?
35
+ @safe_to_read = true
36
+ @safe_to_read_cond = Celluloid::Condition.new
37
+
38
+ _, @port, _, @ip = @socket.peeraddr
39
+
40
+ # stop peer if hello not received in DUMB_REMOTE_TIMEOUT
41
+ after(DUMB_REMOTE_TIMEOUT) { check_if_dumb_remote }
42
+ end
43
+
44
+ ##
45
+ # if peer is responder, then the remote_pubkey will not be available before
46
+ # the first packet is received
47
+ #
48
+ def remote_pubkey
49
+ @mux.remote_pubkey
50
+ end
51
+
52
+ def remote_pubkey=(key)
53
+ @remote_pubkey_available = !!key
54
+ @mux.remote_pubkey = key
55
+ end
56
+
57
+ def to_s
58
+ pn = "#@ip:#@port"
59
+ cv = @remote_client_version.split('/')[0,2].join('/')
60
+ "<Peer #{pn} #{cv}>"
61
+ end
62
+
63
+ def report_error(reason)
64
+ pn = "#@ip:#@port"
65
+ @peermanager.add_error pn, reason, @remote_client_version
66
+ end
67
+
68
+ def connect_service(service)
69
+ raise ArgumentError, "service must be WiredService" unless service.is_a?(WiredService)
70
+
71
+ # create protocol instance which connects peer with service
72
+ protocol_class = service.wire_protocol
73
+ protocol = protocol_class.new Actor.current, service
74
+
75
+ # register protocol
76
+ raise PeerError, 'protocol already connected' if @protocols.has_key?(protocol_class)
77
+ logger.debug "registering protocol", protocol: protocol.name, peer: Actor.current
78
+
79
+ @protocols[protocol_class] = protocol
80
+ @mux.add_protocol protocol.protocol_id
81
+
82
+ protocol.start
83
+ end
84
+
85
+ def has_protocol?(protocol)
86
+ @protocols.has_key?(protocol)
87
+ end
88
+
89
+ def receive_hello(proto, data)
90
+ version = data[:version]
91
+ listen_port = data[:listen_port]
92
+ capabilities = data[:capabilities]
93
+ remote_pubkey = data[:remote_pubkey]
94
+ client_version_string = data[:client_version_string]
95
+
96
+ logger.info 'received hello', version: version, client_version: client_version_string, capabilities: capabilities
97
+
98
+ raise ArgumentError, "invalid remote pubkey" unless remote_pubkey.size == 64
99
+ raise ArgumentError, "remote pubkey mismatch" if @remote_pubkey_available && @remote_pubkey != remote_pubkey
100
+
101
+ @hello_received = true
102
+
103
+ # enable backwards compatibility for legacy peers
104
+ if version < 5
105
+ @offset_based_dispatch = true
106
+ max_window_size = 2**32 # disable chunked transfers
107
+ end
108
+
109
+ # call peermanager
110
+ agree = @peermanager.on_hello_received(proto, version, client_version_string, capabilities, listen_port, remote_pubkey)
111
+ return unless agree
112
+
113
+ @remote_client_version = client_version_string
114
+ @remote_pubkey = remote_pubkey
115
+
116
+ # register in common protocols
117
+ logger.debug 'connecting services', services: @peermanager.wired_services
118
+ remote_services = capabilities.map {|name, version| [name, version] }.to_h
119
+
120
+ @peermanager.wired_services.sort_by(&:name).each do |service|
121
+ raise PeerError, 'invalid service' unless service.is_a?(WiredService)
122
+
123
+ proto = service.wire_protocol
124
+ if remote_services.has_key?(proto.name)
125
+ if remote_services[proto.name] == proto.version
126
+ if service != @peermanager # p2p protocol already registered
127
+ connect_service service
128
+ end
129
+ else
130
+ logger.debug 'wrong version', service: proto.name, local_version: proto.version, remote_version: remote_services[proto.name]
131
+ report_error 'wrong version'
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ def capabilities
138
+ @peermanager.wired_services.map {|s| [s.wire_protocol.name, s.wire_protocol.version] }
139
+ end
140
+
141
+ def send_packet(packet)
142
+ protocol = @protocols.values.find {|pro| pro.protocol_id == packet.protocol_id }
143
+ raise PeerError, "no protocol found" unless protocol
144
+ logger.debug "send packet", cmd: protocol.cmd_by_id[packet.cmd_id], protocol: protocol.name, peer: Actor.current
145
+
146
+ # rewrite cmd_id (backwards compatibility)
147
+ if @offset_based_dispatch
148
+ @protocols.values.each_with_index do |proto, i|
149
+ if packet.protocol_id > i
150
+ packet.cmd_id += (protocol.max_cmd_id == 0 ? 0 : protocol.max_cmd_id + 1)
151
+ end
152
+ if packet.protocol_id == protocol.protocol_id
153
+ protocol = proto
154
+ break
155
+ end
156
+ packet.protocol_id = 0
157
+ end
158
+ end
159
+
160
+ @mux.add_packet packet
161
+ end
162
+
163
+ def send_data(data)
164
+ return if data.nil? || data.empty?
165
+
166
+ @safe_to_read = false
167
+
168
+ @socket.write data
169
+ logger.debug "wrote data", size: data.size
170
+
171
+ @safe_to_read = true
172
+ @safe_to_read_cond.broadcast
173
+ rescue Errno::ETIMEDOUT
174
+ logger.debug "write timeout"
175
+ report_error "write timeout"
176
+ stop
177
+ rescue SystemCallError => e
178
+ logger.debug "write error #{e}"
179
+ report_error "write error #{e}"
180
+ stop
181
+ end
182
+
183
+ def run_ingress_message
184
+ logger.debug "peer starting main loop"
185
+ raise PeerError, 'connection is closed' if @socket.closed?
186
+
187
+ async.run_decoded_packets
188
+ async.run_egress_message
189
+
190
+ while !stopped?
191
+ @safe_to_read_cond.wait unless safe_to_read
192
+
193
+ begin
194
+ @socket.wait_readable
195
+ rescue SystemCallError => e
196
+ logger.debug "read error", error: e, peer: Actor.current
197
+ report_error "network error #{e}"
198
+ if Errno::EBADF === e # bad file descriptor
199
+ stop
200
+ else
201
+ raise e
202
+ break
203
+ end
204
+ end
205
+
206
+ begin
207
+ imsg = @socket.recv(4096)
208
+ rescue EOFError # imsg is empty
209
+ if @socket.closed?
210
+ logger.info "socket closed"
211
+ stop
212
+ else
213
+ imsg = ''
214
+ end
215
+ rescue SystemCallError => e
216
+ logger.debug "read error", error: e, peer: Actor.current
217
+ report_error "network error #{e}"
218
+ if [Errno::ECONNRESET, Errno::ETIMEDOUT, Errno::ENETDOWN, Errno::EHOSTUNREACH].any? {|syserr| e.instance_of?(syserr) }
219
+ stop
220
+ else
221
+ raise e
222
+ break
223
+ end
224
+ end
225
+
226
+ if !imsg.empty?
227
+ logger.debug "read data", size: imsg.size
228
+ @mux.add_message imsg
229
+ end
230
+ end
231
+ rescue RLPxSessionError, DecryptionError => e
232
+ logger.debug "rlpx session error", peer: Actor.current, error: e
233
+ report_error "rlpx session error"
234
+ stop
235
+ rescue MultiplexerError => e
236
+ logger.debug "multiplexer error", peer: Actor.current, error: e
237
+ report_error "multiplexer error"
238
+ stop
239
+ end
240
+ alias run run_ingress_message
241
+
242
+ def start
243
+ @stopped = false
244
+ async.run
245
+ end
246
+
247
+ def stop
248
+ if !stopped?
249
+ @stopped = true
250
+ logger.debug "stopped", peer: Actor.current
251
+
252
+ @protocols.each_value {|proto| proto.stop }
253
+ @peermanager.delete Actor.current
254
+ terminate
255
+ end
256
+ end
257
+
258
+ def stopped?
259
+ @stopped
260
+ end
261
+
262
+ private
263
+
264
+ def logger
265
+ @logger ||= Logger.new "#{@config[:p2p][:listen_port]}.p2p.peer"
266
+ end
267
+
268
+ def handle_packet(packet)
269
+ raise ArgumentError, 'packet must be Packet' unless packet.is_a?(Packet)
270
+
271
+ protocol, cmd_id = protocol_cmd_id_from_packet packet
272
+ logger.debug "recv packet", cmd: protocol.cmd_by_id[cmd_id], protocol: protocol.name, orig_cmd_id: packet.cmd_id
273
+
274
+ packet.cmd_id = cmd_id # rewrite
275
+ protocol.receive_packet packet
276
+ rescue UnknownCommandError
277
+ logger.error 'received unknown cmd', error: e, packet: packet
278
+ end
279
+
280
+ def protocol_cmd_id_from_packet(packet)
281
+ # offset-based dispatch (backwards compatibility)
282
+ if @offset_based_dispatch
283
+ max_id = 0
284
+
285
+ @protocols.each_value do |protocol|
286
+ if packet.cmd_id < max_id + protocol.max_cmd_id + 1
287
+ return protocol, packet.cmd_id - (max_id == 0 ? 0 : max_id + 1)
288
+ max_id += protocol.max_cmd_id
289
+ end
290
+ end
291
+ raise UnknownCommandError, "no protocol for id #{packet.cmd_id}"
292
+ end
293
+
294
+ # new-style dispatch based on protocol_id
295
+ @protocols.values.each_with_index do |protocol, i|
296
+ if packet.protocol_id == protocol.protocol_id
297
+ return protocol, packet.cmd_id
298
+ end
299
+ end
300
+ raise UnknownCommandError, "no protocol for protocol id #{packet.protocol_id}"
301
+ end
302
+
303
+ ##
304
+ # Stop peer if hello not received
305
+ #
306
+ def check_if_dumb_remote
307
+ if !@hello_received
308
+ report_error "No hello in #{DUMB_REMOTE_TIMEOUT} seconds"
309
+ stop
310
+ end
311
+ end
312
+
313
+ def run_egress_message
314
+ while !stopped?
315
+ # TODO: async.send_data?
316
+ send_data @mux.get_message
317
+ end
318
+ end
319
+
320
+ def run_decoded_packets
321
+ while !stopped?
322
+ # TODO: async.handle_packet?
323
+ handle_packet @mux.get_packet # get_packet blocks
324
+ end
325
+ end
326
+
327
+ end
328
+
329
+ end