Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
73 changes: 73 additions & 0 deletions packages/api/src/beacon/routes/lodestar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ export type GossipPeerScoreStat = {
// + Other un-typed options
};

/**
* A multiaddr with peer ID or ENR string.
*
* Supported formats:
* - Multiaddr with peer ID: `/ip4/192.168.1.1/tcp/9000/p2p/16Uiu2HAmKLhW7...`
* - ENR: `enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOo...`
*
* For multiaddrs, the string must contain a /p2p/ component with the peer ID.
* For ENRs, the TCP multiaddr and peer ID are extracted from the encoded record.
*/
export type DirectPeer = string;

export type RegenQueueItem = {
key: string;
args: unknown;
Expand Down Expand Up @@ -240,6 +252,41 @@ export type Endpoints = {
EmptyResponseData,
EmptyMeta
>;

/**
* Add a direct peer at runtime.
* Direct peers maintain permanent mesh connections without GRAFT/PRUNE negotiation.
* Accepts either a multiaddr with peer ID or an ENR string.
*/
addDirectPeer: Endpoint<
// ⏎
"POST",
{peer: DirectPeer},
{query: {peer: string}},
{peerId: string},
EmptyMeta
>;

/** Remove a peer from direct peers */
removeDirectPeer: Endpoint<
// ⏎
"DELETE",
{peerId: string},
{query: {peerId: string}},
{removed: boolean},
EmptyMeta
>;

/** Get list of direct peer IDs */
getDirectPeers: Endpoint<
// ⏎
"GET",
EmptyArgs,
EmptyRequest,
string[],
EmptyMeta
>;

/** Same to node api with new fields */
getPeers: Endpoint<
"GET",
Expand Down Expand Up @@ -443,6 +490,32 @@ export function getDefinitions(_config: ChainForkConfig): RouteDefinitions<Endpo
},
resp: EmptyResponseCodec,
},
addDirectPeer: {
url: "/eth/v1/lodestar/direct_peers",
method: "POST",
req: {
writeReq: ({peer}) => ({query: {peer}}),
parseReq: ({query}) => ({peer: query.peer}),
schema: {query: {peer: Schema.StringRequired}},
},
resp: JsonOnlyResponseCodec,
},
removeDirectPeer: {
url: "/eth/v1/lodestar/direct_peers",
method: "DELETE",
req: {
writeReq: ({peerId}) => ({query: {peerId}}),
parseReq: ({query}) => ({peerId: query.peerId}),
schema: {query: {peerId: Schema.StringRequired}},
},
resp: JsonOnlyResponseCodec,
},
getDirectPeers: {
url: "/eth/v1/lodestar/direct_peers",
method: "GET",
req: EmptyRequestCodec,
resp: JsonOnlyResponseCodec,
},
getPeers: {
url: "/eth/v1/lodestar/peers",
method: "GET",
Expand Down
17 changes: 17 additions & 0 deletions packages/beacon-node/src/api/impl/lodestar/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,23 @@ export function getLodestarApi({
await network.disconnectPeer(peerId);
},

async addDirectPeer({peer}) {
const peerId = await network.addDirectPeer(peer);
if (peerId === null) {
throw new ApiError(400, `Failed to add direct peer: invalid peer address or ENR "${peer}"`);
}
return {data: {peerId}};
},

async removeDirectPeer({peerId}) {
const removed = await network.removeDirectPeer(peerId);
return {data: {removed}};
},

async getDirectPeers() {
return {data: await network.getDirectPeers()};
},

async getPeers({state, direction}) {
const peers = (await network.dumpPeers()).filter(
(nodePeer) =>
Expand Down
12 changes: 12 additions & 0 deletions packages/beacon-node/src/network/core/networkCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,18 @@ export class NetworkCore implements INetworkCore {
await this.libp2p.hangUp(peerIdFromString(peerIdStr));
}

async addDirectPeer(peer: routes.lodestar.DirectPeer): Promise<string | null> {
return this.gossip.addDirectPeer(peer);
}

async removeDirectPeer(peerIdStr: PeerIdStr): Promise<boolean> {
return this.gossip.removeDirectPeer(peerIdStr);
}

async getDirectPeers(): Promise<string[]> {
return this.gossip.getDirectPeers();
}

private _dumpPeer(peerIdStr: string, connections: Connection[]): routes.lodestar.LodestarNodePeer {
const peerData = this.peersData.connectedPeers.get(peerIdStr);
const fork = this.config.getForkName(this.clock.currentSlot);
Expand Down
3 changes: 3 additions & 0 deletions packages/beacon-node/src/network/core/networkCoreWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ const libp2pWorkerApi: NetworkWorkerApi = {
getConnectedPeerCount: () => core.getConnectedPeerCount(),
connectToPeer: (peer, multiaddr) => core.connectToPeer(peer, multiaddr),
disconnectPeer: (peer) => core.disconnectPeer(peer),
addDirectPeer: (peer) => core.addDirectPeer(peer),
removeDirectPeer: (peerId) => core.removeDirectPeer(peerId),
getDirectPeers: () => core.getDirectPeers(),
dumpPeers: () => core.dumpPeers(),
dumpPeer: (peerIdStr) => core.dumpPeer(peerIdStr),
dumpPeerScoreStats: () => core.dumpPeerScoreStats(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,15 @@ export class WorkerNetworkCore implements INetworkCore {
disconnectPeer(peer: PeerIdStr): Promise<void> {
return this.getApi().disconnectPeer(peer);
}
addDirectPeer(peer: routes.lodestar.DirectPeer): Promise<string | null> {
return this.getApi().addDirectPeer(peer);
}
removeDirectPeer(peerId: PeerIdStr): Promise<boolean> {
return this.getApi().removeDirectPeer(peerId);
}
getDirectPeers(): Promise<string[]> {
return this.getApi().getDirectPeers();
}
dumpPeers(): Promise<routes.lodestar.LodestarNodePeer[]> {
return this.getApi().dumpPeers();
}
Expand Down
6 changes: 6 additions & 0 deletions packages/beacon-node/src/network/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ export interface INetworkCorePublic {
// Debug
connectToPeer(peer: PeerIdStr, multiaddr: MultiaddrStr[]): Promise<void>;
disconnectPeer(peer: PeerIdStr): Promise<void>;

// Direct peers management
addDirectPeer(peer: routes.lodestar.DirectPeer): Promise<string | null>;
removeDirectPeer(peerId: PeerIdStr): Promise<boolean>;
getDirectPeers(): Promise<string[]>;

dumpPeers(): Promise<routes.lodestar.LodestarNodePeer[]>;
dumpPeer(peerIdStr: PeerIdStr): Promise<routes.lodestar.LodestarNodePeer | undefined>;
dumpPeerScoreStats(): Promise<PeerScoreStats>;
Expand Down
57 changes: 56 additions & 1 deletion packages/beacon-node/src/network/gossip/gossipsub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {GossipSub, GossipsubEvents} from "@chainsafe/libp2p-gossipsub";
import {MetricsRegister, TopicLabel, TopicStrToLabel} from "@chainsafe/libp2p-gossipsub/metrics";
import {PeerScoreParams} from "@chainsafe/libp2p-gossipsub/score";
import {AddrInfo, SignaturePolicy, TopicStr} from "@chainsafe/libp2p-gossipsub/types";
import {routes} from "@lodestar/api";
import {BeaconConfig, ForkBoundary} from "@lodestar/config";
import {ATTESTATION_SUBNET_COUNT, SLOTS_PER_EPOCH, SYNC_COMMITTEE_SUBNET_COUNT} from "@lodestar/params";
import {SubnetID} from "@lodestar/types";
Expand Down Expand Up @@ -87,6 +88,7 @@ export class Eth2Gossipsub extends GossipSub {
private readonly logger: Logger;
private readonly peersData: PeersData;
private readonly events: NetworkEventBus;
private readonly libp2p: Libp2p;

// Internal caches
private readonly gossipTopicCache: GossipTopicCache;
Expand Down Expand Up @@ -159,6 +161,7 @@ export class Eth2Gossipsub extends GossipSub {
this.logger = logger;
this.peersData = peersData;
this.events = events;
this.libp2p = modules.libp2p;
this.gossipTopicCache = gossipTopicCache;

this.addEventListener("gossipsub:message", this.onGossipsubMessage.bind(this));
Expand Down Expand Up @@ -341,6 +344,58 @@ export class Eth2Gossipsub extends GossipSub {
this.reportMessageValidationResult(data.msgId, data.propagationSource, data.acceptance);
});
}

/**
* Add a peer as a direct peer at runtime. Accepts multiaddr with peer ID or ENR string.
* Direct peers maintain permanent mesh connections without GRAFT/PRUNE negotiation.
*/
async addDirectPeer(peerStr: routes.lodestar.DirectPeer): Promise<string | null> {
const parsed = parseDirectPeers([peerStr], this.logger);
if (parsed.length === 0) {
return null;
}

const {id: peerId, addrs} = parsed[0];
const peerIdStr = peerId.toString();

// Direct peers need addresses to connect - reject if none provided
if (addrs.length === 0) {
this.logger.warn("Cannot add direct peer without addresses", {peerId: peerIdStr});
return null;
}

// Add addresses to peer store first so we can connect
try {
await this.libp2p.peerStore.merge(peerId, {multiaddrs: addrs});
} catch (e) {
this.logger.warn("Failed to add direct peer addresses to peer store", {peerId: peerIdStr}, e as Error);
return null;
}

// Add to direct peers set only after addresses are stored
this.direct.add(peerIdStr);

this.logger.info("Added direct peer via API", {peerId: peerIdStr});
return peerIdStr;
}

/**
* Remove a peer from direct peers.
*/
removeDirectPeer(peerIdStr: string): boolean {
const removed = this.direct.delete(peerIdStr);
if (removed) {
this.logger.info("Removed direct peer via API", {peerId: peerIdStr});
}
return removed;
}

/**
* Get list of current direct peer IDs.
*/
getDirectPeers(): string[] {
return Array.from(this.direct);
}
}

/**
Expand Down Expand Up @@ -406,7 +461,7 @@ function getForkBoundaryLabel(boundary: ForkBoundary): ForkBoundaryLabel {
* For multiaddrs, the string must contain a /p2p/ component with the peer ID.
* For ENRs, the TCP multiaddr and peer ID are extracted from the encoded record.
*/
export function parseDirectPeers(directPeerStrs: string[], logger: Logger): AddrInfo[] {
export function parseDirectPeers(directPeerStrs: routes.lodestar.DirectPeer[], logger: Logger): AddrInfo[] {
const directPeers: AddrInfo[] = [];

for (const peerStr of directPeerStrs) {
Expand Down
12 changes: 12 additions & 0 deletions packages/beacon-node/src/network/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,18 @@ export class Network implements INetwork {
return this.core.disconnectPeer(peer);
}

addDirectPeer(peer: routes.lodestar.DirectPeer): Promise<string | null> {
return this.core.addDirectPeer(peer);
}

removeDirectPeer(peerId: string): Promise<boolean> {
return this.core.removeDirectPeer(peerId);
}

getDirectPeers(): Promise<string[]> {
return this.core.getDirectPeers();
}

dumpPeer(peerIdStr: string): Promise<routes.lodestar.LodestarNodePeer | undefined> {
return this.core.dumpPeer(peerIdStr);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ describe("data serialization through worker boundary", () => {
unsubscribeGossipCoreTopics: [],
connectToPeer: [peerId, ["/ip4/1.2.3.4/tcp/13000"]],
disconnectPeer: [peerId],
addDirectPeer: ["/ip4/1.2.3.4/tcp/13000/p2p/" + peerId],
removeDirectPeer: [peerId],
getDirectPeers: [],
dumpPeers: [],
dumpPeer: [peerId],
dumpPeerScoreStats: [],
Expand Down
Loading