Skip to content

Audit and comprehensive stability improvements in py-libp2p - #1411

Merged
seetadev merged 139 commits into
libp2p:mainfrom
sumanjeet0012:audit-stability-improvements
Aug 2, 2026
Merged

Audit and comprehensive stability improvements in py-libp2p#1411
seetadev merged 139 commits into
libp2p:mainfrom
sumanjeet0012:audit-stability-improvements

Conversation

@sumanjeet0012

@sumanjeet0012 sumanjeet0012 commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Description

This PR is the result of a deep-dive architectural audit of the py-libp2p stack. It contains a comprehensive series of bug fixes, architectural overhauls, and optimizations (specifically ported to the fix-1390 branch).

The primary goal of this audit and the resulting changes is to resolve resource exhaustion, fix memory and stream leaks, improve network stability under heavy load, and achieve closer architectural parity and interoperability with go-libp2p.

Related Issues

Module Issue Fixes
QUIC #1390 Shared QuicLogger crash
Kademlia DHT #1413 8 stability/correctness fixes
Ping #1414 10 stability/security/API fixes
Identify #1415 21 security/correctness/performance fixes
Bitswap #1416 8 architecture/memory/correctness fixes

Below is a module-by-module breakdown of the issues identified during the audit and the improvements implemented:

1. Bitswap (Phases 1-4 Architecture Overhaul) — #1416

  • Session-Based Architecture: Completely rewrote the Bitswap implementation to use a session-based architecture (similar to go-libp2p), providing isolated retrieval states and better concurrent fetching capabilities.
  • Peer & Presence Management: Introduced PeerManager and BlockPresenceManager for efficient tracking of peer wantlists and block availability.
  • Cancel Propagation: Implemented proper WANT_CANCEL propagation to peers once blocks are received, drastically saving bandwidth.
  • Memory Leak Fixes: Addressed unbounded memory growth and cleanup inconsistencies in _pending_requests, _dont_have_responses, _expected_blocks, and _have_confirmed.
  • Race Conditions: Resolved race conditions in _pending_requests when fetching the same CID concurrently.
  • Task Management: Introduced trio.CancelScope to the bitswap client loop to fix lingering task leaks on shutdown.
  • CID Validation & Correctness: Fixed have_block correctness, added structural CID validation, and enforced block size limits.

2. Ping Protocol — #1414

  • Stream Leaks & Inbound Limits (Critical): Added per-peer inbound stream limits (max 2) to prevent malicious or buggy peers from causing unbounded coroutine/memory growth.
  • Outbound Stream Reuse: Refactored PingService to cache and reuse a single outbound stream across multiple ping() calls, avoiding massive stream churn.
  • Timeouts & Cancellation: Reduced RESP_TIMEOUT from 60s to 10s (matching go-libp2p), added write timeouts (wrapping stream.write in trio.fail_after), and added CancelScope support to abort stuck pings.
  • API Semantics: Changed the API to yield an AsyncIterator[int] (async generator) to match the channel semantics of go-libp2p.
  • Metrics Bug: Fixed an unhandled exception in Ping metrics that would silently crash the entire global metrics loop (including gossipsub, kad_dht, and swarm).
  • Correctness: Fixed RTT calculations (microseconds vs milliseconds), safely handled short-reads with read_exactly, and replaced stream close() with reset() on payload mismatches.

3. Identify & IdentifyPush — #1415

  • Unbounded Read Fix (Critical): Bounded varint prefix loops and added timeouts when reading the length-prefixed protobuf, resolving a major issue where a stalled peer could hold streams open forever and leak connection resources.
  • Task Scoping: Scoped identify tasks correctly to ensure they are cleaned up on connection drop, preventing peers from getting permanently stuck in _identify_inflight.
  • Peer Record Handling: Fixed a bug where a mismatched signed peer record would erroneously skip the processing of valid observed addresses.
  • Memory Leaks & Caches: Fixed an unbounded global cache (_UNPARSEABLE_ADDRS_CACHE) memory leak and corrected the use of id(conn) as a dict key to prevent stale observations on reused memory addresses.
  • Concurrency & Polling: Fixed a shared global trio.Semaphore mutable default argument issue and removed a 5s synchronous poll delay that compounded stream exhaustion under load.

