/ src / dpt / message.js
message.js
  1  const ip = require('ip')
  2  const rlp = require('rlp-encoding')
  3  const secp256k1 = require('secp256k1')
  4  const Buffer = require('safe-buffer').Buffer
  5  const { keccak256, int2buffer, buffer2int, assertEq } = require('../util')
  6  
  7  function getTimestamp () {
  8    return (Date.now() / 1000) | 0
  9  }
 10  
 11  const timestamp = {
 12    encode: function (value = getTimestamp() + 60) {
 13      const buffer = Buffer.allocUnsafe(4)
 14      buffer.writeUInt32BE(value)
 15      return buffer
 16    },
 17    decode: function (buffer) {
 18      if (buffer.length !== 4) throw new RangeError(`Invalid timestamp buffer :${buffer.toString('hex')}`)
 19      return buffer.readUInt32BE(0)
 20    }
 21  }
 22  
 23  const address = {
 24    encode: function (value) {
 25      if (ip.isV4Format(value)) return ip.toBuffer(value)
 26      if (ip.isV6Format(value)) return ip.toBuffer(value)
 27      throw new Error(`Invalid address: ${value}`)
 28    },
 29    decode: function (buffer) {
 30      if (buffer.length === 4) return ip.toString(buffer)
 31      if (buffer.length === 16) return ip.toString(buffer)
 32  
 33      const str = buffer.toString()
 34      if (ip.isV4Format(str) || ip.isV6Format(str)) return str
 35  
 36      // also can be host, but skip it right now (because need async function for resolve)
 37      throw new Error(`Invalid address buffer: ${buffer.toString('hex')}`)
 38    }
 39  }
 40  
 41  const port = {
 42    encode: function (value) {
 43      if (value === null) return Buffer.allocUnsafe(0)
 44      if ((value >>> 16) > 0) throw new RangeError(`Invalid port: ${value}`)
 45      return Buffer.from([ (value >>> 8) & 0xff, (value >>> 0) & 0xff ])
 46    },
 47    decode: function (buffer) {
 48      if (buffer.length === 0) return null
 49      // if (buffer.length !== 2) throw new RangeError(`Invalid port buffer: ${buffer.toString('hex')}`)
 50      return buffer2int(buffer)
 51    }
 52  }
 53  
 54  const endpoint = {
 55    encode: function (obj) {
 56      return [
 57        address.encode(obj.address),
 58        port.encode(obj.udpPort),
 59        port.encode(obj.tcpPort)
 60      ]
 61    },
 62    decode: function (payload) {
 63      return {
 64        address: address.decode(payload[0]),
 65        udpPort: port.decode(payload[1]),
 66        tcpPort: port.decode(payload[2])
 67      }
 68    }
 69  }
 70  
 71  const ping = {
 72    encode: function (obj) {
 73      return [
 74        int2buffer(obj.version),
 75        endpoint.encode(obj.from),
 76        endpoint.encode(obj.to),
 77        timestamp.encode(obj.timestamp)
 78      ]
 79    },
 80    decode: function (payload) {
 81      return {
 82        version: buffer2int(payload[0]),
 83        from: endpoint.decode(payload[1]),
 84        to: endpoint.decode(payload[2]),
 85        timestamp: timestamp.decode(payload[3])
 86      }
 87    }
 88  }
 89  
 90  const pong = {
 91    encode: function (obj) {
 92      return [
 93        endpoint.encode(obj.to),
 94        obj.hash,
 95        timestamp.encode(obj.timestamp)
 96      ]
 97    },
 98    decode: function (payload) {
 99      return {
100        to: endpoint.decode(payload[0]),
101        hash: payload[1],
102        timestamp: timestamp.decode(payload[2])
103      }
104    }
105  }
106  
107  const findneighbours = {
108    encode: function (obj) {
109      return [
110        obj.id,
111        timestamp.encode(obj.timestamp)
112      ]
113    },
114    decode: function (payload) {
115      return {
116        id: payload[0],
117        timestamp: timestamp.decode(payload[1])
118      }
119    }
120  }
121  
122  const neighbours = {
123    encode: function (obj) {
124      return [
125        obj.peers.map((peer) => endpoint.encode(peer).concat(peer.id)),
126        timestamp.encode(obj.timestamp)
127      ]
128    },
129    decode: function (payload) {
130      return {
131        peers: payload[0].map((data) => {
132          return { endpoint: endpoint.decode(data), id: data[3] } // hack for id
133        }),
134        timestamp: timestamp.decode(payload[1])
135      }
136    }
137  }
138  
139  const messages = { ping, pong, findneighbours, neighbours }
140  
141  const types = {
142    byName: {
143      ping: 0x01,
144      pong: 0x02,
145      findneighbours: 0x03,
146      neighbours: 0x04
147    },
148    byType: {
149      0x01: 'ping',
150      0x02: 'pong',
151      0x03: 'findneighbours',
152      0x04: 'neighbours'
153    }
154  }
155  
156  // [0, 32) data hash
157  // [32, 96) signature
158  // 96 recoveryId
159  // 97 type
160  // [98, length) data
161  
162  function encode (typename, data, privateKey) {
163    const type = types.byName[typename]
164    if (type === undefined) throw new Error(`Invalid typename: ${typename}`)
165    const encodedMsg = messages[typename].encode(data)
166    const typedata = Buffer.concat([ Buffer.from([ type ]), rlp.encode(encodedMsg) ])
167  
168    const sighash = keccak256(typedata)
169    const sig = secp256k1.sign(sighash, privateKey)
170    const hashdata = Buffer.concat([ sig.signature, Buffer.from([ sig.recovery ]), typedata ])
171    const hash = keccak256(hashdata)
172    return Buffer.concat([ hash, hashdata ])
173  }
174  
175  function decode (buffer) {
176    const hash = keccak256(buffer.slice(32))
177    assertEq(buffer.slice(0, 32), hash, 'Hash verification failed')
178  
179    const typedata = buffer.slice(97)
180    const type = typedata[0]
181    const typename = types.byType[type]
182    if (typename === undefined) throw new Error(`Invalid type: ${type}`)
183    const data = messages[typename].decode(rlp.decode(typedata.slice(1)))
184  
185    const sighash = keccak256(typedata)
186    const signature = buffer.slice(32, 96)
187    const recoverId = buffer[96]
188    const publicKey = secp256k1.recover(sighash, signature, recoverId, false)
189  
190    return { typename, data, publicKey }
191  }
192  
193  module.exports = { encode, decode }