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 keyprivateKeyFromProtobuf(bytes)- deserialize private keygenerateKeyPair(type)- create new key pair
npm install @libp2p/crypto @libp2p/peer-id libp2pimport { 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 })- 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)
- β 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.8libp2p: ^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! π