Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
61 changes: 61 additions & 0 deletions packages/api/src/beacon/routes/lodestar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,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: string},
{query: {peer: string}},
{peerId: string | null},
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 +478,32 @@ export function getDefinitions(_config: ChainForkConfig): RouteDefinitions<Endpo
},
resp: EmptyResponseCodec,
},
addDirectPeer: {
url: "/eth/v1/lodestar/direct_peer",
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_peer",
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
14 changes: 14 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,20 @@ export function getLodestarApi({
await network.disconnectPeer(peerId);
},

async addDirectPeer({peer}) {
const peerId = await network.addDirectPeer(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: string): 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: string): 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: string): 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
53 changes: 53 additions & 0 deletions packages/beacon-node/src/network/gossip/gossipsub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,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 +160,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 +343,57 @@ export class Eth2Gossipsub extends GossipSub {
this.reportMessageValidationResult(data.msgId, data.propagationSource, data.acceptance);
});
}

/**
* Add a peer as a direct peer at runtime.
* Direct peers maintain permanent mesh connections without GRAFT/PRUNE negotiation.
*
* @param peerStr - Either a multiaddr with peer ID or an ENR string
* @returns The peer ID string if successfully added, null if parsing failed
*/
addDirectPeer(peerStr: string): string | null {
const parsed = parseDirectPeers([peerStr], this.logger);
if (parsed.length === 0) {
return null;
}

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

// Add to direct peers set (this is public readonly on GossipSub parent class)
this.direct.add(peerIdStr);

// Add addresses to peer store so we can connect
if (addrs.length > 0) {
this.libp2p.peerStore.merge(peerId, {multiaddrs: addrs}).catch((e) => {
this.logger.warn("Failed to add direct peer addresses to peer store", {peerId: peerIdStr}, e);
});
}

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

/**
* Remove a peer from direct peers.
*
* @param peerIdStr - The peer ID string to remove
* @returns true if the peer was removed, false if it wasn't a direct peer
*/
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
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: string): 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