4. QUIC & Transports — #1390

  • Keep-Alive & Garbage Collection: Root-caused the ConnGarbageCollected (0x1005 GO_AWAY) error to remote peers aggressive-trimming idle connections. Implemented application-layer keepalive streams (transport-level ping frames) for TCP and QUIC.
  • Timeout Tuning: Increased the QUIC idle_timeout to 600 seconds to prevent premature drops.
  • Resource Leaks: Explicitly released resource scopes on connection termination to prevent resource manager leaks. Fixed missing is_client=True configuration when dialing.
  • Race Conditions: Resolved race conditions on shutdown and raised StreamEOF correctly on empty buffer reads.

5. Swarm & Resource Manager (rcmgr)

  • Connection Zombie Sweeping: Implemented a background connection health-check loop in BasicHost to ping active connections periodically and sweep zombie connections, preventing the node from hitting its max limit with dead weight.
  • Duplicate Connections: Added an in-flight dial tracker to the Swarm to prevent duplicate concurrent connections and redundant resource acquisition.
  • Cancellation Leaks: Caught BaseException to close pre_scope on trio.Cancelled, fixing resource leaks during outbound dialing interruptions.
  • Watermarks & Grace: Set connection manager watermarks (low=100 / high=300 / max=300), increased GRACE_PERIOD to 600s, and added a floor to the graceful degradation mechanism to prevent permanent degradation loops.

6. Kademlia DHT & Random Walk — #1413

  • Random Walk Stalls: Fixed an artificial routing table threshold (min_refresh_threshold = 4) that was preventing the DHT from continuously discovering peers, causing it to endlessly redial the same peers.
  • Bucket Splitting & Eviction: Corrected KBucket splitting and eviction logic, hashed target keys before computing XOR distances, and aligned the random walk concurrency limit (10) with go-libp2p.
  • Discovery Integrations: Allowed random walk targets to be configurable, added newly discovered peers to the routing table reliably, and added robust logging for total vs. unique peers discovered.
  • DNS Address Support: Relaxed Kademlia logic to support dnsaddr resolution.

7. Miscellaneous Fixes

  • Datastore (SQLite): Used substr instead of LIKE for BLOB prefix matching, drastically improving prefix search correctness.
  • Payment Ledger: Cleaned up varint usage and fixed a _cid_to_hex parsing bug.
  • Typing & Linting: Resolved massive sweeps of mypy and ruff / pyrefly errors, standardizing Python MRO (e.g., _QUICStreamEOF inheriting MuxedStreamEOF).
  • Debugging: Added debug endpoints for the peerstore and routing table.

…timeout to 600s

- QUICStream.read() was silently returning b"" when the stream received FIN
  and the receive buffer was empty. This caused read_exactly() to loop 100
  times before raising IncompleteReadError, ultimately manifesting as
  'fail to read from multiselect communicator' on every QUIC handshake.
  Fix: define _QUICStreamEOF(IOException, EOFError) locally and raise it
  immediately when buffer is empty and _read_closed=True.

- Increase default QUIC idle_timeout from 30s to 600s (10 minutes) to
  match go-libp2p's default. 30s was too short: DHT queries finish quickly
  then both sides go idle and aioquic drops the connection before the next
  query arrives.

- Remove debug print() calls added during investigation from QUICStream
  and MultiselectCommunicator.
- The AutoConnector was previously dialing exactly `needed` peers per interval.
- In a P2P network like IPFS, dial failure rates are usually > 80% due to offline nodes, NAT, etc.
- We now overdial (needed * 5) and increased the capacity limiter to 50 to aggressively acquire connections and reach the low_watermark much faster.
…t ip4/ip6

The value_for_protocol method raises ProtocolLookupError when the
protocol is not found in the multiaddr, instead of returning None.
This caused test failures for addresses like /ip6/::1/tcp/46499 and
/tcp/8080.
…et_ip_value pattern

