Skip to content

Instantly share code, notes, and snippets.

@yeus
Forked from Realman78/PeerIdManager.ts
Created September 2, 2025 18:52
Show Gist options
  • Select an option

  • Save yeus/a6e81219c8debf06d967029d048e95f3 to your computer and use it in GitHub Desktop.

Select an option

Save yeus/a6e81219c8debf06d967029d048e95f3 to your computer and use it in GitHub Desktop.
Libp2p with static peer IDs

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 key
  • privateKeyFromProtobuf(bytes) - deserialize private key
  • generateKeyPair(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

  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)

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.8
  • libp2p: ^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! πŸ™

import { createLibp2p } from 'libp2p'
import { tcp } from '@libp2p/tcp'
import { mplex } from '@libp2p/mplex'
import { noise } from '@chainsafe/libp2p-noise'
import { PeerIdManager } from './PeerIdManager'
/**
* Example 1: Basic usage - just get the private key
*/
async function basicExample() {
console.log('=== Basic Example ===')
// Load or create a persistent private key
const privateKey = await PeerIdManager.getPrivateKey('./examples/peer-basic.key')
// Create libp2p node with the persistent private key
const node = await createLibp2p({
privateKey, // This ensures the same peer ID every time
addresses: {
listen: ['/ip4/0.0.0.0/tcp/0']
},
transports: [tcp()],
streamMuxers: [mplex()],
connectionEncryption: [noise()]
})
console.log(`Node started with peer ID: ${node.peerId.toString()}`)
// Stop the node
await node.stop()
console.log('Node stopped\n')
}
/**
* Example 2: Advanced usage - get both peerId and privateKey
*/
async function advancedExample() {
console.log('=== Advanced Example ===')
const { peerId, privateKey } = await PeerIdManager.loadOrCreate('./examples/peer-advanced.key')
console.log(`Loaded peer ID: ${peerId.toString()}`)
const node = await createLibp2p({
privateKey,
addresses: {
listen: ['/ip4/0.0.0.0/tcp/0']
},
transports: [tcp()],
streamMuxers: [mplex()],
connectionEncryption: [noise()]
})
// The node.peerId should be the same as the loaded peerId
console.log(`Node peer ID: ${node.peerId.toString()}`)
console.log(`Same peer ID: ${peerId.equals(node.peerId)}`)
await node.stop()
console.log('Node stopped\n')
}
/**
* Example 3: Multiple nodes with different persistent identities
*/
async function multipleNodesExample() {
console.log('=== Multiple Nodes Example ===')
// Create two nodes with different persistent identities
const node1PrivateKey = await PeerIdManager.getPrivateKey('./examples/node1.key')
const node2PrivateKey = await PeerIdManager.getPrivateKey('./examples/node2.key')
const node1 = await createLibp2p({
privateKey: node1PrivateKey,
addresses: { listen: ['/ip4/0.0.0.0/tcp/0'] },
transports: [tcp()],
streamMuxers: [mplex()],
connectionEncryption: [noise()]
})
const node2 = await createLibp2p({
privateKey: node2PrivateKey,
addresses: { listen: ['/ip4/0.0.0.0/tcp/0'] },
transports: [tcp()],
streamMuxers: [mplex()],
connectionEncryption: [noise()]
})
console.log(`Node 1 peer ID: ${node1.peerId.toString()}`)
console.log(`Node 2 peer ID: ${node2.peerId.toString()}`)
console.log(`Different nodes: ${!node1.peerId.equals(node2.peerId)}`)
await Promise.all([node1.stop(), node2.stop()])
console.log('Both nodes stopped\n')
}
/**
* Run all examples
*/
async function runExamples() {
try {
await basicExample()
await advancedExample()
await multipleNodesExample()
console.log('βœ… All examples completed successfully!')
console.log('πŸ”„ Run again to see the same peer IDs loaded from disk')
} catch (error) {
console.error('❌ Error running examples:', error)
}
}
// Run examples if this file is executed directly
if (require.main === module) {
runExamples()
}
export { basicExample, advancedExample, multipleNodesExample }
{
"name": "libp2p-persistent-peer-id-example",
"version": "1.0.0",
"description": "Example showing how to persist peer IDs in libp2p JavaScript (2025)",
"main": "PeerIdManager.ts",
"scripts": {
"build": "tsc",
"start": "tsx examples.ts",
"dev": "tsx --watch examples.ts"
},
"keywords": [
"libp2p",
"peer-id",
"persistence",
"p2p",
"networking"
],
"author": "Your Name",
"license": "MIT",
"dependencies": {
"@chainsafe/libp2p-noise": "^15.1.1",
"@libp2p/crypto": "^5.1.1",
"@libp2p/interface": "^2.2.1",
"@libp2p/mplex": "^10.1.1",
"@libp2p/peer-id": "^5.0.8",
"@libp2p/tcp": "^9.1.1",
"libp2p": "^2.3.1"
},
"devDependencies": {
"@types/node": "^20.0.0",
"tsx": "^4.0.0",
"typescript": "^5.0.0"
},
"engines": {
"node": ">=18.0.0"
}
}
import { readFile, writeFile } from 'fs/promises'
import { existsSync } from 'fs'
import { generateKeyPair, privateKeyToProtobuf, privateKeyFromProtobuf } from '@libp2p/crypto/keys'
import { peerIdFromPrivateKey } from '@libp2p/peer-id'
import type { PrivateKey } from '@libp2p/interface'
import type { PeerId } from '@libp2p/interface'
/**
* Utility class for managing persistent libp2p peer IDs
*
* This class handles the persistence of private keys to ensure
* consistent peer IDs across application restarts.
*/
export class PeerIdManager {
/**
* Loads peer ID from file or creates a new one
*
* @param filePath - Path to the private key file
* @returns The peer ID and private key
*/
static async loadOrCreate(filePath: string): Promise<{ peerId: PeerId; privateKey: PrivateKey }> {
let privateKey: PrivateKey
let peerId: PeerId
if (existsSync(filePath)) {
console.log(`πŸ“ Loading existing private key from ${filePath}`)
try {
// Read the protobuf private key bytes from file
const keyBytes = await readFile(filePath)
// Reconstruct the private key from the protobuf bytes
privateKey = await privateKeyFromProtobuf(keyBytes)
// Create peer ID from the private key
peerId = peerIdFromPrivateKey(privateKey)
console.log(`πŸ“‹ Loaded peer ID: ${peerId.toString()}`)
} catch (err: any) {
console.log('Error loading private key:', err)
console.log(`⚠️ Failed to load private key, creating new one: ${err.message}`)
// Fall back to creating a new key
privateKey = await generateKeyPair('Ed25519')
peerId = peerIdFromPrivateKey(privateKey)
console.log(`πŸ†• Generated new peer ID: ${peerId.toString()}`)
}
} else {
console.log(`πŸ†• Creating new private key and saving to ${filePath}`)
// Generate new private key
privateKey = await generateKeyPair('Ed25519')
peerId = peerIdFromPrivateKey(privateKey)
console.log(`πŸ†• Generated new peer ID: ${peerId.toString()}`)
}
try {
// Save the protobuf private key bytes to file
const keyBytes = privateKeyToProtobuf(privateKey)
await writeFile(filePath, keyBytes)
console.log(`πŸ’Ύ Private key saved to ${filePath}`)
} catch (err: any) {
console.log(`⚠️ Failed to save private key: ${err.message}`)
}
return { peerId, privateKey }
}
/**
* Alternative method that returns just the private key for use with createLibp2p
*
* @param filePath - Path to the private key file
* @returns The private key
*/
static async getPrivateKey(filePath: string): Promise<PrivateKey> {
const { privateKey } = await this.loadOrCreate(filePath)
return privateKey
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment