ciri-p2p 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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +15 -0
- data/.vscode/launch.json +90 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +65 -0
- data/LICENSE.txt +21 -0
- data/README.md +45 -0
- data/Rakefile +6 -0
- data/bin/bundle +105 -0
- data/bin/console +14 -0
- data/bin/htmldiff +29 -0
- data/bin/ldiff +29 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/bin/setup +8 -0
- data/ciri-p2p.gemspec +37 -0
- data/lib/ciri/p2p.rb +7 -0
- data/lib/ciri/p2p/address.rb +51 -0
- data/lib/ciri/p2p/dial_scheduler.rb +73 -0
- data/lib/ciri/p2p/dialer.rb +55 -0
- data/lib/ciri/p2p/discovery/protocol.rb +237 -0
- data/lib/ciri/p2p/discovery/service.rb +255 -0
- data/lib/ciri/p2p/errors.rb +36 -0
- data/lib/ciri/p2p/kad.rb +301 -0
- data/lib/ciri/p2p/network_state.rb +223 -0
- data/lib/ciri/p2p/node.rb +96 -0
- data/lib/ciri/p2p/peer.rb +151 -0
- data/lib/ciri/p2p/peer_store.rb +183 -0
- data/lib/ciri/p2p/protocol.rb +62 -0
- data/lib/ciri/p2p/protocol_context.rb +54 -0
- data/lib/ciri/p2p/protocol_io.rb +65 -0
- data/lib/ciri/p2p/rlpx.rb +29 -0
- data/lib/ciri/p2p/rlpx/connection.rb +182 -0
- data/lib/ciri/p2p/rlpx/encryption_handshake.rb +143 -0
- data/lib/ciri/p2p/rlpx/errors.rb +34 -0
- data/lib/ciri/p2p/rlpx/frame_io.rb +229 -0
- data/lib/ciri/p2p/rlpx/message.rb +45 -0
- data/lib/ciri/p2p/rlpx/protocol_handshake.rb +56 -0
- data/lib/ciri/p2p/rlpx/protocol_messages.rb +71 -0
- data/lib/ciri/p2p/rlpx/secrets.rb +49 -0
- data/lib/ciri/p2p/server.rb +159 -0
- data/lib/ciri/p2p/version.rb +5 -0
- metadata +229 -0
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "ciri/p2p"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/htmldiff
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'htmldiff' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("diff-lcs", "htmldiff")
|
data/bin/ldiff
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'ldiff' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("diff-lcs", "ldiff")
|
data/bin/rake
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rake' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("rake", "rake")
|
data/bin/rspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rspec' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("rspec-core", "rspec")
|
data/bin/setup
ADDED
data/ciri-p2p.gemspec
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "ciri/p2p/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "ciri-p2p"
|
8
|
+
spec.version = Ciri::P2p::VERSION
|
9
|
+
spec.authors = ["jjy"]
|
10
|
+
spec.email = ["jjyruby@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{P2P network implementation for Ciri Ethereum.}
|
13
|
+
spec.description = %q{ciri-p2p is a DevP2P implementation, we also seek to implement LibP2P components upon ciri-p2p codebase in the future.}
|
14
|
+
spec.homepage = "https://github.com/ciri-ethereum/ciri-p2p"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
# Specify which files should be added to the gem when it is released.
|
18
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
19
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
20
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
21
|
+
end
|
22
|
+
spec.bindir = "exe"
|
23
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
24
|
+
spec.require_paths = ["lib"]
|
25
|
+
|
26
|
+
spec.add_dependency 'ciri-utils', '~> 0.2.2'
|
27
|
+
spec.add_dependency 'ciri-rlp', '~> 1.0.1'
|
28
|
+
spec.add_dependency 'ciri-crypto', '~> 0.1.1'
|
29
|
+
spec.add_dependency 'ciri-common', '~> 0.1.0'
|
30
|
+
spec.add_dependency 'async', '~> 1.10.3'
|
31
|
+
spec.add_dependency 'async-io', '~> 1.15.5'
|
32
|
+
spec.add_dependency 'snappy', '~> 0.0.17'
|
33
|
+
|
34
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
35
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
36
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
37
|
+
end
|
data/lib/ciri/p2p.rb
ADDED
@@ -0,0 +1,51 @@
|
|
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
|
+
module Ciri
|
25
|
+
module P2P
|
26
|
+
|
27
|
+
class Address
|
28
|
+
attr_reader :ip, :udp_port, :tcp_port
|
29
|
+
|
30
|
+
def initialize(ip:, udp_port:, tcp_port: udp_port)
|
31
|
+
@ip = ip.is_a?(IPAddr) ? ip : IPAddr.new(ip)
|
32
|
+
@udp_port = udp_port
|
33
|
+
@tcp_port = tcp_port
|
34
|
+
end
|
35
|
+
|
36
|
+
def ==(other)
|
37
|
+
self.class == other.class && ip == other.ip && udp_port == other.udp_port
|
38
|
+
end
|
39
|
+
|
40
|
+
def <=>(other)
|
41
|
+
ip <=> other.ip
|
42
|
+
end
|
43
|
+
|
44
|
+
def inspect
|
45
|
+
"<PeerStore::Address #{ip.inspect} udp_port: #{udp_port} tcp_port: #{tcp_port}>"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
@@ -0,0 +1,73 @@
|
|
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
|
+
|
28
|
+
module Ciri
|
29
|
+
module P2P
|
30
|
+
|
31
|
+
# DialScheduler
|
32
|
+
# establish outoging connections
|
33
|
+
class DialScheduler
|
34
|
+
include Utils::Logger
|
35
|
+
|
36
|
+
def initialize(network_state, dialer, dial_outgoing_interval_secs: 15)
|
37
|
+
@network_state = network_state
|
38
|
+
@dialer = dialer
|
39
|
+
@dial_outgoing_interval_secs = dial_outgoing_interval_secs
|
40
|
+
end
|
41
|
+
|
42
|
+
def run(task: Async::Task.current)
|
43
|
+
dial_bootnodes
|
44
|
+
# dial outgoing peers every 15 seconds
|
45
|
+
task.reactor.every(@dial_outgoing_interval_secs) do
|
46
|
+
task.async do
|
47
|
+
schedule_dialing_tasks
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def dial_bootnodes
|
55
|
+
@network_state.peer_store.find_bootnodes(@network_state.number_of_attemp_outgoing).each do |node|
|
56
|
+
conn, handshake = @dialer.dial(node)
|
57
|
+
@network_state.new_peer_connected(conn, handshake, way_for_connection: Peer::OUTGOING)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def schedule_dialing_tasks
|
62
|
+
@network_state.peer_store.find_attempt_peers(@network_state.number_of_attemp_outgoing).each do |node|
|
63
|
+
# avoid dial self or connected peers
|
64
|
+
next if @network_state.peers.include?(node.raw_node_id) || node.raw_node_id == @network_state.local_node_id
|
65
|
+
conn, handshake = @dialer.dial(node)
|
66
|
+
@network_state.new_peer_connected(conn, handshake, way_for_connection: Peer::OUTGOING)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
@@ -0,0 +1,55 @@
|
|
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/io'
|
26
|
+
require 'async/io/stream'
|
27
|
+
require_relative 'rlpx/connection'
|
28
|
+
|
29
|
+
module Ciri
|
30
|
+
module P2P
|
31
|
+
# Discovery and dial new nodes
|
32
|
+
class Dialer
|
33
|
+
include RLPX
|
34
|
+
|
35
|
+
def initialize(private_key:, handshake:)
|
36
|
+
@private_key = private_key
|
37
|
+
@handshake = handshake
|
38
|
+
end
|
39
|
+
|
40
|
+
# setup a new connection to node
|
41
|
+
def dial(node)
|
42
|
+
# connect tcp socket
|
43
|
+
# Use Stream to buffer IO operation
|
44
|
+
address = node.addresses&.first
|
45
|
+
return unless address
|
46
|
+
socket = Async::IO::Stream.new(Async::IO::Endpoint.tcp(address.ip.to_s, address.tcp_port).connect)
|
47
|
+
c = Connection.new(socket)
|
48
|
+
c.encryption_handshake!(private_key: @private_key, remote_node_id: node.node_id)
|
49
|
+
remote_handshake = c.protocol_handshake!(@handshake)
|
50
|
+
[c, remote_handshake]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
@@ -0,0 +1,237 @@
|
|
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/logger'
|
26
|
+
require 'ciri/key'
|
27
|
+
require 'ciri/rlp'
|
28
|
+
require 'ciri/p2p/node'
|
29
|
+
require 'ciri/p2p/errors'
|
30
|
+
require 'ipaddr'
|
31
|
+
|
32
|
+
module Ciri
|
33
|
+
module P2P
|
34
|
+
module Discovery
|
35
|
+
module Protocol
|
36
|
+
|
37
|
+
# implement the DiscV4 protocol
|
38
|
+
# https://github.com/ethereum/devp2p/blob/master/discv4.md
|
39
|
+
class Message
|
40
|
+
|
41
|
+
MAX_LEN=1280
|
42
|
+
|
43
|
+
attr_reader :message_hash, :packet_type, :packet_data
|
44
|
+
|
45
|
+
def initialize(message_hash:, signature:, packet_type:, packet_data:)
|
46
|
+
@message_hash = message_hash
|
47
|
+
@signature = signature
|
48
|
+
@packet_type = packet_type
|
49
|
+
@packet_data = packet_data
|
50
|
+
end
|
51
|
+
|
52
|
+
# compute key and return NodeID
|
53
|
+
def sender
|
54
|
+
@sender ||= begin
|
55
|
+
encoded_packet_type = Utils.big_endian_encode(packet_type)
|
56
|
+
public_key = Key.ecdsa_recover(Utils.keccak(encoded_packet_type + packet_data), @signature)
|
57
|
+
NodeID.new(public_key)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def packet
|
62
|
+
packet_class = case @packet_type
|
63
|
+
when Ping::CODE
|
64
|
+
Ping
|
65
|
+
when Pong::CODE
|
66
|
+
Pong
|
67
|
+
when FindNode::CODE
|
68
|
+
FindNode
|
69
|
+
when Neighbors::CODE
|
70
|
+
Neighbors
|
71
|
+
else
|
72
|
+
raise UnknownMessageCodeError.new("unkonwn discovery message code: #{@packet_type}")
|
73
|
+
end
|
74
|
+
# TODO according discv4 protocol, rlp_decode should support ignore additional elements
|
75
|
+
# we should support ignore_extra_data option in Ciri::RLP
|
76
|
+
packet_class.rlp_decode @packet_data
|
77
|
+
end
|
78
|
+
|
79
|
+
# validate message hash and signature
|
80
|
+
def validate
|
81
|
+
encoded_packet_type = Utils.big_endian_encode(packet_type)
|
82
|
+
raise InvalidMessageError.new("mismatch hash") if message_hash != Utils.keccak(@signature + encoded_packet_type + packet_data)
|
83
|
+
begin
|
84
|
+
sender
|
85
|
+
rescue StandardError => e
|
86
|
+
raise InvalidMessageError.new("recover sender error: #{e}")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# encode message to string
|
91
|
+
def encode_message
|
92
|
+
buf = String.new
|
93
|
+
buf << message_hash
|
94
|
+
buf << @signature
|
95
|
+
buf << packet_type
|
96
|
+
buf << packet_data
|
97
|
+
buf
|
98
|
+
end
|
99
|
+
|
100
|
+
class << self
|
101
|
+
# return a Message
|
102
|
+
def decode_message(raw_bytes)
|
103
|
+
hash = raw_bytes[0...32]
|
104
|
+
# signature is 65 length r,s,v
|
105
|
+
signature = raw_bytes[32...97]
|
106
|
+
packet_type = Utils.big_endian_decode raw_bytes[97]
|
107
|
+
packet_data = raw_bytes[98..-1]
|
108
|
+
Message.new(message_hash: hash, signature: signature, packet_type: packet_type, packet_data: packet_data)
|
109
|
+
end
|
110
|
+
|
111
|
+
# return a new message instance include packet
|
112
|
+
def pack(packet, private_key:)
|
113
|
+
packet_data = Ciri::RLP.encode(packet)
|
114
|
+
packet_type = packet.class.code
|
115
|
+
encoded_packet_type = Utils.big_endian_encode(packet_type)
|
116
|
+
signature = private_key.ecdsa_signature(Utils.keccak(encoded_packet_type + packet_data)).to_s
|
117
|
+
hash = Utils.keccak(signature + encoded_packet_type + packet_data)
|
118
|
+
if (msg_size=hash.size + signature.size + encoded_packet_type.size + packet_data.size) > MAX_LEN
|
119
|
+
raise InvalidMessageError.new("failed to pack, message size is too long, size: #{msg_size}, max_len: #{MAX_LEN}")
|
120
|
+
end
|
121
|
+
Message.new(message_hash: hash, signature: signature, packet_type: packet_type, packet_data: packet_data)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# a struct represent which node send this packet
|
127
|
+
class From
|
128
|
+
include Ciri::RLP::Serializable
|
129
|
+
|
130
|
+
# we should not trust the sender_ip field
|
131
|
+
schema(
|
132
|
+
sender_ip: Integer,
|
133
|
+
sender_udp_port: Integer,
|
134
|
+
sender_tcp_port: Integer,
|
135
|
+
)
|
136
|
+
end
|
137
|
+
|
138
|
+
# a struct represent which node is target of this packet
|
139
|
+
class To
|
140
|
+
include Ciri::RLP::Serializable
|
141
|
+
|
142
|
+
# because discv4 protocol has not give us a name of last field,
|
143
|
+
# we just keep the field value 0 and guess it name should be recipient_tcp_port
|
144
|
+
# https://github.com/ethereum/devp2p/blob/master/discv4.md#ping-packet-0x01
|
145
|
+
|
146
|
+
schema(
|
147
|
+
recipient_ip: Integer,
|
148
|
+
recipient_udp_port: Integer,
|
149
|
+
recipient_tcp_port: Integer,
|
150
|
+
)
|
151
|
+
default_data(recipient_tcp_port: 0)
|
152
|
+
|
153
|
+
class << self
|
154
|
+
def from_inet_addr(address)
|
155
|
+
from_host_port(address.ip_address, address.ip_port)
|
156
|
+
end
|
157
|
+
|
158
|
+
def from_host_port(host, port)
|
159
|
+
new(recipient_ip: host.is_a?(IPAddr) ? host.to_i : IPAddr.new(host).to_i, recipient_udp_port: port)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# abstract class
|
165
|
+
class Packet
|
166
|
+
def self.code
|
167
|
+
self::CODE
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
class Ping < Packet
|
172
|
+
include Ciri::RLP::Serializable
|
173
|
+
|
174
|
+
CODE = 0x01
|
175
|
+
|
176
|
+
schema(
|
177
|
+
version: Integer,
|
178
|
+
from: From,
|
179
|
+
to: To,
|
180
|
+
expiration: Integer,
|
181
|
+
)
|
182
|
+
|
183
|
+
default_data(version: 0)
|
184
|
+
end
|
185
|
+
|
186
|
+
class Pong < Packet
|
187
|
+
include Ciri::RLP::Serializable
|
188
|
+
|
189
|
+
CODE = 0x02
|
190
|
+
|
191
|
+
schema(
|
192
|
+
to: To,
|
193
|
+
ping_hash: RLP::Bytes,
|
194
|
+
expiration: Integer,
|
195
|
+
)
|
196
|
+
end
|
197
|
+
|
198
|
+
class FindNode < Packet
|
199
|
+
include Ciri::RLP::Serializable
|
200
|
+
|
201
|
+
CODE = 0x03
|
202
|
+
|
203
|
+
schema(
|
204
|
+
target: RLP::Bytes,
|
205
|
+
expiration: Integer,
|
206
|
+
)
|
207
|
+
end
|
208
|
+
|
209
|
+
class Neighbors < Packet
|
210
|
+
include Ciri::RLP::Serializable
|
211
|
+
|
212
|
+
CODE = 0x04
|
213
|
+
|
214
|
+
# neighbour info
|
215
|
+
class Node
|
216
|
+
include Ciri::RLP::Serializable
|
217
|
+
|
218
|
+
schema(
|
219
|
+
ip: Integer,
|
220
|
+
udp_port: Integer,
|
221
|
+
tcp_port: Integer,
|
222
|
+
node_id: RLP::Bytes,
|
223
|
+
)
|
224
|
+
end
|
225
|
+
|
226
|
+
schema(
|
227
|
+
nodes: [Node],
|
228
|
+
expiration: Integer,
|
229
|
+
)
|
230
|
+
end
|
231
|
+
|
232
|
+
end
|
233
|
+
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|