ciri 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +45 -0
- data/LICENSE.txt +21 -0
- data/README.md +49 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/ciri.gemspec +32 -0
- data/lib/ciri.rb +26 -0
- data/lib/ciri/crypto.rb +143 -0
- data/lib/ciri/devp2p/actor.rb +224 -0
- data/lib/ciri/devp2p/peer.rb +132 -0
- data/lib/ciri/devp2p/protocol.rb +44 -0
- data/lib/ciri/devp2p/protocol_io.rb +66 -0
- data/lib/ciri/devp2p/rlpx.rb +28 -0
- data/lib/ciri/devp2p/rlpx/connection.rb +179 -0
- data/lib/ciri/devp2p/rlpx/encryption_handshake.rb +143 -0
- data/lib/ciri/devp2p/rlpx/error.rb +34 -0
- data/lib/ciri/devp2p/rlpx/frame_io.rb +221 -0
- data/lib/ciri/devp2p/rlpx/message.rb +45 -0
- data/lib/ciri/devp2p/rlpx/node.rb +77 -0
- data/lib/ciri/devp2p/rlpx/protocol_handshake.rb +55 -0
- data/lib/ciri/devp2p/rlpx/protocol_messages.rb +69 -0
- data/lib/ciri/devp2p/rlpx/secrets.rb +49 -0
- data/lib/ciri/devp2p/server.rb +207 -0
- data/lib/ciri/key.rb +81 -0
- data/lib/ciri/rlp.rb +88 -0
- data/lib/ciri/rlp/decode.rb +81 -0
- data/lib/ciri/rlp/encode.rb +79 -0
- data/lib/ciri/rlp/serializable.rb +268 -0
- data/lib/ciri/utils.rb +75 -0
- data/lib/ciri/version.rb +5 -0
- metadata +179 -0
@@ -0,0 +1,224 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) 2018, by Jiang Jinyang. <https://justjjy.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 'logger'
|
25
|
+
|
26
|
+
module Ciri
|
27
|
+
module DevP2P
|
28
|
+
|
29
|
+
# simple actor model implementation
|
30
|
+
# Example:
|
31
|
+
#
|
32
|
+
# class Hello
|
33
|
+
# include Actor
|
34
|
+
#
|
35
|
+
# def say_hello
|
36
|
+
# puts 'hello world'
|
37
|
+
# 'hello world'
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# actor = Hello.new
|
42
|
+
# # start actor loop
|
43
|
+
# actor.start
|
44
|
+
# # push message to actor inbox
|
45
|
+
# actor << :say_hello
|
46
|
+
# # push message and wait until get response
|
47
|
+
# actor.call(:say_hello).value
|
48
|
+
#
|
49
|
+
# # raise error
|
50
|
+
# actor.call(:hello).value # NoMethodError
|
51
|
+
#
|
52
|
+
# # stop actor
|
53
|
+
# actor.send_stop
|
54
|
+
# actor.wait
|
55
|
+
#
|
56
|
+
module Actor
|
57
|
+
|
58
|
+
LOGGER = Logger.new(STDERR, datetime_format: '%Y-%m-%d %H:%M:%S', level: Logger::INFO)
|
59
|
+
|
60
|
+
# future, use this to wait actor msg respond
|
61
|
+
class Future
|
62
|
+
def initialize
|
63
|
+
@value = nil
|
64
|
+
@done = false
|
65
|
+
end
|
66
|
+
|
67
|
+
def value=(val)
|
68
|
+
if @done
|
69
|
+
raise RuntimeError.new('future value duplicated set')
|
70
|
+
end
|
71
|
+
@done = true
|
72
|
+
@queue << :done if @queue
|
73
|
+
@value = val
|
74
|
+
end
|
75
|
+
|
76
|
+
def value
|
77
|
+
loop do
|
78
|
+
if @done
|
79
|
+
return @value
|
80
|
+
elsif @error
|
81
|
+
raise @error
|
82
|
+
else
|
83
|
+
queue.pop
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def raise_error(error)
|
89
|
+
error.set_backtrace(caller) if error.backtrace.nil?
|
90
|
+
@error = error
|
91
|
+
@queue << :error if @queue
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
def queue
|
96
|
+
@queue ||= Queue.new
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class Error < StandardError
|
101
|
+
end
|
102
|
+
|
103
|
+
# stop actor
|
104
|
+
class StopError < Error
|
105
|
+
end
|
106
|
+
|
107
|
+
class StateError < Error
|
108
|
+
end
|
109
|
+
|
110
|
+
attr_accessor :executor
|
111
|
+
|
112
|
+
def initialize(executor: nil)
|
113
|
+
@inbox = Queue.new
|
114
|
+
@executor = executor
|
115
|
+
@future = Future.new
|
116
|
+
@running = false
|
117
|
+
end
|
118
|
+
|
119
|
+
# async call
|
120
|
+
def enqueue(method, *args)
|
121
|
+
self << [method, *args]
|
122
|
+
end
|
123
|
+
|
124
|
+
def <<(args)
|
125
|
+
@inbox << args
|
126
|
+
end
|
127
|
+
|
128
|
+
# sync call, push msg to inbox, and return future
|
129
|
+
#
|
130
|
+
# Example:
|
131
|
+
# future = actor.call(:result) # future
|
132
|
+
# future.value # blocking and wait for result
|
133
|
+
#
|
134
|
+
def call(method, *args)
|
135
|
+
future = Future.new
|
136
|
+
self << [future, method, *args]
|
137
|
+
future
|
138
|
+
end
|
139
|
+
|
140
|
+
# start actor
|
141
|
+
def start
|
142
|
+
raise Error.new("must set executor before start") unless executor
|
143
|
+
|
144
|
+
@running = true
|
145
|
+
executor.post do
|
146
|
+
start_loop
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# send stop to actor
|
151
|
+
#
|
152
|
+
# Example:
|
153
|
+
# actor.send_stop
|
154
|
+
# # wait for actor actually stopped
|
155
|
+
# actor.wait
|
156
|
+
#
|
157
|
+
def send_stop
|
158
|
+
self << [:raise_error, StopError.new]
|
159
|
+
end
|
160
|
+
|
161
|
+
# wait until an error occurs
|
162
|
+
def wait
|
163
|
+
raise StateError.new('actor not running!') unless @running
|
164
|
+
@future.value
|
165
|
+
end
|
166
|
+
|
167
|
+
# start loop
|
168
|
+
def start_loop
|
169
|
+
loop_callback do |wait_message: true|
|
170
|
+
# check inbox
|
171
|
+
next Thread.pass if @inbox.empty? && !wait_message
|
172
|
+
msg = @inbox.pop
|
173
|
+
|
174
|
+
# extract sync or async call
|
175
|
+
future = nil
|
176
|
+
method, *args = msg
|
177
|
+
if method.is_a?(Future)
|
178
|
+
future = method
|
179
|
+
method, *args = args
|
180
|
+
end
|
181
|
+
begin
|
182
|
+
val = send(method, *args)
|
183
|
+
rescue StandardError => e
|
184
|
+
future.raise_error(e) if future
|
185
|
+
raise
|
186
|
+
end
|
187
|
+
# if future not nil, set value
|
188
|
+
future.value = val if future
|
189
|
+
end while true
|
190
|
+
|
191
|
+
rescue StopError
|
192
|
+
# actor stop
|
193
|
+
@future.value = nil
|
194
|
+
rescue StandardError => e
|
195
|
+
@future.raise_error e
|
196
|
+
LOGGER.error("Actor #{self}") {"#{e}\n#{e.backtrace.join("\n")}"}
|
197
|
+
ensure
|
198
|
+
@running = false
|
199
|
+
end
|
200
|
+
|
201
|
+
# allow inject callback into actor loop
|
202
|
+
# Example:
|
203
|
+
#
|
204
|
+
# class A
|
205
|
+
# include Actor
|
206
|
+
#
|
207
|
+
# def loop_callback
|
208
|
+
# # before handle msg
|
209
|
+
# yield
|
210
|
+
# # after handle msg
|
211
|
+
# end
|
212
|
+
# end
|
213
|
+
#
|
214
|
+
def loop_callback
|
215
|
+
yield
|
216
|
+
end
|
217
|
+
|
218
|
+
def raise_error(e)
|
219
|
+
raise e
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
end
|
224
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
|
4
|
+
# Copyright (c) 2018, by Jiang Jinyang. <https://justjjy.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_relative 'rlpx'
|
26
|
+
require_relative 'actor'
|
27
|
+
require_relative 'protocol_io'
|
28
|
+
|
29
|
+
module Ciri
|
30
|
+
module DevP2P
|
31
|
+
|
32
|
+
# represent a connected remote node
|
33
|
+
class Peer
|
34
|
+
|
35
|
+
class DiscoverError < StandardError
|
36
|
+
end
|
37
|
+
class UnknownMessageCodeError < StandardError
|
38
|
+
end
|
39
|
+
|
40
|
+
include Actor
|
41
|
+
|
42
|
+
attr_reader :connection
|
43
|
+
|
44
|
+
def initialize(connection, handshake, protocols)
|
45
|
+
@connection = connection
|
46
|
+
@handshake = handshake
|
47
|
+
@protocols = protocols
|
48
|
+
@protocol_io_hash = make_protocol_io_hash(protocols, handshake.caps, connection)
|
49
|
+
super()
|
50
|
+
end
|
51
|
+
|
52
|
+
def node_id
|
53
|
+
@node_id ||= RLPX::NodeID.from_raw_id(@handshake.id)
|
54
|
+
end
|
55
|
+
|
56
|
+
# start peer
|
57
|
+
# handle msg, handle sub protocols
|
58
|
+
def start
|
59
|
+
executor.post {read_loop}
|
60
|
+
start_protocols
|
61
|
+
super
|
62
|
+
end
|
63
|
+
|
64
|
+
# read and handle msg
|
65
|
+
def read_loop
|
66
|
+
loop do
|
67
|
+
msg = connection.read_msg
|
68
|
+
msg.received_at = Time.now
|
69
|
+
handle(msg)
|
70
|
+
end
|
71
|
+
rescue StandardError => e
|
72
|
+
self << [:raise_error, e]
|
73
|
+
end
|
74
|
+
|
75
|
+
def start_protocols
|
76
|
+
@protocols.each do |protocol|
|
77
|
+
protocol.start.(self, @protocol_io_hash[protocol.name])
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def handle(msg)
|
82
|
+
if msg.code == RLPX::MESSAGES[:ping]
|
83
|
+
#TODO send pong
|
84
|
+
elsif msg.code == RLPX::MESSAGES[:discover]
|
85
|
+
reason = RLP.decode_with_type(msg.payload, Integer)
|
86
|
+
raise DiscoverError.new("receive error discovery message, reason: #{reason}")
|
87
|
+
else
|
88
|
+
# send msg to sub protocol
|
89
|
+
if (protocol_io = find_protocol_io_by_msg_code(msg.code)).nil?
|
90
|
+
raise UnknownMessageCodeError.new("can't find protocol with msg code #{msg.code}")
|
91
|
+
end
|
92
|
+
protocol_io.msg_queue << msg
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
def find_protocol_io_by_msg_code(code)
|
98
|
+
@protocol_io_hash.values.find do |protocol_io|
|
99
|
+
offset = protocol_io.offset
|
100
|
+
protocol = protocol_io.protocol
|
101
|
+
code >= offset && code < offset + protocol.length
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# return protocol_io_hash
|
106
|
+
# handle multiple sub protocols upon one io
|
107
|
+
def make_protocol_io_hash(protocols, caps, io)
|
108
|
+
# sub protocol offset
|
109
|
+
offset = RLPX::BASE_PROTOCOL_LENGTH
|
110
|
+
result = {}
|
111
|
+
# [name, version] as key
|
112
|
+
protocols_hash = protocols.map {|protocol| [[protocol.name, protocol.version], protocol]}.to_h
|
113
|
+
sorted_caps = caps.sort_by {|c| [c.name, c.version]}
|
114
|
+
|
115
|
+
sorted_caps.each do |cap|
|
116
|
+
protocol = protocols_hash[[cap.name, cap.version]]
|
117
|
+
next unless protocol
|
118
|
+
# ignore same name old protocols
|
119
|
+
if (old = result[cap.name])
|
120
|
+
result.delete(cap.name)
|
121
|
+
offset -= old.protocol.length
|
122
|
+
end
|
123
|
+
result[cap.name] = ProtocolIO.new(protocol, offset, io)
|
124
|
+
# move offset, to support next protocol
|
125
|
+
offset += protocol.length
|
126
|
+
end
|
127
|
+
result
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
|
4
|
+
# Copyright (c) 2018, by Jiang Jinyang. <https://justjjy.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_relative 'actor'
|
26
|
+
|
27
|
+
module Ciri
|
28
|
+
module DevP2P
|
29
|
+
|
30
|
+
# protocol represent DevP2P sub protocols
|
31
|
+
class Protocol
|
32
|
+
|
33
|
+
attr_reader :name, :version, :length
|
34
|
+
attr_accessor :node_info, :peer_info, :start
|
35
|
+
|
36
|
+
def initialize(name:, version:, length:)
|
37
|
+
@name = name
|
38
|
+
@version = version
|
39
|
+
@length = length
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) 2018, by Jiang Jinyang. <https://justjjy.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_relative 'actor'
|
25
|
+
require_relative 'rlpx/message'
|
26
|
+
|
27
|
+
module Ciri
|
28
|
+
module DevP2P
|
29
|
+
|
30
|
+
# send/read sub protocol msg
|
31
|
+
class ProtocolIO
|
32
|
+
|
33
|
+
class Error < StandardError
|
34
|
+
end
|
35
|
+
class InvalidMessageCode < Error
|
36
|
+
end
|
37
|
+
|
38
|
+
attr_reader :protocol, :offset, :msg_queue
|
39
|
+
|
40
|
+
def initialize(protocol, offset, frame_io)
|
41
|
+
@protocol = protocol
|
42
|
+
@offset = offset
|
43
|
+
@frame_io = frame_io
|
44
|
+
@msg_queue = Queue.new
|
45
|
+
end
|
46
|
+
|
47
|
+
def send_data(code, data)
|
48
|
+
msg = RLPX::Message.new(code: code, size: data.size, payload: data)
|
49
|
+
write_msg(msg)
|
50
|
+
end
|
51
|
+
|
52
|
+
def write_msg(msg)
|
53
|
+
raise InvalidMessageCode, "code #{code} must less than length #{protocol.length}" if msg.code > protocol.length
|
54
|
+
msg.code += offset
|
55
|
+
@frame_io.write_msg(msg)
|
56
|
+
end
|
57
|
+
|
58
|
+
def read_msg
|
59
|
+
msg = msg_queue.pop
|
60
|
+
msg.code -= offset
|
61
|
+
msg
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|