fix: add nim libp2p echo interop

This commit is contained in:
Akash Mondal
2025-08-30 07:10:22 +00:00
committed by lla-dane
parent 2c03ac46ea
commit d97b86081b
5 changed files with 457 additions and 1 deletions

View File

@ -20,6 +20,7 @@ dependencies = [
"base58>=1.0.3",
"coincurve==21.0.0",
"exceptiongroup>=1.2.0; python_version < '3.11'",
"fastecdsa==2.3.2; sys_platform != 'win32'",
"grpcio>=1.41.0",
"lru-dict>=1.1.6",
"multiaddr (>=0.0.9,<0.0.10)",
@ -32,7 +33,6 @@ dependencies = [
"rpcudp>=3.0.0",
"trio-typing>=0.0.4",
"trio>=0.26.0",
"fastecdsa==2.3.2; sys_platform != 'win32'",
"zeroconf (>=0.147.0,<0.148.0)",
]
classifiers = [
@ -282,4 +282,5 @@ project_excludes = [
"**/*pb2.py",
"**/*.pyi",
".venv/**",
"./tests/interop/nim_libp2p",
]

8
tests/interop/nim_libp2p/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
nimble.develop
nimble.paths
*.nimble
nim-libp2p/
nim_echo_server
config.nims

View File

@ -0,0 +1,108 @@
{.used.}
import chronos
import stew/byteutils
import libp2p
##
# Simple Echo Protocol Implementation for py-libp2p Interop Testing
##
const EchoCodec = "/echo/1.0.0"
type EchoProto = ref object of LPProtocol
proc new(T: typedesc[EchoProto]): T =
proc handle(conn: Connection, proto: string) {.async: (raises: [CancelledError]).} =
try:
echo "Echo server: Received connection from ", conn.peerId
# Read and echo messages in a loop
while not conn.atEof:
try:
# Read length-prefixed message using nim-libp2p's readLp
let message = await conn.readLp(1024 * 1024) # Max 1MB
if message.len == 0:
echo "Echo server: Empty message, closing connection"
break
let messageStr = string.fromBytes(message)
echo "Echo server: Received (", message.len, " bytes): ", messageStr
# Echo back using writeLp
await conn.writeLp(message)
echo "Echo server: Echoed message back"
except CatchableError as e:
echo "Echo server: Error processing message: ", e.msg
break
except CancelledError as e:
echo "Echo server: Connection cancelled"
raise e
except CatchableError as e:
echo "Echo server: Exception in handler: ", e.msg
finally:
echo "Echo server: Connection closed"
await conn.close()
return T.new(codecs = @[EchoCodec], handler = handle)
##
# Create QUIC-enabled switch
##
proc createSwitch(ma: MultiAddress, rng: ref HmacDrbgContext): Switch =
var switch = SwitchBuilder
.new()
.withRng(rng)
.withAddress(ma)
.withQuicTransport()
.build()
result = switch
##
# Main server
##
proc main() {.async.} =
let
rng = newRng()
localAddr = MultiAddress.init("/ip4/0.0.0.0/udp/0/quic-v1").tryGet()
echoProto = EchoProto.new()
echo "=== Nim Echo Server for py-libp2p Interop ==="
# Create switch
let switch = createSwitch(localAddr, rng)
switch.mount(echoProto)
# Start server
await switch.start()
# Print connection info
echo "Peer ID: ", $switch.peerInfo.peerId
echo "Listening on:"
for addr in switch.peerInfo.addrs:
echo " ", $addr, "/p2p/", $switch.peerInfo.peerId
echo "Protocol: ", EchoCodec
echo "Ready for py-libp2p connections!"
echo ""
# Keep running
try:
await sleepAsync(100.hours)
except CancelledError:
echo "Shutting down..."
finally:
await switch.stop()
# Graceful shutdown handler
proc signalHandler() {.noconv.} =
echo "\nShutdown signal received"
quit(0)
when isMainModule:
setControlCHook(signalHandler)
try:
waitFor(main())
except CatchableError as e:
echo "Error: ", e.msg
quit(1)

View File

@ -0,0 +1,98 @@
#!/usr/bin/env bash
# Simple setup script for nim echo server interop testing
set -euo pipefail
# Colors
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="${SCRIPT_DIR}/.."
NIM_LIBP2P_DIR="${PROJECT_ROOT}/nim-libp2p"
# Check prerequisites
check_nim() {
if ! command -v nim &> /dev/null; then
log_error "Nim not found. Install with: curl -sSf https://nim-lang.org/choosenim/init.sh | sh"
exit 1
fi
if ! command -v nimble &> /dev/null; then
log_error "Nimble not found. Please install Nim properly."
exit 1
fi
}
# Setup nim-libp2p dependency
setup_nim_libp2p() {
log_info "Setting up nim-libp2p dependency..."
if [ ! -d "${NIM_LIBP2P_DIR}" ]; then
log_info "Cloning nim-libp2p..."
git clone https://github.com/status-im/nim-libp2p.git "${NIM_LIBP2P_DIR}"
fi
cd "${NIM_LIBP2P_DIR}"
log_info "Installing nim-libp2p dependencies..."
nimble install -y --depsOnly
}
# Build nim echo server
build_echo_server() {
log_info "Building nim echo server..."
cd "${PROJECT_ROOT}"
# Create nimble file if it doesn't exist
cat > nim_echo_test.nimble << 'EOF'
# Package
version = "0.1.0"
author = "py-libp2p interop"
description = "nim echo server for interop testing"
license = "MIT"
# Dependencies
requires "nim >= 1.6.0"
requires "libp2p"
requires "chronos"
requires "stew"
# Binary
bin = @["nim_echo_server"]
EOF
# Build the server
log_info "Compiling nim echo server..."
nim c -d:release -d:chronicles_log_level=INFO -d:libp2p_quic_support --opt:speed --gc:orc -o:nim_echo_server nim_echo_server.nim
if [ -f "nim_echo_server" ]; then
log_info "✅ nim_echo_server built successfully"
else
log_error "❌ Failed to build nim_echo_server"
exit 1
fi
}
main() {
log_info "Setting up nim echo server for interop testing..."
# Create logs directory
mkdir -p "${PROJECT_ROOT}/logs"
# Clean up any existing processes
pkill -f "nim_echo_server" || true
check_nim
setup_nim_libp2p
build_echo_server
log_info "🎉 Setup complete! You can now run: python -m pytest test_echo_interop.py -v"
}
main "$@"

View File

@ -0,0 +1,241 @@
#!/usr/bin/env python3
"""
Simple echo protocol interop test between py-libp2p and nim-libp2p.
Tests that py-libp2p QUIC clients can communicate with nim-libp2p echo servers.
"""
import logging
from pathlib import Path
import subprocess
from subprocess import Popen
import time
import pytest
import multiaddr
import trio
from libp2p import new_host
from libp2p.crypto.secp256k1 import create_new_key_pair
from libp2p.custom_types import TProtocol
from libp2p.peer.peerinfo import info_from_p2p_addr
from libp2p.transport.quic.config import QUICTransportConfig
from libp2p.utils.varint import encode_varint_prefixed, read_varint_prefixed_bytes
# Configuration
PROTOCOL_ID = TProtocol("/echo/1.0.0")
TEST_TIMEOUT = 15.0 # Reduced timeout
SERVER_START_TIMEOUT = 10.0
# Setup logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class NimEchoServer:
"""Simple nim echo server manager."""
def __init__(self, binary_path: Path):
self.binary_path = binary_path
self.process: None | Popen = None
self.peer_id = None
self.listen_addr = None
async def start(self):
"""Start nim echo server and get connection info."""
logger.info(f"Starting nim echo server: {self.binary_path}")
self.process: Popen[str] = subprocess.Popen(
[str(self.binary_path)],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
if self.process is None:
return None, None
# Parse output for connection info
start_time = time.time()
while (
self.process is not None and time.time() - start_time < SERVER_START_TIMEOUT
):
if self.process.poll() is not None:
IOout = self.process.stdout
if IOout:
output = IOout.read()
raise RuntimeError(f"Server exited early: {output}")
IOin = self.process.stdout
if IOin:
line = IOin.readline().strip()
if not line:
continue
logger.info(f"Server: {line}")
if line.startswith("Peer ID:"):
self.peer_id = line.split(":", 1)[1].strip()
elif "/quic-v1/p2p/" in line and self.peer_id:
if line.strip().startswith("/"):
self.listen_addr = line.strip()
logger.info(f"Server ready: {self.listen_addr}")
return self.peer_id, self.listen_addr
await self.stop()
raise TimeoutError(f"Server failed to start within {SERVER_START_TIMEOUT}s")
async def stop(self):
"""Stop the server."""
if self.process:
logger.info("Stopping nim echo server...")
try:
self.process.terminate()
self.process.wait(timeout=5)
except subprocess.TimeoutExpired:
self.process.kill()
self.process.wait()
self.process = None
async def run_echo_test(server_addr: str, messages: list[str]):
"""Test echo protocol against nim server with proper timeout handling."""
# Create py-libp2p QUIC client with shorter timeouts
quic_config = QUICTransportConfig(
idle_timeout=10.0,
max_concurrent_streams=10,
connection_timeout=5.0,
enable_draft29=False,
)
host = new_host(
key_pair=create_new_key_pair(),
transport_opt={"quic_config": quic_config},
)
listen_addr = multiaddr.Multiaddr("/ip4/0.0.0.0/udp/0/quic-v1")
responses = []
try:
async with host.run(listen_addrs=[listen_addr]):
logger.info(f"Connecting to nim server: {server_addr}")
# Connect to nim server
maddr = multiaddr.Multiaddr(server_addr)
info = info_from_p2p_addr(maddr)
await host.connect(info)
# Create stream
stream = await host.new_stream(info.peer_id, [PROTOCOL_ID])
logger.info("Stream created")
# Test each message
for i, message in enumerate(messages, 1):
logger.info(f"Testing message {i}: {message}")
# Send with varint length prefix
data = message.encode("utf-8")
prefixed_data = encode_varint_prefixed(data)
await stream.write(prefixed_data)
# Read response
response_data = await read_varint_prefixed_bytes(stream)
response = response_data.decode("utf-8")
logger.info(f"Got echo: {response}")
responses.append(response)
# Verify echo
assert message == response, (
f"Echo failed: sent {message!r}, got {response!r}"
)
await stream.close()
logger.info("✅ All messages echoed correctly")
finally:
await host.close()
return responses
@pytest.fixture
def nim_echo_binary():
"""Path to nim echo server binary."""
current_dir = Path(__file__).parent
binary_path = current_dir / "nim_echo_server"
if not binary_path.exists():
pytest.skip(
f"Nim echo server not found at {binary_path}. Run setup script first."
)
return binary_path
@pytest.fixture
async def nim_server(nim_echo_binary):
"""Start and stop nim echo server for tests."""
server = NimEchoServer(nim_echo_binary)
try:
peer_id, listen_addr = await server.start()
yield server, peer_id, listen_addr
finally:
await server.stop()
@pytest.mark.trio
async def test_basic_echo_interop(nim_server):
"""Test basic echo functionality between py-libp2p and nim-libp2p."""
server, peer_id, listen_addr = nim_server
test_messages = [
"Hello from py-libp2p!",
"QUIC transport working",
"Echo test successful!",
"Unicode: Ñoël, 测试, Ψυχή",
]
logger.info(f"Testing against nim server: {peer_id}")
# Run test with timeout
with trio.move_on_after(TEST_TIMEOUT - 2): # Leave 2s buffer for cleanup
responses = await run_echo_test(listen_addr, test_messages)
# Verify all messages echoed correctly
assert len(responses) == len(test_messages)
for sent, received in zip(test_messages, responses):
assert sent == received
logger.info("✅ Basic echo interop test passed!")
@pytest.mark.trio
async def test_large_message_echo(nim_server):
"""Test echo with larger messages."""
server, peer_id, listen_addr = nim_server
large_messages = [
"x" * 1024, # 1KB
"y" * 10000,
]
logger.info("Testing large message echo...")
# Run test with timeout
with trio.move_on_after(TEST_TIMEOUT - 2): # Leave 2s buffer for cleanup
responses = await run_echo_test(listen_addr, large_messages)
assert len(responses) == len(large_messages)
for sent, received in zip(large_messages, responses):
assert sent == received
logger.info("✅ Large message echo test passed!")
if __name__ == "__main__":
# Run tests directly
pytest.main([__file__, "-v", "--tb=short"])