Persistent Peer IDs in libp2p JavaScript

3 months ago 3

The problem: libp2p generates a new peer ID every time you restart your application, making it impossible to maintain a consistent identity on the network.

The solution: Persist the private key and reuse it on subsequent starts.

The libp2p documentation and many online examples are outdated. The API has changed significantly, and the old marshalPrivateKey/unmarshalPrivateKey functions no longer exist. After digging through the source code, I found the correct modern approach.

P.S. I tried using Cursor and Kiro for this exact task, but unfortunately, they weren’t able to provide a working solution. I ended up spending two days reviewing code and questioning whether the task was even feasible. I had even given them access to the source code of the relevant libraries, but despite that, they couldn’t figure it out. It seems they still need a fair amount of human guidance. That said, they were quite helpful when it came to adding comments and logs—as you’ll see in the code attached to this Gist!

Looking at the createLibp2p source code:

export async function createLibp2p(options = {}) { options.privateKey ??= await generateKeyPair('Ed25519') const node = new Libp2pClass({ ...await validateConfig(options), peerId: peerIdFromPrivateKey(options.privateKey) }) return node }

The solution is to persist the private key, not the peer ID itself.

The @libp2p/crypto/keys module exports these functions:

  • privateKeyToProtobuf(privateKey) - serialize private key
  • privateKeyFromProtobuf(bytes) - deserialize private key
  • generateKeyPair(type) - create new key pair
npm install @libp2p/crypto @libp2p/peer-id libp2p
import { PeerIdManager } from './PeerIdManager' import { createLibp2p } from 'libp2p' // Get persistent private key (creates one if it doesn't exist) const privateKey = await PeerIdManager.getPrivateKey('./peer-id.key') // Create libp2p node with consistent peer ID const node = await createLibp2p({ privateKey, // Same peer ID every time! addresses: { listen: ['/ip4/0.0.0.0/tcp/0'] } // ... other config }) console.log(`Node started with peer ID: ${node.peerId.toString()}`)
// Get both peer ID and private key const { peerId, privateKey } = await PeerIdManager.loadOrCreate('./peer-id.key') console.log(`My persistent peer ID: ${peerId.toString()}`) const node = await createLibp2p({ privateKey })
  1. First run: Generates a new Ed25519 private key and saves it as protobuf bytes
  2. Subsequent runs: Loads the saved key and recreates the same peer ID
  3. File format: Binary protobuf format (the standard libp2p uses internally)
  • Consistent identity across application restarts
  • Modern API - works with latest libp2p versions
  • Simple - just pass the privateKey to createLibp2p
  • Standard format - uses libp2p's internal protobuf serialization
  • Automatic fallback - creates new key if loading fails

Don't try to persist the peer ID object directly
Don't use the old marshalPrivateKey/unmarshalPrivateKey (they don't exist anymore)
Don't use privateKey.marshal() directly (different format)
Do use privateKeyToProtobuf/privateKeyFromProtobuf
Do pass the privateKey to createLibp2p({ privateKey })

  • @libp2p/crypto: ^5.1.1
  • @libp2p/peer-id: ^5.0.8
  • libp2p: ^2.3.1
  • Node.js 18+

Found an issue or improvement? Please share in the comments below!


This guide was created after struggling with outdated documentation and API changes. Hope it saves you time! 🙏

Read Entire Article