Skip to content

Commit 29b7d67

Browse files
authored
Merge branch 'main' into fix/interop-issues
2 parents 5bdba9e + 702b98d commit 29b7d67

File tree

6 files changed

+243
-9
lines changed

6 files changed

+243
-9
lines changed

libp2p/__init__.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,11 @@
3333
from libp2p.crypto.keys import (
3434
KeyPair,
3535
)
36+
from libp2p.crypto.ed25519 import (
37+
create_new_key_pair as create_new_ed25519_key_pair,
38+
)
3639
from libp2p.crypto.rsa import (
37-
create_new_key_pair,
40+
create_new_key_pair as create_new_rsa_key_pair,
3841
)
3942
from libp2p.crypto.x25519 import create_new_key_pair as create_new_x25519_key_pair
4043
from libp2p.custom_types import (
@@ -154,7 +157,17 @@ def create_mplex_muxer_option() -> TMuxerOptions:
154157

155158

156159
def generate_new_rsa_identity() -> KeyPair:
157-
return create_new_key_pair()
160+
return create_new_rsa_key_pair()
161+
162+
163+
def generate_new_ed25519_identity() -> KeyPair:
164+
"""
165+
Generate a new Ed25519 identity key pair.
166+
167+
Ed25519 is preferred for better interoperability with other libp2p implementations
168+
(e.g., Rust, Go) which often disable RSA support.
169+
"""
170+
return create_new_ed25519_key_pair()
158171

159172

160173
def generate_peer_id_from(key_pair: KeyPair) -> ID:
@@ -207,9 +220,14 @@ def new_swarm(
207220
due to its improved performance and features.
208221
Mplex (/mplex/6.7.0) is retained for backward compatibility
209222
but may be deprecated in the future.
223+
224+
Note: Ed25519 keys are used by default for better interoperability with
225+
other libp2p implementations (Rust, Go) which often disable RSA support.
210226
"""
211227
if key_pair is None:
212-
key_pair = generate_new_rsa_identity()
228+
# Use Ed25519 by default for better interoperability with Rust/Go libp2p
229+
# which often compile without RSA support
230+
key_pair = generate_new_ed25519_identity()
213231

214232
id_opt = generate_peer_id_from(key_pair)
215233

libp2p/stream_muxer/yamux/yamux.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,17 @@ async def handle_incoming(self) -> None:
653653
self.streams[stream_id] = stream
654654
self.stream_buffers[stream_id] = bytearray()
655655
self.stream_events[stream_id] = trio.Event()
656+
657+
# Read any data that came with the SYN frame
658+
if length > 0:
659+
data = await self.secured_conn.read(length)
660+
self.stream_buffers[stream_id].extend(data)
661+
self.stream_events[stream_id].set()
662+
logger.debug(
663+
f"Read {length} bytes with SYN "
664+
f"for stream {stream_id}"
665+
)
666+
656667
ack_header = struct.pack(
657668
YAMUX_HEADER_FORMAT,
658669
0,
@@ -680,10 +691,20 @@ async def handle_incoming(self) -> None:
680691
elif typ == TYPE_DATA and flags & FLAG_ACK:
681692
async with self.streams_lock:
682693
if stream_id in self.streams:
683-
logger.debug(
684-
f"Received ACK for stream"
685-
f"{stream_id} for peer {self.peer_id}"
686-
)
694+
# Read any data that came with the ACK
695+
if length > 0:
696+
data = await self.secured_conn.read(length)
697+
self.stream_buffers[stream_id].extend(data)
698+
self.stream_events[stream_id].set()
699+
logger.debug(
700+
f"Received ACK with {length} bytes for stream "
701+
f"{stream_id} for peer {self.peer_id}"
702+
)
703+
else:
704+
logger.debug(
705+
f"Received ACK (no data) for stream {stream_id} "
706+
f"for peer {self.peer_id}"
707+
)
687708
elif typ == TYPE_GO_AWAY:
688709
error_code = length
689710
if error_code == GO_AWAY_NORMAL:

newsfragments/1034.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed interoperability with rust-libp2p by switching default key generation to Ed25519 and enhancing Yamux to handle data with SYN/ACK frames.

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ dependencies = [
2525
"grpcio>=1.41.0",
2626
"lru-dict>=1.1.6",
2727
"miniupnpc>=2.3",
28-
"multiaddr>=0.0.11",
28+
"multiaddr==0.0.11",
2929
"mypy-protobuf>=3.0.0",
3030
"noiseprotocol>=0.3.0",
3131
"protobuf>=4.25.0,<7.0.0",
3232
"pycryptodome>=3.9.2",
33-
"pymultihash>=0.8.2",
33+
"pymultihash==0.8.2",
3434
"pynacl>=1.3.0",
3535
"rpcudp>=3.0.0",
3636
"trio-typing>=0.0.4",

tests/core/network/test_swarm.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,18 @@
88
)
99

1010
from libp2p import (
11+
generate_new_ed25519_identity,
12+
generate_new_rsa_identity,
1113
new_swarm,
1214
)
15+
from libp2p.crypto.ed25519 import (
16+
Ed25519PrivateKey,
17+
Ed25519PublicKey,
18+
)
19+
from libp2p.crypto.rsa import (
20+
RSAPrivateKey,
21+
RSAPublicKey,
22+
)
1323
from libp2p.network.exceptions import (
1424
SwarmException,
1525
)
@@ -259,6 +269,41 @@ def test_new_swarm_quic_multiaddr_supported():
259269
assert isinstance(swarm.transport, QUICTransport)
260270

261271

272+
def test_new_swarm_defaults_to_ed25519():
273+
"""Test that new_swarm() generates Ed25519 keys by default (not RSA)."""
274+
# Test that new_swarm() without key_pair parameter generates a valid swarm
275+
swarm = new_swarm()
276+
assert isinstance(swarm, Swarm)
277+
278+
# The swarm's peer ID should be valid (indicating successful key generation)
279+
peer_id = swarm.get_peer_id()
280+
assert peer_id is not None
281+
282+
# Test that explicitly providing Ed25519 keys works
283+
ed25519_key_pair = generate_new_ed25519_identity()
284+
swarm_ed25519 = new_swarm(key_pair=ed25519_key_pair)
285+
assert isinstance(swarm_ed25519, Swarm)
286+
assert swarm_ed25519.get_peer_id() is not None
287+
288+
# Verify that Ed25519 keys are indeed being used by checking key type
289+
assert isinstance(ed25519_key_pair.private_key, Ed25519PrivateKey)
290+
assert isinstance(ed25519_key_pair.public_key, Ed25519PublicKey)
291+
292+
# Test that RSA keys can still be explicitly provided
293+
rsa_key_pair = generate_new_rsa_identity()
294+
swarm_rsa = new_swarm(key_pair=rsa_key_pair)
295+
assert isinstance(swarm_rsa, Swarm)
296+
assert swarm_rsa.get_peer_id() is not None
297+
298+
# Verify RSA keys are being used when explicitly provided
299+
assert isinstance(rsa_key_pair.private_key, RSAPrivateKey)
300+
assert isinstance(rsa_key_pair.public_key, RSAPublicKey)
301+
302+
# Ensure different key types produce different peer IDs
303+
# (This is expected since RSA and Ed25519 generate different keys)
304+
assert swarm_ed25519.get_peer_id() != swarm_rsa.get_peer_id()
305+
306+
262307
@pytest.mark.trio
263308
async def test_swarm_listen_multiple_addresses(security_protocol):
264309
"""Test that swarm can listen on multiple addresses simultaneously."""

tests/core/stream_muxer/test_yamux.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@
2424
MuxedConnUnavailable,
2525
)
2626
from libp2p.stream_muxer.yamux.yamux import (
27+
FLAG_ACK,
2728
FLAG_FIN,
2829
FLAG_RST,
2930
FLAG_SYN,
3031
GO_AWAY_PROTOCOL_ERROR,
32+
TYPE_DATA,
3133
TYPE_PING,
3234
TYPE_WINDOW_UPDATE,
3335
YAMUX_HEADER_FORMAT,
@@ -624,3 +626,150 @@ async def accept_should_unblock():
624626
"accept_stream() should have raised MuxedConnUnavailable"
625627
)
626628
logging.debug("test_yamux_accept_stream_unblocks_on_error complete")
629+
630+
631+
@pytest.mark.trio
632+
async def test_yamux_syn_with_data(yamux_pair):
633+
"""Test that data sent with SYN frame is properly received and buffered."""
634+
logging.debug("Starting test_yamux_syn_with_data")
635+
client_yamux, server_yamux = yamux_pair
636+
637+
# Manually construct a SYN frame with accompanying data
638+
test_data = b"data with SYN frame"
639+
stream_id = 1 # Client stream ID (odd number)
640+
641+
# Create SYN header with data length
642+
syn_header = struct.pack(
643+
YAMUX_HEADER_FORMAT,
644+
0, # version
645+
TYPE_DATA, # type
646+
FLAG_SYN, # flags
647+
stream_id,
648+
len(test_data), # length of accompanying data
649+
)
650+
651+
# Send SYN with data directly
652+
await client_yamux.secured_conn.write(syn_header + test_data)
653+
logging.debug(f"Sent SYN with {len(test_data)} bytes of data")
654+
655+
# Server should accept the stream and have data already buffered
656+
server_stream = await server_yamux.accept_stream()
657+
assert server_stream.stream_id == stream_id
658+
659+
# Verify the data was buffered and is immediately available
660+
received = await server_stream.read(len(test_data))
661+
assert received == test_data, "Data sent with SYN should be immediately available"
662+
logging.debug("test_yamux_syn_with_data complete")
663+
664+
665+
@pytest.mark.trio
666+
async def test_yamux_ack_with_data(yamux_pair):
667+
"""Test that data sent with ACK frame is properly received and buffered."""
668+
logging.debug("Starting test_yamux_ack_with_data")
669+
client_yamux, server_yamux = yamux_pair
670+
671+
# Client opens a stream (sends SYN)
672+
client_stream = await client_yamux.open_stream()
673+
stream_id = client_stream.stream_id
674+
675+
# Wait for server to receive SYN and respond with ACK
676+
await trio.sleep(0.1)
677+
678+
# Now manually send data with an ACK flag from server to client
679+
test_data = b"data with ACK frame"
680+
ack_header = struct.pack(
681+
YAMUX_HEADER_FORMAT,
682+
0, # version
683+
TYPE_DATA, # type
684+
FLAG_ACK, # flags (ACK flag set)
685+
stream_id,
686+
len(test_data), # length of accompanying data
687+
)
688+
689+
# Send ACK with data from server to client
690+
await server_yamux.secured_conn.write(ack_header + test_data)
691+
logging.debug(f"Sent ACK with {len(test_data)} bytes of data")
692+
693+
# Wait for the data to be processed
694+
await trio.sleep(0.1)
695+
696+
# Verify the data was buffered on the client side
697+
# Since the stream is already open, the data should be in the buffer
698+
async with client_yamux.streams_lock:
699+
assert stream_id in client_yamux.stream_buffers
700+
assert len(client_yamux.stream_buffers[stream_id]) >= len(test_data)
701+
buffered_data = bytes(client_yamux.stream_buffers[stream_id][: len(test_data)])
702+
# Remove the data we just checked
703+
client_yamux.stream_buffers[stream_id] = client_yamux.stream_buffers[stream_id][
704+
len(test_data) :
705+
]
706+
707+
assert buffered_data == test_data, "Data sent with ACK should be buffered"
708+
logging.debug("test_yamux_ack_with_data complete")
709+
710+
711+
@pytest.mark.trio
712+
async def test_yamux_syn_with_empty_data(yamux_pair):
713+
"""Test that SYN frame with zero-length data is handled correctly."""
714+
logging.debug("Starting test_yamux_syn_with_empty_data")
715+
client_yamux, server_yamux = yamux_pair
716+
717+
# Manually construct a SYN frame with no data (length = 0)
718+
stream_id = 3 # Client stream ID (odd number)
719+
720+
syn_header = struct.pack(
721+
YAMUX_HEADER_FORMAT,
722+
0, # version
723+
TYPE_DATA, # type
724+
FLAG_SYN, # flags
725+
stream_id,
726+
0, # length = 0, no accompanying data
727+
)
728+
729+
# Send SYN with no data
730+
await client_yamux.secured_conn.write(syn_header)
731+
logging.debug("Sent SYN with no data")
732+
733+
# Server should accept the stream
734+
server_stream = await server_yamux.accept_stream()
735+
assert server_stream.stream_id == stream_id
736+
737+
# Verify no data is in the buffer
738+
async with server_yamux.streams_lock:
739+
assert len(server_yamux.stream_buffers[stream_id]) == 0
740+
741+
logging.debug("test_yamux_syn_with_empty_data complete")
742+
743+
744+
@pytest.mark.trio
745+
async def test_yamux_syn_with_large_data(yamux_pair):
746+
"""Test that large data sent with SYN frame is properly handled."""
747+
logging.debug("Starting test_yamux_syn_with_large_data")
748+
client_yamux, server_yamux = yamux_pair
749+
750+
# Create large test data (but within window size)
751+
test_data = b"X" * 1024 # 1KB of data
752+
stream_id = 5 # Client stream ID (odd number)
753+
754+
syn_header = struct.pack(
755+
YAMUX_HEADER_FORMAT,
756+
0, # version
757+
TYPE_DATA, # type
758+
FLAG_SYN, # flags
759+
stream_id,
760+
len(test_data),
761+
)
762+
763+
# Send SYN with large data
764+
await client_yamux.secured_conn.write(syn_header + test_data)
765+
logging.debug(f"Sent SYN with {len(test_data)} bytes of large data")
766+
767+
# Server should accept the stream and have all data buffered
768+
server_stream = await server_yamux.accept_stream()
769+
assert server_stream.stream_id == stream_id
770+
771+
# Verify all data was buffered correctly
772+
received = await server_stream.read(len(test_data))
773+
assert received == test_data
774+
assert len(received) == 1024
775+
logging.debug("test_yamux_syn_with_large_data complete")

0 commit comments

Comments
 (0)