Skip to content

Commit 2099b1a

Browse files
committed
WIP: Progress on py-libp2p and rust-libp2p interop for Noise and Yamux
1 parent 65bc17a commit 2099b1a

File tree

8 files changed

+398
-95
lines changed

8 files changed

+398
-95
lines changed

examples/ping/ping.py

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,53 @@
11
import argparse
2+
import logging
3+
from typing import (
4+
cast,
5+
)
26

37
import multiaddr
48
import trio
59

6-
from libp2p import (
7-
new_host,
10+
from libp2p.crypto.ed25519 import (
11+
Ed25519PrivateKey,
12+
Ed25519PublicKey,
13+
KeyPair,
814
)
915
from libp2p.custom_types import (
1016
TProtocol,
1117
)
18+
from libp2p.host.basic_host import (
19+
BasicHost,
20+
)
1221
from libp2p.network.stream.net_stream import (
1322
INetStream,
1423
)
24+
from libp2p.network.swarm import (
25+
Swarm,
26+
)
27+
from libp2p.peer.id import (
28+
ID,
29+
)
1530
from libp2p.peer.peerinfo import (
1631
info_from_p2p_addr,
1732
)
33+
from libp2p.peer.peerstore import (
34+
PeerStore,
35+
)
36+
from libp2p.security.noise.transport import (
37+
PROTOCOL_ID,
38+
Transport,
39+
)
40+
from libp2p.stream_muxer.yamux.yamux import (
41+
Yamux,
42+
)
43+
from libp2p.transport.tcp.tcp import (
44+
TCP,
45+
)
46+
from libp2p.transport.upgrader import (
47+
TransportUpgrader,
48+
)
49+
50+
logging.basicConfig(level=logging.DEBUG)
1851

1952
PING_PROTOCOL_ID = TProtocol("/ipfs/ping/1.0.0")
2053
PING_LENGTH = 32
@@ -28,10 +61,8 @@ async def handle_ping(stream: INetStream) -> None:
2861
peer_id = stream.muxed_conn.peer_id
2962
if payload is not None:
3063
print(f"received ping from {peer_id}")
31-
3264
await stream.write(payload)
3365
print(f"responded with pong to {peer_id}")
34-
3566
except Exception:
3667
await stream.reset()
3768
break
@@ -41,45 +72,61 @@ async def send_ping(stream: INetStream) -> None:
4172
try:
4273
payload = b"\x01" * PING_LENGTH
4374
print(f"sending ping to {stream.muxed_conn.peer_id}")
44-
4575
await stream.write(payload)
46-
4776
with trio.fail_after(RESP_TIMEOUT):
4877
response = await stream.read(PING_LENGTH)
49-
5078
if response == payload:
5179
print(f"received pong from {stream.muxed_conn.peer_id}")
52-
5380
except Exception as e:
5481
print(f"error occurred : {e}")
5582

5683

5784
async def run(port: int, destination: str) -> None:
5885
localhost_ip = "127.0.0.1"
5986
listen_addr = multiaddr.Multiaddr(f"/ip4/0.0.0.0/tcp/{port}")
60-
host = new_host()
87+
88+
rust_privkey_bytes = bytes.fromhex(
89+
"6866d018650288fef506b818f04e6aafeadd96d70752fb16ac4b7f0a5f88f18d"
90+
)
91+
rust_pubkey_bytes = bytes.fromhex(
92+
"0d3fb3dc6cc19c6e58d6f61829fd56af9bbd28aa28d76ccb519f56a79bdace2a"
93+
)
94+
private_key = Ed25519PrivateKey.from_bytes(rust_privkey_bytes)
95+
public_key = Ed25519PublicKey.from_bytes(rust_pubkey_bytes)
96+
key_pair = KeyPair(private_key, public_key)
97+
peer_id = ID.from_pubkey(key_pair.public_key)
98+
logging.debug(f"Local Peer ID: {peer_id.pretty()}")
99+
logging.debug(f"Public Key from Private: {key_pair.public_key.to_bytes().hex()}")
100+
101+
noise_transport = Transport(key_pair, noise_privkey=key_pair.private_key)
102+
tcp_transport = TCP()
103+
peerstore = PeerStore()
104+
upgrader = TransportUpgrader(
105+
secure_transports_by_protocol={PROTOCOL_ID: noise_transport},
106+
muxer_transports_by_protocol={cast(TProtocol, "/yamux/1.0.0"): Yamux},
107+
)
108+
swarm = Swarm(
109+
peer_id=peer_id, peerstore=peerstore, upgrader=upgrader, transport=tcp_transport
110+
)
111+
host = BasicHost(network=swarm)
61112

62113
async with host.run(listen_addrs=[listen_addr]), trio.open_nursery() as nursery:
63114
if not destination:
64115
host.set_stream_handler(PING_PROTOCOL_ID, handle_ping)
65-
66-
print(
67-
"Run this from the same folder in another console:\n\n"
116+
cmd = (
117+
f"Run this from the same folder in another console:\n\n"
68118
f"ping-demo -p {int(port) + 1} "
69119
f"-d /ip4/{localhost_ip}/tcp/{port}/p2p/{host.get_id().pretty()}\n"
70120
)
121+
print(cmd)
71122
print("Waiting for incoming connection...")
72-
73123
else:
74124
maddr = multiaddr.Multiaddr(destination)
75125
info = info_from_p2p_addr(maddr)
76126
await host.connect(info)
77127
stream = await host.new_stream(info.peer_id, [PING_PROTOCOL_ID])
78-
79128
nursery.start_soon(send_ping, stream)
80-
81129
return
82-
83130
await trio.sleep_forever()
84131

85132

@@ -90,13 +137,10 @@ def main() -> None:
90137
Then, run another instance with 'python ping.py -p <ANOTHER_PORT> -d <DESTINATION>',
91138
where <DESTINATION> is the multiaddress of the previous listener host.
92139
"""
93-
94140
example_maddr = (
95141
"/ip4/127.0.0.1/tcp/8000/p2p/QmQn4SwGkDZKkUEpBRBvTmheQycxAHJUNmVEnjA2v1qe8Q"
96142
)
97-
98143
parser = argparse.ArgumentParser(description=description)
99-
100144
parser.add_argument(
101145
"-p", "--port", default=8000, type=int, help="source port number"
102146
)
@@ -107,10 +151,8 @@ def main() -> None:
107151
help=f"destination multiaddr string, e.g. {example_maddr}",
108152
)
109153
args = parser.parse_args()
110-
111154
if not args.port:
112155
raise RuntimeError("failed to determine local port")
113-
114156
try:
115157
trio.run(run, *(args.port, args.destination))
116158
except KeyboardInterrupt:

libp2p/__init__.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
1+
from collections.abc import (
2+
Mapping,
3+
)
14
from importlib.metadata import version as __version
5+
from typing import (
6+
Type,
7+
cast,
8+
)
29

310
from libp2p.abc import (
411
IHost,
12+
IMuxedConn,
513
INetworkService,
614
IPeerRouting,
715
IPeerStore,
16+
ISecureTransport,
817
)
18+
from libp2p.crypto.ed25519 import create_new_key_pair as create_ed25519_key_pair
919
from libp2p.crypto.keys import (
1020
KeyPair,
1121
)
@@ -36,11 +46,18 @@
3646
PLAINTEXT_PROTOCOL_ID,
3747
InsecureTransport,
3848
)
49+
from libp2p.security.noise.transport import (
50+
PROTOCOL_ID,
51+
Transport,
52+
)
3953
import libp2p.security.secio.transport as secio
4054
from libp2p.stream_muxer.mplex.mplex import (
4155
MPLEX_PROTOCOL_ID,
4256
Mplex,
4357
)
58+
from libp2p.stream_muxer.yamux.yamux import (
59+
Yamux,
60+
)
4461
from libp2p.transport.tcp.tcp import (
4562
TCP,
4663
)
@@ -74,24 +91,25 @@ def new_swarm(
7491
:return: return a default swarm instance
7592
"""
7693
if key_pair is None:
77-
key_pair = generate_new_rsa_identity()
94+
key_pair = create_ed25519_key_pair() # Use Ed25519 instead of RSA
7895

7996
id_opt = generate_peer_id_from(key_pair)
80-
81-
# TODO: Parse `listen_addrs` to determine transport
8297
transport = TCP()
98+
# noise_key_pair = key_pair # Alternatively: create_ed25519_key_pair()
8399

84-
muxer_transports_by_protocol = muxer_opt or {MPLEX_PROTOCOL_ID: Mplex}
85-
security_transports_by_protocol = sec_opt or {
86-
TProtocol(PLAINTEXT_PROTOCOL_ID): InsecureTransport(key_pair),
87-
TProtocol(secio.ID): secio.Transport(key_pair),
100+
secure_transports_by_protocol: Mapping[TProtocol, ISecureTransport] = {
101+
PROTOCOL_ID: Transport(key_pair, noise_privkey=key_pair.private_key)
102+
}
103+
muxer_transports_by_protocol: Mapping[TProtocol, type[IMuxedConn]] = {
104+
cast(TProtocol, "/yamux/1.0.0"): Yamux
88105
}
106+
89107
upgrader = TransportUpgrader(
90-
security_transports_by_protocol, muxer_transports_by_protocol
108+
secure_transports_by_protocol=secure_transports_by_protocol,
109+
muxer_transports_by_protocol=muxer_transports_by_protocol,
91110
)
92111

93112
peerstore = peerstore_opt or PeerStore()
94-
# Store our key pair in peerstore
95113
peerstore.add_key_pair(id_opt, key_pair)
96114

97115
return Swarm(id_opt, peerstore, upgrader, transport)

libp2p/crypto/ed25519.py

Lines changed: 19 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
1-
from Crypto.Hash import (
2-
SHA256,
1+
from cryptography.exceptions import (
2+
InvalidSignature,
33
)
4-
from nacl.exceptions import (
5-
BadSignatureError,
4+
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
5+
Ed25519PrivateKey as CryptoEd25519PrivateKey,
66
)
7-
from nacl.public import PrivateKey as PrivateKeyImpl
8-
from nacl.public import PublicKey as PublicKeyImpl
9-
from nacl.signing import (
10-
SigningKey,
11-
VerifyKey,
7+
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
8+
Ed25519PublicKey as CryptoEd25519PublicKey,
129
)
13-
import nacl.utils as utils
1410

1511
from libp2p.crypto.keys import (
1612
KeyPair,
@@ -21,58 +17,52 @@
2117

2218

2319
class Ed25519PublicKey(PublicKey):
24-
def __init__(self, impl: PublicKeyImpl) -> None:
20+
def __init__(self, impl: CryptoEd25519PublicKey) -> None:
2521
self.impl = impl
2622

2723
def to_bytes(self) -> bytes:
28-
return bytes(self.impl)
24+
return self.impl.public_bytes_raw()
2925

3026
@classmethod
3127
def from_bytes(cls, key_bytes: bytes) -> "Ed25519PublicKey":
32-
return cls(PublicKeyImpl(key_bytes))
28+
return cls(CryptoEd25519PublicKey.from_public_bytes(key_bytes))
3329

3430
def get_type(self) -> KeyType:
3531
return KeyType.Ed25519
3632

3733
def verify(self, data: bytes, signature: bytes) -> bool:
38-
verify_key = VerifyKey(self.to_bytes())
3934
try:
40-
verify_key.verify(data, signature)
41-
except BadSignatureError:
35+
self.impl.verify(signature, data)
36+
return True
37+
except InvalidSignature:
4238
return False
43-
return True
4439

4540

4641
class Ed25519PrivateKey(PrivateKey):
47-
def __init__(self, impl: PrivateKeyImpl) -> None:
42+
def __init__(self, impl: CryptoEd25519PrivateKey) -> None:
4843
self.impl = impl
4944

5045
@classmethod
5146
def new(cls, seed: bytes = None) -> "Ed25519PrivateKey":
5247
if not seed:
53-
seed = utils.random()
54-
55-
private_key_impl = PrivateKeyImpl.from_seed(seed)
56-
return cls(private_key_impl)
48+
return cls(CryptoEd25519PrivateKey.generate())
49+
return cls(CryptoEd25519PrivateKey.from_private_bytes(seed[:32]))
5750

5851
def to_bytes(self) -> bytes:
59-
return bytes(self.impl)
52+
return self.impl.private_bytes_raw()
6053

6154
@classmethod
6255
def from_bytes(cls, data: bytes) -> "Ed25519PrivateKey":
63-
impl = PrivateKeyImpl(data)
64-
return cls(impl)
56+
return cls(CryptoEd25519PrivateKey.from_private_bytes(data))
6557

6658
def get_type(self) -> KeyType:
6759
return KeyType.Ed25519
6860

6961
def sign(self, data: bytes) -> bytes:
70-
h = SHA256.new(data)
71-
signing_key = SigningKey(self.to_bytes())
72-
return signing_key.sign(h.digest())
62+
return self.impl.sign(data)
7363

7464
def get_public_key(self) -> PublicKey:
75-
return Ed25519PublicKey(self.impl.public_key)
65+
return Ed25519PublicKey(self.impl.public_key())
7666

7767

7868
def create_new_key_pair(seed: bytes = None) -> KeyPair:

libp2p/security/noise/io.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
from typing import (
23
cast,
34
)
@@ -68,6 +69,20 @@ async def close(self) -> None:
6869

6970

7071
class NoiseHandshakeReadWriter(BaseNoiseMsgReadWriter):
72+
def __init__(self, read_writer: IRawConnection, noise_state: NoiseState) -> None:
73+
self.read_writer = NoisePacketReadWriter(cast(ReadWriteCloser, read_writer))
74+
self.noise_state = noise_state
75+
self.last_written_data = b""
76+
77+
async def write_msg(self, data: bytes, prefix_encoded: bool = False) -> None:
78+
data_encrypted = self.encrypt(data)
79+
if prefix_encoded:
80+
await self.read_writer.write_msg(self.prefix + data_encrypted)
81+
else:
82+
await self.read_writer.write_msg(data_encrypted)
83+
self.last_written_data = data_encrypted
84+
logging.debug(f"Wrote encrypted message: {data_encrypted.hex()}")
85+
7186
def encrypt(self, data: bytes) -> bytes:
7287
return self.noise_state.write_message(data)
7388

0 commit comments

Comments
 (0)