From b840eaa7e176b5962e8013caaadae4ac916efadb Mon Sep 17 00:00:00 2001 From: yashksaini-coder Date: Sat, 9 Aug 2025 01:22:03 +0530 Subject: [PATCH 01/21] Implement advanced network discovery example and address validation utilities - Added `network_discover.py` to demonstrate Thin Waist address handling. - Introduced `address_validation.py` with functions for discovering available network interfaces, expanding wildcard addresses, and determining optimal binding addresses. - Included fallback mechanisms for environments lacking Thin Waist support. --- examples/advanced/network_discover.py | 60 +++++++++++++ libp2p/utils/address_validation.py | 125 ++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 examples/advanced/network_discover.py create mode 100644 libp2p/utils/address_validation.py diff --git a/examples/advanced/network_discover.py b/examples/advanced/network_discover.py new file mode 100644 index 00000000..a1a22052 --- /dev/null +++ b/examples/advanced/network_discover.py @@ -0,0 +1,60 @@ +""" +Advanced demonstration of Thin Waist address handling. + +Run: + python -m examples.advanced.network_discovery +""" + +from __future__ import annotations + +from multiaddr import Multiaddr + +try: + from libp2p.utils.address_validation import ( + get_available_interfaces, + expand_wildcard_address, + get_optimal_binding_address, + ) +except ImportError: + # Fallbacks if utilities are missing + def get_available_interfaces(port: int, protocol: str = "tcp"): + return [Multiaddr(f"/ip4/0.0.0.0/{protocol}/{port}")] + + def expand_wildcard_address(addr: Multiaddr, port: int | None = None): + return [addr if port is None else Multiaddr(str(addr).rsplit("/", 1)[0] + f"/{port}")] + + def get_optimal_binding_address(port: int, protocol: str = "tcp"): + return Multiaddr(f"/ip4/0.0.0.0/{protocol}/{port}") + + +def main() -> None: + port = 8080 + interfaces = get_available_interfaces(port) + print(f"Discovered interfaces for port {port}:") + for a in interfaces: + print(f" - {a}") + + wildcard_v4 = Multiaddr(f"/ip4/0.0.0.0/tcp/{port}") + expanded_v4 = expand_wildcard_address(wildcard_v4) + print("\nExpanded IPv4 wildcard:") + for a in expanded_v4: + print(f" - {a}") + + wildcard_v6 = Multiaddr(f"/ip6/::/tcp/{port}") + expanded_v6 = expand_wildcard_address(wildcard_v6) + print("\nExpanded IPv6 wildcard:") + for a in expanded_v6: + print(f" - {a}") + + print("\nOptimal binding address heuristic result:") + print(f" -> {get_optimal_binding_address(port)}") + + override_port = 9000 + overridden = expand_wildcard_address(wildcard_v4, port=override_port) + print(f"\nPort override expansion to {override_port}:") + for a in overridden: + print(f" - {a}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/libp2p/utils/address_validation.py b/libp2p/utils/address_validation.py new file mode 100644 index 00000000..be7f8082 --- /dev/null +++ b/libp2p/utils/address_validation.py @@ -0,0 +1,125 @@ +from __future__ import annotations +from typing import List, Optional +from multiaddr import Multiaddr + +try: + from multiaddr.utils import get_thin_waist_addresses, get_network_addrs # type: ignore + _HAS_THIN_WAIST = True +except ImportError: # pragma: no cover - only executed in older environments + _HAS_THIN_WAIST = False + get_thin_waist_addresses = None # type: ignore + get_network_addrs = None # type: ignore + + +def _safe_get_network_addrs(ip_version: int) -> List[str]: + """ + Internal safe wrapper. Returns a list of IP addresses for the requested IP version. + Falls back to minimal defaults when Thin Waist helpers are missing. + + :param ip_version: 4 or 6 + """ + if _HAS_THIN_WAIST and get_network_addrs: + try: + return get_network_addrs(ip_version) or [] + except Exception: # pragma: no cover - defensive + return [] + # Fallback behavior (very conservative) + if ip_version == 4: + return ["127.0.0.1"] + if ip_version == 6: + return ["::1"] + return [] + + +def _safe_expand(addr: Multiaddr, port: Optional[int] = None) -> List[Multiaddr]: + """ + Internal safe expansion wrapper. Returns a list of Multiaddr objects. + If Thin Waist isn't available, returns [addr] (identity). + """ + if _HAS_THIN_WAIST and get_thin_waist_addresses: + try: + if port is not None: + return get_thin_waist_addresses(addr, port=port) or [] + return get_thin_waist_addresses(addr) or [] + except Exception: # pragma: no cover - defensive + return [addr] + return [addr] + + +def get_available_interfaces(port: int, protocol: str = "tcp") -> List[Multiaddr]: + """ + Discover available network interfaces (IPv4 + IPv6 if supported) for binding. + + :param port: Port number to bind to. + :param protocol: Transport protocol (e.g., "tcp" or "udp"). + :return: List of Multiaddr objects representing candidate interface addresses. + """ + addrs: List[Multiaddr] = [] + + # IPv4 enumeration + for ip in _safe_get_network_addrs(4): + addrs.append(Multiaddr(f"/ip4/{ip}/{protocol}/{port}")) + + # IPv6 enumeration (optional: only include if we have at least one global or loopback) + for ip in _safe_get_network_addrs(6): + # Avoid returning unusable wildcard expansions if the environment does not support IPv6 + addrs.append(Multiaddr(f"/ip6/{ip}/{protocol}/{port}")) + + # Fallback if nothing discovered + if not addrs: + addrs.append(Multiaddr(f"/ip4/0.0.0.0/{protocol}/{port}")) + + return addrs + + +def expand_wildcard_address(addr: Multiaddr, port: Optional[int] = None) -> List[Multiaddr]: + """ + Expand a wildcard (e.g. /ip4/0.0.0.0/tcp/0) into all concrete interfaces. + + :param addr: Multiaddr to expand. + :param port: Optional override for port selection. + :return: List of concrete Multiaddr instances. + """ + expanded = _safe_expand(addr, port=port) + if not expanded: # Safety fallback + return [addr] + return expanded + + +def get_optimal_binding_address(port: int, protocol: str = "tcp") -> Multiaddr: + """ + Choose an optimal address for an example to bind to: + - Prefer non-loopback IPv4 + - Then non-loopback IPv6 + - Fallback to loopback + - Fallback to wildcard + + :param port: Port number. + :param protocol: Transport protocol. + :return: A single Multiaddr chosen heuristically. + """ + candidates = get_available_interfaces(port, protocol) + + def is_non_loopback(ma: Multiaddr) -> bool: + s = str(ma) + return not ("/ip4/127." in s or "/ip6/::1" in s) + + for c in candidates: + if "/ip4/" in str(c) and is_non_loopback(c): + return c + for c in candidates: + if "/ip6/" in str(c) and is_non_loopback(c): + return c + for c in candidates: + if "/ip4/127." in str(c) or "/ip6/::1" in str(c): + return c + + # As a final fallback, produce a wildcard + return Multiaddr(f"/ip4/0.0.0.0/{protocol}/{port}") + + +__all__ = [ + "get_available_interfaces", + "get_optimal_binding_address", + "expand_wildcard_address", +] \ No newline at end of file From fa174230baa836cd83da09a7a505b5221f7cad36 Mon Sep 17 00:00:00 2001 From: yashksaini-coder Date: Sat, 9 Aug 2025 01:22:17 +0530 Subject: [PATCH 02/21] Refactor echo example to use optimal binding address - Replaced hardcoded listen address with `get_optimal_binding_address` for improved flexibility. - Imported address validation utilities in `echo.py` and updated `__init__.py` to include new functions. --- examples/echo/echo.py | 10 ++++++++-- libp2p/utils/__init__.py | 9 +++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/examples/echo/echo.py b/examples/echo/echo.py index 126a7da2..15c40c25 100644 --- a/examples/echo/echo.py +++ b/examples/echo/echo.py @@ -19,6 +19,11 @@ from libp2p.peer.peerinfo import ( info_from_p2p_addr, ) +from libp2p.utils.address_validation import ( + get_optimal_binding_address, + get_available_interfaces, +) + PROTOCOL_ID = TProtocol("/echo/1.0.0") MAX_READ_LEN = 2**32 - 1 @@ -31,8 +36,9 @@ async def _echo_stream_handler(stream: INetStream) -> None: async def run(port: int, destination: str, seed: int | None = None) -> None: - listen_addr = multiaddr.Multiaddr(f"/ip4/0.0.0.0/tcp/{port}") - + # CHANGED: previously hardcoded 0.0.0.0 + listen_addr = get_optimal_binding_address(port) + if seed: import random diff --git a/libp2p/utils/__init__.py b/libp2p/utils/__init__.py index 0f78bfcb..0f68e701 100644 --- a/libp2p/utils/__init__.py +++ b/libp2p/utils/__init__.py @@ -15,6 +15,12 @@ from libp2p.utils.version import ( get_agent_version, ) +from libp2p.utils.address_validation import ( + get_available_interfaces, + get_optimal_binding_address, + expand_wildcard_address, +) + __all__ = [ "decode_uvarint_from_stream", "encode_delim", @@ -26,4 +32,7 @@ __all__ = [ "decode_varint_from_bytes", "decode_varint_with_size", "read_length_prefixed_protobuf", + "get_available_interfaces", + "get_optimal_binding_address", + "expand_wildcard_address", ] From 59a898c8cee163af6b5dd85787fc17c03c43f20d Mon Sep 17 00:00:00 2001 From: yashksaini-coder Date: Sat, 9 Aug 2025 01:24:14 +0530 Subject: [PATCH 03/21] Add tests for echo example and address validation utilities - Introduced `test_echo_thin_waist.py` to validate the echo example's output for Thin Waist lines. - Added `test_address_validation.py` to cover functions for available interfaces, optimal binding addresses, and wildcard address expansion. - Included parameterized tests and environment checks for IPv6 support. --- tests/examples/test_echo_thin_waist.py | 51 +++++++++++++++++++++++ tests/utils/test_address_validation.py | 56 ++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 tests/examples/test_echo_thin_waist.py create mode 100644 tests/utils/test_address_validation.py diff --git a/tests/examples/test_echo_thin_waist.py b/tests/examples/test_echo_thin_waist.py new file mode 100644 index 00000000..9da85928 --- /dev/null +++ b/tests/examples/test_echo_thin_waist.py @@ -0,0 +1,51 @@ +import asyncio +import contextlib +import subprocess +import sys +import time +from pathlib import Path + +import pytest + +# This test is intentionally lightweight and can be marked as 'integration'. +# It ensures the echo example runs and prints the new Thin Waist lines. + +EXAMPLES_DIR = Path(__file__).parent.parent.parent / "examples" / "echo" + + +@pytest.mark.timeout(20) +def test_echo_example_starts_and_prints_thin_waist(monkeypatch, tmp_path): + # We run: python examples/echo/echo.py -p 0 + cmd = [sys.executable, str(EXAMPLES_DIR / "echo.py"), "-p", "0"] + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + assert proc.stdout is not None + + found_selected = False + found_interfaces = False + start = time.time() + + try: + while time.time() - start < 10: + line = proc.stdout.readline() + if not line: + time.sleep(0.1) + continue + if "Selected binding address:" in line: + found_selected = True + if "Available candidate interfaces:" in line: + found_interfaces = True + if "Waiting for incoming connections..." in line: + break + finally: + with contextlib.suppress(ProcessLookupError): + proc.terminate() + with contextlib.suppress(ProcessLookupError): + proc.kill() + + assert found_selected, "Did not capture Thin Waist binding log line" + assert found_interfaces, "Did not capture Thin Waist interfaces log line" \ No newline at end of file diff --git a/tests/utils/test_address_validation.py b/tests/utils/test_address_validation.py new file mode 100644 index 00000000..80ae27e8 --- /dev/null +++ b/tests/utils/test_address_validation.py @@ -0,0 +1,56 @@ +import os + +import pytest +from multiaddr import Multiaddr + +from libp2p.utils.address_validation import ( + get_available_interfaces, + get_optimal_binding_address, + expand_wildcard_address, +) + + +@pytest.mark.parametrize("proto", ["tcp"]) +def test_get_available_interfaces(proto: str) -> None: + interfaces = get_available_interfaces(0, protocol=proto) + assert len(interfaces) > 0 + for addr in interfaces: + assert isinstance(addr, Multiaddr) + assert f"/{proto}/" in str(addr) + + +def test_get_optimal_binding_address() -> None: + addr = get_optimal_binding_address(0) + assert isinstance(addr, Multiaddr) + # At least IPv4 or IPv6 prefix present + s = str(addr) + assert ("/ip4/" in s) or ("/ip6/" in s) + + +def test_expand_wildcard_address_ipv4() -> None: + wildcard = Multiaddr("/ip4/0.0.0.0/tcp/0") + expanded = expand_wildcard_address(wildcard) + assert len(expanded) > 0 + for e in expanded: + assert isinstance(e, Multiaddr) + assert "/tcp/" in str(e) + + +def test_expand_wildcard_address_port_override() -> None: + wildcard = Multiaddr("/ip4/0.0.0.0/tcp/7000") + overridden = expand_wildcard_address(wildcard, port=9001) + assert len(overridden) > 0 + for e in overridden: + assert str(e).endswith("/tcp/9001") + + +@pytest.mark.skipif( + os.environ.get("NO_IPV6") == "1", + reason="Environment disallows IPv6", +) +def test_expand_wildcard_address_ipv6() -> None: + wildcard = Multiaddr("/ip6/::/tcp/0") + expanded = expand_wildcard_address(wildcard) + assert len(expanded) > 0 + for e in expanded: + assert "/ip6/" in str(e) \ No newline at end of file From b363d1d6d0d04223551affb52243caa6732b0967 Mon Sep 17 00:00:00 2001 From: yashksaini-coder Date: Mon, 18 Aug 2025 12:38:04 +0530 Subject: [PATCH 04/21] fix: update listening address handling to use all available interfaces --- examples/echo/echo.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/examples/echo/echo.py b/examples/echo/echo.py index 15c40c25..ba52fe76 100644 --- a/examples/echo/echo.py +++ b/examples/echo/echo.py @@ -36,22 +36,20 @@ async def _echo_stream_handler(stream: INetStream) -> None: async def run(port: int, destination: str, seed: int | None = None) -> None: - # CHANGED: previously hardcoded 0.0.0.0 - listen_addr = get_optimal_binding_address(port) - + # Use all available interfaces for listening (JS parity) + listen_addrs = get_available_interfaces(port) + if seed: import random - random.seed(seed) secret_number = random.getrandbits(32 * 8) secret = secret_number.to_bytes(length=32, byteorder="big") else: import secrets - secret = secrets.token_bytes(32) host = new_host(key_pair=create_new_key_pair(secret)) - async with host.run(listen_addrs=[listen_addr]), trio.open_nursery() as nursery: + async with host.run(listen_addrs=listen_addrs), trio.open_nursery() as nursery: # Start the peer-store cleanup task nursery.start_soon(host.get_peerstore().start_cleanup_task, 60) @@ -60,8 +58,14 @@ async def run(port: int, destination: str, seed: int | None = None) -> None: if not destination: # its the server host.set_stream_handler(PROTOCOL_ID, _echo_stream_handler) + # Print all listen addresses with peer ID (JS parity) + print("Listener ready, listening on:") + peer_id = host.get_id().to_string() + for addr in listen_addrs: + print(f"{addr}/p2p/{peer_id}") + print( - "Run this from the same folder in another console:\n\n" + "\nRun this from the same folder in another console:\n\n" f"echo-demo " f"-d {host.get_addrs()[0]}\n" ) From a2fcf33bc173fb12d7dd2fb8524490583329c840 Mon Sep 17 00:00:00 2001 From: yashksaini-coder Date: Mon, 18 Aug 2025 12:38:10 +0530 Subject: [PATCH 05/21] refactor: migrate echo example test to use Trio for process handling --- tests/examples/test_echo_thin_waist.py | 78 +++++++++++++++----------- 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/tests/examples/test_echo_thin_waist.py b/tests/examples/test_echo_thin_waist.py index 9da85928..c861f547 100644 --- a/tests/examples/test_echo_thin_waist.py +++ b/tests/examples/test_echo_thin_waist.py @@ -1,51 +1,65 @@ -import asyncio import contextlib -import subprocess import sys -import time from pathlib import Path import pytest +import trio # This test is intentionally lightweight and can be marked as 'integration'. -# It ensures the echo example runs and prints the new Thin Waist lines. +# It ensures the echo example runs and prints the new Thin Waist lines using Trio primitives. EXAMPLES_DIR = Path(__file__).parent.parent.parent / "examples" / "echo" -@pytest.mark.timeout(20) -def test_echo_example_starts_and_prints_thin_waist(monkeypatch, tmp_path): - # We run: python examples/echo/echo.py -p 0 +@pytest.mark.trio +async def test_echo_example_starts_and_prints_thin_waist() -> None: cmd = [sys.executable, str(EXAMPLES_DIR / "echo.py"), "-p", "0"] - proc = subprocess.Popen( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - ) - assert proc.stdout is not None found_selected = False found_interfaces = False - start = time.time() - try: - while time.time() - start < 10: - line = proc.stdout.readline() - if not line: - time.sleep(0.1) - continue - if "Selected binding address:" in line: - found_selected = True - if "Available candidate interfaces:" in line: - found_interfaces = True - if "Waiting for incoming connections..." in line: - break - finally: - with contextlib.suppress(ProcessLookupError): - proc.terminate() - with contextlib.suppress(ProcessLookupError): - proc.kill() + # Use a cancellation scope as timeout (similar to previous 10s loop) + with trio.move_on_after(10) as cancel_scope: + # Start process streaming stdout + proc = await trio.open_process( + cmd, + stdout=trio.SUBPROCESS_PIPE, + stderr=trio.STDOUT, + ) + + assert proc.stdout is not None # for type checkers + buffer = b"" + + try: + while not (found_selected and found_interfaces): + # Read some bytes (non-blocking with timeout scope) + data = await proc.stdout.receive_some(1024) + if not data: + # Process might still be starting; yield control + await trio.sleep(0.05) + continue + buffer += data + # Process complete lines + *lines, buffer = buffer.split(b"\n") if b"\n" in buffer else ([], buffer) + for raw in lines: + line = raw.decode(errors="ignore") + if "Selected binding address:" in line: + found_selected = True + if "Available candidate interfaces:" in line: + found_interfaces = True + if "Waiting for incoming connections..." in line: + # We have reached steady state; can stop reading further + if found_selected and found_interfaces: + break + finally: + # Terminate the long-running echo example + with contextlib.suppress(Exception): + proc.terminate() + with contextlib.suppress(Exception): + await trio.move_on_after(2)(proc.wait) # best-effort wait + if cancel_scope.cancelled_caught: + # Timeout occurred + pass assert found_selected, "Did not capture Thin Waist binding log line" assert found_interfaces, "Did not capture Thin Waist interfaces log line" \ No newline at end of file From 9378490dcb4b333d558ccebaed26fc67b6a3d799 Mon Sep 17 00:00:00 2001 From: yashksaini-coder Date: Mon, 18 Aug 2025 12:40:38 +0530 Subject: [PATCH 06/21] fix: ensure loopback addresses are included in available interfaces --- libp2p/utils/address_validation.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/libp2p/utils/address_validation.py b/libp2p/utils/address_validation.py index be7f8082..493ed120 100644 --- a/libp2p/utils/address_validation.py +++ b/libp2p/utils/address_validation.py @@ -57,13 +57,23 @@ def get_available_interfaces(port: int, protocol: str = "tcp") -> List[Multiaddr addrs: List[Multiaddr] = [] # IPv4 enumeration + seen_v4: set[str] = set() for ip in _safe_get_network_addrs(4): + seen_v4.add(ip) addrs.append(Multiaddr(f"/ip4/{ip}/{protocol}/{port}")) + # Ensure loopback IPv4 explicitly present (JS echo parity) even if not returned + if "127.0.0.1" not in seen_v4: + addrs.append(Multiaddr(f"/ip4/127.0.0.1/{protocol}/{port}")) + # IPv6 enumeration (optional: only include if we have at least one global or loopback) + seen_v6: set[str] = set() for ip in _safe_get_network_addrs(6): - # Avoid returning unusable wildcard expansions if the environment does not support IPv6 + seen_v6.add(ip) addrs.append(Multiaddr(f"/ip6/{ip}/{protocol}/{port}")) + # Optionally ensure IPv6 loopback when any IPv6 present but loopback missing + if seen_v6 and "::1" not in seen_v6: + addrs.append(Multiaddr(f"/ip6/::1/{protocol}/{port}")) # Fallback if nothing discovered if not addrs: From 05b372b1eb50a0f266851b6cdf4f4c6e5ecdb202 Mon Sep 17 00:00:00 2001 From: acul71 Date: Tue, 19 Aug 2025 01:11:48 +0200 Subject: [PATCH 07/21] Fix linting and type checking issues for Thin Waist feature --- examples/advanced/network_discover.py | 9 ++- examples/echo/echo.py | 4 +- libp2p/utils/address_validation.py | 28 ++++--- tests/examples/test_echo_thin_waist.py | 101 +++++++++++++++++++------ tests/utils/test_address_validation.py | 4 +- 5 files changed, 106 insertions(+), 40 deletions(-) diff --git a/examples/advanced/network_discover.py b/examples/advanced/network_discover.py index a1a22052..87b44ddf 100644 --- a/examples/advanced/network_discover.py +++ b/examples/advanced/network_discover.py @@ -11,8 +11,8 @@ from multiaddr import Multiaddr try: from libp2p.utils.address_validation import ( - get_available_interfaces, expand_wildcard_address, + get_available_interfaces, get_optimal_binding_address, ) except ImportError: @@ -21,7 +21,10 @@ except ImportError: return [Multiaddr(f"/ip4/0.0.0.0/{protocol}/{port}")] def expand_wildcard_address(addr: Multiaddr, port: int | None = None): - return [addr if port is None else Multiaddr(str(addr).rsplit("/", 1)[0] + f"/{port}")] + if port is None: + return [addr] + addr_str = str(addr).rsplit("/", 1)[0] + return [Multiaddr(addr_str + f"/{port}")] def get_optimal_binding_address(port: int, protocol: str = "tcp"): return Multiaddr(f"/ip4/0.0.0.0/{protocol}/{port}") @@ -57,4 +60,4 @@ def main() -> None: if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/examples/echo/echo.py b/examples/echo/echo.py index 15c40c25..caf80b37 100644 --- a/examples/echo/echo.py +++ b/examples/echo/echo.py @@ -18,10 +18,8 @@ from libp2p.network.stream.net_stream import ( from libp2p.peer.peerinfo import ( info_from_p2p_addr, ) - from libp2p.utils.address_validation import ( get_optimal_binding_address, - get_available_interfaces, ) PROTOCOL_ID = TProtocol("/echo/1.0.0") @@ -38,7 +36,7 @@ async def _echo_stream_handler(stream: INetStream) -> None: async def run(port: int, destination: str, seed: int | None = None) -> None: # CHANGED: previously hardcoded 0.0.0.0 listen_addr = get_optimal_binding_address(port) - + if seed: import random diff --git a/libp2p/utils/address_validation.py b/libp2p/utils/address_validation.py index be7f8082..c0709920 100644 --- a/libp2p/utils/address_validation.py +++ b/libp2p/utils/address_validation.py @@ -1,9 +1,13 @@ from __future__ import annotations -from typing import List, Optional + from multiaddr import Multiaddr try: - from multiaddr.utils import get_thin_waist_addresses, get_network_addrs # type: ignore + from multiaddr.utils import ( # type: ignore + get_network_addrs, + get_thin_waist_addresses, + ) + _HAS_THIN_WAIST = True except ImportError: # pragma: no cover - only executed in older environments _HAS_THIN_WAIST = False @@ -11,7 +15,7 @@ except ImportError: # pragma: no cover - only executed in older environments get_network_addrs = None # type: ignore -def _safe_get_network_addrs(ip_version: int) -> List[str]: +def _safe_get_network_addrs(ip_version: int) -> list[str]: """ Internal safe wrapper. Returns a list of IP addresses for the requested IP version. Falls back to minimal defaults when Thin Waist helpers are missing. @@ -31,7 +35,7 @@ def _safe_get_network_addrs(ip_version: int) -> List[str]: return [] -def _safe_expand(addr: Multiaddr, port: Optional[int] = None) -> List[Multiaddr]: +def _safe_expand(addr: Multiaddr, port: int | None = None) -> list[Multiaddr]: """ Internal safe expansion wrapper. Returns a list of Multiaddr objects. If Thin Waist isn't available, returns [addr] (identity). @@ -46,7 +50,7 @@ def _safe_expand(addr: Multiaddr, port: Optional[int] = None) -> List[Multiaddr] return [addr] -def get_available_interfaces(port: int, protocol: str = "tcp") -> List[Multiaddr]: +def get_available_interfaces(port: int, protocol: str = "tcp") -> list[Multiaddr]: """ Discover available network interfaces (IPv4 + IPv6 if supported) for binding. @@ -54,15 +58,17 @@ def get_available_interfaces(port: int, protocol: str = "tcp") -> List[Multiaddr :param protocol: Transport protocol (e.g., "tcp" or "udp"). :return: List of Multiaddr objects representing candidate interface addresses. """ - addrs: List[Multiaddr] = [] + addrs: list[Multiaddr] = [] # IPv4 enumeration for ip in _safe_get_network_addrs(4): addrs.append(Multiaddr(f"/ip4/{ip}/{protocol}/{port}")) - # IPv6 enumeration (optional: only include if we have at least one global or loopback) + # IPv6 enumeration (optional: only include if we have at least one global or + # loopback) for ip in _safe_get_network_addrs(6): - # Avoid returning unusable wildcard expansions if the environment does not support IPv6 + # Avoid returning unusable wildcard expansions if the environment does not + # support IPv6 addrs.append(Multiaddr(f"/ip6/{ip}/{protocol}/{port}")) # Fallback if nothing discovered @@ -72,7 +78,9 @@ def get_available_interfaces(port: int, protocol: str = "tcp") -> List[Multiaddr return addrs -def expand_wildcard_address(addr: Multiaddr, port: Optional[int] = None) -> List[Multiaddr]: +def expand_wildcard_address( + addr: Multiaddr, port: int | None = None +) -> list[Multiaddr]: """ Expand a wildcard (e.g. /ip4/0.0.0.0/tcp/0) into all concrete interfaces. @@ -122,4 +130,4 @@ __all__ = [ "get_available_interfaces", "get_optimal_binding_address", "expand_wildcard_address", -] \ No newline at end of file +] diff --git a/tests/examples/test_echo_thin_waist.py b/tests/examples/test_echo_thin_waist.py index 9da85928..47e5e495 100644 --- a/tests/examples/test_echo_thin_waist.py +++ b/tests/examples/test_echo_thin_waist.py @@ -1,45 +1,60 @@ -import asyncio import contextlib +import os +from pathlib import Path import subprocess import sys import time -from pathlib import Path -import pytest +from multiaddr import Multiaddr +from multiaddr.protocols import P_IP4, P_IP6, P_P2P, P_TCP + +# pytestmark = pytest.mark.timeout(20) # Temporarily disabled for debugging # This test is intentionally lightweight and can be marked as 'integration'. # It ensures the echo example runs and prints the new Thin Waist lines. -EXAMPLES_DIR = Path(__file__).parent.parent.parent / "examples" / "echo" +current_file = Path(__file__) +project_root = current_file.parent.parent.parent +EXAMPLES_DIR: Path = project_root / "examples" / "echo" -@pytest.mark.timeout(20) def test_echo_example_starts_and_prints_thin_waist(monkeypatch, tmp_path): - # We run: python examples/echo/echo.py -p 0 - cmd = [sys.executable, str(EXAMPLES_DIR / "echo.py"), "-p", "0"] - proc = subprocess.Popen( + """Run echo server and validate printed multiaddr and peer id.""" + # Run echo example as server + cmd = [sys.executable, "-u", str(EXAMPLES_DIR / "echo.py"), "-p", "0"] + env = {**os.environ, "PYTHONUNBUFFERED": "1"} + proc: subprocess.Popen[str] = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, + env=env, ) - assert proc.stdout is not None - found_selected = False - found_interfaces = False + if proc.stdout is None: + proc.terminate() + raise RuntimeError("Process stdout is None") + out_stream = proc.stdout + + peer_id: str | None = None + printed_multiaddr: str | None = None + saw_waiting = False + start = time.time() - + timeout_s = 8.0 try: - while time.time() - start < 10: - line = proc.stdout.readline() + while time.time() - start < timeout_s: + line = out_stream.readline() if not line: - time.sleep(0.1) + time.sleep(0.05) continue - if "Selected binding address:" in line: - found_selected = True - if "Available candidate interfaces:" in line: - found_interfaces = True - if "Waiting for incoming connections..." in line: + s = line.strip() + if s.startswith("I am "): + peer_id = s.partition("I am ")[2] + if s.startswith("echo-demo -d "): + printed_multiaddr = s.partition("echo-demo -d ")[2] + if "Waiting for incoming connections..." in s: + saw_waiting = True break finally: with contextlib.suppress(ProcessLookupError): @@ -47,5 +62,47 @@ def test_echo_example_starts_and_prints_thin_waist(monkeypatch, tmp_path): with contextlib.suppress(ProcessLookupError): proc.kill() - assert found_selected, "Did not capture Thin Waist binding log line" - assert found_interfaces, "Did not capture Thin Waist interfaces log line" \ No newline at end of file + assert peer_id, "Did not capture peer ID line" + assert printed_multiaddr, "Did not capture multiaddr line" + assert saw_waiting, "Did not capture waiting-for-connections line" + + # Validate multiaddr structure using py-multiaddr protocol methods + ma = Multiaddr(printed_multiaddr) # should parse without error + + # Check that the multiaddr contains the p2p protocol + try: + peer_id_from_multiaddr = ma.value_for_protocol("p2p") + assert peer_id_from_multiaddr is not None, ( + "Multiaddr missing p2p protocol value" + ) + assert peer_id_from_multiaddr == peer_id, ( + f"Peer ID mismatch: {peer_id_from_multiaddr} != {peer_id}" + ) + except Exception as e: + raise AssertionError(f"Failed to extract p2p protocol value: {e}") + + # Validate the multiaddr structure by checking protocols + protocols = ma.protocols() + + # Should have at least IP, TCP, and P2P protocols + assert any(p.code == P_IP4 or p.code == P_IP6 for p in protocols), ( + "Missing IP protocol" + ) + assert any(p.code == P_TCP for p in protocols), "Missing TCP protocol" + assert any(p.code == P_P2P for p in protocols), "Missing P2P protocol" + + # Extract the p2p part and validate it matches the captured peer ID + p2p_part = Multiaddr(f"/p2p/{peer_id}") + try: + # Decapsulate the p2p part to get the transport address + transport_addr = ma.decapsulate(p2p_part) + # Verify the decapsulated address doesn't contain p2p + transport_protocols = transport_addr.protocols() + assert not any(p.code == P_P2P for p in transport_protocols), ( + "Decapsulation failed - still contains p2p" + ) + # Verify the original multiaddr can be reconstructed + reconstructed = transport_addr.encapsulate(p2p_part) + assert str(reconstructed) == str(ma), "Reconstruction failed" + except Exception as e: + raise AssertionError(f"Multiaddr decapsulation failed: {e}") diff --git a/tests/utils/test_address_validation.py b/tests/utils/test_address_validation.py index 80ae27e8..5b108d09 100644 --- a/tests/utils/test_address_validation.py +++ b/tests/utils/test_address_validation.py @@ -4,9 +4,9 @@ import pytest from multiaddr import Multiaddr from libp2p.utils.address_validation import ( + expand_wildcard_address, get_available_interfaces, get_optimal_binding_address, - expand_wildcard_address, ) @@ -53,4 +53,4 @@ def test_expand_wildcard_address_ipv6() -> None: expanded = expand_wildcard_address(wildcard) assert len(expanded) > 0 for e in expanded: - assert "/ip6/" in str(e) \ No newline at end of file + assert "/ip6/" in str(e) From a1b16248d3ca941ad16f624867387faabfa60a06 Mon Sep 17 00:00:00 2001 From: yashksaini-coder Date: Tue, 19 Aug 2025 20:47:18 +0530 Subject: [PATCH 08/21] fix: correct listening address variable in echo example and streamline address printing --- examples/echo/echo.py | 5 ++--- libp2p/utils/address_validation.py | 10 ++++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/examples/echo/echo.py b/examples/echo/echo.py index 67e82e07..0cf8c449 100644 --- a/examples/echo/echo.py +++ b/examples/echo/echo.py @@ -47,7 +47,7 @@ async def run(port: int, destination: str, seed: int | None = None) -> None: secret = secrets.token_bytes(32) host = new_host(key_pair=create_new_key_pair(secret)) - async with host.run(listen_addrs=listen_addrs), trio.open_nursery() as nursery: + async with host.run(listen_addr=listen_addr), trio.open_nursery() as nursery: # Start the peer-store cleanup task nursery.start_soon(host.get_peerstore().start_cleanup_task, 60) @@ -59,8 +59,7 @@ async def run(port: int, destination: str, seed: int | None = None) -> None: # Print all listen addresses with peer ID (JS parity) print("Listener ready, listening on:") peer_id = host.get_id().to_string() - for addr in listen_addrs: - print(f"{addr}/p2p/{peer_id}") + print(f"{listen_addr}/p2p/{peer_id}") print( "\nRun this from the same folder in another console:\n\n" diff --git a/libp2p/utils/address_validation.py b/libp2p/utils/address_validation.py index e323dbd5..67299270 100644 --- a/libp2p/utils/address_validation.py +++ b/libp2p/utils/address_validation.py @@ -62,16 +62,18 @@ def get_available_interfaces(port: int, protocol: str = "tcp") -> list[Multiaddr # IPv4 enumeration seen_v4: set[str] = set() + for ip in _safe_get_network_addrs(4): seen_v4.add(ip) addrs.append(Multiaddr(f"/ip4/{ip}/{protocol}/{port}")) + seen_v6: set[str] = set() + for ip in _safe_get_network_addrs(6): + seen_v6.add(ip) + addrs.append(Multiaddr(f"/ip6/{ip}/{protocol}/{port}")) + # IPv6 enumeration (optional: only include if we have at least one global or # loopback) - for ip in _safe_get_network_addrs(6): - # Avoid returning unusable wildcard expansions if the environment does not - # support IPv6 - addrs.append(Multiaddr(f"/ip6/{ip}/{protocol}/{port}")) # Optionally ensure IPv6 loopback when any IPv6 present but loopback missing if seen_v6 and "::1" not in seen_v6: addrs.append(Multiaddr(f"/ip6/::1/{protocol}/{port}")) From 69d52748913406f83fab0c23acfcdf22b8057371 Mon Sep 17 00:00:00 2001 From: yashksaini-coder Date: Tue, 19 Aug 2025 22:32:26 +0530 Subject: [PATCH 09/21] fix: update listening address parameter in echo example to accept a list --- examples/echo/echo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/echo/echo.py b/examples/echo/echo.py index 0cf8c449..8075f125 100644 --- a/examples/echo/echo.py +++ b/examples/echo/echo.py @@ -47,7 +47,7 @@ async def run(port: int, destination: str, seed: int | None = None) -> None: secret = secrets.token_bytes(32) host = new_host(key_pair=create_new_key_pair(secret)) - async with host.run(listen_addr=listen_addr), trio.open_nursery() as nursery: + async with host.run(listen_addrs=[listen_addr]), trio.open_nursery() as nursery: # Start the peer-store cleanup task nursery.start_soon(host.get_peerstore().start_cleanup_task, 60) From c2c91b8c58ca8eae4032272316aa41b300db7731 Mon Sep 17 00:00:00 2001 From: yashksaini-coder Date: Wed, 20 Aug 2025 18:05:20 +0530 Subject: [PATCH 10/21] refactor: Improve comment formatting in test_echo_thin_waist.py for clarity --- tests/examples/test_echo_thin_waist.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/examples/test_echo_thin_waist.py b/tests/examples/test_echo_thin_waist.py index e9401225..2bcb52b1 100644 --- a/tests/examples/test_echo_thin_waist.py +++ b/tests/examples/test_echo_thin_waist.py @@ -11,7 +11,8 @@ from multiaddr.protocols import P_IP4, P_IP6, P_P2P, P_TCP # pytestmark = pytest.mark.timeout(20) # Temporarily disabled for debugging # This test is intentionally lightweight and can be marked as 'integration'. -# It ensures the echo example runs and prints the new Thin Waist lines using Trio primitives. +# It ensures the echo example runs and prints the new Thin Waist lines using +# Trio primitives. current_file = Path(__file__) project_root = current_file.parent.parent.parent From 5b9bec8e28820d66d859b6bc3f40fb3b70b80dbb Mon Sep 17 00:00:00 2001 From: yashksaini-coder Date: Wed, 20 Aug 2025 18:29:35 +0530 Subject: [PATCH 11/21] fix: Enhance error handling in echo stream handler to manage stream closure and exceptions --- examples/echo/echo.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/examples/echo/echo.py b/examples/echo/echo.py index 8075f125..73d30df9 100644 --- a/examples/echo/echo.py +++ b/examples/echo/echo.py @@ -15,6 +15,9 @@ from libp2p.custom_types import ( from libp2p.network.stream.net_stream import ( INetStream, ) +from libp2p.network.stream.exceptions import ( + StreamEOF, +) from libp2p.peer.peerinfo import ( info_from_p2p_addr, ) @@ -27,10 +30,19 @@ MAX_READ_LEN = 2**32 - 1 async def _echo_stream_handler(stream: INetStream) -> None: - # Wait until EOF - msg = await stream.read(MAX_READ_LEN) - await stream.write(msg) - await stream.close() + try: + peer_id = stream.muxed_conn.peer_id + print(f"Received connection from {peer_id}") + # Wait until EOF + msg = await stream.read(MAX_READ_LEN) + print(f"Echoing message: {msg.decode('utf-8')}") + await stream.write(msg) + except StreamEOF: + print("Stream closed by remote peer.") + except Exception as e: + print(f"Error in echo handler: {e}") + finally: + await stream.close() async def run(port: int, destination: str, seed: int | None = None) -> None: @@ -63,8 +75,7 @@ async def run(port: int, destination: str, seed: int | None = None) -> None: print( "\nRun this from the same folder in another console:\n\n" - f"echo-demo " - f"-d {host.get_addrs()[0]}\n" + f"echo-demo -d {host.get_addrs()[0]}\n" ) print("Waiting for incoming connections...") await trio.sleep_forever() From ed2716c1bf6ab339569be8277ce3bcdc93e58de0 Mon Sep 17 00:00:00 2001 From: yashksaini-coder Date: Fri, 22 Aug 2025 11:48:37 +0530 Subject: [PATCH 12/21] feat: Enhance echo example to dynamically find free ports and improve address handling - Added a function to find a free port on localhost. - Updated the run function to use the new port finding logic when a non-positive port is provided. - Modified address printing to handle multiple listen addresses correctly. - Improved the get_available_interfaces function to ensure the IPv4 loopback address is included. --- examples/echo/echo.py | 31 ++++++++++++++++++++---------- libp2p/utils/address_validation.py | 4 ++++ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/examples/echo/echo.py b/examples/echo/echo.py index 73d30df9..fe59e6df 100644 --- a/examples/echo/echo.py +++ b/examples/echo/echo.py @@ -1,4 +1,7 @@ import argparse +import random +import secrets +import socket import multiaddr import trio @@ -12,23 +15,30 @@ from libp2p.crypto.secp256k1 import ( from libp2p.custom_types import ( TProtocol, ) -from libp2p.network.stream.net_stream import ( - INetStream, -) from libp2p.network.stream.exceptions import ( StreamEOF, ) +from libp2p.network.stream.net_stream import ( + INetStream, +) from libp2p.peer.peerinfo import ( info_from_p2p_addr, ) from libp2p.utils.address_validation import ( - get_optimal_binding_address, + get_available_interfaces, ) PROTOCOL_ID = TProtocol("/echo/1.0.0") MAX_READ_LEN = 2**32 - 1 +def find_free_port(): + """Find a free port on localhost.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("", 0)) # Bind to a free port provided by the OS + return s.getsockname()[1] + + async def _echo_stream_handler(stream: INetStream) -> None: try: peer_id = stream.muxed_conn.peer_id @@ -47,19 +57,19 @@ async def _echo_stream_handler(stream: INetStream) -> None: async def run(port: int, destination: str, seed: int | None = None) -> None: # CHANGED: previously hardcoded 0.0.0.0 - listen_addr = get_optimal_binding_address(port) + if port <= 0: + port = find_free_port() + listen_addr = get_available_interfaces(port) if seed: - import random random.seed(seed) secret_number = random.getrandbits(32 * 8) secret = secret_number.to_bytes(length=32, byteorder="big") else: - import secrets secret = secrets.token_bytes(32) host = new_host(key_pair=create_new_key_pair(secret)) - async with host.run(listen_addrs=[listen_addr]), trio.open_nursery() as nursery: + async with host.run(listen_addrs=listen_addr), trio.open_nursery() as nursery: # Start the peer-store cleanup task nursery.start_soon(host.get_peerstore().start_cleanup_task, 60) @@ -69,9 +79,10 @@ async def run(port: int, destination: str, seed: int | None = None) -> None: host.set_stream_handler(PROTOCOL_ID, _echo_stream_handler) # Print all listen addresses with peer ID (JS parity) - print("Listener ready, listening on:") + print("Listener ready, listening on:\n") peer_id = host.get_id().to_string() - print(f"{listen_addr}/p2p/{peer_id}") + for addr in listen_addr: + print(f"{addr}/p2p/{peer_id}") print( "\nRun this from the same folder in another console:\n\n" diff --git a/libp2p/utils/address_validation.py b/libp2p/utils/address_validation.py index 67299270..565bef28 100644 --- a/libp2p/utils/address_validation.py +++ b/libp2p/utils/address_validation.py @@ -67,6 +67,10 @@ def get_available_interfaces(port: int, protocol: str = "tcp") -> list[Multiaddr seen_v4.add(ip) addrs.append(Multiaddr(f"/ip4/{ip}/{protocol}/{port}")) + # Ensure IPv4 loopback is always included when IPv4 interfaces are discovered + if seen_v4 and "127.0.0.1" not in seen_v4: + addrs.append(Multiaddr(f"/ip4/127.0.0.1/{protocol}/{port}")) + seen_v6: set[str] = set() for ip in _safe_get_network_addrs(6): seen_v6.add(ip) From b6cbd78943a51af5a1f67665a3efea07979e307b Mon Sep 17 00:00:00 2001 From: acul71 Date: Sun, 24 Aug 2025 01:49:42 +0200 Subject: [PATCH 13/21] Fix multi-address listening bug in swarm.listen() - Fix early return in swarm.listen() that prevented listening on all addresses - Add comprehensive tests for multi-address listening functionality - Ensure all available interfaces are properly bound and connectable --- libp2p/network/swarm.py | 11 +-- tests/core/network/test_swarm.py | 116 +++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 4 deletions(-) diff --git a/libp2p/network/swarm.py b/libp2p/network/swarm.py index 0aa60514..67d46279 100644 --- a/libp2p/network/swarm.py +++ b/libp2p/network/swarm.py @@ -249,9 +249,11 @@ class Swarm(Service, INetworkService): # We need to wait until `self.listener_nursery` is created. await self.event_listener_nursery_created.wait() + success_count = 0 for maddr in multiaddrs: if str(maddr) in self.listeners: - return True + success_count += 1 + continue async def conn_handler( read_write_closer: ReadWriteCloser, maddr: Multiaddr = maddr @@ -302,13 +304,14 @@ class Swarm(Service, INetworkService): # Call notifiers since event occurred await self.notify_listen(maddr) - return True + success_count += 1 + logger.debug("successfully started listening on: %s", maddr) except OSError: # Failed. Continue looping. logger.debug("fail to listen on: %s", maddr) - # No maddr succeeded - return False + # Return true if at least one address succeeded + return success_count > 0 async def close(self) -> None: """ diff --git a/tests/core/network/test_swarm.py b/tests/core/network/test_swarm.py index 6389bcb3..605913ec 100644 --- a/tests/core/network/test_swarm.py +++ b/tests/core/network/test_swarm.py @@ -16,6 +16,9 @@ from libp2p.network.exceptions import ( from libp2p.network.swarm import ( Swarm, ) +from libp2p.tools.async_service import ( + background_trio_service, +) from libp2p.tools.utils import ( connect_swarm, ) @@ -184,3 +187,116 @@ def test_new_swarm_quic_multiaddr_raises(): addr = Multiaddr("/ip4/127.0.0.1/udp/9999/quic") with pytest.raises(ValueError, match="QUIC not yet supported"): new_swarm(listen_addrs=[addr]) + + +@pytest.mark.trio +async def test_swarm_listen_multiple_addresses(security_protocol): + """Test that swarm can listen on multiple addresses simultaneously.""" + from libp2p.utils.address_validation import get_available_interfaces + + # Get multiple addresses to listen on + listen_addrs = get_available_interfaces(0) # Let OS choose ports + + # Create a swarm and listen on multiple addresses + swarm = SwarmFactory.build(security_protocol=security_protocol) + async with background_trio_service(swarm): + # Listen on all addresses + success = await swarm.listen(*listen_addrs) + assert success, "Should successfully listen on at least one address" + + # Check that we have listeners for the addresses + actual_listeners = list(swarm.listeners.keys()) + assert len(actual_listeners) > 0, "Should have at least one listener" + + # Verify that all successful listeners are in the listeners dict + successful_count = 0 + for addr in listen_addrs: + addr_str = str(addr) + if addr_str in actual_listeners: + successful_count += 1 + # This address successfully started listening + listener = swarm.listeners[addr_str] + listener_addrs = listener.get_addrs() + assert len(listener_addrs) > 0, ( + f"Listener for {addr} should have addresses" + ) + + # Check that the listener address matches the expected address + # (port might be different if we used port 0) + expected_ip = addr.value_for_protocol("ip4") + expected_protocol = addr.value_for_protocol("tcp") + if expected_ip and expected_protocol: + found_matching = False + for listener_addr in listener_addrs: + if ( + listener_addr.value_for_protocol("ip4") == expected_ip + and listener_addr.value_for_protocol("tcp") is not None + ): + found_matching = True + break + assert found_matching, ( + f"Listener for {addr} should have matching IP" + ) + + assert successful_count == len(listen_addrs), ( + f"All {len(listen_addrs)} addresses should be listening, " + f"but only {successful_count} succeeded" + ) + + +@pytest.mark.trio +async def test_swarm_listen_multiple_addresses_connectivity(security_protocol): + """Test that real libp2p connections can be established to all listening addresses.""" # noqa: E501 + from libp2p.peer.peerinfo import info_from_p2p_addr + from libp2p.utils.address_validation import get_available_interfaces + + # Get multiple addresses to listen on + listen_addrs = get_available_interfaces(0) # Let OS choose ports + + # Create a swarm and listen on multiple addresses + swarm1 = SwarmFactory.build(security_protocol=security_protocol) + async with background_trio_service(swarm1): + # Listen on all addresses + success = await swarm1.listen(*listen_addrs) + assert success, "Should successfully listen on at least one address" + + # Verify all available interfaces are listening + assert len(swarm1.listeners) == len(listen_addrs), ( + f"All {len(listen_addrs)} interfaces should be listening, " + f"but only {len(swarm1.listeners)} are" + ) + + # Create a second swarm to test connections + swarm2 = SwarmFactory.build(security_protocol=security_protocol) + async with background_trio_service(swarm2): + # Test connectivity to each listening address using real libp2p connections + for addr_str, listener in swarm1.listeners.items(): + listener_addrs = listener.get_addrs() + for listener_addr in listener_addrs: + # Create a full multiaddr with peer ID for libp2p connection + peer_id = swarm1.get_peer_id() + full_addr = listener_addr.encapsulate(f"/p2p/{peer_id}") + + # Test real libp2p connection + try: + peer_info = info_from_p2p_addr(full_addr) + + # Add the peer info to swarm2's peerstore so it knows where to connect # noqa: E501 + swarm2.peerstore.add_addrs( + peer_info.peer_id, [listener_addr], 10000 + ) + + await swarm2.dial_peer(peer_info.peer_id) + + # Verify connection was established + assert peer_info.peer_id in swarm2.connections, ( + f"Connection to {full_addr} should be established" + ) + assert swarm2.get_peer_id() in swarm1.connections, ( + f"Connection from {full_addr} should be established" + ) + + except Exception as e: + pytest.fail( + f"Failed to establish libp2p connection to {full_addr}: {e}" + ) From 3bd6d1f579d454b68853d7bb03c2615d064b3f8b Mon Sep 17 00:00:00 2001 From: acul71 Date: Sun, 24 Aug 2025 02:29:23 +0200 Subject: [PATCH 14/21] doc: add newsfragment --- newsfragments/863.bugfix.rst | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 newsfragments/863.bugfix.rst diff --git a/newsfragments/863.bugfix.rst b/newsfragments/863.bugfix.rst new file mode 100644 index 00000000..64de57b4 --- /dev/null +++ b/newsfragments/863.bugfix.rst @@ -0,0 +1,5 @@ +Fix multi-address listening bug in swarm.listen() + +- Fix early return in swarm.listen() that prevented listening on all addresses +- Add comprehensive tests for multi-address listening functionality +- Ensure all available interfaces are properly bound and connectable From 88a1f0a390b5716ba7287788a64391e78613fcc7 Mon Sep 17 00:00:00 2001 From: yashksaini-coder Date: Sun, 24 Aug 2025 21:17:29 +0530 Subject: [PATCH 15/21] cherry pick https://github.com/acul71/py-libp2p-fork/blob/7a1198c8c6e9a69c1ab5044adf04a859828c0a95/libp2p/utils/address_validation.py --- libp2p/utils/address_validation.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/libp2p/utils/address_validation.py b/libp2p/utils/address_validation.py index 565bef28..189f00cc 100644 --- a/libp2p/utils/address_validation.py +++ b/libp2p/utils/address_validation.py @@ -71,16 +71,22 @@ def get_available_interfaces(port: int, protocol: str = "tcp") -> list[Multiaddr if seen_v4 and "127.0.0.1" not in seen_v4: addrs.append(Multiaddr(f"/ip4/127.0.0.1/{protocol}/{port}")) - seen_v6: set[str] = set() - for ip in _safe_get_network_addrs(6): - seen_v6.add(ip) - addrs.append(Multiaddr(f"/ip6/{ip}/{protocol}/{port}")) - - # IPv6 enumeration (optional: only include if we have at least one global or - # loopback) - # Optionally ensure IPv6 loopback when any IPv6 present but loopback missing - if seen_v6 and "::1" not in seen_v6: - addrs.append(Multiaddr(f"/ip6/::1/{protocol}/{port}")) + # TODO: IPv6 support temporarily disabled due to libp2p handshake issues + # IPv6 connections fail during protocol negotiation (SecurityUpgradeFailure) + # Re-enable IPv6 support once the following issues are resolved: + # - libp2p security handshake over IPv6 + # - multiselect protocol over IPv6 + # - connection establishment over IPv6 + # + # seen_v6: set[str] = set() + # for ip in _safe_get_network_addrs(6): + # seen_v6.add(ip) + # addrs.append(Multiaddr(f"/ip6/{ip}/{protocol}/{port}")) + # + # # Always include IPv6 loopback for testing purposes when IPv6 is available + # # This ensures IPv6 functionality can be tested even without global IPv6 addresses + # if "::1" not in seen_v6: + # addrs.append(Multiaddr(f"/ip6/::1/{protocol}/{port}")) # Fallback if nothing discovered if not addrs: @@ -141,4 +147,4 @@ __all__ = [ "get_available_interfaces", "get_optimal_binding_address", "expand_wildcard_address", -] +] \ No newline at end of file From cf48d2e9a4ed61349ee88684d28206fb23e08ea0 Mon Sep 17 00:00:00 2001 From: yashksaini-coder Date: Sun, 24 Aug 2025 22:03:31 +0530 Subject: [PATCH 16/21] chore(app): Add 811.internal.rst --- newsfragments/811.internal.rst | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 newsfragments/811.internal.rst diff --git a/newsfragments/811.internal.rst b/newsfragments/811.internal.rst new file mode 100644 index 00000000..8d86c55d --- /dev/null +++ b/newsfragments/811.internal.rst @@ -0,0 +1,7 @@ +Add Thin Waist address validation utilities and integrate into echo example + +- Add ``libp2p/utils/address_validation.py`` with dynamic interface discovery +- Implement ``get_available_interfaces()``, ``get_optimal_binding_address()``, and ``expand_wildcard_address()`` +- Update echo example to use dynamic address discovery instead of hardcoded wildcard +- Add safe fallbacks for environments lacking Thin Waist support +- Temporarily disable IPv6 support due to libp2p handshake issues (TODO: re-enable when resolved) \ No newline at end of file From 75ffb791acd57051ce1aa4db06f7b1da6823ae2a Mon Sep 17 00:00:00 2001 From: yashksaini-coder Date: Sun, 24 Aug 2025 22:06:07 +0530 Subject: [PATCH 17/21] fix: Ensure newline at end of file in address_validation.py and update news fragment formatting --- libp2p/utils/address_validation.py | 2 +- newsfragments/811.internal.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libp2p/utils/address_validation.py b/libp2p/utils/address_validation.py index 189f00cc..c0da78a6 100644 --- a/libp2p/utils/address_validation.py +++ b/libp2p/utils/address_validation.py @@ -147,4 +147,4 @@ __all__ = [ "get_available_interfaces", "get_optimal_binding_address", "expand_wildcard_address", -] \ No newline at end of file +] diff --git a/newsfragments/811.internal.rst b/newsfragments/811.internal.rst index 8d86c55d..59804430 100644 --- a/newsfragments/811.internal.rst +++ b/newsfragments/811.internal.rst @@ -4,4 +4,4 @@ Add Thin Waist address validation utilities and integrate into echo example - Implement ``get_available_interfaces()``, ``get_optimal_binding_address()``, and ``expand_wildcard_address()`` - Update echo example to use dynamic address discovery instead of hardcoded wildcard - Add safe fallbacks for environments lacking Thin Waist support -- Temporarily disable IPv6 support due to libp2p handshake issues (TODO: re-enable when resolved) \ No newline at end of file +- Temporarily disable IPv6 support due to libp2p handshake issues (TODO: re-enable when resolved) From ed91ee0c311c74f4fb9bb4edb4acf07887a49521 Mon Sep 17 00:00:00 2001 From: yashksaini-coder Date: Sun, 24 Aug 2025 23:28:02 +0530 Subject: [PATCH 18/21] refactor(app): 804 refactored find_free_port() in address_validation.py --- examples/echo/echo.py | 9 +-------- examples/pubsub/pubsub.py | 11 +++-------- libp2p/utils/address_validation.py | 10 ++++++++++ 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/examples/echo/echo.py b/examples/echo/echo.py index fe59e6df..e713a759 100644 --- a/examples/echo/echo.py +++ b/examples/echo/echo.py @@ -1,7 +1,6 @@ import argparse import random import secrets -import socket import multiaddr import trio @@ -25,6 +24,7 @@ from libp2p.peer.peerinfo import ( info_from_p2p_addr, ) from libp2p.utils.address_validation import ( + find_free_port, get_available_interfaces, ) @@ -32,13 +32,6 @@ PROTOCOL_ID = TProtocol("/echo/1.0.0") MAX_READ_LEN = 2**32 - 1 -def find_free_port(): - """Find a free port on localhost.""" - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind(("", 0)) # Bind to a free port provided by the OS - return s.getsockname()[1] - - async def _echo_stream_handler(stream: INetStream) -> None: try: peer_id = stream.muxed_conn.peer_id diff --git a/examples/pubsub/pubsub.py b/examples/pubsub/pubsub.py index 1ab6d650..41545658 100644 --- a/examples/pubsub/pubsub.py +++ b/examples/pubsub/pubsub.py @@ -1,6 +1,5 @@ import argparse import logging -import socket import base58 import multiaddr @@ -31,6 +30,9 @@ from libp2p.stream_muxer.mplex.mplex import ( from libp2p.tools.async_service.trio_service import ( background_trio_service, ) +from libp2p.utils.address_validation import ( + find_free_port, +) # Configure logging logging.basicConfig( @@ -77,13 +79,6 @@ async def publish_loop(pubsub, topic, termination_event): await trio.sleep(1) # Avoid tight loop on error -def find_free_port(): - """Find a free port on localhost.""" - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind(("", 0)) # Bind to a free port provided by the OS - return s.getsockname()[1] - - async def monitor_peer_topics(pubsub, nursery, termination_event): """ Monitor for new topics that peers are subscribed to and diff --git a/libp2p/utils/address_validation.py b/libp2p/utils/address_validation.py index c0da78a6..77b797a1 100644 --- a/libp2p/utils/address_validation.py +++ b/libp2p/utils/address_validation.py @@ -1,5 +1,7 @@ from __future__ import annotations +import socket + from multiaddr import Multiaddr try: @@ -35,6 +37,13 @@ def _safe_get_network_addrs(ip_version: int) -> list[str]: return [] +def find_free_port() -> int: + """Find a free port on localhost.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("", 0)) # Bind to a free port provided by the OS + return s.getsockname()[1] + + def _safe_expand(addr: Multiaddr, port: int | None = None) -> list[Multiaddr]: """ Internal safe expansion wrapper. Returns a list of Multiaddr objects. @@ -147,4 +156,5 @@ __all__ = [ "get_available_interfaces", "get_optimal_binding_address", "expand_wildcard_address", + "find_free_port", ] From 63a8458d451fe8d73ab8fca2502b42eaffb5a77b Mon Sep 17 00:00:00 2001 From: yashksaini-coder Date: Sun, 24 Aug 2025 23:40:05 +0530 Subject: [PATCH 19/21] add import to __init__ --- libp2p/utils/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libp2p/utils/__init__.py b/libp2p/utils/__init__.py index 0f68e701..b881eb92 100644 --- a/libp2p/utils/__init__.py +++ b/libp2p/utils/__init__.py @@ -19,6 +19,7 @@ from libp2p.utils.address_validation import ( get_available_interfaces, get_optimal_binding_address, expand_wildcard_address, + find_free_port, ) __all__ = [ @@ -35,4 +36,5 @@ __all__ = [ "get_available_interfaces", "get_optimal_binding_address", "expand_wildcard_address", + "find_free_port", ] From 6a0a7c21e85a589550ea3b83a0b77d93735769b8 Mon Sep 17 00:00:00 2001 From: yashksaini-coder Date: Mon, 25 Aug 2025 01:31:30 +0530 Subject: [PATCH 20/21] chore(app): Add newsfragment for 811.feature.rst --- newsfragments/811.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/811.feature.rst diff --git a/newsfragments/811.feature.rst b/newsfragments/811.feature.rst new file mode 100644 index 00000000..47a0aa68 --- /dev/null +++ b/newsfragments/811.feature.rst @@ -0,0 +1 @@ + Added Thin Waist address validation utilities (with support for interface enumeration, optimal binding, and wildcard expansion). From 6c6adf7459dbeb12f5a3ff9804bf52775da532a4 Mon Sep 17 00:00:00 2001 From: yashksaini-coder Date: Mon, 25 Aug 2025 12:43:18 +0530 Subject: [PATCH 21/21] chore(app): 804 Suggested changes - Remove the comment --- examples/echo/echo.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/echo/echo.py b/examples/echo/echo.py index e713a759..19e98377 100644 --- a/examples/echo/echo.py +++ b/examples/echo/echo.py @@ -49,7 +49,6 @@ async def _echo_stream_handler(stream: INetStream) -> None: async def run(port: int, destination: str, seed: int | None = None) -> None: - # CHANGED: previously hardcoded 0.0.0.0 if port <= 0: port = find_free_port() listen_addr = get_available_interfaces(port)