import { RLP } from '@ethereumjs/rlp' import { EthereumJSErrorWithoutCode, bytesToHex, bytesToUnprefixedHex, concatBytes, equalsBytes, } from '@ethereumjs/util' import debug from 'debug' import { publicKeyConvert } from 'ethereum-cryptography/secp256k1-compat.js' import { secp256k1 } from 'ethereum-cryptography/secp256k1.js' import type { EthStatusMsg } from './protocol/eth.ts' // Do not use :# here, no logging without sub namespace occurring and current code structure // otherwise creates loggers like `devp2p:#:eth` export const devp2pDebug = debug('devp2p') export function genPrivateKey(): Uint8Array { const privateKey = secp256k1.utils.randomPrivateKey() return secp256k1.utils.isValidPrivateKey(privateKey) === true ? privateKey : genPrivateKey() } export function pk2id(pk: Uint8Array): Uint8Array { if (pk.length === 33) { pk = publicKeyConvert(pk, false) } return pk.subarray(1) } export function id2pk(id: Uint8Array): Uint8Array { return concatBytes(Uint8Array.from([0x04]), id) } export function zfill(bytes: Uint8Array, size: number, leftpad: boolean = true): Uint8Array { if (bytes.length >= size) return bytes if (leftpad === undefined) leftpad = true const pad = new Uint8Array(size - bytes.length).fill(0x00) return leftpad ? concatBytes(pad, bytes) : concatBytes(bytes, pad) } export function xor(a: Uint8Array, b: any): Uint8Array { const length = Math.min(a.length, b.length) const bytes = new Uint8Array(length) for (let i = 0; i < length; ++i) bytes[i] = a[i] ^ b[i] return bytes } type assertInput = Uint8Array | Uint8Array[] | EthStatusMsg | number | null export function assertEq( expected: assertInput, actual: assertInput, msg: string, debug: Function, messageName?: string, ): void { let fullMsg if (expected instanceof Uint8Array && actual instanceof Uint8Array) { if (equalsBytes(expected, actual)) return fullMsg = `${msg}: ${bytesToHex(expected)} / ${bytesToHex(actual)}` const debugMsg = `[ERROR] ${fullMsg}` if (messageName !== undefined) { debug(messageName, debugMsg) } else { debug(debugMsg) } throw EthereumJSErrorWithoutCode(fullMsg) } if (expected === actual) return fullMsg = `${msg}: ${expected} / ${actual}` if (messageName !== undefined) { debug(messageName, fullMsg) } else { debug(fullMsg) } throw EthereumJSErrorWithoutCode(fullMsg) } export function formatLogId(id: string, verbose: boolean): string { const numChars = 7 if (verbose) { return id } else { return `${id.substring(0, numChars)}` } } export function formatLogData(data: string, verbose: boolean): string { const maxChars = 60 if (verbose || data.length <= maxChars) { return data } else { return `${data.substring(0, maxChars)}...` } } export class Deferred { promise: Promise resolve: (...args: any[]) => any = () => {} reject: (...args: any[]) => any = () => {} constructor() { this.promise = new Promise((resolve, reject) => { this.resolve = resolve this.reject = reject }) } } export function createDeferred(): Deferred { return new Deferred() } export function unstrictDecode(value: Uint8Array) { // rlp library throws on remainder.length !== 0 // this utility function bypasses that return RLP.decode(value, true).data } /*************************** ************************************************************/ // Methods borrowed from `node-ip` by Fedor Indutny (https://github.com/indutny/node-ip) // and modified to use Uint8Arrays instead of Buffers export const ipToString = (bytes: Uint8Array, offset?: number, length?: number): string => { offset = offset !== undefined ? ~~offset : 0 length = length ?? bytes.length - offset const tempArray: Array = [] let result: string = '' if (length === 4) { // IPv4 for (let i = 0; i < length; i++) { tempArray.push(bytes[offset + i]) } result = tempArray.join('.') } else if (length === 16) { // IPv6 for (let i = 0; i < length; i += 2) { tempArray.push(new DataView(bytes.buffer).getUint16(offset + i).toString(16)) } result = tempArray.join(':') result = result.replace(/(^|:)0(:0)*:0(:|$)/, '$1::$3') result = result.replace(/:{3,4}/, '::') } return result } const ipv4Regex = /^(\d{1,3}\.){3,3}\d{1,3}$/ const ipv6Regex = /^(::)?(((\d{1,3}\.){3}(\d{1,3}){1})?([0-9a-f]){0,4}:{0,2}){1,8}(::)?$/i export const isV4Format = function (ip: string): boolean { return ipv4Regex.test(ip) } export const isV6Format = function (ip: string): boolean { return ipv6Regex.test(ip) } export const ipToBytes = (ip: string, bytes?: Uint8Array, offset: number = 0): Uint8Array => { offset = ~~offset let result: Uint8Array if (isV4Format(ip)) { result = bytes ?? new Uint8Array(offset + 4) ip.split(/\./g).map((byte) => { result[offset++] = parseInt(byte, 10) & 0xff }) } else if (isV6Format(ip)) { const sections = ip.split(':', 8) let i for (i = 0; i < sections.length; i++) { const isv4 = isV4Format(sections[i]) let v4Bytes: Uint8Array = new Uint8Array([]) if (isv4) { v4Bytes = ipToBytes(sections[i]) sections[i] = bytesToUnprefixedHex(v4Bytes.subarray(0, 2)) } if (v4Bytes.length > 0 && ++i < 8) { sections.splice(i, 0, bytesToUnprefixedHex(v4Bytes.subarray(2, 4))) } } if (sections[0] === '') { while (sections.length < 8) sections.unshift('0') } else if (sections[sections.length - 1] === '') { while (sections.length < 8) sections.push('0') } else if (sections.length < 8) { for (i = 0; i < sections.length && sections[i] !== ''; i++); const argv: any = [i, 1] for (i = 9 - sections.length; i > 0; i--) { argv.push('0') } // eslint-disable-next-line prefer-spread sections.splice.apply(sections, argv) } result = bytes ?? new Uint8Array(offset + 16) for (i = 0; i < sections.length; i++) { const word = parseInt(sections[i], 16) result[offset++] = (word >> 8) & 0xff result[offset++] = word & 0xff } } else { throw Error(`Invalid ip format: ${ip}`) } if (result === undefined) { throw Error(`Invalid ip address: ${ip}`) } return result } /************ End of methods borrowed from `node-ip` ***************************/