Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/discv5/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ const libp2p = createLibp2p({
});
```

## Dual-stack

With both `ip4` and `ip6` bind addresses, a peer that advertises both families is contacted over IPv4 first. Pass `config: {dualStackPreferredFamily: 6}` to try IPv6 first. Once a session exists with a peer, later requests reuse its address. A request whose packet cannot be sent (for example, no route to the peer's IPv6 address) fails at once with `SendFailed` instead of waiting for the request timeout.

## License

Apache-2.0
1 change: 1 addition & 0 deletions packages/discv5/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type IDiscv5Config = ISessionConfig &
export const defaultConfig: IDiscv5Config = {
addrVotesToUpdateEnr: 10,
allowUnverifiedSessions: false,
dualStackPreferredFamily: 4,
enrUpdate: true,
lookupNumResults: 16,
lookupParallelism: 3,
Expand Down
7 changes: 7 additions & 0 deletions packages/discv5/src/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface IDiscv5Metrics {

sentMessageCount: Gauge<{type: string}>;
rcvdMessageCount: Gauge<{type: string}>;
requestFailedCount: Gauge<{reason: string}>;

rateLimitHitIP: Gauge;
rateLimitHitTotal: Gauge;
Expand Down Expand Up @@ -72,6 +73,12 @@ export function createDiscv5Metrics(register: MetricsRegister) {
labelNames: ["type"],
name: "discv5_rcvd_message_count",
}),
/** Total number of failed requests by failure reason */
requestFailedCount: register.gauge<{reason: string}>({
help: "Count of the discv5 requests that failed by reason",
labelNames: ["reason"],
name: "discv5_request_failed_count",
}),
/** Total number messages sent by message type */
sentMessageCount: register.gauge<{type: string}>({
help: "Count of the discv5 messages sent by message type",
Expand Down
62 changes: 50 additions & 12 deletions packages/discv5/src/service/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import {
createPeerIdFromPublicKey,
} from "@chainsafe/enr";
import type {PeerId, PrivateKey} from "@libp2p/interface";
import type {Multiaddr} from "@multiformats/multiaddr";
import {type Multiaddr, isMultiaddr} from "@multiformats/multiaddr";
import {randomBytes} from "@noble/hashes/utils.js";
import {LRUCache} from "lru-cache";
import debug from "weald";
import {type IDiscv5Config, defaultConfig} from "../config/index.js";
import {
Expand Down Expand Up @@ -48,6 +49,7 @@ import {
type INodeAddress,
type NodeContact,
createNodeContact,
createNodeContactAt,
getNodeAddress,
getNodeId,
} from "../session/nodeInfo.js";
Expand Down Expand Up @@ -168,6 +170,15 @@ export class Discv5 extends (EventEmitter as {new (): Discv5EventEmitter}) {
*/
private connectedPeers: Map<NodeId, NodeJS.Timeout>;

/**
* Socket address each node last sent us an authenticated message from. Later requests reuse it
* instead of re-selecting a family from the ENR, so a peer that reached us over one family stays
* on it. Written only on inbound traffic: an outgoing handshake proves nothing about the address.
*
* BOUNDED: same bounds as the session cache
*/
private confirmedAddrs: LRUCache<NodeId, Multiaddr>;

/**
* Id for the next lookup that we start
*/
Expand Down Expand Up @@ -197,6 +208,7 @@ export class Discv5 extends (EventEmitter as {new (): Discv5EventEmitter}) {
this.activeRequests = new Map();
this.activeNodesResponses = new Map();
this.connectedPeers = new Map();
this.confirmedAddrs = new LRUCache({max: config.sessionCacheCapacity, ttl: config.sessionTimeout});
this.nextLookupId = 1;
this.ipMode = this.sessionService.transport.ipMode;
this.addrVotes = {
Expand Down Expand Up @@ -282,6 +294,7 @@ export class Discv5 extends (EventEmitter as {new (): Discv5EventEmitter}) {
clearInterval(intervalId);
}
this.connectedPeers.clear();
this.confirmedAddrs.clear();
this.sessionService.off("established", this.onEstablished);
this.sessionService.off("request", this.handleRpcRequest);
this.sessionService.off("response", this.handleRpcResponse);
Expand Down Expand Up @@ -404,7 +417,7 @@ export class Discv5 extends (EventEmitter as {new (): Discv5EventEmitter}) {
* Send FINDNODE message to remote and returns response
*/
async sendFindNode(remote: ENR | Multiaddr, distances: number[]): Promise<ENR[]> {
const contact = createNodeContact(remote, this.ipMode);
const contact = this.contactFor(remote);
const request = createFindNodeMessage(distances);

return await new Promise((resolve, reject) => {
Expand All @@ -423,7 +436,7 @@ export class Discv5 extends (EventEmitter as {new (): Discv5EventEmitter}) {
* Send TALKREQ message to dstId and returns response
*/
async sendTalkReq(remote: ENR | Multiaddr, payload: Uint8Array, protocol: string | Uint8Array): Promise<Uint8Array> {
const contact = createNodeContact(remote, this.ipMode);
const contact = this.contactFor(remote);
const request = createTalkRequestMessage(payload, protocol);

return await new Promise((resolve, reject) => {
Expand Down Expand Up @@ -456,8 +469,8 @@ export class Discv5 extends (EventEmitter as {new (): Discv5EventEmitter}) {
/**
* Sends a PING request to a node and returns response
*/
async sendPing(nodeAddr: ENR | Multiaddr): Promise<PongResponse> {
const contact = createNodeContact(nodeAddr, this.ipMode);
async sendPing(nodeAddr: ENR | Multiaddr, opts?: {preferFamily?: 4 | 6; probe?: boolean}): Promise<PongResponse> {
const contact = this.contactFor(nodeAddr, opts?.preferFamily);
const request = createPingMessage(this.enr.seq);

return await new Promise((resolve, reject) => {
Expand All @@ -467,11 +480,30 @@ export class Discv5 extends (EventEmitter as {new (): Discv5EventEmitter}) {
resolve: resolve as (val: PongResponse) => void,
},
contact,
probe: opts?.probe,
request,
});
});
}

/**
* Builds the contact for a remote. An explicit family wins, then the address the node last sent
* us an authenticated message from, then the configured dual-stack preference.
*/
private contactFor(remote: ENR | Multiaddr, preferFamily?: 4 | 6): NodeContact {
if (isMultiaddr(remote)) {
return createNodeContact(remote, this.ipMode);
}
if (preferFamily !== undefined) {
return createNodeContact(remote, this.ipMode, preferFamily);
}
const confirmedAddr = this.confirmedAddrs.get(remote.nodeId);
if (confirmedAddr) {
return createNodeContactAt(remote, confirmedAddr);
}
return createNodeContact(remote, this.ipMode, this.config.dualStackPreferredFamily ?? 4);
}

/**
* Ping all peers connected in the routing table
*/
Expand Down Expand Up @@ -568,7 +600,7 @@ export class Discv5 extends (EventEmitter as {new (): Discv5EventEmitter}) {
}

this.sendRpcRequest({
contact: createNodeContact(enr, this.ipMode),
contact: this.contactFor(enr),
lookupId,
request,
});
Expand All @@ -589,7 +621,7 @@ export class Discv5 extends (EventEmitter as {new (): Discv5EventEmitter}) {
const nodeAddr = getNodeAddress(activeRequest.contact);
log("Sending %s to node: %o", MessageType[activeRequest.request.type], nodeAddr);
try {
this.sessionService.sendRequest(activeRequest.contact, activeRequest.request);
this.sessionService.sendRequest(activeRequest.contact, activeRequest.request, {probe: activeRequest.probe});
this.metrics?.sentMessageCount.inc({type: MessageType[activeRequest.request.type]});
} catch (e) {
this.activeRequests.delete(bytesToBigint(activeRequest.request.id));
Expand Down Expand Up @@ -692,7 +724,7 @@ export class Discv5 extends (EventEmitter as {new (): Discv5EventEmitter}) {
) {
const enr = newStatus.enr;
setTimeout(() => {
this.sendPing(enr).catch((e) =>
this.sendPing(enr, {preferFamily: 6, probe: true}).catch((e) =>
log("Error pinging peer for IP vote %o: %s", enr, (e as Error).message)
);
}, 0);
Expand Down Expand Up @@ -731,6 +763,7 @@ export class Discv5 extends (EventEmitter as {new (): Discv5EventEmitter}) {
}
clearInterval(this.connectedPeers.get(nodeId) as NodeJS.Timeout);
this.connectedPeers.delete(nodeId);
this.confirmedAddrs.delete(nodeId);
break;
}
}
Expand Down Expand Up @@ -840,6 +873,7 @@ export class Discv5 extends (EventEmitter as {new (): Discv5EventEmitter}) {
private handleRpcRequest = (nodeAddr: INodeAddress, request: RequestMessage): void => {
const requestType = MessageType[request.type];
this.metrics?.rcvdMessageCount.inc({type: requestType});
this.confirmedAddrs.set(nodeAddr.nodeId, nodeAddr.socketAddr);

try {
switch (request.type) {
Expand All @@ -866,7 +900,7 @@ export class Discv5 extends (EventEmitter as {new (): Discv5EventEmitter}) {
// check if we need to update the known ENR
const entry = this.kbuckets.getWithPending(nodeAddr.nodeId);
if (entry && entry.value.seq < message.enrSeq) {
this.requestEnr(createNodeContact(entry.value, this.ipMode));
this.requestEnr(this.contactFor(entry.value));
}

const ipUDP = multiaddrToSocketAddress(nodeAddr.socketAddr);
Expand Down Expand Up @@ -980,6 +1014,7 @@ export class Discv5 extends (EventEmitter as {new (): Discv5EventEmitter}) {
);
return;
}
this.confirmedAddrs.set(nodeAddr.nodeId, nodeAddr.socketAddr);

// Check that the response type matches the request
if (!requestMatchesResponse(activeRequest.request, response)) {
Expand Down Expand Up @@ -1104,11 +1139,12 @@ export class Discv5 extends (EventEmitter as {new (): Discv5EventEmitter}) {
*/
private rpcFailure = (rpcId: RequestId, error: RequestErrorType): void => {
log("RPC error, removing request. Reason: %s, id %s", RequestErrorType[error], rpcId);
this.metrics?.requestFailedCount.inc({reason: RequestErrorType[error]});
const req = this.activeRequests.get(bytesToBigint(rpcId));
if (!req) {
return;
}
const {request, contact, lookupId, callbackPromise} = req;
const {request, contact, lookupId, callbackPromise, probe} = req;
this.activeRequests.delete(bytesToBigint(request.id));

const nodeId = getNodeId(contact);
Expand Down Expand Up @@ -1143,8 +1179,10 @@ export class Discv5 extends (EventEmitter as {new (): Discv5EventEmitter}) {
}
}

// report the node as being disconnected
this.connectionUpdated(nodeId, {type: ConnectionStatusType.Disconnected});
// report the node as being disconnected, unless the request was only a probe
if (!probe) {
this.connectionUpdated(nodeId, {type: ConnectionStatusType.Disconnected});
}

// If this is initiated by the user, return the error on the callback.
callbackPromise?.reject(new CodeError("RPC failure", RequestErrorType[error]));
Expand Down
4 changes: 4 additions & 0 deletions packages/discv5/src/service/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ export interface IActiveRequest<T extends RequestMessage = RequestMessage, U ext
* The lookup ID if the request was related to a lookup
*/
lookupId?: number;
/**
* Sent for a side effect only; its failure must not change the node's connection status
*/
probe?: boolean;
/**
* Callback if this request was from a user level request.
*/
Expand Down
22 changes: 17 additions & 5 deletions packages/discv5/src/session/nodeInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export type NodeContact =
*
* Note: this function may error if the input can't derive a public key or a valid socket address
*/
export function createNodeContact(input: ENR | Multiaddr, ipMode: IPMode): NodeContact {
export function createNodeContact(input: ENR | Multiaddr, ipMode: IPMode, preferFamily: 4 | 6 = 4): NodeContact {
if (isMultiaddr(input)) {
const options = multiaddrToObject(input);
if ((options.family === 4 && !ipMode.ip4) || (options.family === 6 && !ipMode.ip6)) {
Expand All @@ -93,17 +93,29 @@ export function createNodeContact(input: ENR | Multiaddr, ipMode: IPMode): NodeC
type: INodeContactType.Raw,
};
}
const socketAddr = getSocketAddressMultiaddrOnENR(input, ipMode);
const socketAddr = getSocketAddressMultiaddrOnENR(input, ipMode, preferFamily);
if (!socketAddr) {
throw new Error("ENR has no suitable udp multiaddr given the IP mode");
}
return createNodeContactAt(input, socketAddr);
}

/**
* Create an ENR contact at a known socket address instead of the address the ENR would select,
* e.g. the address of an already established session.
*/
export function createNodeContactAt(enr: ENR, socketAddr: Multiaddr): NodeContact {
const components = socketAddr.getComponents();
if (components.length !== 2 || !["ip4", "ip6"].includes(components[0].name) || components[1].name !== "udp") {
throw new Error("Socket address must be a bare /ip4|ip6/../udp/.. multiaddr");
}
return {
enr: input,
enr,
nodeAddress: {
nodeId: input.nodeId,
nodeId: enr.nodeId,
socketAddr,
},
publicKey: createKeypair({publicKey: input.publicKey, type: input.keypairType}),
publicKey: createKeypair({publicKey: enr.publicKey, type: enr.keypairType}),
type: INodeContactType.Enr,
};
}
Expand Down
Loading
Loading