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,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__)
@@ -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")
@@ -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")
@@ -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")
@@ -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")
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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
@@ -0,0 +1,7 @@
1
+ require "ciri/p2p/version"
2
+
3
+ module Ciri
4
+ module P2p
5
+ # Your code goes here...
6
+ end
7
+ end
@@ -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
+