import { RLP } from '@ethereumjs/rlp' import { EthereumJSErrorWithoutCode, bigIntToBytes, bytesToHex, bytesToInt, bytesToUtf8, concatBytes, intToBytes, setLengthLeft, } from '@ethereumjs/util' import debugDefault from 'debug' import { keccak256 } from 'ethereum-cryptography/keccak.js' import { ecdsaRecover } from 'ethereum-cryptography/secp256k1-compat.js' import { assertEq, ipToBytes, ipToString, isV4Format, isV6Format, unstrictDecode } from '../util.ts' import type { Common } from '@ethereumjs/common' import { secp256k1 } from 'ethereum-cryptography/secp256k1' import type { PeerInfo } from '../types.ts' const debug = debugDefault('devp2p:dpt:server') function getTimestamp() { return (Date.now() / 1000) | 0 } const timestamp = { encode(value = getTimestamp() + 60) { const bytes = new Uint8Array(4) new DataView(bytes.buffer).setUint32(0, value) return bytes }, decode(bytes: Uint8Array) { if (bytes.length !== 4) throw new RangeError(`Invalid timestamp bytes :${bytesToHex(bytes)}`) return new DataView(bytes.buffer).getUint32(0) }, } const address = { encode(value: string) { if (isV4Format(value)) return ipToBytes(value) if (isV6Format(value)) return ipToBytes(value) throw EthereumJSErrorWithoutCode(`Invalid address: ${value}`) }, decode(bytes: Uint8Array) { if (bytes.length === 4) return ipToString(bytes) if (bytes.length === 16) return ipToString(bytes) const str = bytesToUtf8(bytes) if (isV4Format(str) || isV6Format(str)) return str // also can be host, but skip it right now (because need async function for resolve) throw EthereumJSErrorWithoutCode(`Invalid address bytes: ${bytesToHex(bytes)}`) }, } const port = { encode(value: number | null): Uint8Array { if (value === null) return new Uint8Array() if (value >>> 16 > 0) throw new RangeError(`Invalid port: ${value}`) return Uint8Array.from([(value >>> 8) & 0xff, (value >>> 0) & 0xff]) }, decode(bytes: Uint8Array): number | null { if (bytes.length === 0) return null return bytesToInt(bytes) }, } const endpoint = { encode(obj: PeerInfo): Uint8Array[] { return [ address.encode(obj.address!), port.encode(obj.udpPort ?? null), port.encode(obj.tcpPort ?? null), ] }, decode(payload: Uint8Array[]): PeerInfo { return { address: address.decode(payload[0]), udpPort: port.decode(payload[1]), tcpPort: port.decode(payload[2]), } }, } type InPing = { [0]: Uint8Array; [1]: Uint8Array[]; [2]: Uint8Array[]; [3]: Uint8Array } type OutPing = { version: number; from: PeerInfo; to: PeerInfo; timestamp: number } const ping = { encode(obj: OutPing): InPing { return [ intToBytes(obj.version), endpoint.encode(obj.from), endpoint.encode(obj.to), timestamp.encode(obj.timestamp), ] }, decode(payload: InPing): OutPing { return { version: bytesToInt(payload[0]), from: endpoint.decode(payload[1]), to: endpoint.decode(payload[2]), timestamp: timestamp.decode(payload[3]), } }, } type OutPong = { to: PeerInfo; hash: Uint8Array; timestamp: number } type InPong = { [0]: Uint8Array[]; [1]: Uint8Array[]; [2]: Uint8Array } const pong = { encode(obj: OutPong) { return [endpoint.encode(obj.to), obj.hash, timestamp.encode(obj.timestamp)] }, decode(payload: InPong) { return { to: endpoint.decode(payload[0]), hash: payload[1], timestamp: timestamp.decode(payload[2]), } }, } type OutFindMsg = { id: string; timestamp: number } type InFindMsg = { [0]: string; [1]: Uint8Array } const findneighbours = { encode(obj: OutFindMsg): InFindMsg { return [obj.id, timestamp.encode(obj.timestamp)] }, decode(payload: InFindMsg): OutFindMsg { return { id: payload[0], timestamp: timestamp.decode(payload[1]), } }, } type InNeighborMsg = { peers: PeerInfo[]; timestamp: number } type OutNeighborMsg = { [0]: Uint8Array[][]; [1]: Uint8Array } const neighbours = { encode(obj: InNeighborMsg): OutNeighborMsg { return [ obj.peers.map((peer: PeerInfo) => endpoint.encode(peer).concat(peer.id as Uint8Array)), timestamp.encode(obj.timestamp), ] }, decode(payload: OutNeighborMsg): InNeighborMsg { return { peers: payload[0].map((data) => { return { endpoint: endpoint.decode(data), id: data[3] } // hack for id }), timestamp: timestamp.decode(payload[1]), } }, } const messages: any = { ping, pong, findneighbours, neighbours, } type Types = { [index: string]: { [index: string]: number | string } } const types: Types = { byName: { ping: 0x01, pong: 0x02, findneighbours: 0x03, neighbours: 0x04, }, byType: { 0x01: 'ping', 0x02: 'pong', 0x03: 'findneighbours', 0x04: 'neighbours', }, } // [0, 32) data hash // [32, 96) signature // 96 recoveryId // 97 type // [98, length) data export function encode(typename: string, data: T, privateKey: Uint8Array, common?: Common) { const type: number = types.byName[typename] as number if (type === undefined) throw EthereumJSErrorWithoutCode(`Invalid typename: ${typename}`) const encodedMsg = messages[typename].encode(data) const typedata = concatBytes(Uint8Array.from([type]), RLP.encode(encodedMsg)) const sighash = (common?.customCrypto.keccak256 ?? keccak256)(typedata) const sig = (common?.customCrypto.ecsign ?? secp256k1.sign)(sighash, privateKey) const hashdata = concatBytes( setLengthLeft(bigIntToBytes(sig.r), 32), setLengthLeft(bigIntToBytes(sig.s), 32), Uint8Array.from([sig.recovery]), typedata, ) const hash = (common?.customCrypto.keccak256 ?? keccak256)(hashdata) return concatBytes(hash, hashdata) } export function decode(bytes: Uint8Array, common?: Common) { const hash = (common?.customCrypto.keccak256 ?? keccak256)(bytes.subarray(32)) assertEq(bytes.subarray(0, 32), hash, 'Hash verification failed', debug) const typedata = bytes.subarray(97) const type = typedata[0] const typename = types.byType[type] if (typename === undefined) throw EthereumJSErrorWithoutCode(`Invalid type: ${type}`) const data = messages[typename].decode(unstrictDecode(typedata.subarray(1))) const sighash = (common?.customCrypto.keccak256 ?? keccak256)(typedata) const signature = bytes.subarray(32, 96) const recoverId = bytes[96] const publicKey = (common?.customCrypto.ecdsaRecover ?? ecdsaRecover)( signature, recoverId, sighash, false, ) return { typename, data, publicKey } }