devp2p 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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