Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added =0.9.20, base58
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like these files are created in error.

Empty file.
Empty file added =1.0.3, coincurve
Empty file.
1 change: 1 addition & 0 deletions =1.2.0
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
when
Empty file added =10.0.0, exceptiongroup
Empty file.
24 changes: 24 additions & 0 deletions docs/libp2p.transport.quic.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
libp2p.transport.quic package
=============================

Submodules
----------

libp2p.transport.quic.transport module
--------------------------------------

.. automodule:: libp2p.transport.quic.transport
:members:
:undoc-members:
:show-inheritance:

Module contents
---------------

.. automodule:: libp2p.transport.quic
:members:
:undoc-members:
:show-inheritance:

.. toctree::
:maxdepth: 4
1 change: 1 addition & 0 deletions docs/libp2p.transport.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Subpackages
:maxdepth: 4

libp2p.transport.tcp
libp2p.transport.quic

Submodules
----------
Expand Down
3 changes: 3 additions & 0 deletions libp2p/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
MPLEX_PROTOCOL_ID,
Mplex,
)
from libp2p.transport.quic.transport import (
QuicTransport,
)
from libp2p.transport.tcp.tcp import (
TCP,
)
Expand Down
4 changes: 4 additions & 0 deletions libp2p/transport/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
)


class TransportError(BaseLibp2pError):
"""Raised when there is an error in the transport layer."""


class OpenConnectionError(BaseLibp2pError):
pass

Expand Down
16 changes: 16 additions & 0 deletions libp2p/transport/quic/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
QUIC transport implementation for libp2p.

