Class: DEVp2p::Discovery::Protocol

Inherits:
Kademlia::WireInterface show all
Defined in:
lib/devp2p/discovery/protocol.rb

Constant Summary collapse

VERSION =
4
EXPIRATION =

let messages expire after N seconds

60
CMD_ID_MAP =
{
  ping: 1,
  pong: 2,
  find_node: 3,
  neighbours: 4
}.freeze
REV_CMD_ID_MAP =
CMD_ID_MAP.map {|k,v| [v,k] }.to_h.freeze
CMD_ELEM_COUNT_MAP =

number of required top-level list elements for each cmd_id. elements beyond this length are trimmed.

{
  ping: 4,
  poing: 3,
  find_node: 2,
  neighbours: 2
}

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app, service) ⇒ Protocol

Returns a new instance of Protocol.



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/devp2p/discovery/protocol.rb', line 31

def initialize(app, service)
  @app = app
  @service = service

  @privkey = Utils.decode_hex app.config[:node][:privkey_hex]
  @pubkey = Crypto.privtopub @privkey

  @nodes = {} # nodeid => Node
  @node = Node.new(pubkey, @service.address)

  @kademlia = KademliaProtocolAdapter.new @node, self

  uri = Utils.host_port_pubkey_to_uri(ip, udp_port, pubkey)
  logger.info "starting discovery proto", enode: uri
end

Instance Attribute Details

#pubkeyObject (readonly)

Returns the value of attribute pubkey.



29
30
31
# File 'lib/devp2p/discovery/protocol.rb', line 29

def pubkey
  @pubkey
end

Instance Method Details

#bootstrap(nodes) ⇒ Object



47
48
49
# File 'lib/devp2p/discovery/protocol.rb', line 47

def bootstrap(nodes)
  @kademlia.bootstrap(nodes) unless nodes.empty?
end

#get_node(nodeid, address = nil) ⇒ Object

return node or create new, update address if supplied

Raises:

  • (ArgumentError)


54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/devp2p/discovery/protocol.rb', line 54

def get_node(nodeid, address=nil)
  raise ArgumentError, 'invalid nodeid' unless nodeid.size == Kademlia::PUBKEY_SIZE / 8
  raise ArgumentError, 'must give either address or existing nodeid' unless address || @nodes.has_key?(nodeid)

  @nodes[nodeid] = Node.new nodeid, address if !@nodes.has_key?(nodeid)
  node = @nodes[nodeid]

  if address
    raise ArgumentError, 'address must be Address' unless address.instance_of?(Address)
    node.address = address
  end

  node
end

#ipObject



307
308
309
# File 'lib/devp2p/discovery/protocol.rb', line 307

def ip
  @app.config[:discovery][:listen_host]
end

#pack(cmd_id, payload) ⇒ Object

UDP packets are structured as follows:

hash || signature || packet-type || packet-data
  • packet-type: single byte < 2**7 // valid values are [1,4]

  • packet-data: RLP encoded list. Packet properties are serialized in the order in which they’re defined. See packet-data below.

