devp2p 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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