@ethereumjs/devp2p
Version:
A JavaScript implementation of ÐΞVp2p
276 lines (235 loc) • 8.68 kB
text/typescript
import {
EthereumJSErrorWithoutCode,
bytesToInt,
bytesToUnprefixedHex,
randomBytes,
} from '@ethereumjs/util'
import { keccak256 } from 'ethereum-cryptography/keccak.js'
import { secp256k1 } from 'ethereum-cryptography/secp256k1.js'
import { EventEmitter } from 'eventemitter3'
import { DNS } from '../dns/index.ts'
import { devp2pDebug, pk2id } from '../util.ts'
import { BanList } from './ban-list.ts'
import { KBucket } from './kbucket.ts'
import { Server as DPTServer } from './server.ts'
import type { Debugger } from 'debug'
import type { DPTEvent, DPTOptions, PeerInfo } from '../types.ts'
const DEBUG_BASE_NAME = 'dpt'
export class DPT {
public events: EventEmitter<DPTEvent>
protected _privateKey: Uint8Array
protected _banlist: BanList
protected _dns: DNS
private _debug: Debugger
public readonly id: Uint8Array | undefined
protected _kbucket: KBucket
protected _server: DPTServer
protected _refreshIntervalId: NodeJS.Timeout
protected _refreshIntervalSelectionCounter: number = 0
protected _shouldFindNeighbours: boolean
protected _shouldGetDnsPeers: boolean
protected _dnsRefreshQuantity: number
protected _dnsNetworks: string[]
protected _dnsAddr: string
protected _onlyConfirmed: boolean
protected _confirmedPeers: Set<string>
protected _keccakFunction: (msg: Uint8Array) => Uint8Array
private DEBUG: boolean
constructor(privateKey: Uint8Array, options: DPTOptions) {
this.events = new EventEmitter<DPTEvent>()
this._privateKey = privateKey
this.id = pk2id(secp256k1.getPublicKey(this._privateKey, false))
this._shouldFindNeighbours = options.shouldFindNeighbours ?? true
this._shouldGetDnsPeers = options.shouldGetDnsPeers ?? false
// By default, tries to connect to 12 new peers every 3s
this._dnsRefreshQuantity = Math.floor((options.dnsRefreshQuantity ?? 25) / 2)
this._dnsNetworks = options.dnsNetworks ?? []
this._dnsAddr = options.dnsAddr ?? '8.8.8.8'
this._dns = new DNS({ dnsServerAddress: this._dnsAddr, common: options.common })
this._banlist = new BanList()
this._onlyConfirmed = options.onlyConfirmed ?? false
this._confirmedPeers = new Set()
this._keccakFunction = options.common?.customCrypto.keccak256 ?? keccak256
this._kbucket = new KBucket(this.id)
this._kbucket.events.on('added', (peer: PeerInfo) => this.events.emit('peer:added', peer))
this._kbucket.events.on('removed', (peer: PeerInfo) => this.events.emit('peer:removed', peer))
this._kbucket.events.on('ping', this._onKBucketPing.bind(this))
this._server = new DPTServer(this, this._privateKey, {
timeout: options.timeout,
endpoint: options.endpoint,
createSocket: options.createSocket,
common: options.common,
})
this._server.events.once('listening', () => this.events.emit('listening'))
this._server.events.once('close', () => this.events.emit('close'))
this._server.events.on('error', (err) => this.events.emit('error', err))
this._debug = devp2pDebug.extend(DEBUG_BASE_NAME)
// When not using peer neighbour discovery we don't add peers here
// because it results in duplicate calls for the same targets
this._server.events.on('peers', (peers) => {
if (!this._shouldFindNeighbours) return
this._addPeerBatch(peers)
})
// By default calls refresh every 3s
const refreshIntervalSubdivided = Math.floor((options.refreshInterval ?? 60000) / 10) // 60 sec * 1000
this._refreshIntervalId = setInterval(() => this.refresh(), refreshIntervalSubdivided)
this.DEBUG =
typeof window === 'undefined' ? (process?.env?.DEBUG?.includes('ethjs') ?? false) : false
}
bind(...args: any[]): void {
this._server.bind(...args)
}
destroy(...args: any[]): void {
clearInterval(this._refreshIntervalId)
this._server.destroy(...args)
}
_onKBucketPing(oldPeers: PeerInfo[], newPeer: PeerInfo): void {
if (this._banlist.has(newPeer)) return
let count = 0
let err: Error | null = null
for (const peer of oldPeers) {
this._server
.ping(peer)
.then(() => {
if (++count < oldPeers.length) return
if (err === null)
this._banlist.add(newPeer, 300000) // 5 min * 60 * 1000
else this._kbucket.add(newPeer)
})
.catch((_err: Error) => {
this._banlist.add(peer, 300000) // 5 min * 60 * 1000
this._kbucket.remove(peer)
err = err ?? _err
})
}
}
_addPeerBatch(peers: PeerInfo[]): void {
const DIFF_TIME_MS = 200
let ms = 0
for (const peer of peers) {
setTimeout(() => {
this.addPeer(peer).catch((error) => {
this.events.emit('error', error)
})
}, ms)
ms += DIFF_TIME_MS
}
}
async bootstrap(peer: PeerInfo): Promise<void> {
try {
peer = await this.addPeer(peer)
if (peer.id !== undefined) {
this._confirmedPeers.add(bytesToUnprefixedHex(peer.id))
}
} catch (error: any) {
this.events.emit('error', error)
return
}
if (!this.id) return
if (this._shouldFindNeighbours) {
this._server.findneighbours(peer, this.id)
}
}
async addPeer(obj: PeerInfo): Promise<PeerInfo> {
if (this._banlist.has(obj)) throw EthereumJSErrorWithoutCode('Peer is banned')
if (this.DEBUG) {
this._debug(`attempt adding peer ${obj.address}:${obj.udpPort}`)
}
// check k-bucket first
const peer = this._kbucket.get(obj)
if (peer !== null) return peer
// check that peer is alive
try {
const peer = await this._server.ping(obj)
this.events.emit('peer:new', peer)
this._kbucket.add(peer)
return peer
} catch (err: any) {
this._banlist.add(obj, 300000) // 5 min * 60 * 1000
throw err
}
}
/**
* Add peer to a confirmed list of peers (peers meeting some
* level of quality, e.g. being on the same network) to allow
* for a more selective findNeighbours request and sending
* (with activated `onlyConfirmed` setting)
*
* @param id Unprefixed hex id
*/
confirmPeer(id: string) {
if (this._confirmedPeers.size < 5000) {
this._confirmedPeers.add(id)
}
}
getPeer(obj: string | Uint8Array | PeerInfo) {
return this._kbucket.get(obj)
}
getPeers() {
return this._kbucket.getAll()
}
numPeers() {
return this._kbucket.getAll().length
}
getClosestPeers(id: Uint8Array) {
let peers = this._kbucket.closest(id)
if (this._onlyConfirmed && this._confirmedPeers.size > 0) {
peers = peers.filter((peer) =>
this._confirmedPeers.has(bytesToUnprefixedHex(peer.id as Uint8Array)) ? true : false,
)
}
return peers
}
removePeer(obj: string | PeerInfo | Uint8Array) {
const peer = this._kbucket.get(obj)
if (peer?.id !== undefined) {
this._confirmedPeers.delete(bytesToUnprefixedHex(peer.id as Uint8Array))
}
this._kbucket.remove(obj)
}
banPeer(obj: string | PeerInfo | Uint8Array, maxAge?: number) {
this._banlist.add(obj, maxAge)
this._kbucket.remove(obj)
}
async getDnsPeers(): Promise<PeerInfo[]> {
return this._dns.getPeers(this._dnsRefreshQuantity, this._dnsNetworks)
}
async refresh(): Promise<void> {
if (this._shouldFindNeighbours) {
// Rotating selection counter going in loop from 0..9
this._refreshIntervalSelectionCounter = (this._refreshIntervalSelectionCounter + 1) % 10
const peers = this.getPeers()
if (this.DEBUG) {
this._debug(
`call .refresh() (selector ${this._refreshIntervalSelectionCounter}) (${peers.length} peers in table)`,
)
}
for (const peer of peers) {
// Randomly distributed selector based on peer ID
// to decide on subdivided execution
const selector = bytesToInt((peer.id as Uint8Array).subarray(0, 1)) % 10
let confirmed = true
if (this._onlyConfirmed && this._confirmedPeers.size > 0) {
const id = bytesToUnprefixedHex(peer.id as Uint8Array)
if (!this._confirmedPeers.has(id)) {
confirmed = false
}
}
if (confirmed && selector === this._refreshIntervalSelectionCounter) {
this._server.findneighbours(peer, randomBytes(64))
}
}
}
if (this._shouldGetDnsPeers) {
const dnsPeers = await this.getDnsPeers()
if (this.DEBUG) {
this._debug(
`.refresh() Adding ${dnsPeers.length} from DNS tree, (${
this.getPeers().length
} current peers in table)`,
)
}
this._addPeerBatch(dnsPeers)
}
}
}