Using 'except Exception' instead of 'multiaddr.exceptions.ProtocolLookupError'
since pyrefly can't resolve the 'exceptions' submodule on the multiaddr module.
The test_examples_use_address_paradigm test requires networking examples
to use both get_available_interfaces and get_optimal_binding_address.
The ping example was updated to use get_optimal_binding_address for
displaying the optimal connection address.
@sumanjeet0012
sumanjeet0012 force-pushed the audit-stability-improvements branch from 51559b1 to fa3b09a Compare July 31, 2026 20:17
…cks_v100 and blocks_v110

create_message does not support providing both blocks_v100 and blocks_v110
simultaneously. The test was updated to only use blocks_v100.
… type

The conns attribute was changed from dict[str, float] to dict[int, float]
to match the type annotation. Updated the test assertion to use id() for
the key.
- peerdata: revert first_supported_protocol return to '' (matches abc.py
  contract and go-libp2p convention; no callers use 'None supported')
- multiaddr_utils: remove DNS fallback from extract_ip_from_multiaddr
  (function extracts IPs, not hostnames; fixes 3 tests at once and
  prevents incorrect peer scoring in gossipsub)
- io/utils: add early return for n=0 in read_exactly (zero-byte reads
  are valid per Python stream contract; fixes noise empty bytes test)
- test_connection: update test_connection_start_closed for new silent
  shutdown-race handling in QUICConnection.start()
- mplex tests: increase sleep times and add wait_all_tasks_blocked()
  for timing-sensitive close/reset propagation tests
Adds --durations=20 --durations-min=1.0 to pytest commands so that
the 20 slowest tests (taking 1s+) are always reported in CI logs.
This helps track test performance regressions.
The dedicated nursery per connection caused deadlock because trio-websocket's
background reader/writer tasks keep the nursery alive indefinitely, preventing
trio cleanup and causing all 8 websocket integration tests to timeout.
@sumanjeet0012
sumanjeet0012 force-pushed the audit-stability-improvements branch from 9f41097 to 244ef3a Compare August 1, 2026 08:50
@sumanjeet0012
sumanjeet0012 force-pushed the audit-stability-improvements branch from 244ef3a to a6c5ffa Compare August 1, 2026 09:00
@sumanjeet0012
sumanjeet0012 marked this pull request as ready for review August 1, 2026 09:22
@sumanjeet0012

Copy link
Copy Markdown
Collaborator Author

@seetadev

During the audit, I found and fixed 48 issues across 5 modules:

All fixes verified in the current codebase and tracked in the linked issues above.

I’ve run the interoperability tests with the changes introduced in this PR, and all tests are now passing, as demonstrated in libp2p/unified-testing#117.

…or audit fixes

- Wrap global metrics receive loop in try/except to prevent single event
  failures from crashing the entire metrics collection task
- Add newsfragments for issues libp2p#1390, libp2p#1413, libp2p#1414, libp2p#1415, libp2p#1416
@seetadev

seetadev commented Aug 2, 2026

Copy link
Copy Markdown
Member

This is a key milestone for py-libp2p, and I wish to thank @sumanjeet0012 for the depth and breadth of work that has gone into this PR. Also, appreciate the regular coordination with me and following up on key enhancements.

The architectural audit and the resulting stability improvements touch some of the most critical parts of the stack: Bitswap, Ping, Identify/IdentifyPush, QUIC/transports, Swarm/resource management, and Kademlia DHT. The focus on resource exhaustion, memory and stream leaks, concurrency/race conditions, cancellation, protocol correctness, and closer go-libp2p interoperability makes this a particularly important step for the maturity and reliability of py-libp2p.

The fact that the identified issues have been tracked through the corresponding audit issues, interoperability tests are passing, and the PR currently has all checks passing with no conflicts gives us a strong basis for moving toward final review.

Given the scope and significance of this work, wish to encourage @sumanjeet0012 to open a detailed discussion page on each of the improvements made and issues resolved, particularly around the architectural changes, resource lifecycle/cancellation paths, protocol interoperability, and any potential regressions.

@sumanjeet0012, excellent work on taking this audit through such a broad set of issues and bringing the branch to this stage. This PR is indeed ready for merge. We will be doing a new py-libp2p release this week and update at the blog.

CCing @acul71, @pacrob, @johannamoran, @mhchia, @momack2 and @mishmosh.

@seetadev
seetadev merged commit 838384a into libp2p:main Aug 2, 2026
38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants