Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
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_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
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
50 changes: 50 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,54 @@ 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: string): 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();

// Add addresses to peer store first so we can connect
if (addrs.length > 0) {
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
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