Class: DEVp2p::Discovery::Protocol
- Inherits:
-
Kademlia::WireInterface
- Object
- Kademlia::WireInterface
- DEVp2p::Discovery::Protocol
- 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
-
#pubkey ⇒ Object
readonly
Returns the value of attribute pubkey.
Instance Method Summary collapse
- #bootstrap(nodes) ⇒ Object
-
#get_node(nodeid, address = nil) ⇒ Object
return node or create new, update address if supplied.
-
#initialize(app, service) ⇒ Protocol
constructor
A new instance of Protocol.
- #ip ⇒ Object
-
#pack(cmd_id, payload) ⇒ Object
UDP packets are structured as follows:.
- #receive_message(address, message) ⇒ Object
- #recv_find_node(nodeid, payload, mdc) ⇒ Object
- #recv_neighbours(nodeid, payload, mdc) ⇒ Object
-
#recv_ping(nodeid, payload, mdc) ⇒ Object
Update ip, port in node table.
- #recv_pong(nodeid, payload, mdc) ⇒ Object
- #send_find_node(node, target_node_id) ⇒ Object
- #send_message(node, message) ⇒ Object
- #send_neighbours(node, neighbours) ⇒ Object
- #send_ping(node) ⇒ Object
- #send_pong(node, token) ⇒ Object
- #sign(msg) ⇒ Object
- #tcp_port ⇒ Object
- #udp_port ⇒ Object
-
#unpack(message) ⇒ Object
macSize = 256 / 8 = 32 sigSize = 520 / 8 = 65 headSize = macSize + sigSize = 97.
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
#pubkey ⇒ Object (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
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 |
#ip ⇒ Object
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.
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 (address, ) logger.debug "<<< message", address: address raise ArgumentError, 'address must be Address' unless address.instance_of?(Address) begin remote_pubkey, cmd_id, payload, mdc = unpack # 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 = pack CMD_ID_MAP[:find_node], [target_node_id] node, end |
#send_message(node, message) ⇒ Object
179 180 181 182 183 |
# File 'lib/devp2p/discovery/protocol.rb', line 179 def (node, ) raise ArgumentError, 'node must have address' unless node.address logger.debug ">>> message", address: node.address @service.async. node.address, end |
#send_neighbours(node, neighbours) ⇒ Object
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 = pack CMD_ID_MAP[:neighbours], [nodes] node, end |
#send_ping(node) ⇒ Object
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 ] = pack CMD_ID_MAP[:ping], payload node, [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) = pack CMD_ID_MAP[:pong], payload node, 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_port ⇒ Object
315 316 317 |
# File 'lib/devp2p/discovery/protocol.rb', line 315 def tcp_port @app.config[:p2p][:listen_port] end |
#udp_port ⇒ Object
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() mdc = [0,32] if mdc != Crypto.keccak256([32..-1]) logger.warn 'packet with wrong mcd' raise InvalidMessageMAC end signature = [32,65] raise InvalidSignatureError unless signature.size == 65 signed_data = Crypto.keccak256([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 [97] cmd = REV_CMD_ID_MAP[cmd_id] payload = RLP.decode [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 |