ciri-p2p 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +15 -0
  5. data/.vscode/launch.json +90 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +6 -0
  8. data/Gemfile.lock +65 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +45 -0
  11. data/Rakefile +6 -0
  12. data/bin/bundle +105 -0
  13. data/bin/console +14 -0
  14. data/bin/htmldiff +29 -0
  15. data/bin/ldiff +29 -0
  16. data/bin/rake +29 -0
  17. data/bin/rspec +29 -0
  18. data/bin/setup +8 -0
  19. data/ciri-p2p.gemspec +37 -0
  20. data/lib/ciri/p2p.rb +7 -0
  21. data/lib/ciri/p2p/address.rb +51 -0
  22. data/lib/ciri/p2p/dial_scheduler.rb +73 -0
  23. data/lib/ciri/p2p/dialer.rb +55 -0
  24. data/lib/ciri/p2p/discovery/protocol.rb +237 -0
  25. data/lib/ciri/p2p/discovery/service.rb +255 -0
  26. data/lib/ciri/p2p/errors.rb +36 -0
  27. data/lib/ciri/p2p/kad.rb +301 -0
  28. data/lib/ciri/p2p/network_state.rb +223 -0
  29. data/lib/ciri/p2p/node.rb +96 -0
  30. data/lib/ciri/p2p/peer.rb +151 -0
  31. data/lib/ciri/p2p/peer_store.rb +183 -0
  32. data/lib/ciri/p2p/protocol.rb +62 -0
  33. data/lib/ciri/p2p/protocol_context.rb +54 -0
  34. data/lib/ciri/p2p/protocol_io.rb +65 -0
  35. data/lib/ciri/p2p/rlpx.rb +29 -0
  36. data/lib/ciri/p2p/rlpx/connection.rb +182 -0
  37. data/lib/ciri/p2p/rlpx/encryption_handshake.rb +143 -0
  38. data/lib/ciri/p2p/rlpx/errors.rb +34 -0
  39. data/lib/ciri/p2p/rlpx/frame_io.rb +229 -0
  40. data/lib/ciri/p2p/rlpx/message.rb +45 -0
  41. data/lib/ciri/p2p/rlpx/protocol_handshake.rb +56 -0
  42. data/lib/ciri/p2p/rlpx/protocol_messages.rb +71 -0
  43. data/lib/ciri/p2p/rlpx/secrets.rb +49 -0
  44. data/lib/ciri/p2p/server.rb +159 -0
  45. data/lib/ciri/p2p/version.rb +5 -0
  46. metadata +229 -0
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ # Copyright (c) 2018 by Jiang Jinyang <jjyruby@gmail.com>
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ # THE SOFTWARE.
23
+
24
+
25
+ require 'async'
26
+ require 'ciri/utils/logger'
27
+ require_relative 'peer'
28
+ require_relative 'errors'
29
+ require_relative 'protocol_context'
30
+
31
+ module Ciri
32
+ module P2P
33
+
34
+ # NetworkState
35
+ # maintaining current connected peers
36
+ class NetworkState
37
+ include Utils::Logger
38
+
39
+ attr_reader :peers, :caps, :peer_store, :local_node_id
40
+
41
+ def initialize(protocols:, peer_store:, local_node_id:, max_outgoing: 10, max_incoming: 10, ping_interval_secs: 15)
42
+ @peers = {}
43
+ @peer_store = peer_store
44
+ @protocols = protocols
45
+ @local_node_id = local_node_id
46
+ @max_outgoing = max_outgoing
47
+ @max_incoming = max_incoming
48
+ @ping_interval_secs = ping_interval_secs
49
+ end
50
+
51
+ def initialize_protocols(task: Async::Task.current)
52
+ # initialize protocols
53
+ @protocols.each do |protocol|
54
+ context = ProtocolContext.new(self)
55
+ task.async {protocol.initialized(context)}
56
+ end
57
+ end
58
+
59
+ def number_of_attemp_outgoing
60
+ @max_outgoing - @peers.values.select(&:outgoing?).count
61
+ end
62
+
63
+ def new_peer_connected(connection, handshake, way_for_connection:, task: Async::Task.current)
64
+ protocol_handshake_checks(handshake)
65
+ peer = Peer.new(connection, handshake, @protocols, way_for_connection: way_for_connection)
66
+ # disconnect already connected peers
67
+ if @peers.include?(peer.raw_node_id)
68
+ debug("[#{local_node_id.short_hex}] peer #{peer} is already connected")
69
+ return
70
+ end
71
+ @peers[peer.raw_node_id] = peer
72
+ debug "[#{local_node_id.short_hex}] connect to new peer #{peer}"
73
+ @peer_store.update_peer_status(peer.raw_node_id, PeerStore::Status::CONNECTED)
74
+ # run peer logic
75
+ task.async do
76
+ register_peer_protocols(peer)
77
+ handling_peer(peer)
78
+ end
79
+ end
80
+
81
+ def remove_peer(peer)
82
+ @peers.delete(peer.raw_node_id)
83
+ deregister_peer_protocols(peer)
84
+ end
85
+
86
+ def disconnect_peer(peer, reason: nil)
87
+ return unless @peers.include?(peer.raw_node_id)
88
+ debug("[#{local_node_id.short_hex}] disconnect peer: #{peer}, reason: #{reason}")
89
+ remove_peer(peer)
90
+ peer.disconnect
91
+ @peer_store.update_peer_status(peer.raw_node_id, PeerStore::Status::DISCONNECTED)
92
+ end
93
+
94
+ def disconnect_all
95
+ debug("[#{local_node_id.short_hex}] disconnect all")
96
+ peers.each_value do |peer|
97
+ disconnect_peer(peer, reason: "disconnect all...")
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ def register_peer_protocols(peer, task: Async::Task.current)
104
+ peer.protocol_ios.dup.each do |protocol_io|
105
+ task.async do
106
+ # Protocol#connected
107
+ context = ProtocolContext.new(self, peer: peer, protocol: protocol_io.protocol, protocol_io: protocol_io)
108
+ context.protocol.connected(context)
109
+ rescue StandardError => e
110
+ error("Protocol#connected error: {e}\nbacktrace: #{e.backtrace.join "\n"}")
111
+ disconnect_peer(peer, reason: "Protocol#connected callback error: #{e}")
112
+ end
113
+ end
114
+ end
115
+
116
+ def deregister_peer_protocols(peer, task: Async::Task.current)
117
+ peer.protocol_ios.dup.each do |protocol_io|
118
+ task.async do
119
+ # Protocol#connected
120
+ context = ProtocolContext.new(self, peer: peer, protocol: protocol_io.protocol, protocol_io: protocol_io)
121
+ context.protocol.disconnected(context)
122
+ rescue StandardError => e
123
+ error("Protocol#disconnected error: {e}\nbacktrace: #{e.backtrace.join "\n"}")
124
+ disconnect_peer(peer, reason: "Protocol#disconnected callback error: #{e}")
125
+ end
126
+ end
127
+ end
128
+
129
+ # handling peer IO
130
+ def handling_peer(peer, task: Async::Task.current)
131
+ start_peer_io(peer)
132
+ rescue Exception => e
133
+ remove_peer(peer)
134
+ error("remove peer #{peer}, error: #{e}")
135
+ end
136
+
137
+ # starting peer IO loop
138
+ def start_peer_io(peer, task: Async::Task.current)
139
+ ping_timer = task.reactor.every(@ping_interval_secs) do
140
+ task.async do
141
+ ping(peer)
142
+ rescue StandardError => e
143
+ disconnect_peer(peer, reason: "ping error: #{e}")
144
+ end
145
+ end
146
+
147
+ message_service = task.async do
148
+ loop do
149
+ raise DisconnectError.new("disconnect peer") if @disconnect
150
+ msg = peer.connection.read_msg
151
+ msg.received_at = Time.now
152
+ handle_message(peer, msg)
153
+ end
154
+ rescue StandardError => e
155
+ disconnect_peer(peer, reason: "io error: #{e}\n#{e.backtrace.join "\n"}")
156
+ end
157
+
158
+ message_service.wait
159
+ end
160
+
161
+ BLANK_PAYLOAD = RLP.encode([]).freeze
162
+
163
+ # response pong to message
164
+ def ping(peer)
165
+ peer.connection.send_data(RLPX::Code::PING, BLANK_PAYLOAD)
166
+ end
167
+
168
+ # response pong to message
169
+ def pong(peer)
170
+ peer.connection.send_data(RLPX::Code::PONG, BLANK_PAYLOAD)
171
+ end
172
+
173
+ # handle peer message
174
+ def handle_message(peer, msg, task: Async::Task.current)
175
+ if msg.code == RLPX::Code::PING
176
+ pong(peer)
177
+ elsif msg.code == RLPX::Code::DISCONNECT
178
+ reason = RLP.decode_with_type(msg.payload, Integer)
179
+ raise DisconnectError.new("receive disconnect message, reason: #{reason}")
180
+ elsif msg.code == RLPX::Code::PONG
181
+ # TODO update peer node
182
+ else
183
+ # send msg to sub protocol
184
+ if (protocol_io = peer.find_protocol_io_by_msg_code(msg.code)).nil?
185
+ raise UnknownMessageCodeError.new("can't find protocol with msg code #{msg.code}")
186
+ end
187
+ # fix msg code
188
+ msg.code -= protocol_io.offset
189
+ task.async do
190
+ # Protocol#received
191
+ context = ProtocolContext.new(self, peer: peer, protocol: protocol_io.protocol, protocol_io: protocol_io)
192
+ context.protocol.received(context, msg)
193
+ end
194
+ end
195
+ end
196
+
197
+ def protocol_handshake_checks(handshake)
198
+ if @protocols && count_matching_protocols(handshake.caps) == 0
199
+ raise UselessPeerError.new('discovery useless peer')
200
+ end
201
+ end
202
+
203
+ # {cap_name => cap_version}
204
+ def caps_hash
205
+ @caps_hash ||= @protocols.sort_by do |cap|
206
+ cap.version
207
+ end.reduce({}) do |caps_hash, cap|
208
+ caps_hash[cap.name] = cap.version
209
+ caps_hash
210
+ end
211
+ end
212
+
213
+ # calculate count of matched protocols caps
214
+ def count_matching_protocols(caps)
215
+ caps.select do |cap|
216
+ caps_hash[cap.name] == cap.version
217
+ end.count
218
+ end
219
+ end
220
+
221
+ end
222
+ end
223
+
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2018 by Jiang Jinyang <jjyruby@gmail.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+
23
+
24
+ require 'ciri/key'
25
+ require 'ciri/utils'
26
+
27
+ module Ciri
28
+ module P2P
29
+
30
+ # present node id
31
+ class NodeID
32
+
33
+ class << self
34
+ def from_raw_id(raw_id)
35
+ NodeID.new(Ciri::Key.new(raw_public_key: "\x04".b + raw_id))
36
+ end
37
+ end
38
+
39
+ attr_reader :public_key
40
+
41
+ alias key public_key
42
+
43
+ def initialize(public_key)
44
+ unless public_key.is_a?(Ciri::Key)
45
+ raise TypeError.new("expect Ciri::Key but get #{public_key.class}")
46
+ end
47
+ @public_key = public_key
48
+ end
49
+
50
+ def id
51
+ @id ||= key.raw_public_key[1..-1]
52
+ end
53
+
54
+ alias to_bytes id
55
+
56
+ def == (other)
57
+ self.class == other.class && id == other.id
58
+ end
59
+
60
+ def to_hex
61
+ Ciri::Utils.to_hex id
62
+ end
63
+
64
+ alias to_s to_hex
65
+
66
+ def short_hex
67
+ @short_hex ||= to_hex[0..8]
68
+ end
69
+
70
+ end
71
+
72
+ class Node
73
+ attr_reader :node_id, :added_at
74
+ attr_accessor :addresses
75
+
76
+ def initialize(raw_node_id: nil,
77
+ node_id: raw_node_id && NodeID.from_raw_id(raw_node_id),
78
+ addresses:,
79
+ added_at: nil)
80
+ @node_id = node_id
81
+ @addresses = addresses
82
+ @added_at = added_at
83
+ end
84
+
85
+ def == (other)
86
+ self.class == other.class && node_id == other.node_id
87
+ end
88
+
89
+ def raw_node_id
90
+ node_id.to_bytes
91
+ end
92
+ end
93
+
94
+ end
95
+ end
96
+
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ # Copyright (c) 2018 by Jiang Jinyang <jjyruby@gmail.com>
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ # THE SOFTWARE.
23
+
24
+
25
+ require 'ciri/utils'
26
+ require 'ciri/rlp'
27
+ require_relative 'rlpx'
28
+ require_relative 'protocol_io'
29
+
30
+ module Ciri
31
+ module P2P
32
+
33
+ # represent a connected remote node
34
+ class Peer
35
+ OUTGOING = :outgoing
36
+ INCOMING = :incoming
37
+
38
+ attr_reader :connection
39
+
40
+ def initialize(connection, handshake, protocols, way_for_connection:)
41
+ @connection = connection
42
+ @handshake = handshake
43
+ @protocols = protocols
44
+ @protocol_io_hash = make_protocol_io_hash(protocols, handshake.caps, connection)
45
+ @way_for_connection = way_for_connection
46
+ end
47
+
48
+ def outgoing?
49
+ @way_for_connection == OUTGOING
50
+ end
51
+
52
+ def incoming?
53
+ @way_for_connection == INCOMING
54
+ end
55
+
56
+ def to_s
57
+ @display_name ||= begin
58
+ Utils.to_hex(node_id.id)[0..8]
59
+ end
60
+ end
61
+
62
+ def inspect
63
+ "<Peer:#{to_s}>"
64
+ end
65
+
66
+ def hash
67
+ raw_node_id.hash
68
+ end
69
+
70
+ def ==(peer)
71
+ self.class == peer.class && raw_node_id == peer.raw_node_id
72
+ end
73
+
74
+ alias eql? ==
75
+
76
+ # get id of node in bytes form
77
+ def raw_node_id
78
+ node_id.to_bytes
79
+ end
80
+
81
+ # get NodeID object
82
+ def node_id
83
+ @node_id ||= NodeID.from_raw_id(@handshake.id)
84
+ end
85
+
86
+ # disconnect peer connections
87
+ def disconnect
88
+ @connection.close
89
+ end
90
+
91
+ def disconnected?
92
+ @connection.closed?
93
+ end
94
+
95
+ def protocol_ios
96
+ @protocol_io_hash.values
97
+ end
98
+
99
+ def find_protocol(name)
100
+ @protocol.find do |protocol|
101
+ protocol.name == name
102
+ end
103
+ end
104
+
105
+ def find_protocol_io(name)
106
+ protocol_ios.find do |protocol_io|
107
+ protocol_io.protocol.name == name
108
+ end
109
+ end
110
+
111
+ # find ProtocolIO by raw message code
112
+ # used by DEVP2P to find stream of sub-protocol
113
+ def find_protocol_io_by_msg_code(raw_code)
114
+ @protocol_io_hash.values.find do |protocol_io|
115
+ offset = protocol_io.offset
116
+ protocol = protocol_io.protocol
117
+ raw_code >= offset && raw_code < offset + protocol.length
118
+ end
119
+ end
120
+
121
+ private
122
+
123
+ # return protocol_io_hash
124
+ # handle multiple sub protocols upon one io
125
+ def make_protocol_io_hash(protocols, caps, io)
126
+ # sub protocol offset
127
+ offset = RLPX::BASE_PROTOCOL_LENGTH
128
+ result = {}
129
+ # [name, version] as key
130
+ protocols_hash = protocols.map {|protocol| [[protocol.name, protocol.version], protocol]}.to_h
131
+ sorted_caps = caps.sort_by {|c| [c.name, c.version]}
132
+
133
+ sorted_caps.each do |cap|
134
+ protocol = protocols_hash[[cap.name, cap.version]]
135
+ next unless protocol
136
+ # ignore same name old protocols
137
+ if (old = result[cap.name])
138
+ result.delete(cap.name)
139
+ offset -= old.protocol.length
140
+ end
141
+ result[cap.name] = ProtocolIO.new(protocol, offset, io)
142
+ # move offset, to support next protocol
143
+ offset += protocol.length
144
+ end
145
+ result
146
+ end
147
+ end
148
+
149
+ end
150
+ end
151
+