Replies: 5 comments 2 replies
-
|
So if I understood correctly there will be some Interfaces definition but the actual implementation can be done in separate python package? Is this possible in python ? 1. Interfaces in core, implementations in separate packages — yes, it’s possible in PythonPython supports this pattern:
So: interfaces in core, implementations in separate packages is possible and common in Python. 2. What
|
| Area | In core today | Can be externalized? | Note |
|---|---|---|---|
| Interfaces (abc) | Yes | N/A | Stays; no impl deps. |
| Transport registry | Yes | Partially | Core keeps registry; default registration (TCP, WS, QUIC) can move to extras or separate package. |
| Security / muxer | Defaults wired in __init__.py |
Yes | Already injectable; remove default imports from core and ship defaults elsewhere. |
| Swarm | Uses ITransport, but also QUIC* types | Yes, with refactor | Replace isinstance(QUICTransport/QUICConnection) with interface or capability (e.g. “already_muxed”). |
| BasicHost | Same QUIC/TLS coupling | Yes, with refactor | Same as Swarm: use interfaces/capabilities instead of concrete QUIC/TLS types. |
| custom_types | QUICStream/QUICConnection in type aliases | Yes, with refactor | Use abc types or move QUIC-specific aliases to QUIC package. |
So: core can be externalized, and some modules will still be in the main repo (abc, peer, io, protocol_muxer, network, host, transport upgrader/registry, crypto interfaces, utils, rcmgr, etc.). What changes is that the concrete transports, security, and muxers (TCP, QUIC, WebSocket, Noise, TLS, Mplex, Yamux) and their default wiring can live in separate packages or optional extras; the core would only depend on interfaces and, where needed, small capability flags or adapter interfaces instead of QUIC/TLS-specific types.
Beta Was this translation helpful? Give feedback.
-
|
I need to add a caveat that, when I wrote #395, I was following an entirely internal idea for connections reflecting the codebase and libp2p landscape at the time (2020), with the core goal of allowing py-libp2p to be a development kit for current and future protocols, this means absolute abstraction from any assumptions of applications (such as IPFS at the time), and a reduction of everything back to a structured hierarchy, which can then be rearranged or changed to reflect any new protocol development work. |
Beta Was this translation helpful? Give feedback.
-
|
And to clarify some concepts (as they're understood in my head) further, let's start off with the posit that everything here is IO, "just" "IO", in the sense that each of these provide an IO channel from... somewhere, to... somewhere. However, on top of that, there's a core distinction between "terminus" IO (IO channels that don't feed or get fed by other IO channels), and "transitive" IO ( In this case, then;
By abstracting things like this, one could potentially go from a "push"-based connectivity system ('when setting up a connection to another node, "just" create a secure protocol and then a muxer'), to a "pull"-based system, where the application "just" provides a Protocol class to the libp2p library, and then libp2p will "resolve dependencies" (depth-first), trying transports, connection layering, and the likes, with a tree of registered Transport and Connection classes, until, it has created a Connection with all the required prerequisites, which it then layers the Protocol on, initializes it, and then gives it back to the application to perform IO on, in its own fashion. Example:
Via this way, protocols could potentially identify nebulous or otherwise "semi-protocols" which would be transparent passthrough IO channels, but otherwise signal interesting or important requirements or property requirements on the underlying connection.
With this, you could say that the I took inspiration of this from systemd, of it's ordering system, and I'll take further inspiration to discuss a problem not yet mentioned; Systemd has both There's a problem with the Also, if a protocol would "simply" require a secure connection and a muxed connection, then the resolver isn't exactly deterministic; it might create a muxer on top of the transport, and then a secure connection, or vice versa. As such, I'm recommending another idea; With this, the muxer could have this annotation of Consequently, these relations could be specified virtually as well, with This would require internal bookkeeping, at-runtime resolvers, and the likes, but it could be an incredibly powerful way of building up building blocks of supporting internal/external protocols, which can then be seamlessly inserted in any connection buildup. With this, even the basic core principle of "secure connections" and "muxed connections" could be externalised to other libraries, or at the very least, properly isolated, making py-libp2p only a smart connection-stacking resolver; complete abstraction from any implementation details, no matter how straightforward or "core". Edit: And just to be extra clear, I personally split "protocol" into "application protocol" and "wire protocol". The first one (any protocol required by the application) is a With this, |
Beta Was this translation helpful? Give feedback.
-
libp2p IO SchemaBased on comment by ShadowJonathan (Discussion #1204, Feb 2026). Everything is IO; Transport / Connection / Protocol and the resolver. 1. Everything is IOEach layer provides an IO channel from somewhere to somewhere. Two kinds:
2. Three Roles
3. Stack (vertical)
4. Push vs Pull
5. Resolver example (ExoticProtocol → IFTLConnection)
6. Ordering: @after_connection and @requires_connectionInspired by systemd:
So the muxer can be annotated with 7. Application protocol vs wire protocol
|
Beta Was this translation helpful? Give feedback.
-
|
@seetadev @pacrob @sumanjeet0012 @yashksaini-coder @ShadowJonathan What It Would Take to Implement ShadowJonathan’s Concept in py-libp2pThis document maps the current py-libp2p codebase to the target design (Transport/Connection/Protocol as IO layers, pull-based resolver, capability-based checks, optional packaging split) and outlines a concrete implementation roadmap. It is intended for maintainers and contributors. References: Issue #395, Discussion #1204, and the summary in #1204 (comment). 1. Current State (What Exists Today)1.1 Interfaces (abc.py)
1.2 Transport Registry
1.3 Upgrade Pipeline (Push Model)
1.4 QUIC and Concrete-Type Coupling
So the core today depends on concrete QUIC types, not only on interfaces. Why QUIC is singled out: QUIC is the only transport in the current codebase that bundles security and multiplexing inside the transport itself (TLS is inside QUIC; streams are native). So the normal path “raw → add security → add muxer” does not apply: stacking Noise and Mplex on top of QUIC would be wrong. The code had to special-case “if this is QUIC, skip those upgrades.” In the target design, QUIC is not special — any transport that is “already secure and muxed” (QUIC, WebRTC, Iroh, etc.) is handled the same way via capabilities, not type checks. 1.5 Default Wiring in
|
| Aspect | Target |
|---|---|
| Model | Everything is IO; Transport = incoming, Connection = transitive, Protocol = outgoing (see IO.md). |
| Stack building | Pull: app requests a Protocol (or a connection with capabilities); a resolver builds the stack from registered Transports/Connections. |
| Requirements | Protocols declare required connection capabilities via e.g. @requires_connection(ISecureConn) (runtime constraint, not package dependency). |
| Ordering | Layers declare ordering via e.g. @after_connection(ISecureConn) so the resolver never stacks muxer before security. |
| Discovery | Optional: “ProvidesTransport” / “ProvidesConnection” interfaces so the core can discover implementations (e.g. muxer “provides” new connections) without hard-coding. |
| Core vs impl | Core exposes interfaces only; concrete transports/security/muxers can live in separate packages or extras and register at runtime. |
| QUIC / others | No isinstance(QUICTransport) or isinstance(QUICConnection); use capability flags (e.g. is_secure, is_muxed) or interfaces so any “secure+muxed” transport (QUIC, Iroh, WebRTC) is pluggable. |
How the IO concept handles QUIC (and other “secure+muxed” transports)
In the IO model, QUIC is still a Transport (incoming IO): its bottom touches the network, its top exposes a connection. The difference is that QUIC’s “top” already provides both security and multiplexing (its own TLS and streams), so it doesn’t use the separate Noise/Mplex layers. The core should not stack those on top; it should treat the QUIC connection as already satisfying “secure” and “muxed.” That’s exactly what capability flags do (Phase 1): the transport or the connection object declares e.g. provides_secure=True, provides_muxed=True. The upgrader/resolver then skips adding security and muxer layers for that connection. So QUIC (and any future transport with built-in security and muxing) is handled by the same IO concept — no special type, just a transport that advertises higher capabilities. QUIC’s “own” security and muxer modules stay inside the QUIC implementation; the core only sees the resulting connection and its capabilities.
3. Gap Analysis
| Gap | Current | Target |
|---|---|---|
| QUIC special-casing | Swarm and BasicHost branch on QUICTransport / QUICConnection. |
Use capability checks (e.g. transport/connection declares “already_secure”, “already_muxed”); core uses only interfaces. |
| Protocol requirements | No way for a protocol to declare “I need a secure connection.” | Add @requires_connection(IBaseConnection) (or similar) and optionally enforce when attaching protocols. |
| Ordering | Fixed “security then muxer” in upgrader. | Add @after_connection(IBaseConnection) (or similar) so resolver/upgrader respects ordering without hard-coding. |
| Resolver | No resolver; stack is built in one fixed path. | Introduce a component that, given a target (e.g. “run Protocol X”), resolves required capabilities and tries registered Transport/Connection paths (depth-first or similar). |
| Provides discovery* | Transport registry maps multiaddr protocol → class; no “this protocol instance provides connections.” | Optional: ProvideTransport / ProvideConnection interfaces and a registry the resolver consults. |
| Default implementations in core | __init__.py and transport_registry import TCP, Noise, TLS, Mplex, Yamux, QUIC. |
Defaults could move to optional extras or a “batteries-included” package that registers with core. |
| custom_types QUIC | TQUICStreamHandlerFn, TQUICConnHandlerFn reference QUIC types. |
Generalize to INetStream / IMuxedConn or move QUIC-specific types to a QUIC package. |
4. Implementation Roadmap
Phase 1: Capability-Based Checks (Decouple from QUIC)
Goal: Remove all isinstance(..., QUICTransport) and isinstance(..., QUICConnection) from Swarm and BasicHost so QUIC is just one implementation of shared capabilities.
Steps:
- Define capability attributes or a small interface (e.g. in
libp2p/abc.pyor a newlibp2p/capabilities.py):- On transport or connection: e.g.
provides_secure: bool,provides_muxed: bool(or aConnectionCapabilitiesprotocol/ABC). - QUIC transport/connection sets these to
True; TCP + upgrader stack exposes them based on current layer (e.g. after security upgradeprovides_secure=True).
- On transport or connection: e.g.
- Swarm: Replace every
isinstance(self.transport, QUICTransport)andisinstance(..., QUICConnection)with checks on these capabilities (e.g. “if transport/connection is already muxed, skip muxer upgrade”). - BasicHost: Same: replace QUIC-specific branches with capability-based logic (timeouts, stream limits, etc. driven by “is this connection already muxed / already secure” rather than “is this QUIC”).
- custom_types: Replace or complement
TQUICStreamHandlerFn/TQUICConnHandlerFnwith handlers that takeINetStream/IMuxedConn(or keep QUIC-specific handlers in a QUIC-only module if needed for backward compatibility).
Deliverables: No direct QUIC imports in Swarm/BasicHost for control flow; QUIC remains in repo but pluggable via capabilities. New “secure+muxed” transports (e.g. WebRTC) can plug in without new branches.
Risk: Low. Mostly refactor + adding a few attributes or a tiny interface.
Phase 2: Protocol Requirements (@requires_connection)
Goal: Allow application protocols to declare what kind of connection they require; core can optionally enforce or use this for the resolver later.
Steps:
- Define a decorator (e.g. in
libp2p/protocol_requirements.pyor inabc.py):@requires_connection(interface)— e.g.@requires_connection(ISecureConn)or a base type likeISecureConnection.- Store the requirement on the protocol class (e.g.
_required_connection_interfaceor a registry).
- Document that this is a runtime/capability requirement (not a pip dependency).
- Optional enforcement: When the host sets a stream handler for a protocol, or when opening a stream for that protocol, check that the connection (or its stack) satisfies the required interface (e.g. via
isinstance(conn, ISecureConn)or capability flags). If not, log a warning or raise. - Keep existing behaviour: If no decorator is present, behave as today (no extra check).
Deliverables: Protocols can declare “I need a secure connection”; host/swarm can optionally enforce it. Foundation for resolver (Phase 4).
Risk: Low. Additive; default behaviour unchanged.
Phase 3: Connection Ordering (@after_connection)
Goal: Make “security before muxer” (and similar rules) explicit and extensible so a future resolver can respect ordering without hard-coding.
Steps:
- Define a decorator (e.g. in the same module as Phase 2):
@after_connection(interface)— e.g. muxer implementation is annotated with@after_connection(ISecureConn).- Store ordering constraints (e.g. “this connection type must be stacked after ISecureConn”).
- Integrate with TransportUpgrader (or equivalent):
- When building the stack, the upgrader consults these annotations and applies security before muxer (current behaviour), and can later support more layers if needed.
- Document that this drives ordering only (like systemd’s
After=), not “require this to be present” (that’s@requires_connection).
Deliverables: Explicit ordering metadata; upgrader (and later resolver) use it. No change to default stack shape if we keep current security-then-muxer as the only path for now.
Risk: Low. Can be implemented as metadata first, then wired into upgrader.
Phase 4: Resolver (Pull Model)
Goal: Application requests “run this Protocol” (or “give me a connection with these capabilities”); a resolver tries registered Transport/Connection paths and builds the stack automatically.
Steps:
- Define a Resolver API:
- Input: desired protocol class (or desired capability set, e.g. “IMuxedConn”).
- Output: a constructed stack (transport dial → optional security → optional muxer) and the top-level connection/stream for the protocol.
- Registry of “connection providers”:
- Transports that produce
IRawConnection(already exist). - Security and muxer as “connection layers” that consume one connection and produce another (already exist; could be registered as “provides ISecureConn” / “provides IMuxedConn”).
- Use
@after_connectionso the resolver orders layers correctly (e.g. muxer after secure).
- Transports that produce
- Depth-first (or similar) resolution:
- Given “I need IMuxedConn for protocol X”, resolver tries: dial each registered transport → for each, try stacking security then muxer (or use ordering metadata); if a path fails (e.g. wire handshake fails), try next path. Mirror ShadowJonathan’s “Path A / Path B” example.
- Integration with Host:
- When the app asks to dial a peer for a given protocol, host can call the resolver instead of the fixed “dial transport → upgrade security → upgrade muxer” path. Fallback: keep current behaviour if resolver is not used.
Deliverables: A working resolver that can build connection stacks from registries and ordering constraints; host can optionally use it for dial/stream.
Risk: Medium. Touches dial path, error handling, and testing. Should be feature-flagged or opt-in initially.
Phase 5: ProvidesTransport / ProvidesConnection (Optional)
Goal: Let “protocols” (e.g. a muxer instance) be discovered by the core as “this provides new connections” so the resolver can iterate over them without hard-coding Multiplex, Yamux, etc.
Steps:
- Define interfaces (e.g. in
abc.py):ProvidesTransport— e.g. “can dial this multiaddr” / “matches multiaddr”.ProvidesConnection— e.g. “can create a new connection (stream) on top of this”.
- Implementations: Muxer instances could implement
ProvidesConnection; custom transports implementProvidesTransport. Register them in a central registry the resolver uses. - Resolver (from Phase 4) consults these registries when building the stack.
Deliverables: Pluggable discovery of transports and “connection providers”; core does not depend on concrete muxer/transport classes for discovery.
Risk: Medium. Larger API and behavioural change; can be done after Phase 4.
Phase 6: Packaging (Interface vs Implementation Split)
Goal: Core package exposes only interfaces (and peer, io, multiselect, network, host, upgrader, registry, etc.); current implementations (TCP, ws/wss, Noise, TLS, secio, QUIC, Mplex, Yamux, etc.) become installable via pip — either as optional extras of the main package or as separate packages.
Do ws, wss, Noise, secio, QUIC, etc. need to be external / pip-installable?
Yes, in the full vision of the proposal: the core should not depend on concrete implementations. That can be achieved in two ways (or a mix):
- Option A — Single “defaults” extra: One pip extra, e.g.
pip install py-libp2p[defaults], that pulls in all current implementations (still in the same repo as optional subpackages, or as a single “batteries” package). Users who want the current behaviour install the extra; core alone stays interface-only. - Option B — Per-module packages: Each implementation is its own pip package, e.g.
py-libp2p-tcp,py-libp2p-websocket,py-libp2p-noise,py-libp2p-secio,py-libp2p-quic,py-libp2p-mplex,py-libp2p-yamux. Users install only what they need (e.g.pip install py-libp2p py-libp2p-tcp py-libp2p-noise py-libp2p-yamux). A meta-package likepy-libp2p[defaults]can depend on all of them for “batteries included.”
The roadmap does mention this in Phase 6: “default implementations live in extras or separate packages” and “implementations = optional packages.” So the answer is: yes, the current modules (ws, wss, Noise, secio, QUIC, TCP, Mplex, Yamux, etc.) are the ones that would move to extras or separate pip-installable packages; Phase 6 is where that is described.
Can a module like py-libp2p-tcp be resolved/installed at runtime?
- Resolved at runtime — yes. The core can discover which transports are available without the app explicitly importing each one, using Python entry points (e.g. in
pyproject.tomlofpy-libp2p-tcp:[project.entry-points."libp2p.transports"]pointing to the TCP transport class). When the core (or the transport registry) is first used, it can iterate overimportlib.metadata.entry_points(group="libp2p.transports")and register each discovered transport. So the package must be installed beforehand (e.g.pip install py-libp2p-tcp), but the choice of which transport to use for a given multiaddr is made at runtime from whatever is installed and registered. No need for the app toimport libp2p_tcp— the core discovers it at runtime. - Installed at runtime — possible but not recommended. Actually running
pip install py-libp2p-tcpfrom inside the process (e.g. via subprocess or an in-process installer) is possible but raises security, environment, and reproducibility concerns. Standard practice is to install dependencies before running the app (e.g. at deploy time or in a container image). If "optional auto-install" were ever supported, it would be an explicit opt-in and clearly documented.
Steps:
- Core (
py-libp2p):- No imports of TCP, Noise, TLS, Mplex, Yamux, QUIC in
libp2p/__init__.pyand no default registration intransport_registry(or make default registration pluggable via entry points (so installed packages likepy-libp2p-tcpare discovered at runtime) or a function that “batteries” package calls). new_swarm/new_hostacceptsec_opt,muxer_opt, and transport (or transport registry already populated); if not provided, document “install py-libp2p[defaults] or py-libp2p-tcp, etc., and register transports”.
- No imports of TCP, Noise, TLS, Mplex, Yamux, QUIC in
- Optional “batteries-included” extra or package (e.g.
py-libp2p[defaults]orpy-libp2p-defaults):- Depends on core and on the implementation packages (or bundles them). On import, registers transports/security/muxers with the core so
new_swarm()“just works” for users who install the extra.
- Depends on core and on the implementation packages (or bundles them). On import, registers transports/security/muxers with the core so
- Backward compatibility: For a transition period, core can still ship with default implementations but emit a deprecation warning if they are used without explicit registration from an extra; or keep defaults in core but clearly separated (e.g.
libp2p/transports/defaults/that is optional to import).
Deliverables: Clear split between “core = interfaces + orchestration” and “implementations = pip-installable extras or separate packages”; libp2p-bluetooth-style packages can register without touching core.
Risk: Medium–high. Affects install experience, docs, and backward compatibility; should be planned with maintainers and users.
5. Summary Table
| Phase | Description | Risk | Depends on |
|---|---|---|---|
| 1 | Capability-based checks (replace QUIC isinstance) | Low | — |
| 2 | @requires_connection(...) for protocols |
Low | — |
| 3 | @after_connection(...) for ordering |
Low | — |
| 4 | Resolver (pull-based stack building) | Medium | 1, 2, 3 |
| 5 | ProvidesTransport / ProvidesConnection discovery | Medium | 4 |
| 6 | Packaging (defaults in extras/separate packages) | Medium–High | 1 (optional) |
Phases 1–3 are incremental alignment with the proposal and can be done independently. Phase 4 is the main behavioural change (pull model). Phases 5 and 6 are optional or follow-on.
6. Files to Touch (Indicative)
- Phase 1:
libp2p/abc.py(or new capabilities module),libp2p/network/swarm.py,libp2p/host/basic_host.py,libp2p/custom_types.py,libp2p/transport/quic/transport.py,libp2p/transport/quic/connection.py(add capabilities). - Phase 2–3: New module (e.g.
libp2p/protocol_requirements.pyor inabc.py),libp2p/host/basic_host.py(optional enforcement),libp2p/transport/upgrader.py(ordering). - Phase 4: New
libp2p/resolver.py(or underlibp2p/network/),libp2p/network/swarm.py,libp2p/host/basic_host.py. - Phase 5:
libp2p/abc.py, resolver and registries. - Phase 6:
libp2p/__init__.py,libp2p/transport/transport_registry.py,pyproject.toml/setup.py, optional new package(s).
7. References
- Issue #395 — Restructuring py-libp2p
- Discussion #1204 — Modularity, interfaces, developer experience
IO.mdin this folder (summary of Transport/Connection/Protocol and pull model)
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
This discussion is opened to continue the design conversation from issue #395 (Restructuring py-libp2p to be more modular and developer-friendly), so that the issue and PRs stay easier to manage. Please carry on the discussion here.
Summary of the proposal (from #395)
The issue proposes refactoring py-libp2p along three axes:
ProvidesTransport,ProvidesConnection,ISecureConnection) so that protocols can depend on capabilities (e.g. “requires secure connection”) without depending on concrete implementations.libp2p-bluetoothcan register with py-libp2p via a single function without tight coupling.Research: current state of py-libp2p
A quick pass over the codebase shows that a lot of this is already in place, but naming and layering don’t fully match the issue’s vocabulary.
1. Transport, Connection, Protocol (concepts)
libp2p.abc.ITransport—dial(maddr) -> IRawConnection,create_listener(handler) -> IListener. Implementations:TCP,WebsocketTransport,QUICTransport,CircuitV2Transport, etc. A transport registry (libp2p.transport.transport_registry) maps multiaddr protocols (e.g.tcp,ws,quic) to transport classes and is used to create the right transport for a given multiaddr.IRawConnection(raw I/O)ISecureConn(secure; get_local_peer, get_remote_peer, etc.) — security is applied viaTransportUpgrader/SecurityMultistream(e.g. Noise, TLS, secio, plaintext).IMuxedConn(multiplexed; open_stream, accept_stream) — muxer is selected viaMuxerMultistream(e.g. Yamux, Mplex).TProtocoland protocol handlers on the host; (b) is represented by security transports and muxers that are selected by multistream and then produceISecureConn/IMuxedConn. So the three concepts are present, but “Protocol” in the issue’s sense is split across “stream protocol”, “security transport”, and “muxer” in the code.2. Interfaces
ITransport,ISecureTransport(secure_inbound / secure_outbound).IRawConnection,ISecureConn(extendsAbstractSecureConn+IRawConnection),IMuxedConn,IMuxedStream,INetStream,INetConn.@requires_connection(ISecureConnection)to declare that a protocol only runs over a secure connection. Today, security is enforced by the upgrader/swarm layer, not by protocol-level checks.3. Interface vs implementation
libp2p/abc.py(andlibp2p/io/abc.pyforReadWriteCloser, etc.).libp2p/transport/,libp2p/security/,libp2p/stream_muxer/, etc., and implement those ABCs. So the split “interface in core, implementation elsewhere” is partially there; the main gap is that default implementations are still in the same repo and wired in by default (e.g. transport registry registers TCP, WS, QUIC at init). Moving default transports to separate packages and having the core only depend on interfaces would complete the picture.Formulated answers / discussion points
Alignment with the proposal: The codebase already has Transport and Connection as first-class concepts, a transport registry, and a clear interface layer in
abc.py. The main differences are: (i) “Protocol” in the issue is spread over stream protocols, security, and muxer; (ii) no “ProvidesTransport”/“ProvidesConnection” style discovery from protocol instances; (iii) no@requires_connection(ISecureConnection)-style decorator.Possible next steps (for discussion):
@requires_connection(ISecureConn)(or a base interface likeISecureConnectionif we want to match the issue’s name) and have the host/swarm only run such protocols on connections that have been secured. This would be an addition on top of the current upgrader-based security.py-libp2p-tcp,py-libp2p-websocket) and having the main library only depend onlibp2p.abcwould match “interface vs implementation” and make it easier forlibp2p-bluetooth-style packages to plug in without polluting the core.Risk / scope: The proposal is from 2020; the codebase has evolved (e.g. TransportRegistry, clearer ABCs). So “restructuring” today might mean incremental alignment (docs, small API additions, optional decorators/registries) rather than a big-bang rewrite. A discussion here can help decide which parts of the original proposal are still desired and which are already satisfied.
Please add your thoughts, especially on: (a) whether the “ProvidesTransport”/“ProvidesConnection” discovery model is still desired, (b) whether a
@requires_connection(...)-style mechanism would be useful, and (c) whether splitting default transports into separate packages is a goal. If there’s consensus, we can turn this into a short design doc and reference it from issue #395 and any follow-up PRs.Beta Was this translation helpful? Give feedback.
All reactions