devp2p 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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +22 -0
  4. data/lib/devp2p.rb +57 -0
  5. data/lib/devp2p/app_helper.rb +85 -0
  6. data/lib/devp2p/base_app.rb +80 -0
  7. data/lib/devp2p/base_protocol.rb +136 -0
  8. data/lib/devp2p/base_service.rb +55 -0
  9. data/lib/devp2p/command.rb +82 -0
  10. data/lib/devp2p/configurable.rb +32 -0
  11. data/lib/devp2p/connection_monitor.rb +77 -0
  12. data/lib/devp2p/control.rb +32 -0
  13. data/lib/devp2p/crypto.rb +73 -0
  14. data/lib/devp2p/crypto/ecc_x.rb +133 -0
  15. data/lib/devp2p/crypto/ecies.rb +134 -0
  16. data/lib/devp2p/discovery.rb +118 -0
  17. data/lib/devp2p/discovery/address.rb +83 -0
  18. data/lib/devp2p/discovery/kademlia_protocol_adapter.rb +11 -0
  19. data/lib/devp2p/discovery/node.rb +32 -0
  20. data/lib/devp2p/discovery/protocol.rb +342 -0
  21. data/lib/devp2p/discovery/transport.rb +105 -0
  22. data/lib/devp2p/exception.rb +30 -0
  23. data/lib/devp2p/frame.rb +197 -0
  24. data/lib/devp2p/kademlia.rb +48 -0
  25. data/lib/devp2p/kademlia/k_bucket.rb +178 -0
  26. data/lib/devp2p/kademlia/node.rb +40 -0
  27. data/lib/devp2p/kademlia/protocol.rb +284 -0
  28. data/lib/devp2p/kademlia/routing_table.rb +131 -0
  29. data/lib/devp2p/kademlia/wire_interface.rb +30 -0
  30. data/lib/devp2p/multiplexed_session.rb +110 -0
  31. data/lib/devp2p/multiplexer.rb +358 -0
  32. data/lib/devp2p/p2p_protocol.rb +170 -0
  33. data/lib/devp2p/packet.rb +35 -0
  34. data/lib/devp2p/peer.rb +329 -0
  35. data/lib/devp2p/peer_errors.rb +35 -0
  36. data/lib/devp2p/peer_manager.rb +274 -0
  37. data/lib/devp2p/rlpx_session.rb +434 -0
  38. data/lib/devp2p/sync_queue.rb +76 -0
  39. data/lib/devp2p/utils.rb +106 -0
  40. data/lib/devp2p/version.rb +13 -0
  41. data/lib/devp2p/wired_service.rb +30 -0
  42. metadata +227 -0
