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,118 @@
1
+ ##
2
+ # # Node Discovery Protocol
3
+ #
4
+ # * [Node] - an entity on the network
5
+ # * [Node] ID - 512 bit public key of node
6
+ #
7
+ # The Node Discovery protocol provides a way to find RLPx nodes that can be
8
+ # connected to. It uses a Kademlia-like protocol to maintain a distributed
9
+ # database of the IDs and endpoints of all listening nodes.
10
+ #
11
+ # Each node keeps a node table as described in the Kademlia paper (Maymounkov,
12
+ # Mazières 2002). The node table is configured with a bucket size of 16
13
+ # (denoted `k` in Kademlia), concurrency of 3 (denoted `α` in Kademlia), and 8
14
+ # bits per hop (denoted `b` in Kademlia) for routing. The eviction check
15
+ # interval is 75 milliseconds, and the idle bucket-refresh interval is 3600
16
+ # seconds.
17
+ #
18
+ # In order to maintain a well-formed network, RLPx nodes should try to connect
19
+ # to an unspecified number of close nodes. To increase resilience against Sybil
20
+ # attacks, nodes should also connect to randomly chosen, non-close nodes.
21
+ #
22
+ # Each node runs the UDP-based RPC protocol defined below. The `FIND_DATA` and
23
+ # `STORE` requests from the Kademlia paper are not part of the protocol since
24
+ # the Node Discovery Protocol does not provide DHT functionality.
25
+ #
26
+ # ## Joining the network
27
+ #
28
+ # When joining the network, fills its node table by performing a recursive Find
29
+ # Node operation with its own ID as the 'Target'. The initial Find Node request
30
+ # is sent to one or more bootstrap nodes.
31
+ #
32
+ # ## RPC Protocol
33
+ #
34
+ # RLPx nodes that want to accept incoming connections should listen on the same
35
+ # port number for UDP packets (Node Discovery Protocol) and TCP connections
36
+ # (RLPx protocol).
37
+ #
38
+ # All requests time out after 300ms. Requests are not re-sent.
39
+ #
40
+ # ## Packet Data
41
+ #
42
+ # All packets contain an `Expiration` date to guard against replay attacks. The
43
+ # date should be interpreted as a UNIX timestamp. The receiver should discard
44
+ # any packet whose `Expiration` value is in the past.
45
+ #
46
+ # ### Ping (type 0x01)
47
+ #
48
+ # Ping packets can be sent and received at any time. The receiver should reply
49
+ # with a Pong packet and update the IP/Port of the sender in its node table.
50
+ #
51
+ # PingNode packet-type: 0x01
52
+ #
53
+ # struct PingNode <= 59 bytes
54
+ # {
55
+ # h256 version = 0x3; <= 1
56
+ # Endpoint from; <= 23
57
+ # Endpoint to; <= 23
58
+ # unsigned expiration; <= 9
59
+ # }
60
+ #
61
+ # struct Endpoint <= 24 = [17,3,3]
62
+ # {
63
+ # unsigned address; // BE encoded 32-bit or 128-bit unsigned (layer3 address; size determins ipv4 vs ipv6)
64
+ # unsigned udpPort; // BE encoded 16-bit unsigned
65
+ # unsigned tcpPort; // BE encoded 16-bit unsigned
66
+ # }
67
+ #
68
+ # ### Pong (type 0x02)
69
+ #
70
+ # Pong is the reply to a Ping packet.
71
+ #
72
+ # Pong packet-type: 0x02
73
+ #
74
+ # struct Pong <= 66 bytes
75
+ # {
76
+ # Endpoint to;
77
+ # h256 echo;
78
+ # unsigned expiration;
79
+ # }
80
+ #
81
+ # ### Find Node (type 0x03)
82
+ #
83
+ # Find Node packets are sent to locate nodes close to a given target ID. The
84
+ # receiver should reply with a Neighbours packet containing the `k` nodes
85
+ # closest to target that it knows about.
86
+ #
87
+ # FindNode packet-type: 0x03
88
+ #
89
+ # struct FindNode <= 76 bytes
90
+ # {
91
+ # NodeId target; // Id of a node. The responding node will send back nodes closest to the target.
92
+ # unsigned expiration;
93
+ # }
94
+ #
95
+ # ### Neighbours (type 0x04)
96
+ #
97
+ # Neighbours is the reply to Find Node. It contains up to `k` nodes that the
98
+ # sender knows which are closest to the requested 'Target`.
99
+ #
100
+ # Neighbours packet-type: 0x04
101
+ #
102
+ # struct Neighbours <= 1423
103
+ # {
104
+ # list nodes: struct Neighbours <= 88...1411; 76...1219
105
+ # {
106
+ # inline Endpoint endpoint;
107
+ # NodeId node;
108
+ # };
109
+ # unsigned expiration;
110
+ # }
111
+ #
112
+
113
+
114
+ require 'devp2p/discovery/address'
115
+ require 'devp2p/discovery/node'
116
+ require 'devp2p/discovery/kademlia_protocol_adapter'
117
+ require 'devp2p/discovery/protocol'
118
+ require 'devp2p/discovery/transport'
@@ -0,0 +1,83 @@
1
+ # -*- encoding : ascii-8bit -*-
2
+
3
+ require 'ipaddr'
4
+ require 'resolv'
5
+
6
+ module DEVp2p
7
+ module Discovery
8
+
9
+ class Address
10
+
11
+ attr :udp_port, :tcp_port
12
+
13
+ def self.from_endpoint(ip, udp_port, tcp_port="\x00\x00")
14
+ new(ip, udp_port, tcp_port, true)
15
+ end
16
+
17
+ def initialize(ip, udp_port, tcp_port=nil, from_binary=false)
18
+ tcp_port ||= udp_port
19
+
20
+ if from_binary
21
+ raise ArgumentError, "invalid ip" unless [4,16].include?(ip.size)
22
+
23
+ @udp_port = dec_port udp_port
24
+ @tcp_port = dec_port tcp_port
25
+ else
26
+ raise ArgumentError, "udp_port must be Integer" unless udp_port.is_a?(Integer)
27
+ raise ArgumentError, "tcp_port must be Integer" unless tcp_port.is_a?(Integer)
28
+
29
+ @udp_port = udp_port
30
+ @tcp_port = tcp_port
31
+ end
32
+
33
+ begin
34
+ @ip = from_binary ? IPAddr.new_ntoh(ip) : IPAddr.new(ip)
35
+ rescue IPAddr::InvalidAddressError => e
36
+ ips = Resolv.getaddresses(ip).sort {|addr| addr =~ /:/ ? 1 : 0 } # use ipv4 first
37
+ raise e if ips.empty?
38
+ @ip = ips[0]
39
+ end
40
+ end
41
+
42
+ def ip
43
+ @ip.to_s
44
+ end
45
+
46
+ def update(addr)
47
+ @tcp_port = addr.tcp_port if @tcp_port.nil? || @tcp_port == 0
48
+ end
49
+
50
+ ##
51
+ # addresses equal if they share ip and udp_port
52
+ #
53
+ def ==(other)
54
+ [ip, udp_port] == [other.ip, other.udp_port]
55
+ end
56
+
57
+ def to_s
58
+ "Address(#{ip}:#{udp_port})"
59
+ end
60
+
61
+ def to_h
62
+ {ip: ip, udp_port: udp_port, tcp_port: tcp_port}
63
+ end
64
+
65
+ def to_b
66
+ [@ip.hton, enc_port(udp_port), enc_port(tcp_port)]
67
+ end
68
+ alias to_endpoint to_b
69
+
70
+ private
71
+
72
+ def enc_port(p)
73
+ Utils.int_to_big_endian4(p)[-2..-1]
74
+ end
75
+
76
+ def dec_port(b)
77
+ Utils.big_endian_to_int(b)
78
+ end
79
+
80
+ end
81
+
82
+ end
83
+ end
@@ -0,0 +1,11 @@
1
+ # -*- encoding : ascii-8bit -*-
2
+
3
+ module DEVp2p
4
+ module Discovery
5
+
6
+ class KademliaProtocolAdapter < Kademlia::Protocol
7
+ # do nothing
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,32 @@
1
+ # -*- encoding : ascii-8bit -*-
2
+
3
+ module DEVp2p
4
+ module Discovery
5
+
6
+ class Node < Kademlia::Node
7
+
8
+ def self.from_uri(uri)
9
+ ip, port, pubkey = Utils.host_port_pubkey_from_uri(uri)
10
+ new(pubkey, Address.new(ip, port.to_i))
11
+ end
12
+
13
+ attr_accessor :address
14
+
15
+ def initialize(pubkey, address=nil)
16
+ raise ArgumentError, 'invalid address' unless address.nil? || address.is_a?(Address)
17
+
18
+ super(pubkey)
19
+
20
+ self.address = address
21
+ @reputation = 0
22
+ @rlpx_version = 0
23
+ end
24
+
25
+ def to_uri
26
+ Utils.host_port_pubkey_to_uri(address.ip, address.udp_port, pubkey)
27
+ end
28
+
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,342 @@
1
+ # -*- encoding : ascii-8bit -*-
2
+
3
+ module DEVp2p
4
+ module Discovery
5
+
6
+ class Protocol < Kademlia::WireInterface
7
+
8
+ VERSION = 4
9
+
10
+ EXPIRATION = 60 # let messages expire after N seconds
11
+
12
+ CMD_ID_MAP = {
13
+ ping: 1,
14
+ pong: 2,
15
+ find_node: 3,
16
+ neighbours: 4
17
+ }.freeze
18
+ REV_CMD_ID_MAP = CMD_ID_MAP.map {|k,v| [v,k] }.to_h.freeze
19
+
20
+ # number of required top-level list elements for each cmd_id.
21
+ # elements beyond this length are trimmed.
22
+ CMD_ELEM_COUNT_MAP = {
23
+ ping: 4,
24
+ poing: 3,
25
+ find_node: 2,
26
+ neighbours: 2
27
+ }
28
+
29
+ attr :pubkey, :kademlia
30
+
31
+ def initialize(app, transport)
32
+ @app = app
33
+ @transport = transport
34
+
35
+ @privkey = Utils.decode_hex app.config[:node][:privkey_hex]
36
+ @pubkey = Crypto.privtopub @privkey
37
+
38
+ @nodes = {} # nodeid => Node
39
+ @node = Node.new(pubkey, @transport.address)
40
+
41
+ @kademlia = KademliaProtocolAdapter.new @node, self
42
+
43
+ uri = Utils.host_port_pubkey_to_uri(ip, udp_port, pubkey)
44
+ logger.info "starting discovery proto", enode: uri
45
+ end
46
+
47
+ def bootstrap(nodes)
48
+ @kademlia.bootstrap(nodes) unless nodes.empty?
49
+ end
50
+
51
+ ##
52
+ # return node or create new, update address if supplied
53
+ #
54
+ def get_node(nodeid, address=nil)
55
+ raise ArgumentError, 'invalid nodeid' unless nodeid.size == Kademlia::PUBKEY_SIZE / 8
56
+ raise ArgumentError, 'must give either address or existing nodeid' unless address || @nodes.has_key?(nodeid)
57
+
58
+ @nodes[nodeid] = Node.new nodeid, address if !@nodes.has_key?(nodeid)
59
+ node = @nodes[nodeid]
60
+
61
+ if address
62
+ raise ArgumentError, 'address must be Address' unless address.instance_of?(Address)
63
+ node.address = address
64
+ end
65
+
66
+ node
67
+ end
68
+
69
+ def sign(msg)
70
+ msg = Crypto.keccak256 msg
71
+ Crypto.ecdsa_sign msg, @privkey
72
+ end
73
+
74
+ ##
75
+ # UDP packets are structured as follows:
76
+ #
77
+ # hash || signature || packet-type || packet-data
78
+ #
79
+ # * packet-type: single byte < 2**7 // valid values are [1,4]
80
+ # * packet-data: RLP encoded list. Packet properties are serialized in
81
+ # the order in which they're defined. See packet-data below.
82
+ #
83
+ # Offset |
84
+ # 0 | MDC | Ensures integrity of packet.
85
+ # 65 | signature | Ensures authenticity of sender, `SIGN(sender-privkey, MDC)`
86
+ # 97 | type | Single byte in range [1, 4] that determines the structure of Data
87
+ # 98 | data | RLP encoded, see section Packet Data
88
+ #
89
+ # The packets are signed and authenticated. The sender's Node ID is
90
+ # determined by recovering the public key from the signature.
91
+ #
92
+ # sender-pubkey = ECRECOVER(Signature)
93
+ #
94
+ # The integrity of the packet can then be verified by computing the
95
+ # expected MDC of the packet as:
96
+ #
97
+ # MDC = keccak256(sender-pubkey || type || data)
98
+ #
99
+ # As an optimization, implementations may look up the public key by the
100
+ # UDP sending address and compute MDC before recovering the sender ID. If
101
+ # the MDC values do not match, the packet can be dropped.
102
+ #
103
+ def pack(cmd_id, payload)
104
+ raise ArgumentError, 'invalid cmd_id' unless REV_CMD_ID_MAP.has_key?(cmd_id)
105
+ raise ArgumentError, 'payload must be Array' unless payload.is_a?(Array)
106
+
107
+ cmd_id = encode_cmd_id cmd_id
108
+ expiration = encode_expiration Time.now.to_i + EXPIRATION
109
+
110
+ encoded_data = RLP.encode(payload + [expiration])
111
+ signed_data = Crypto.keccak256 "#{cmd_id}#{encoded_data}"
112
+ signature = Crypto.ecdsa_sign signed_data, @privkey
113
+
114
+ raise InvalidSignatureError unless signature.size == 65
115
+
116
+ mdc = Crypto.keccak256 "#{signature}#{cmd_id}#{encoded_data}"
117
+ raise InvalidMACError unless mdc.size == 32
118
+
119
+ "#{mdc}#{signature}#{cmd_id}#{encoded_data}"
120
+ end
121
+
122
+ ##
123
+ # macSize = 256 / 8 = 32
124
+ # sigSize = 520 / 8 = 65
125
+ # headSize = macSize + sigSize = 97
126
+ #
127
+ def unpack(message)
128
+ mdc = message[0,32]
129
+ if mdc != Crypto.keccak256(message[32..-1])
130
+ logger.warn 'packet with wrong mcd'
131
+ raise InvalidMessageMAC
132
+ end
133
+
134
+ signature = message[32,65]
135
+ raise InvalidSignatureError unless signature.size == 65
136
+
137
+ signed_data = Crypto.keccak256(message[97..-1])
138
+ remote_pubkey = Crypto.ecdsa_recover(signed_data, signature)
139
+ raise InvalidKeyError unless remote_pubkey.size == Kademlia::PUBKEY_SIZE / 8
140
+
141
+ cmd_id = decode_cmd_id message[97]
142
+ cmd = REV_CMD_ID_MAP[cmd_id]
143
+
144
+ payload = RLP.decode message[98..-1], strict: false
145
+ raise InvalidPayloadError unless payload.instance_of?(Array)
146
+
147
+ # ignore excessive list elements as required by EIP-8
148
+ payload = payload[0, CMD_ELEM_COUNT_MAP[cmd]||payload.size]
149
+
150
+ return remote_pubkey, cmd_id, payload, mdc
151
+ end
152
+
153
+ def receive_message(address, message)
154
+ logger.debug "<<< message", address: address
155
+ raise ArgumentError, 'address must be Address' unless address.instance_of?(Address)
156
+
157
+ begin
158
+ remote_pubkey, cmd_id, payload, mdc = unpack message
159
+
160
+ # Note: as of discovery version 4, expiration is the last element for
161
+ # all packets. This might not be the case for a later version, but
162
+ # just popping the last element is good enough for now.
163
+ expiration = decode_expiration payload.pop
164
+ raise PacketExpired if Time.now.to_i > expiration
165
+ rescue DefectiveMessage
166
+ logger.debug $!
167
+ return
168
+ end
169
+
170
+ cmd = "recv_#{REV_CMD_ID_MAP[cmd_id]}"
171
+ nodeid = remote_pubkey
172
+
173
+ get_node(nodeid, address) unless @nodes.has_key?(nodeid)
174
+ send cmd, nodeid, payload, mdc
175
+ end
176
+
177
+ def send_message(node, message)
178
+ raise ArgumentError, 'node must have address' unless node.address
179
+ logger.debug ">>> message", address: node.address
180
+ @transport.send_message node.address, message
181
+ end
182
+
183
+ def send_ping(node)
184
+ raise ArgumentError, "node must be Node" unless node.is_a?(Node)
185
+ raise ArgumentError, "cannot ping self" if node == @node
186
+
187
+ logger.debug ">>> ping", remoteid: node
188
+
189
+ version = RLP::Sedes.big_endian_int.serialize VERSION
190
+ payload = [
191
+ version,
192
+ Address.new(ip, udp_port, tcp_port).to_endpoint,
193
+ node.address.to_endpoint
194
+ ]
195
+
196
+ message = pack CMD_ID_MAP[:ping], payload
197
+ send_message node, message
198
+
199
+ message[0,32] # return the MDC to identify pongs
200
+ end
201
+
202
+ ##
203
+ # Update ip, port in node table. Addresses can only be learned by ping
204
+ # messages.
205
+ #
206
+ def recv_ping(nodeid, payload, mdc)
207
+ if payload.size != 3
208
+ logger.error "invalid ping payload", payload: payload
209
+ return
210
+ end
211
+
212
+ node = get_node nodeid
213
+ logger.debug "<<< ping", node: node
214
+
215
+ remote_address = Address.from_endpoint(*payload[1]) # from
216
+ my_address = Address.from_endpoint(*payload[2]) # my address
217
+
218
+ get_node(nodeid).address.update remote_address
219
+ @kademlia.recv_ping node, mdc
220
+ end
221
+
222
+ def send_pong(node, token)
223
+ logger.debug ">>> pong", remoteid: node
224
+
225
+ payload = [node.address.to_endpoint, token]
226
+ raise InvalidPayloadError unless [4,16].include?(payload[0][0].size)
227
+
228
+ message = pack CMD_ID_MAP[:pong], payload
229
+ send_message node, message
230
+ end
231
+
232
+ def recv_pong(nodeid, payload, mdc)
233
+ if payload.size != 2
234
+ logger.error 'invalid pong payload', payload: payload
235
+ return
236
+ end
237
+
238
+ raise InvalidPayloadError unless payload[0].size == 3
239
+ raise InvalidPayloadError unless [4,16].include?(payload[0][0].size)
240
+
241
+ my_address = Address.from_endpoint *payload[0]
242
+ echoed = payload[1]
243
+
244
+ if @nodes.include?(nodeid)
245
+ node = get_node nodeid
246
+ @kademlia.recv_pong node, echoed
247
+ else
248
+ logger.debug "<<< unexpected pong from unknown node"
249
+ end
250
+ end
251
+
252
+ def send_find_node(node, target_node_id)
253
+ target_node_id = Utils.zpad_int target_node_id, Kademlia::PUBKEY_SIZE/8
254
+ logger.debug ">>> find_node", remoteid: node
255
+
256
+ message = pack CMD_ID_MAP[:find_node], [target_node_id]
257
+ send_message node, message
258
+ end
259
+
260
+ def recv_find_node(nodeid, payload, mdc)
261
+ node = get_node nodeid
262
+
263
+ logger.debug "<<< find_node", remoteid: node
264
+ raise InvalidPayloadError unless payload[0].size == Kademlia::PUBKEY_SIZE/8
265
+
266
+ target = Utils.big_endian_to_int payload[0]
267
+ @kademlia.recv_find_node node, target
268
+ end
269
+
270
+ def send_neighbours(node, neighbours)
271
+ raise ArgumentError, 'neighbours must be Array' unless neighbours.instance_of?(Array)
272
+ raise ArgumentError, 'neighbours must be Node' unless neighbours.all? {|n| n.is_a?(Node) }
273
+
274
+ nodes = neighbours.map {|n| n.address.to_endpoint + [n.pubkey] }
275
+ logger.debug ">>> neighbours", remoteid: node, count: nodes.size
276
+
277
+ message = pack CMD_ID_MAP[:neighbours], [nodes]
278
+ send_message node, message
279
+ end
280
+
281
+ def recv_neighbours(nodeid, payload, mdc)
282
+ node = get_node nodeid
283
+ raise InvalidPayloadError unless payload.size == 1
284
+ raise InvalidPayloadError unless payload[0].instance_of?(Array)
285
+ logger.debug "<<< neighbours", remoteid: node, count: payload[0].size
286
+
287
+ neighbours_set = payload[0].uniq
288
+ logger.warn "received duplicates" if neighbours_set.size < payload[0].size
289
+
290
+ neighbours = neighbours_set.map do |n|
291
+ if n.size != 4 || ![4,16].include?(n[0].size)
292
+ logger.error "invalid neighbours format", neighbours: n
293
+ return
294
+ end
295
+
296
+ n = n.dup
297
+ nodeid = n.pop
298
+ address = Address.from_endpoint *n
299
+ get_node nodeid, address
300
+ end
301
+
302
+ @kademlia.recv_neighbours node, neighbours
303
+ end
304
+
305
+ def ip
306
+ @app.config[:discovery][:listen_host]
307
+ end
308
+
309
+ def udp_port
310
+ @app.config[:discovery][:listen_port]
311
+ end
312
+
313
+ def tcp_port
314
+ @app.config[:p2p][:listen_port]
315
+ end
316
+
317
+ private
318
+
319
+ def logger
320
+ @logger ||= Logger.new("#{udp_port}.p2p.discovery").tap {|l| l.level = :info }
321
+ end
322
+
323
+ def encode_cmd_id(cmd_id)
324
+ cmd_id.chr
325
+ end
326
+
327
+ def decode_cmd_id(byte)
328
+ byte.ord
329
+ end
330
+
331
+ def encode_expiration(i)
332
+ RLP::Sedes.big_endian_int.serialize(i)
333
+ end
334
+
335
+ def decode_expiration(b)
336
+ RLP::Sedes.big_endian_int.deserialize(b)
337
+ end
338
+
339
+ end
340
+
341
+ end
342
+ end