This module provides QUIC transport functionality for libp2p, enabling
high-performance, secure communication between peers using the QUIC protocol.
"""

from libp2p.transport.quic.transport import (
Libp2pQuicProtocol,
QuicTransport,
)

__all__ = [
"QuicTransport",
"Libp2pQuicProtocol",
]
229 changes: 229 additions & 0 deletions libp2p/transport/quic/transport.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import asyncio
import time
from typing import (
Optional,
)

from aioquic.asyncio import (
QuicConnectionProtocol,
)
from aioquic.quic.configuration import (
QuicConfiguration,
)
from aioquic.quic.connection import (
QuicConnection,
)
from aioquic.quic.events import (
QuicEvent,
StreamDataReceived,
)
from aioquic.tls import (
SessionTicket,
)
from multiaddr import (
Multiaddr,
)
from trio import (
Nursery,
)

from libp2p.abc import (
IListener,
IRawConnection,
ITransport,
)
from libp2p.custom_types import (
THandler,
)
from libp2p.peer.id import (
ID,
)
from libp2p.transport.exceptions import (
TransportError,
)


class QuicListener(IListener):
def __init__(self, transport: "QuicTransport", handler_function: THandler):
self.transport = transport
self.handler_function = handler_function

async def listen(self, maddr: Multiaddr, nursery: Nursery) -> bool:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nursery arg is unused here.

"""Start listening on the given multiaddr."""
try:
# Extract host and port from multiaddr
host = maddr.value_for_protocol(4) # IPv4
port = int(maddr.value_for_protocol(17)) # UDP

# Update transport settings
self.transport.host = host
self.transport.port = port

# Start listening
await self.transport.listen()
return True
except Exception as e:
raise TransportError(f"Failed to start listening: {e}") from e

def get_addrs(self) -> tuple[Multiaddr, ...]:
"""Get the listening addresses."""
if not self.transport._server:
return ()
socket = self.transport._server.get_extra_info("socket")
addr = socket.getsockname()
return (Multiaddr(f"/ip4/{addr[0]}/udp/{addr[1]}/quic"),)

async def close(self) -> None:
"""Close the listener."""
await self.transport.close()


class QuicTransport(ITransport):
def __init__(self, host: str = "0.0.0.0", port: int = 0):
self.host = host
self.port = port
self.connections: dict[ID, QuicConnection] = {}
self._server: Optional[asyncio.DatagramTransport] = None

# QUIC configuration
self.config = QuicConfiguration(
is_client=False,
alpn_protocols=["libp2p-quic"],
verify_mode=None, # TODO: Implement proper certificate verification
)

def create_listener(self, handler_function: THandler) -> IListener:
"""Create a new QUIC listener."""
return QuicListener(self, handler_function)

async def listen(self) -> None:
"""Start listening for QUIC connections."""
try:
# Create a UDP endpoint
loop = asyncio.get_event_loop()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're aiming to use trio in place of asyncio. Is there a reason we need asyncio here?

transport, protocol = await loop.create_datagram_endpoint(
lambda: self._create_protocol(), local_addr=(self.host, self.port)
)

# Store the transport and protocol
self._server = transport
self.port = transport.get_extra_info("socket").getsockname()[1]

except Exception as e:
raise TransportError(f"Failed to start QUIC server: {e}") from e

def _create_protocol(self) -> QuicConnectionProtocol:
"""Create a new QUIC protocol instance."""
return Libp2pQuicProtocol(self)

async def dial(self, maddr: Multiaddr) -> IRawConnection:
"""Dial a peer using QUIC."""
try:
# Extract host and port from multiaddr
host = maddr.value_for_protocol(4) # IPv4
port = int(maddr.value_for_protocol(17)) # UDP

# Create client configuration
client_config = QuicConfiguration(
is_client=True,
alpn_protocols=["libp2p-quic"],
verify_mode=None, # TODO: Implement proper certificate verification
)

# Create connection
connection = QuicConnection(
configuration=client_config,
session_ticket_handler=self._handle_session_ticket,
)

# Connect to peer
connection.connect((host, port), now=time.time())

# Create protocol
protocol = Libp2pQuicProtocol(self)
protocol._connection = connection # Use protected attribute

# Store connection
self.connections[protocol.peer_id] = connection

return protocol

except Exception as e:
raise TransportError(f"Failed to dial peer: {e}") from e

def _handle_session_ticket(self, ticket: SessionTicket) -> None:
"""Handle session ticket for connection resumption."""
# TODO: Implement session ticket handling

async def close(self) -> None:
"""Close all connections and stop listening."""
# Close all connections
for connection in self.connections.values():
connection.close()
self.connections.clear()

# Stop server if running
if self._server:
self._server.close()
self._server = None


class Libp2pQuicProtocol(QuicConnectionProtocol, IRawConnection):
def __init__(self, transport: QuicTransport):
# Create QUIC configuration for the protocol
quic = QuicConnection(
configuration=QuicConfiguration(
is_client=True,
alpn_protocols=["libp2p-quic"],
verify_mode=None,
)
)
super().__init__(quic=quic, stream_handler=None)
self.transport = transport
self._connection: Optional[QuicConnection] = None
self._peer_id: Optional[ID] = None
self._remote_address: Optional[tuple[str, int]] = None

@property
def connection(self) -> Optional[QuicConnection]:
"""Get the QUIC connection."""
return self._connection

@property
def peer_id(self) -> ID:
"""Get the peer ID."""
if not self._peer_id:
# TODO: Generate peer ID from connection certificate
self._peer_id = ID(b"peer_id") # Placeholder
return self._peer_id

def get_remote_address(self) -> Optional[tuple[str, int]]:
"""Get the remote address of the connected peer."""
if not self._remote_address and self._connection:
try:
# Use the correct method to retrieve the remote address
self._remote_address = self._connection._network_paths[0].addr
except Exception as e:
# Handle any exceptions that might occur
print(f"Error getting remote address: {e}")
return self._remote_address

async def read(self, n: Optional[int] = None) -> bytes:
"""Read data from the connection."""
# TODO: Implement read using QUIC streams
raise NotImplementedError("Read not implemented yet")

async def write(self, data: bytes) -> None:
"""Write data to the connection."""
# TODO: Implement write using QUIC streams
raise NotImplementedError("Write not implemented yet")

def quic_event_received(self, event: QuicEvent) -> None:
"""Handle QUIC events."""
if isinstance(event, StreamDataReceived):
# TODO: Handle incoming stream data
pass

async def stream_data_received(self, stream_id: int, data: bytes) -> None:
"""Handle incoming stream data."""
# TODO: Implement stream data handling
1 change: 1 addition & 0 deletions newsfragments/423.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add initial QUIC support.
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
long_description = description

install_requires = [
"aioquic>=0.9.20",
"base58>=1.0.3",
"coincurve>=10.0.0",
"exceptiongroup>=1.2.0; python_version < '3.11'",
Expand Down
Loading