@@ -0,0 +1,105 @@
1
+ # -*- encoding : ascii-8bit -*-
2
+
3
+ module DEVp2p
4
+ module Discovery
5
+
6
+ ##
7
+ # Persist the list of known nodes with their reputation.
8
+ #
9
+ class Transport < BaseService
10
+ include Celluloid::IO
11
+
12
+ name 'discovery'
13
+
14
+ default_config(
15
+ discovery: {
16
+ listen_port: 30303,
17
+ listen_host: '0.0.0.0'
18
+ },
19
+ node: {
20
+ privkey_hex: ''
21
+ }
22
+ )
23
+
24
+ attr :protocol
25
+
26
+ def initialize(app)
27
+ super(app)
28
+ logger.info "Discovery service init"
29
+
30
+ @server = nil # will be UDPSocket
31
+ @protocol = Protocol.new app, Actor.current
32
+ end
33
+
34
+ def address
35
+ ip = @app.config[:discovery][:listen_host]
36
+ port = @app.config[:discovery][:listen_port]
37
+ Address.new ip, port
38
+ end
39
+
40
+ def send_message(address, message)
41
+ raise ArgumentError, 'address must be Address' unless address.instance_of?(Address)
42
+ logger.debug "sending", size: message.size, to: address
43
+
44
+ begin
45
+ @server.send message, 0, address.ip, address.udp_port
46
+ rescue
47
+ # should never reach here? udp has no connection!
48
+ logger.error "udp write error", error: $!
49
+ logger.error "waiting for recovery"
50
+ sleep 5
51
+ end
52
+ end
53
+
54
+ def receive_message(address, message)
55
+ raise ArgumentError, 'address must be Address' unless address.instance_of?(Address)
56
+ @protocol.receive_message address, message
57
+ end
58
+
59
+ def start
60
+ logger.info 'starting discovery'
61
+
62
+ ip = @app.config[:discovery][:listen_host]
63
+ port = @app.config[:discovery][:listen_port]
64
+
65
+ logger.info "starting udp listener", port: port, host: ip
66
+
67
+ @server = UDPSocket.new
68
+ @server.bind ip, port
69
+
70
+ super
71
+
72
+ @protocol.bootstrap(
73
+ @app.config[:discovery][:bootstrap_nodes].map {|x| Node.from_uri(x) }
74
+ )
75
+ end
76
+
77
+ def stop
78
+ logger.info "stopping discovery"
79
+ super
80
+ end
81
+
82
+ private
83
+
84
+ def logger
85
+ @logger ||= Logger.new "#{@app.config[:discovery][:listen_port]}.p2p.discovery"
86
+ end
87
+
88
+ def _run
89
+ maxlen = Multiplexer.max_window_size * 2
90
+ loop do
91
+ break if stopped?
92
+ message, info = @server.recvfrom maxlen
93
+ async.handle_packet message, info[3], info[1]
94
+ end
95
+ end
96
+
97
+ def handle_packet(message, ip, port)
98
+ logger.debug "handling packet", ip: ip, port: port, size: message.size
99
+ receive_message Address.new(ip, port), message
100
+ end
101
+
102
+ end
103
+
104
+ end
105
+ end
@@ -0,0 +1,30 @@
1
+ # -*- encoding : ascii-8bit -*-
2
+
3
+ module DEVp2p
4
+
5
+ class MissingRequiredServiceError < StandardError; end
6
+ class InvalidCommandStructure < StandardError; end
7
+ class DuplicatedCommand < StandardError; end
8
+ class UnknownCommandError < StandardError; end
9
+ class FrameError < StandardError; end
10
+ class MultiplexerError < StandardError; end
11
+ class RLPxSessionError < StandardError; end
12
+ class MultiplexedSessionError < StandardError; end
13
+ class AuthenticationError < StandardError; end
14
+ class FormatError < StandardError; end
15
+ class InvalidKeyError < StandardError; end
16
+ class InvalidSignatureError < StandardError; end
17
+ class InvalidMACError < StandardError; end
18
+ class InvalidPayloadError < StandardError; end
19
+ class EncryptionError < StandardError; end
20
+ class DecryptionError < StandardError; end
21
+ class KademliaRoutingError < StandardError; end
22
+ class KademliaNodeNotFound < StandardError; end
23
+ class PeerError < StandardError; end
24
+ class ProtocolError < StandardError; end
25
+
26
+ class DefectiveMessage < StandardError; end
27
+ class PacketExpired < DefectiveMessage; end
28
+ class InvalidMessageMAC < DefectiveMessage; end
29
+
30
+ end
@@ -0,0 +1,197 @@
1
+ # -*- encoding : ascii-8bit -*-
2
+
3
+ module DEVp2p
4
+
5
+ ##
6
+ # When sending a packet over RLPx, the packet will be framed. The frame
7
+ # provides information about the size of the packet and the packet's source
8
+ # protocol. There are three slightly different frames, depending on whether
9
+ # or not the frame is delivering a multi-frame packet. A multi-frame packet
10
+ # is a packet which is split (aka chunked) into multiple frames because it's
11
+ # size is larger than the protocol window size (pws, see Multiplexing). When
12
+ # a packet is chunked into multiple frames, there is an implicit difference
13
+ # between the first frame and all subsequent frames.
14
+ #
15
+ # Thus, the three frame types are normal, chunked-0 (first frame of a
16
+ # multi-frame packet), and chunked-n (subsequent frames of a multi-frame
17
+ # packet).
18
+ #
19
+ # * Single-frame packet:
20
+ #
21
+ # header || header-mac || frame || mac
22
+ #
23
+ # * Multi-frame packet:
24
+ #
25
+ # header || header-mac || frame-0 ||
26
+ # [ header || header-mac || frame-n || ... || ]
27
+ # header || header-mac || frame-last || mac
28
+ #
29
+ class Frame
30
+
31
+ extend Configurable
32
+ add_config(
33
+ header_size: 16,
34
+ mac_size: 16,
35
+ padding: 16,
36
+ header_sedes: RLP::Sedes::List.new(elements: [RLP::Sedes.big_endian_int]*3, strict: false)
37
+ )
38
+
39
+ class <<self
40
+ def encode_body_size(size)
41
+ [size].pack('I>')[1..-1]
42
+ end
43
+
44
+ def decode_body_size(header)
45
+ "\x00#{header[0,3]}".unpack('I>').first
46
+ end
47
+ end
48
+
49
+ attr :protocol_id, :cmd_id, :sequence_id, :payload, :is_chunked_n, :total_payload_size, :frames
50
+
51
+ def initialize(protocol_id, cmd_id, payload, sequence_id, window_size, is_chunked_n=false, frames=nil, frame_cipher=nil)
52
+ raise ArgumentError, 'invalid protocol_id' unless protocol_id < TT16
53
+ raise ArgumentError, 'invalid sequence_id' unless sequence_id.nil? || sequence_id < TT16
54
+ raise ArgumentError, 'invalid window_size' unless window_size % padding == 0
55
+ raise ArgumentError, 'invalid cmd_id' unless cmd_id < 256
56
+
57
+ @protocol_id = protocol_id
58
+ @cmd_id = cmd_id
59
+ @payload = payload
60
+ @sequence_id = sequence_id
61
+ @is_chunked_n = is_chunked_n
62
+ @frame_cipher = frame_cipher
63
+
64
+ @frames = frames || []
65
+ @frames.push self
66
+
67
+ # chunk payloads resulting in frames exceeing window_size
68
+ fs = frame_size
69
+ if fs > window_size
70
+ unless is_chunked_n
71
+ @is_chunked_0 = true
72
+ @total_payload_size = body_size
73
+ end
74
+
75
+ # chunk payload
76
+ @payload = payload[0...(window_size-fs)]
77
+ raise FrameError, "invalid frame size" unless frame_size <= window_size
78
+
79
+ remain = payload[@payload.size..-1]
80
+ raise FrameError, "invalid remain size" unless (remain.size + @payload.size) == payload.size
81
+
82
+ Frame.new(protocol_id, cmd_id, remain, sequence_id, window_size, true, @frames, frame_cipher)
83
+ end
84
+
85
+ raise FrameError, "invalid frame size" unless frame_size <= window_size
86
+ end
87
+
88
+ def frame_type
89
+ return :normal if normal?
90
+ @is_chunked_n ? :chunked_n : :chunked_0
91
+ end
92
+
93
+ def frame_size
94
+ # header16 || mac16 || dataN + [padding] || mac16
95
+ header_size + mac_size + body_size(true) + mac_size
96
+ end
97
+
98
+ ##
99
+ # frame-size: 3-byte integer, size of frame, big endian encoded (excludes
100
+ # padding)
101
+ #
102
+ def body_size(padded=false)
103
+ l = enc_cmd_id.size + payload.size
104
+ padded ? Utils.ceil16(l) : l
105
+ end
106
+
107
+ def normal?
108
+ !@is_chunked_n && !@is_chunked_0
109
+ end
110
+
111
+ ##
112
+ # header: frame-size || header-data || padding
113
+ #
114
+ # frame-size: 3-byte integer, size of frame, big endian encoded
115
+ # header-data:
116
+ # normal: RLP::Sedes::List.new(protocol_type[, sequence_id])
117
+ # chunked_0: RLP::Sedes::List.new(protocol_type, sequence_id, total_packet_size)
118
+ # chunked_n: RLP::Sedes::List.new(protocol_type, sequence_id)
119
+ # normal, chunked_n: RLP::Sedes::List.new(protocol_type[, sequence_id])
120
+ # values:
121
+ # protocol_type: < 2**16
122
+ # sequence_id: < 2**16 (this value is optional for normal frames)
123
+ # total_packet_size: < 2**32
124
+ # padding: zero-fill to 16-byte boundary
125
+ #
126
+ def header
127
+ raise FrameError, "invalid protocol id" unless protocol_id < 2**16
128
+ raise FrameError, "invalid sequence id" unless sequence_id.nil? || sequence_id < TT16
129
+
130
+ l = [protocol_id]
131
+ if @is_chunked_0
132
+ raise FrameError, 'chunked_0 must have sequence_id' if sequence_id.nil?
133
+ l.push sequence_id
134
+ l.push total_payload_size
135
+ elsif sequence_id
136
+ l.push sequence_id
137
+ end
138
+
139
+ header_data = RLP.encode l, sedes: header_sedes
140
+ raise FrameError, 'invalid rlp' unless l == RLP.decode(header_data, sedes: header_sedes, strict: false)
141
+
142
+ bs = body_size
143
+ raise FrameError, 'invalid body size' unless bs < 256**3
144
+
145
+ header = Frame.encode_body_size(body_size) + header_data
146
+ header = Utils.rzpad16 header
147
+ raise FrameError, 'invalid header' unless header.size == header_size
148
+
149
+ header
150
+ end
151
+
152
+ def enc_cmd_id
153
+ @is_chunked_n ? '' : RLP.encode(cmd_id, sedes: RLP::Sedes.big_endian_int)
154
+ end
155
+
156
+ ##
157
+ # frame:
158
+ # normal: rlp(packet_type) [|| rlp(packet_data)] || padding
159
+ # chunked_0: rlp(packet_type) || rlp(packet_data ...)
160
+ # chunked_n: rlp(...packet_data) || padding
161
+ # padding: zero-fill to 16-byte boundary (only necessary for last frame)
162
+ #
163
+ def body
164
+ Utils.rzpad16 "#{enc_cmd_id}#{payload}"
165
+ end
166
+
167
+ def as_bytes
168
+ raise FrameError, 'can only be called once' if @cipher_called
169
+
170
+ if @frame_cipher
171
+ @cipher_called = true
172
+ e = @frame_cipher.encrypt(header, body)
173
+ raise FrameError, 'invalid frame size of encrypted frame' unless e.size == frame_size
174
+ e
175
+ else
176
+ h = header
177
+ raise FrameError, 'invalid header size' unless h.size == header_size
178
+
179
+ b = body
180
+ raise FrameError, 'invalid body size' unless b.size == body_size(true)
181
+
182
+ dummy_mac = "\x00" * mac_size
183
+ r = h + dummy_mac + b + dummy_mac
184
+ raise FrameError, 'invalid frame' unless r.size == frame_size
185
+
186
+ r
187
+ end
188
+ end
189
+
190
+ def to_s
191
+ "<Frame(#{frame_type}, len=#{frame_size}, protocol=#{protocol_id} sid=#{sequence_id})"
192
+ end
193
+ alias :inspect :to_s
194
+
195
+ end
196
+
197
+ end
@@ -0,0 +1,48 @@
1
+ # -*- encoding : ascii-8bit -*-
2
+
3
+ module DEVp2p
4
+
5
+ ##
6
+ # Node discovery and network formation are implemented via a Kademlia-like
7
+ # protocol. The major differences are that packets are signed, node ids are
8
+ # the public keys, and DHT-related features are excluded. The FIND_VALUE and
9
+ # STORE packets are not implemented.
10
+ #
11
+ # The parameters necessary to implement the protocol are:
12
+ #
13
+ # * bucket size of 16 (denoted k in Kademlia)
14
+ # * concurrency of 3 (denoted alpha)
15
+ # * 8 bits per hop (denoted b) for routing
16
+ # * The eviction check interval is 75 milliseconds
17
+ # * request timeouts are 300ms
18
+ # * idle bucket-refresh interval is 3600 seconds
19
+ #
20
+ # Aside from the previously described exclusions, node discovery closely
21
+ # follows system and protocol described by Maymounkov and Mazieres.
22
+ #
23
+ module Kademlia
24
+ B = 8 # bits per hop for routing
25
+ K = 16 # bucket size
26
+ A = 3 # alpha, parallel find node lookups
27
+
28
+ REQUEST_TIMEOUT = 3 * 300 / 1000.0 # timeout of message round trips
29
+ IDLE_BUCKET_REFRESH_INTERVAL = 3600 # ping all nodes in bucket if bucket was idle
30
+ PUBKEY_SIZE = 512
31
+ ID_SIZE = 256
32
+ MAX_NODE_ID = 2 ** ID_SIZE - 1
33
+
34
+ class <<self
35
+ def random_nodeid
36
+ SecureRandom.random_number(MAX_NODE_ID+1)
37
+ end
38
+ end
39
+
40
+ end
41
+
42
+ require 'devp2p/kademlia/node'
43
+ require 'devp2p/kademlia/k_bucket'
44
+ require 'devp2p/kademlia/routing_table'
45
+ require 'devp2p/kademlia/wire_interface'
46
+ require 'devp2p/kademlia/protocol'
47
+
48
+ end
@@ -0,0 +1,178 @@
1
+ # -*- encoding : ascii-8bit -*-
2
+
3
+ module DEVp2p
4
+ module Kademlia
5
+
6
+ ##
7
+ # Each k-bucket is kept sorted by time last seen - least-recently seen node
8
+ # at the head, most-recently seen at the tail. For small values of i, the
9
+ # k-buckets will generally be empty (as no appropriate nodes will exist).
10
+ # For large values of i, the lists can grow up to size k, where k is a
11
+ # system-wide replication parameter.
12
+ #
13
+ # k is chosen such that any given k nodes are very unlikely to fail within
14
+ # an hour of each other (for example k = 20).
15
+ #
16
+ class KBucket
17
+
18
+ attr :left, :right, :last_updated
19
+
20
+ def initialize(left, right)
21
+ @left, @right = left, right
22
+ @nodes = []
23
+ @replacement_cache = []
24
+ @last_updated = Time.now
25
+ end
26
+
27
+ include Enumerable
28
+ def each(&block)
29
+ @nodes.each(&block)
30
+ end
31
+
32
+ ##
33
+ # If the sending node already exists in the recipient's k-bucket, the
34
+ # recipient moves it to the tail of the list.
35
+ #
36
+ # If the node is not already in the appropriate k-bucket and the bucket
37
+ # has fewer than k entries, then the recipient just inserts the new
38
+ # sender at the tail of the list.
39
+ #
40
+ # If the appropriate k-bucket is full, however, then the recipient pings
41
+ # the k-bucket's least-recently seen node to decide what to do:
42
+ #
43
+ # * on success: return nil
44
+ # * on bucket full: return least recently seen node for eviction check
45
+ #
46
+ def add(node)
47
+ @last_updated = Time.now
48
+
49
+ if include?(node) # already exists
50
+ delete node
51
+ @nodes.push node
52
+ nil
53
+ elsif size < K # add if fewer than k entries
54
+ @nodes.push node
55
+ nil
56
+ else # bucket is full
57
+ head
58
+ end
59
+ end
60
+
61
+ def add_replacement(node)
62
+ @replacement_cache.push node
63
+ end
64
+
65
+ def delete(node)
66
+ return unless include?(node)
67
+ @nodes.delete node
68
+ end
69
+
70
+ ##
71
+ # least recently seen
72
+ #
73
+ def head
74
+ @nodes.first
75
+ end
76
+
77
+ ##
78
+ # last recently seen
79
+ #
80
+ def tail
81
+ @nodes.last
82
+ end
83
+
84
+ def range
85
+ [left, right]
86
+ end
87
+
88
+ def midpoint
89
+ left + (right - left) / 2
90
+ end
91
+
92
+ def distance(node)
93
+ midpoint ^ node.id
94
+ end
95
+
96
+ def id_distance(id)
97
+ midpoint ^ id
98
+ end
99
+
100
+ def nodes_by_id_distance(id)
101
+ raise ArgumentError, 'invalid id' unless id.is_a?(Integer)
102
+ @nodes.sort_by {|n| n.id_distance(id) }
103
+ end
104
+
105
+ def should_split?
106
+ full? && splitable?
107
+ end
108
+
109
+ def splitable?
110
+ d = depth
111
+ d % B != 0 && d != ID_SIZE
112
+ end
113
+
114
+ ##
115
+ # split at the median id
116
+ #
117
+ def split
118
+ split_id = midpoint
119
+
120
+ lower = self.class.new left, split_id
121
+ upper = self.class.new split_id + 1, right
122
+
123
+ # distribute nodes
124
+ @nodes.each do |node|
125
+ bucket = node.id <= split_id ? lower : upper
126
+ bucket.add node
127
+ end
128
+
129
+ # distribute replacement nodes
130
+ @replacement_cache.each do |node|
131
+ bucket = node.id <= split_id ? lower : upper
132
+ bucket.add_replacement node
133
+ end
134
+
135
+ return lower, upper
136
+ end
137
+
138
+ ##
139
+ # depth is the prefix shared by all nodes in bucket. i.e. the number of
140
+ # shared leading bits.
141
+ #
142
+ def depth
143
+ return ID_SIZE if size < 2
144
+
145
+ bits = @nodes.map {|n| Utils.bpad(n.id, ID_SIZE) }
146
+ ID_SIZE.times do |i|
147
+ if bits.map {|b| b[0,i] }.uniq.size != 1
148
+ return i - 1
149
+ end
150
+ end
151
+
152
+ raise "should never be here"
153
+ end
154
+
155
+ def in_range?(node)
156
+ left <= node.id && node.id <= right
157
+ end
158
+
159
+ def full?
160
+ size == K
161
+ end
162
+
163
+ def size
164
+ @nodes.size
165
+ end
166
+
167
+ def include?(node)
168
+ @nodes.include?(node)
169
+ end
170
+
171
+ def empty?
172
+ @nodes.empty?
173
+ end
174
+
175
+ end
176
+
177
+ end
178
+ end