ciri 0.0.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/.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
|