Persistent Peer IDs in libp2p JavaScript (2025)
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.
Why This Guide Exists
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!
The Key Insight
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.
Current API (2025)
The @libp2p/crypto/keys module exports these functions:
privateKeyToProtobuf(privateKey)- serialize private keyprivateKeyFromProtobuf(bytes)- deserialize private keygenerateKeyPair(type)- create new key pair
Installation
npm install @libp2p/crypto @libp2p/peer-id libp2p
Usage
Basic Usage
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()}`)
Advanced Usage
// 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 })
How It Works
- First run: Generates a new Ed25519 private key and saves it as protobuf bytes
- Subsequent runs: Loads the saved key and recreates the same peer ID
- File format: Binary protobuf format (the standard libp2p uses internally)
Key Benefits
- ✅ 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
Common Pitfalls to Avoid
❌ 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 })
Tested With
@libp2p/crypto: ^5.1.1@libp2p/peer-id: ^5.0.8libp2p: ^2.3.1- Node.js 18+
Contributing
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! 🙏