libp2p / libp2p/js-libp2p

libp2p/circuit-relay-v2: a static relay reservation (`CircuitListen`) is never re-established after the relay connection closes

Open
#3,601 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

kind/bug
Dominant language
TypeScript
Stars
2.6k
Forks
546
Avg merge
8h 18m
Merged PRs (30d)
16

Description

  • Version:
    Verified in @libp2p/circuit-relay-v2@4.2.11 (with libp2p@3.3.8), and the
    relevant code is unchanged on main at the time of filing
    (packages/transport-circuit-relay-v2/src/transport/index.ts and
    transport/listener.ts).

  • Platform:
    Linux 7.0.0-29-generic #29-Ubuntu SMP PREEMPT_DYNAMIC Fri Jul 17 20:52:35 UTC 2026 x86_64 GNU/Linux

  • Subsystem:
    circuit-relay-v2

Severity:

High

Description:

If you give libp2p a static relay in your listen addresses (/dns4/relay/.../p2p/Qm.../p2p-circuit), the reservation only ever gets made once. When the relay restarts (or the connection drops for any reason), the client reconnects fine but never re-reserves, so it's unreachable through the relay until you restart the whole process. There's no error either, it just silently stays broken. The internal re-add path assumes every relay was found via discovery and throws HadEnoughRelaysError for static ones, then swallows it. Bonus problem: if the relay happens to be down when your node boots, start() just throws. Repro script attached, ~100 lines, fails on 4.2.11 and current main.

Steps to reproduce the error:

Scenario A — the headline bug:

  1. Relay up; client listens on <relayAddr>/p2p/<relayId>/p2p-circuit
  2. Reservation established — client announces .../p2p-circuit (sanity check)
  3. Relay stops; reservation removed
  4. Relay restarts with the same key and port; client dials it again, identify completes
  5. Expected: the /p2p-circuit address comes back
    Actual (bug): it never does

Scenario B — the boot-time sharp edge:

With the relay down, a node configured with the same listen address fails start() outright (no retry exists).

import { createLibp2p } from "libp2p";
import { webSockets } from "@libp2p/websockets";
import { noise } from "@chainsafe/libp2p-noise";
import { yamux } from "@chainsafe/libp2p-yamux";
import { identify } from "@libp2p/identify";
import { circuitRelayServer, circuitRelayTransport } from "@libp2p/circuit-relay-v2";
import { generateKeyPair } from "@libp2p/crypto/keys";
import { peerIdFromPrivateKey } from "@libp2p/peer-id";
import { multiaddr } from "@multiformats/multiaddr";

const RELAY_PORT = 40881;

function makeRelay(privateKey) {
  return createLibp2p({
    privateKey,
    addresses: { listen: [`/ip4/127.0.0.1/tcp/${RELAY_PORT}/ws`] },
    transports: [webSockets()],
    connectionEncrypters: [noise()],
    streamMuxers: [yamux()],
    services: {
      identify: identify(),
      relay: circuitRelayServer(),
    },
  });
}

function makeClient(relayListenAddr) {
  return createLibp2p({
    addresses: { listen: [relayListenAddr] },
    transports: [webSockets(), circuitRelayTransport()],
    connectionEncrypters: [noise()],
    streamMuxers: [yamux()],
    services: {
      identify: identify(),
    },
  });
}

const hasCircuitAddr = (node) =>
  node.getMultiaddrs().some((ma) => ma.toString().includes("/p2p-circuit"));

async function pollFor(label, predicate, timeoutMs) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    if (predicate()) {
      return true;
    }
    await new Promise((resolve) => setTimeout(resolve, 250));
  }
  return false;
}

const results = [];
const report = (name, ok, detail) => {
  results.push({ name, ok });
  console.log(`${ok ? "  PASS" : "  FAIL"}  ${name}${detail ? ` — ${detail}` : ""}`);
};

// ---------------------------------------------------------------- scenario A
console.log("scenario A: reservation is never re-established after relay restart");

const relayKey = await generateKeyPair("Ed25519");
const relayId = peerIdFromPrivateKey(relayKey);
const relayWsAddr = `/ip4/127.0.0.1/tcp/${RELAY_PORT}/ws/p2p/${relayId}`;
const relayListenAddr = `${relayWsAddr}/p2p-circuit`;

let relay = await makeRelay(relayKey);
const client = await makeClient(relayListenAddr);

report(
  "initial reservation established",
  await pollFor("reserve", () => hasCircuitAddr(client), 10_000),
  client.getMultiaddrs().map(String).join(", ") || "no addresses announced",
);

await relay.stop();
report(
  "reservation withdrawn after relay stops",
  await pollFor("withdraw", () => !hasCircuitAddr(client), 10_000),
);

relay = await makeRelay(relayKey);
await client.dial(multiaddr(relayWsAddr));
const reconnected = await pollFor(
  "reconnect",
  () => client.getConnections(relayId).length > 0,
  10_000,
);
report("client reconnected to the restarted relay", reconnected);

// Give the topology/identify/relay:discover path every opportunity to heal it.
const healed = await pollFor("re-reserve", () => hasCircuitAddr(client), 30_000);
if (healed) {
  report("reservation re-established after reconnect", true, "bug NOT reproduced on this version");
} else {
  report(
    "BUG REPRODUCED: connected to the relay for 30s, reservation never re-established",
    true,
    `announced: [${client.getMultiaddrs().map(String).join(", ") || "nothing"}]`,
  );
}

await client.stop();
await relay.stop();

// ---------------------------------------------------------------- scenario B
console.log("scenario B: node start() fails outright when the configured relay is down");
try {
  const orphan = await makeClient(relayListenAddr); // relay is stopped now
  await orphan.stop();
  report("BUG NOT REPRODUCED: start() succeeded with the relay down", true);
} catch (error) {
  report(
    `BUG REPRODUCED: start() threw with the relay down`,
    true,
    `${error.name}: ${String(error.message).slice(0, 120)}`,
  );
}

console.log(`\n${results.every((r) => r.ok) ? "all checks completed" : "CHECKS FAILED"}`);
process.exit(results.every((r) => r.ok) ? 0 : 1);

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with packages/transport-circuit-relay-v2/src/transport/index.ts and transport/listener.ts, then run the attached reproduction script for scenarios A and B. Trace static relay reservation setup, reconnect handling, and startup when the relay is unavailable. Done means reservations return after reconnect and startup does not fail permanently when the relay is down.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
networking
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.