Offset | 0 | MDC | Ensures integrity of packet. 65 | signature | Ensures authenticity of sender, ‘SIGN(sender-privkey, MDC)` 97 | type | Single byte in range [1, 4] that determines the structure of Data 98 | data | RLP encoded, see section Packet Data

The packets are signed and authenticated. The sender’s Node ID is determined by recovering the public key from the signature.

sender-pubkey = ECRECOVER(Signature)

The integrity of the packet can then be verified by computing the expected MDC of the packet as:

MDC = keccak256(sender-pubkey || type || data)

As an optimization, implementations may look up the public key by the UDP sending address and compute MDC before recovering the sender ID. If the MDC values do not match, the packet can be dropped.

Raises:

  • (ArgumentError)


103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/devp2p/discovery/protocol.rb', line 103

def pack(cmd_id, payload)
  raise ArgumentError, 'invalid cmd_id' unless REV_CMD_ID_MAP.has_key?(cmd_id)
  raise ArgumentError, 'payload must be Array' unless payload.is_a?(Array)

  cmd_id = encode_cmd_id cmd_id
  expiration = encode_expiration Time.now.to_i + EXPIRATION

  encoded_data = RLP.encode(payload + [expiration])
  signed_data = Crypto.keccak256 "#{cmd_id}#{encoded_data}"
  signature = Crypto.ecdsa_sign signed_data, @privkey

  raise InvalidSignatureError unless signature.size == 65

  mdc = Crypto.keccak256 "#{signature}#{cmd_id}#{encoded_data}"
  raise InvalidMACError unless mdc.size == 32

  "#{mdc}#{signature}#{cmd_id}#{encoded_data}"
end

#receive_message(address, message) ⇒ Object



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/devp2p/discovery/protocol.rb', line 153

def receive_message(address, message)
  logger.debug "<<< message", address: address
  raise ArgumentError, 'address must be Address' unless address.instance_of?(Address)

  begin
    remote_pubkey, cmd_id, payload, mdc = unpack message

    # Note: as of discovery version 4, expiration is the last element for
    # all packets. This might not be the case for a later version, but
    # just popping the last element is good enough for now.
    expiration = decode_expiration payload.pop
    raise PacketExpired if Time.now.to_i > expiration
  rescue DefectiveMessage
    logger.debug $!
    return
  end

  cmd = "recv_#{REV_CMD_ID_MAP[cmd_id]}"
  nodeid = remote_pubkey

  get_node(nodeid, address) unless @nodes.has_key?(nodeid)
  send cmd, nodeid, payload, mdc
rescue
  logger.error 'invalid message', error: $!, from: address
end

#recv_find_node(nodeid, payload, mdc) ⇒ Object



262
263
264
265
266
267
268
269
270
# File 'lib/devp2p/discovery/protocol.rb', line 262

def recv_find_node(nodeid, payload, mdc)
  node = get_node nodeid

  logger.debug "<<< find_node", remoteid: node
  raise InvalidPayloadError unless payload[0].size == Kademlia::PUBKEY_SIZE/8

  target = Utils.big_endian_to_int payload[0]
  @kademlia.recv_find_node node, target
end

#recv_neighbours(nodeid, payload, mdc) ⇒ Object



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/devp2p/discovery/protocol.rb', line 283

def recv_neighbours(nodeid, payload, mdc)
  node = get_node nodeid
  raise InvalidPayloadError unless payload.size == 1
  raise InvalidPayloadError unless payload[0].instance_of?(Array)
  logger.debug "<<< neighbours", remoteid: node, count: payload[0].size

  neighbours_set = payload[0].uniq
  logger.warn "received duplicates" if neighbours_set.size < payload[0].size

  neighbours = neighbours_set.map do |n|
    if n.size != 4 || ![4,16].include?(n[0].size)
      logger.error "invalid neighbours format", neighbours: n
      return
    end

    n = n.dup
    nodeid = n.pop
    address = Address.from_endpoint *n
    get_node nodeid, address
  end

  @kademlia.recv_neighbours node, neighbours
end

#recv_ping(nodeid, payload, mdc) ⇒ Object

Update ip, port in node table. Addresses can only be learned by ping messages.



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/devp2p/discovery/protocol.rb', line 208

def recv_ping(nodeid, payload, mdc)
  if payload.size != 3
    logger.error "invalid ping payload", payload: payload
    return
  end

  node = get_node nodeid
  logger.debug "<<< ping", node: node

  remote_address = Address.from_endpoint(*payload[1]) # from
  my_address = Address.from_endpoint(*payload[2]) # my address

  get_node(nodeid).address.update remote_address
  @kademlia.recv_ping node, mdc
end

#recv_pong(nodeid, payload, mdc) ⇒ Object



234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/devp2p/discovery/protocol.rb', line 234

def recv_pong(nodeid, payload, mdc)
  if payload.size != 2
    logger.error 'invalid pong payload', payload: payload
    return
  end

  raise InvalidPayloadError unless payload[0].size == 3
  raise InvalidPayloadError unless [4,16].include?(payload[0][0].size)

  my_address = Address.from_endpoint *payload[0]
  echoed = payload[1]

  if @nodes.include?(nodeid)
    node = get_node nodeid
    @kademlia.recv_pong node, echoed
  else
    logger.debug "<<< unexpected pong from unknown node"
  end
end

#send_find_node(node, target_node_id) ⇒ Object



254
255
256
257
258
259
260
# File 'lib/devp2p/discovery/protocol.rb', line 254

def send_find_node(node, target_node_id)
  target_node_id = Utils.zpad_int target_node_id, Kademlia::PUBKEY_SIZE/8
  logger.debug ">>> find_node", remoteid: node

  message = pack CMD_ID_MAP[:find_node], [target_node_id]
  send_message node, message
end

#send_message(node, message) ⇒ Object

Raises:

  • (ArgumentError)


179
180
181
182
183
# File 'lib/devp2p/discovery/protocol.rb', line 179

def send_message(node, message)
  raise ArgumentError, 'node must have address' unless node.address
  logger.debug ">>> message", address: node.address
  @service.async.send_message node.address, message
end

#send_neighbours(node, neighbours) ⇒ Object

Raises:

  • (ArgumentError)


272
273
274
275
276
277
278
279
280
281
# File 'lib/devp2p/discovery/protocol.rb', line 272

def send_neighbours(node, neighbours)
  raise ArgumentError, 'neighbours must be Array' unless neighbours.instance_of?(Array)
  raise ArgumentError, 'neighbours must be Node' unless neighbours.all? {|n| n.is_a?(Node) }

  nodes = neighbours.map {|n| n.address.to_endpoint + [n.pubkey] }
  logger.debug ">>> neighbours", remoteid: node, count: nodes.size

  message = pack CMD_ID_MAP[:neighbours], [nodes]
  send_message node, message
end

#send_ping(node) ⇒ Object

Raises:

  • (ArgumentError)


185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/devp2p/discovery/protocol.rb', line 185

def send_ping(node)
  raise ArgumentError, "node must be Node" unless node.is_a?(Node)
  raise ArgumentError, "cannot ping self" if node == @node

  logger.debug ">>> ping", remoteid: node

  version = RLP::Sedes.big_endian_int.serialize VERSION
  payload = [
    version,
    Address.new(ip, udp_port, tcp_port).to_endpoint,
    node.address.to_endpoint
  ]

  message = pack CMD_ID_MAP[:ping], payload
  send_message node, message

  message[0,32] # return the MDC to identify pongs
end

#send_pong(node, token) ⇒ Object



224
225
226
227
228
229
230
231
232
# File 'lib/devp2p/discovery/protocol.rb', line 224

def send_pong(node, token)
  logger.debug ">>> pong", remoteid: node

  payload = [node.address.to_endpoint, token]
  raise InvalidPayloadError unless [4,16].include?(payload[0][0].size)

  message = pack CMD_ID_MAP[:pong], payload
  send_message node, message
end

#sign(msg) ⇒ Object



69
70
71
72
# File 'lib/devp2p/discovery/protocol.rb', line 69

def sign(msg)
  msg = Crypto.keccak256 msg
  Crypto.ecdsa_sign msg, @privkey
end

#tcp_portObject



315
316
317
# File 'lib/devp2p/discovery/protocol.rb', line 315

def tcp_port
  @app.config[:p2p][:listen_port]
end

#udp_portObject



311
312
313
# File 'lib/devp2p/discovery/protocol.rb', line 311

def udp_port
  @app.config[:discovery][:listen_port]
end

#unpack(message) ⇒ Object

macSize = 256 / 8 = 32 sigSize = 520 / 8 = 65 headSize = macSize + sigSize = 97



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/devp2p/discovery/protocol.rb', line 127

def unpack(message)
  mdc = message[0,32]
  if mdc != Crypto.keccak256(message[32..-1])
    logger.warn 'packet with wrong mcd'
    raise InvalidMessageMAC
  end

  signature = message[32,65]
  raise InvalidSignatureError unless signature.size == 65

  signed_data = Crypto.keccak256(message[97..-1])
  remote_pubkey = Crypto.ecdsa_recover(signed_data, signature)
  raise InvalidKeyError unless remote_pubkey.size == Kademlia::PUBKEY_SIZE / 8

  cmd_id = decode_cmd_id message[97]
  cmd = REV_CMD_ID_MAP[cmd_id]

  payload = RLP.decode message[98..-1], strict: false
  raise InvalidPayloadError unless payload.instance_of?(Array)

  # ignore excessive list elements as required by EIP-8
  payload = payload[0, CMD_ELEM_COUNT_MAP[cmd]||payload.size]

  return remote_pubkey, cmd_id, payload, mdc
end