-
Notifications
You must be signed in to change notification settings - Fork 204
Feat/persist peer identity 312 #1185
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Feat/persist peer identity 312 #1185
Conversation
- Add docstring to generate_peer_id_from() explaining deterministic peer ID generation - Add inline comments in new_swarm() explaining identity persistence options - Add comprehensive docstring to ID.from_pubkey() with examples - Document that same keypair always produces same peer ID This commit adds documentation only - no functional changes. Relates to libp2p#312
- Add identity_utils.py module with helper functions - save_identity(): Save keypair to disk with secure permissions - load_identity(): Load keypair from disk - create_identity_from_seed(): Generate deterministic identity from 32-byte seed - identity_exists(): Check if identity file exists - Export helper functions from main libp2p module These utilities enable opt-in identity persistence without changing default behavior. Users can now easily save/load identities or create deterministic identities from seeds. Relates to libp2p#312
- test_same_keypair_produces_same_peer_id: Verify deterministic peer ID generation - test_no_keypair_produces_different_peer_ids: Verify default random behavior - test_identity_from_seed_is_deterministic: Verify seed-based identity - test_different_seeds_produce_different_identities: Verify seed uniqueness - test_seed_must_be_32_bytes: Verify seed validation - test_save_and_load_identity: Verify file persistence - test_save/load_identity_with_string_path: Verify path type flexibility - test_load_nonexistent_identity_raises_error: Verify error handling - test_identity_exists: Verify file existence check - test_host_with_saved_identity: Integration test for saved identity - test_host_with_seed_identity: Integration test for seed-based identity - test_default_host_generates_random_identity: Verify backward compatibility All 14 tests pass successfully. Relates to libp2p#312
|
@seetadev Sir I have raised this PR. |
- Apply ruff formatting fixes (trailing whitespace, etc.) - Fix long line in identity_utils.py (E501) - Ensure codebase passes ruff check
7ba2584 to
da05feb
Compare
|
@seetadev please can you look at this pr. Thank you. |
|
@Smartdevs17 : Thanks so much for sharing—really appreciate it. CC’ing @lla-dane, @acul71, @yashksaini-coder and @Winter-Soren, who’ve worked closely with me in this area and can help with a peer review. Please feel free to reach out to them on Discord. |
|
Hello @Smartdevs17 thanks for this PR. AI PR Review — PR #1185: Feat/persist peer identity 312Reviewer: AI (claude-4.6-opus) 1. Summary of ChangesType: New Feature Related Issue: #312 — Provide utilities to persist network identities between runs of a node Problem: Solution: This PR introduces opt-in peer identity persistence through a new
Files Changed (4):
Breaking Changes: None. Default behavior is unchanged. 2. Branch Sync Status and Merge ConflictsBranch Sync Status
Merge Conflict Analysis
3. Strengths
4. Issues FoundCritical4.1 Doctest Failure Breaks Documentation Build
4.2 Trailing Whitespace in Docstrings
Major4.3 Must Use Existing Protobuf Serialization Instead of Raw Bytes
4.5
|
| Check | Result | Notes |
|---|---|---|
Lint (make lint) |
Trailing whitespace in libp2p/__init__.py docstring was auto-fixed by pre-commit hooks. First pass failed. |
|
Type Check (make typecheck) |
✅ Pass | mypy and pyrefly both passed cleanly. |
Tests (make test) |
✅ Pass | 1933 passed, 16 skipped, 25 warnings in 92.84s |
Docs (make linux-docs) |
❌ FAIL | Doctest failures in libp2p.peer.rst — 2 failures due to missing ID import in doctest example. Exit code 2. |
Test Coverage Analysis
The 14 new tests in tests/core/test_identity_persistence.py cover:
- ✅ Same keypair → same peer ID (determinism)
- ✅ Different keypairs → different peer IDs (uniqueness)
- ✅ Seed-based identity determinism
- ✅ Different seeds → different identities
- ✅ Seed length validation (too short, too long)
- ✅ Save and load round-trip
- ✅ String and Path type acceptance
- ✅ Nonexistent file raises
FileNotFoundError - ✅
identity_exists()helper - ✅ Integration with
new_host()(saved identity, seed identity, default random)
Missing Test Cases
- ❌ No test for saving a non-Ed25519 key (e.g., RSA) — should verify it either works or raises a clear error
- ❌ No test for loading a corrupt/invalid key file
- ❌ No test for loading a file with wrong size (e.g., 0 bytes, 100 bytes)
- ❌ No test for file permission verification (that
chmod 0o600is applied) - ❌ No test for overwriting an existing identity file
Warnings (from test output)
25 warnings were reported during the test run. These are pre-existing warnings unrelated to this PR (primarily trio and pytest-asyncio deprecation warnings).
9. Recommendations for Improvement
For This PR (Required)
-
Fix the doctest failure (BLOCKER): Add
from libp2p.peer.id import IDto the doctest example inID.from_pubkey(). -
Add the newsfragment (BLOCKER): Create
newsfragments/312.feature.rstwith a user-facing description. -
Use protobuf serialization (CRITICAL): Replace raw byte serialization with
PrivateKey.serialize()/deserialize_private_key()— the existing py-libp2p infrastructure that matches go-libp2p'sMarshalPrivateKey/UnmarshalPrivateKey. See Issue 4.3 for the exact code changes. This resolves Issues 4.3, 4.4, 5.2, and 5.3 in one change. -
Fix the TOCTOU file permission issue: Use
os.open()with mode0o600instead of write-then-chmod. -
Fix trailing whitespace: Run
pre-commit run --all-filesbefore submitting. -
Standardize docstring style: Use
:param:/:return:/:raises:format to match the codebase convention. -
Add missing test cases: Corrupt files, wrong key types (now testable with protobuf), permission checks, overwrite behavior.
For a Follow-Up PR (Recommended)
-
Add optional password-based encryption for key files. See Appendix A for a complete design proposal with code, including a
password_provider: Callablepattern for flexible secret management across deployment contexts (interactive, environment variable, OS keyring, cloud KMS). -
Consolidate with existing
save_keypair/load_keypair: Once protobuf serialization is in place, deprecate or remove the PEM-basedsave_keypair()/load_keypair()inlibp2p/__init__.pyto avoid having two incompatible persistence paths. -
Acknowledge remaining Provide utilities to persist network identities between runs of a node #312 design work: The "key pair provider" pattern (
Callable[[], KeyPair]) andKeyStore/PeerStoreabstraction from Provide utilities to persist network identities between runs of a node #312 are not yet implemented. Open a follow-up issue to track this.
10. Questions for the Author
-
Why not use the existing protobuf serialization? py-libp2p already has
PrivateKey.serialize()anddeserialize_private_key()inlibp2p/crypto/keys.pyandlibp2p/crypto/serialization.py— the exact equivalent of go-libp2p'sMarshalPrivateKey/UnmarshalPrivateKey. These handle key type tagging and support all key types automatically. Why was raw byte serialization chosen instead? -
Relationship to existing
save_keypair/load_keypair: The codebase already hassave_keypair()andload_keypair()inlibp2p/__init__.pythat use PEM format. How do you envision these coexisting with the newsave_identity()/load_identity()which use a third, incompatible format (raw bytes)? Should one approach be consolidated? -
Key pair provider pattern: The original issue Provide utilities to persist network identities between runs of a node #312 proposed a
Callable[[], KeyPair]provider pattern for maximum flexibility. Was this considered? The utility-function approach works but is less composable. -
Why not build on
PeerStore/KeyStore? A maintainer suggested in Provide utilities to persist network identities between runs of a node #312 building onKeyStore/PeerStoreabstractions (similar to Go's datastore-backed keybook atp2p/host/peerstore/pstoreds/keybook.go). Was this approach considered and rejected for simplicity?
11. Overall Assessment
| Criterion | Rating |
|---|---|
| Quality Rating | Needs Work |
| Security Impact | Low |
| Merge Readiness | Needs fixes |
| Confidence | High |
Summary
The PR addresses a genuine need (issue #312) with a clean, opt-in design that preserves backward compatibility. The test coverage is good and the code is well-documented. However, there are two blockers that must be resolved before merge:
- Missing newsfragment (
newsfragments/312.feature.rst) - Doctest failure breaking the documentation build
Additionally, the PR introduces functional duplication with the existing save_keypair/load_keypair functions and has a minor file permission TOCTOU issue that should be addressed given we're handling private key material.
The PR is a solid first step toward identity persistence, but needs the blockers fixed and the duplication question addressed before it can be merged.
Appendix A: Follow-Up PR Design — Password-Encrypted Key Files with Provider Pattern
Status: Recommended for a follow-up PR after the protobuf serialization fix lands.
Neither go-libp2p nor py-libp2p currently support password-encrypted key files.
This would make py-libp2p the first libp2p implementation with built-in key encryption.
Encrypted File Format
┌──────────────────────────────────────────────────────┐
│ Encrypted Key File Format │
├──────────────────────────────────────────────────────┤
│ magic: b"LIBP2P-KEY\x01" (11 bytes, version 1) │
│ salt: 16 random bytes (for scrypt KDF) │
│ nonce: 12 bytes (for AES-256-GCM) │
│ ciphertext: AES-256-GCM( │
│ key = scrypt(password, salt), │
│ plaintext = protobuf_serialized_key │
│ ) │
└──────────────────────────────────────────────────────┘
Unencrypted files remain plain protobuf (no magic header), so load_identity() auto-detects the format.
Implementation
Using the cryptography library (already a py-libp2p dependency):
import os
import getpass
from pathlib import Path
from typing import Callable
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
from libp2p.crypto.keys import KeyPair
from libp2p.crypto.serialization import deserialize_private_key
_MAGIC = b"LIBP2P-KEY\x01"
def save_identity(
key_pair: KeyPair,
filepath: str | Path,
password: str | None = None,
) -> None:
"""
Save a keypair to disk using protobuf serialization.
If a password is provided, the key is encrypted with AES-256-GCM
using a scrypt-derived key. Otherwise, the key is saved as plain
protobuf (matching go-libp2p's MarshalPrivateKey format).
:param key_pair: The KeyPair to save
:param filepath: Path where the key will be saved
:param password: Optional password for encryption
"""
filepath = Path(filepath)
# Serialize using protobuf (type-tagged, like Go)
data = key_pair.private_key.serialize()
if password is not None and password != "":
salt = os.urandom(16)
nonce = os.urandom(12)
kdf = Scrypt(salt=salt, length=32, n=2**18, r=8, p=1)
aes_key = kdf.derive(password.encode("utf-8"))
ciphertext = AESGCM(aes_key).encrypt(nonce, data, None)
data = _MAGIC + salt + nonce + ciphertext
# Atomic file creation with restrictive permissions
fd = os.open(str(filepath), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
try:
os.write(fd, data)
finally:
os.close(fd)
def load_identity(
filepath: str | Path,
password: str | None = None,
password_provider: Callable[[], str] | None = None,
) -> KeyPair:
"""
Load a keypair from disk.
Auto-detects encrypted vs. plain protobuf format. If encrypted,
resolves the password from (in order):
1. Explicit ``password`` parameter
2. ``password_provider()`` callback
3. ``LIBP2P_KEY_PASSPHRASE`` environment variable
4. Interactive prompt via ``getpass``
:param filepath: Path to the saved key file
:param password: Optional password for decryption
:param password_provider: Optional callback that returns the password
:return: KeyPair loaded from the file
:raises FileNotFoundError: If the file doesn't exist
:raises ValueError: If the file is encrypted and no password is available,
or if the password is incorrect
"""
filepath = Path(filepath)
data = filepath.read_bytes()
# Auto-detect encrypted format
if data.startswith(_MAGIC):
# Resolve password from tiered sources
if password is None and password_provider is not None:
password = password_provider()
if password is None:
password = os.environ.get("LIBP2P_KEY_PASSPHRASE")
if password is None:
password = getpass.getpass(
f"Enter passphrase for {filepath.name}: "
)
if password is None or password == "":
raise ValueError(
"Key file is encrypted but no password was provided"
)
salt = data[11:27]
nonce = data[27:39]
ciphertext = data[39:]
kdf = Scrypt(salt=salt, length=32, n=2**18, r=8, p=1)
aes_key = kdf.derive(password.encode("utf-8"))
try:
data = AESGCM(aes_key).decrypt(nonce, ciphertext, None)
except Exception as e:
raise ValueError("Incorrect password or corrupted key file") from e
# Deserialize protobuf (auto-detects key type)
private_key = deserialize_private_key(data)
public_key = private_key.get_public_key()
return KeyPair(private_key, public_key)Password Provider Pattern — Usage Examples
The password_provider: Callable[[], str] parameter enables flexible secret management across all deployment contexts:
# 1. Interactive desktop application — prompt the user
key_pair = load_identity("peer.key")
# → Falls through to getpass.getpass() automatically
# 2. Server with environment variable
# LIBP2P_KEY_PASSPHRASE=mysecret python my_node.py
key_pair = load_identity("peer.key")
# → Reads from $LIBP2P_KEY_PASSPHRASE automatically
# 3. Kubernetes / Docker — mounted secret file
key_pair = load_identity(
"peer.key",
password_provider=lambda: Path("/run/secrets/libp2p_pw").read_text().strip()
)
# 4. OS Keyring (macOS Keychain, GNOME Keyring, Windows Credential Manager)
import keyring
key_pair = load_identity(
"peer.key",
password_provider=lambda: keyring.get_password("py-libp2p", "peer.key")
)
# 5. HashiCorp Vault / AWS Secrets Manager
key_pair = load_identity(
"peer.key",
password_provider=lambda: vault_client.read("secret/libp2p")["password"]
)
# 6. GUI application
key_pair = load_identity(
"peer.key",
password_provider=lambda: gui_password_dialog("Enter key passphrase")
)
# 7. No encryption (default, matches current go-libp2p behavior)
save_identity(key_pair, "peer.key") # no password → plain protobuf
key_pair = load_identity("peer.key") # loads directly, no promptPassword Resolution Order
┌─────────────────────────────────────────────────────────┐
│ load_identity("peer.key", ...) called │
└───────────────┬─────────────────────────────────────────┘
│
▼
┌──── Is the file encrypted? (check magic bytes) ────┐
│ │
No Yes
│ │
▼ ▼
Deserialize protobuf ┌── Resolve password ──┐
directly (any key type) │ │
│ 1. password= param │
│ 2. password_provider │
│ 3. $LIBP2P_KEY_... │
│ 4. getpass() prompt │
│ │
└──────────┬───────────┘
│
▼
Decrypt → Deserialize protobuf
Why This Design
-
Library provides the mechanism, application chooses the policy. The
password_providercallback lets each deployment context plug in its own secret source without the library needing to know about Vault, Kubernetes, keyrings, etc. -
Backward compatible. Unencrypted files (plain protobuf) load without any password interaction. The encryption is fully opt-in.
-
Auto-detection. The magic header (
LIBP2P-KEY\x01) distinguishes encrypted files from plain protobuf, so a singleload_identity()call handles both. -
Graceful fallback chain. Servers use env vars, desktops get an interactive prompt, containers use mounted secrets — all through the same API.
-
py-libp2p would lead. go-libp2p relies solely on file permissions (
0400) with no encryption support. This would make py-libp2p the first libp2p implementation with built-in password-based key encryption.
|
Few differences that I found during analysis, which I have resolved @Smartdevs17 Key Differences1. Documentation Build Status — RESOLVEDAnalysis: The doctest failure identified in 2. Trailing Whitespace — RESOLVEDAnalysis: The previous code was pushed without proper linting applied, so there were Code quality issues which now have been cleaned up. 3. 📊 Test Execution Metrics — MINOR CHANGES
Analysis: Test warnings have been reduced (improvement), but execution time has increased significantly (possibly due to additional test infrastructure or different test environment). ❌ Critical Issues🚨 BLOCKER 1: Missing Newsfragment
🚨 BLOCKER 2: Raw Byte Serialization vs. Protobuf
Security Assessment — ConsistencyThese areas need some looking into it, you have not taken the following points into account, avoiding these can lead to security problems.
Documentation Issues — Consistency
The PR has made incremental progress (fixed doctest, cleaned up lint), but still needs work a lot. |
| public_key = private_key.get_public_key() | ||
|
|
||
| return KeyPair(private_key, public_key) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No Private Key Validation on Load
Issue:
- No validation of file integrity (checksum, length check, version header)
- No error handling for corrupted/truncated files
- Directly passes raw bytes to Ed25519PrivateKey.from_bytes()
Impact: Corrupted identity files could:
- Produce invalid keys silently
- Cause subtle cryptographic failures
- Be difficult to debug
Recommendation: Add validation:
def load_identity(filepath: str | Path) -> KeyPair:
filepath = Path(filepath)
private_key_bytes = filepath.read_bytes()
# Ed25519 private keys are exactly 32 bytes
if len(private_key_bytes) != 32:
raise ValueError(f"Invalid private key file: expected 32 bytes, got {len(private_key_bytes)}")
# Reconstruct and validate
private_key = Ed25519PrivateKey.from_bytes(private_key_bytes)
# Verify key is valid by attempting to get public key
try:
public_key = private_key.get_public_key()
# Additional validation: verify we can serialize/deserialize roundtrip
_ = public_key.serialize()
except Exception as e:
raise ValueError(f"Invalid private key in file: {e}")
return KeyPair(private_key, public_key)| >>> save_identity(key_pair, "my_peer_identity.key") | ||
|
|
||
| """ | ||
| filepath = Path(filepath) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Issue: Between write_bytes() (line 55) and chmod() (line 59), the file exists with default permissions (often 0644). In a multi-user system, another process could read the private key during this window.
Recommendation: Create file with restrictive permissions atomically:
import os
def save_identity(key_pair: KeyPair, filepath: str | Path) -> None:
filepath = Path(filepath)
private_key_bytes = key_pair.private_key.to_bytes()
# Create file with 0600 permissions atomically
fd = os.open(filepath, os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 0o600)
try:
os.write(fd, private_key_bytes)
finally:
os.close(fd)|
|
||
| # Write to file with restrictive permissions (owner read/write only) | ||
| filepath.write_bytes(private_key_bytes) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Issue: If the parent directory doesn't exist, filepath.write_bytes() raises FileNotFoundError.
Recommendation: Create parent directories:
filepath = Path(filepath)
filepath.parent.mkdir(parents=True, exist_ok=True)
private_key_bytes = key_pair.private_key.to_bytes()
filepath.write_bytes(private_key_bytes)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing Test: File Permissions Not Verified
Issue: No test verifies that save_identity() actually sets file permissions to 0600.
Recommendation: Add a test:
def test_save_identity_sets_restrictive_permissions():
"""Verify that saved identity files have restrictive permissions."""
with tempfile.TemporaryDirectory() as tmpdir:
filepath = Path(tmpdir) / "test_identity.key"
key_pair = create_new_key_pair()
save_identity(key_pair, filepath)
# Check file permissions (Unix-like systems)
import os
import stat
if os.name != 'nt': # Skip on Windows
mode = filepath.stat().st_mode
# Should be 0600 (owner read/write only)
assert stat.S_IMODE(mode) == 0o600,
f"Expected permissions 0600, got {oct(stat.S_IMODE(mode))}"Missing Test: Corrupted File Not Tested
Location: Test file - Missing test
Issue: No test for loading corrupted/truncated identity files.
Recommendation: Add a test:
def test_load_corrupted_identity_raises_error():
"""Verify that loading a corrupted identity file raises ValueError."""
with tempfile.TemporaryDirectory() as tmpdir:
filepath = Path(tmpdir) / "corrupted.key"
# Write invalid data
filepath.write_bytes(b"not a valid private key")
with pytest.raises(ValueError):
load_identity(filepath)
yashksaini-coder
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR needs to fix these bugs
-
Add private key validation on load (length check, format validation)
-
Fix race condition in file permissions (use atomic create with 0600)
-
Add
filepath.parent.mkdir(parents=True, exist_ok=True)before writing -
Fix docstring example (line comparing identical objects)
-
Consider adding format version header for future algorithm support
-
Add file permissions verification test
-
Add corrupted file handling test
…ersistence Security Fixes: - Fix race condition in save_identity() by creating file atomically with 0600 permissions - Add validation on load_identity() to detect corrupted/invalid key files - Create parent directories automatically to prevent FileNotFoundError Compatibility Fixes: - Use protobuf serialization (serialize/deserialize_private_key) for interoperability - Support Ed25519, RSA, and Secp256k1 keys automatically via protobuf Documentation: - Fix doctest in ID.from_pubkey() by adding missing import - Add newsfragment for identity persistence feature Addresses review feedback from PR libp2p#1185
Added 7 new test cases addressing PR review feedback: Security Tests: - test_save_identity_sets_restrictive_permissions: Verify 0600 file permissions - test_load_corrupted_identity_raises_error: Validate error handling for invalid data - test_load_truncated_file_raises_error: Handle interrupted writes - test_load_empty_file_raises_error: Reject empty files Functionality Tests: - test_save_and_load_rsa_identity: Verify RSA key support via protobuf - test_overwrite_existing_identity: Ensure file overwrites work correctly - test_save_identity_creates_parent_directories: Auto-create nested paths All tests pass. Addresses review feedback from yashksaini-coder and main reviewer.
|
Hi @Smartdevs17. Full review here: AI PR Review — PR #1185: Feat/persist peer identity 312Reviewer: AI (Cursor / py-libp2p PR Review Prompt) 1. Summary of ChangesType: New feature Related Issue: #312 — Provide utilities to persist network identities between runs of a node Problem: Solution: This PR adds opt-in peer identity persistence via a new
Files changed:
Breaking changes / deprecations: None. 2. Branch Sync Status and Merge ConflictsBranch sync status
Merge conflict analysis
3. Strengths
4. Issues FoundCriticalNone. MajorNone. Minor
5. Security Review
No further security issues identified for this change set. 6. Documentation and Examples
7. Newsfragment Requirement
No blocker; requirement satisfied. 8. Tests and ValidationLint (
|
| Request | Status |
|---|---|
Fix doctest (add ID import in ID.from_pubkey()) |
✅ Fulfilled |
Add newsfragment 312.feature.rst |
✅ Fulfilled |
| Use protobuf serialization (not raw bytes) | ✅ Fulfilled |
| Fix TOCTOU: atomic create with 0600 | ✅ Fulfilled |
| Add missing tests (corrupt file, permissions, overwrite) | ✅ Fulfilled |
| Run pre-commit so lint passes on first run | test_identity_persistence_enhanced.py; author should run pre-commit run --all-files and commit |
Standardize docstrings to ReST (:param:, :return:) in identity_utils |
❌ Not done: module still uses Google style (Args/Returns/Raises) |
| Acknowledge #312 design (key pair provider / keystore) in PR or follow-up | ❌ Not done (optional) |
Summary: All critical/blocker items are fulfilled. Remaining: pre-commit + optional docstring style and #312 acknowledgment.
yashksaini-coder (reviewer — “The PR needs to fix these bugs”)
| # | Request | Fulfilled? |
|---|---|---|
| 1 | Private key validation on load (length/format) | ✅ Yes — deserialize_private_key + get_public_key + roundtrip |
| 2 | Fix race condition: atomic create with 0600 | ✅ Yes |
| 3 | filepath.parent.mkdir(parents=True, exist_ok=True) |
✅ Yes |
| 4 | Fix docstring example (comparing identical objects) | ✅ Yes — doctest compares distinct ID instances correctly |
| 5 | Consider format version header | ❌ Not done (optional) |
| 6 | File permissions verification test | ✅ Yes — test_save_identity_sets_restrictive_permissions |
| 7 | Corrupted file handling test | ✅ Yes — corrupted, truncated, empty file tests |
Summary: All relevant items (1–4, 6, 7) are fulfilled. Item 5 (version header) is optional.
12. Overall Assessment
- Quality rating: Good — Clear feature, correct use of protobuf and existing APIs, solid tests and docs; only minor lint/style fixes needed before merge.
- Security impact: Low — Private keys on disk with 0600 and atomic create; no encryption (documented); no new high-risk patterns.
- Merge readiness: Needs small fixes — Run pre-commit (and optionally rename the one test) so lint is green and naming is accurate; then ready to merge.
- Confidence: High — Checked branch sync, merge, lint, typecheck, tests, and docs; behavior and design match the stated goal and issue Provide utilities to persist network identities between runs of a node #312.
Related Issue: #312
py-libp2pgenerated a new random peer identity on every restart.As a result, nodes could not maintain a stable Peer ID across sessions, which made long-lived peers, reputation, and reproducible networking setups difficult.
How was it fixed?
This PR introduces opt-in peer identity persistence, allowing users to explicitly control how a peer identity is created and reused, while keeping the default behavior unchanged.
By default,
py-libp2pstill generates a new random identity on each host creation.Users who need a stable Peer ID can now provide or persist their identity explicitly.
Usage Options
Option 1: Provide Your Own
KeyPairYou can directly supply a
key_pairwhen creating a host.As long as the same key pair is reused, the Peer ID will remain stable.
Option 2: Save and Load Identity from Disk
Helper utilities are provided to persist and reload peer identities across restarts.
Option 3: Deterministic Identity from Seed
For reproducible setups (e.g. testing), a deterministic identity can be derived from a 32-byte seed.
Security Notes
save_identity()helper sets restrictive file permissions (0600) where supported.Backward Compatibility
Testing
Added tests to ensure: