Merge pull request #271 from mhchia/fix/pubsub-interop

Pubsub interop with go-libp2p-daemon
This commit is contained in:
Kevin Mai-Husan Chia 2019-09-04 22:28:14 +08:00 committed by GitHub
commit e9ca372fb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 766 additions and 262 deletions

View File

@ -2,13 +2,13 @@ language: python
matrix: matrix:
include: include:
- python: 3.7 - python: 3.7-dev
dist: xenial dist: xenial
env: TOXENV=py37-test env: TOXENV=py37-test
- python: 3.7 - python: 3.7-dev
dist: xenial dist: xenial
env: TOXENV=lint env: TOXENV=lint
- python: 3.7 - python: 3.7-dev
dist: xenial dist: xenial
env: TOXENV=py37-interop env: TOXENV=py37-interop
sudo: true sudo: true
@ -18,7 +18,7 @@ matrix:
- export GOPATH=$HOME/go - export GOPATH=$HOME/go
- export GOROOT=/usr/local/go - export GOROOT=/usr/local/go
- export PATH=$GOROOT/bin:$GOPATH/bin:$PATH - export PATH=$GOROOT/bin:$GOPATH/bin:$PATH
- ./install_interop_go_pkgs.sh - ./tests/interop/go_pkgs/install_interop_go_pkgs.sh
install: install:
- pip install --upgrade pip - pip install --upgrade pip

View File

@ -1,5 +0,0 @@
#!/bin/bash
go version
cd tests/interop/go_pkgs/
go install ./...

View File

@ -1,11 +1,11 @@
import heapq import heapq
from operator import itemgetter from operator import itemgetter
import random import random
from typing import List
from multiaddr import Multiaddr from multiaddr import Multiaddr
from libp2p.peer.id import ID from libp2p.peer.id import ID
from libp2p.peer.peerdata import PeerData
from libp2p.peer.peerinfo import PeerInfo from libp2p.peer.peerinfo import PeerInfo
from .utils import digest from .utils import digest
@ -15,16 +15,16 @@ P_UDP = "udp"
class KadPeerInfo(PeerInfo): class KadPeerInfo(PeerInfo):
def __init__(self, peer_id, peer_data=None): def __init__(self, peer_id, addrs):
super(KadPeerInfo, self).__init__(peer_id, peer_data) super(KadPeerInfo, self).__init__(peer_id, addrs)
self.peer_id_bytes = peer_id.to_bytes() self.peer_id_bytes = peer_id.to_bytes()
self.xor_id = peer_id.xor_id self.xor_id = peer_id.xor_id
self.addrs = peer_data.get_addrs() if peer_data else None self.addrs = addrs
self.ip = self.addrs[0].value_for_protocol(P_IP) if peer_data else None self.ip = self.addrs[0].value_for_protocol(P_IP) if addrs else None
self.port = int(self.addrs[0].value_for_protocol(P_UDP)) if peer_data else None self.port = int(self.addrs[0].value_for_protocol(P_UDP)) if addrs else None
def same_home_as(self, node): def same_home_as(self, node):
return sorted(self.addrs) == sorted(node.addrs) return sorted(self.addrs) == sorted(node.addrs)
@ -142,14 +142,14 @@ def create_kad_peerinfo(node_id_bytes=None, sender_ip=None, sender_port=None):
node_id = ( node_id = (
ID(node_id_bytes) if node_id_bytes else ID(digest(random.getrandbits(255))) ID(node_id_bytes) if node_id_bytes else ID(digest(random.getrandbits(255)))
) )
peer_data = None addrs: List[Multiaddr]
if sender_ip and sender_port: if sender_ip and sender_port:
peer_data = PeerData() addrs = [
addr = [
Multiaddr( Multiaddr(
"/" + P_IP + "/" + str(sender_ip) + "/" + P_UDP + "/" + str(sender_port) "/" + P_IP + "/" + str(sender_ip) + "/" + P_UDP + "/" + str(sender_port)
) )
] ]
peer_data.add_addrs(addr) else:
addrs = []
return KadPeerInfo(node_id, peer_data) return KadPeerInfo(node_id, addrs)

View File

@ -7,6 +7,7 @@ from .net_stream_interface import INetStream
class NetStream(INetStream): class NetStream(INetStream):
muxed_stream: IMuxedStream muxed_stream: IMuxedStream
# TODO: Why we expose `mplex_conn` here?
mplex_conn: IMuxedConn mplex_conn: IMuxedConn
protocol_id: TProtocol protocol_id: TProtocol

View File

@ -4,6 +4,7 @@ from typing import Callable, Dict, List, Sequence
from multiaddr import Multiaddr from multiaddr import Multiaddr
from libp2p.peer.id import ID from libp2p.peer.id import ID
from libp2p.peer.peerstore import PeerStoreError
from libp2p.peer.peerstore_interface import IPeerStore from libp2p.peer.peerstore_interface import IPeerStore
from libp2p.protocol_muxer.multiselect import Multiselect from libp2p.protocol_muxer.multiselect import Multiselect
from libp2p.protocol_muxer.multiselect_client import MultiselectClient from libp2p.protocol_muxer.multiselect_client import MultiselectClient
@ -92,55 +93,55 @@ class Swarm(INetwork):
:return: muxed connection :return: muxed connection
""" """
# Get peer info from peer store if peer_id in self.connections:
addrs = self.peerstore.addrs(peer_id) # If muxed connection already exists for peer_id,
# set muxed connection equal to existing muxed connection
return self.connections[peer_id]
try:
# Get peer info from peer store
addrs = self.peerstore.addrs(peer_id)
except PeerStoreError:
raise SwarmException(f"No known addresses to peer {peer_id}")
if not addrs: if not addrs:
raise SwarmException("No known addresses to peer") raise SwarmException(f"No known addresses to peer {peer_id}")
if not self.router: if not self.router:
multiaddr = addrs[0] multiaddr = addrs[0]
else: else:
multiaddr = self.router.find_peer(peer_id) multiaddr = self.router.find_peer(peer_id)
# Dial peer (connection to peer does not yet exist)
# Transport dials peer (gets back a raw conn)
raw_conn = await self.transport.dial(multiaddr, self.self_id)
if peer_id in self.connections: # Per, https://discuss.libp2p.io/t/multistream-security/130, we first secure
# If muxed connection already exists for peer_id, # the conn and then mux the conn
# set muxed connection equal to existing muxed connection try:
muxed_conn = self.connections[peer_id] secured_conn = await self.upgrader.upgrade_security(raw_conn, peer_id, True)
else: except SecurityUpgradeFailure as error:
# Dial peer (connection to peer does not yet exist) # TODO: Add logging to indicate the failure
# Transport dials peer (gets back a raw conn) await raw_conn.close()
raw_conn = await self.transport.dial(multiaddr, self.self_id) raise SwarmException(
f"fail to upgrade the connection to a secured connection from {peer_id}"
) from error
try:
muxed_conn = await self.upgrader.upgrade_connection(
secured_conn, self.generic_protocol_handler, peer_id
)
except MuxerUpgradeFailure as error:
# TODO: Add logging to indicate the failure
await secured_conn.close()
raise SwarmException(
f"fail to upgrade the connection to a muxed connection from {peer_id}"
) from error
# Per, https://discuss.libp2p.io/t/multistream-security/130, we first secure # Store muxed connection in connections
# the conn and then mux the conn self.connections[peer_id] = muxed_conn
try:
secured_conn = await self.upgrader.upgrade_security(
raw_conn, peer_id, True
)
except SecurityUpgradeFailure as error:
# TODO: Add logging to indicate the failure
await raw_conn.close()
raise SwarmException(
f"fail to upgrade the connection to a secured connection from {peer_id}"
) from error
try:
muxed_conn = await self.upgrader.upgrade_connection(
secured_conn, self.generic_protocol_handler, peer_id
)
except MuxerUpgradeFailure as error:
# TODO: Add logging to indicate the failure
await secured_conn.close()
raise SwarmException(
f"fail to upgrade the connection to a muxed connection from {peer_id}"
) from error
# Store muxed connection in connections # Call notifiers since event occurred
self.connections[peer_id] = muxed_conn for notifee in self.notifees:
await notifee.connected(self, muxed_conn)
# Call notifiers since event occurred
for notifee in self.notifees:
await notifee.connected(self, muxed_conn)
return muxed_conn return muxed_conn
@ -152,11 +153,6 @@ class Swarm(INetwork):
:param protocol_id: protocol id :param protocol_id: protocol id
:return: net stream instance :return: net stream instance
""" """
# Get peer info from peer store
addrs = self.peerstore.addrs(peer_id)
if not addrs:
raise SwarmException("No known addresses to peer")
muxed_conn = await self.dial_peer(peer_id) muxed_conn = await self.dial_peer(peer_id)

View File

@ -1,9 +1,8 @@
from typing import List from typing import List, Sequence
import multiaddr import multiaddr
from .id import ID from .id import ID
from .peerdata import PeerData
class PeerInfo: class PeerInfo:
@ -11,9 +10,9 @@ class PeerInfo:
peer_id: ID peer_id: ID
addrs: List[multiaddr.Multiaddr] addrs: List[multiaddr.Multiaddr]
def __init__(self, peer_id: ID, peer_data: PeerData = None) -> None: def __init__(self, peer_id: ID, addrs: Sequence[multiaddr.Multiaddr]) -> None:
self.peer_id = peer_id self.peer_id = peer_id
self.addrs = peer_data.get_addrs() if peer_data else None self.addrs = list(addrs)
def info_from_p2p_addr(addr: multiaddr.Multiaddr) -> PeerInfo: def info_from_p2p_addr(addr: multiaddr.Multiaddr) -> PeerInfo:
@ -44,11 +43,7 @@ def info_from_p2p_addr(addr: multiaddr.Multiaddr) -> PeerInfo:
if len(parts) > 1: if len(parts) > 1:
addr = multiaddr.Multiaddr.join(*parts[:-1]) addr = multiaddr.Multiaddr.join(*parts[:-1])
peer_data = PeerData() return PeerInfo(peer_id, [addr])
peer_data.add_addrs([addr])
peer_data.set_protocols([p.code for p in addr.protocols()])
return PeerInfo(peer_id, peer_data)
class InvalidAddrError(ValueError): class InvalidAddrError(ValueError):

View File

@ -33,7 +33,7 @@ class PeerStore(IPeerStore):
def peer_info(self, peer_id: ID) -> Optional[PeerInfo]: def peer_info(self, peer_id: ID) -> Optional[PeerInfo]:
if peer_id in self.peer_map: if peer_id in self.peer_map:
peer_data = self.peer_map[peer_id] peer_data = self.peer_map[peer_id]
return PeerInfo(peer_id, peer_data) return PeerInfo(peer_id, peer_data.addrs)
return None return None
def get_protocols(self, peer_id: ID) -> List[str]: def get_protocols(self, peer_id: ID) -> List[str]:

View File

@ -2,11 +2,14 @@ from typing import Iterable, List, Sequence
from libp2p.peer.id import ID from libp2p.peer.id import ID
from libp2p.typing import TProtocol from libp2p.typing import TProtocol
from libp2p.utils import encode_varint_prefixed
from .pb import rpc_pb2 from .pb import rpc_pb2
from .pubsub import Pubsub from .pubsub import Pubsub
from .pubsub_router_interface import IPubsubRouter from .pubsub_router_interface import IPubsubRouter
PROTOCOL_ID = TProtocol("/floodsub/1.0.0")
class FloodSub(IPubsubRouter): class FloodSub(IPubsubRouter):
@ -76,7 +79,7 @@ class FloodSub(IPubsubRouter):
stream = self.pubsub.peers[peer_id] stream = self.pubsub.peers[peer_id]
# FIXME: We should add a `WriteMsg` similar to write delimited messages. # FIXME: We should add a `WriteMsg` similar to write delimited messages.
# Ref: https://github.com/libp2p/go-libp2p-pubsub/blob/master/comm.go#L107 # Ref: https://github.com/libp2p/go-libp2p-pubsub/blob/master/comm.go#L107
await stream.write(rpc_msg.SerializeToString()) await stream.write(encode_varint_prefixed(rpc_msg.SerializeToString()))
async def join(self, topic: str) -> None: async def join(self, topic: str) -> None:
""" """

View File

@ -4,13 +4,17 @@ import random
from typing import Any, Dict, Iterable, List, Sequence, Set from typing import Any, Dict, Iterable, List, Sequence, Set
from libp2p.peer.id import ID from libp2p.peer.id import ID
from libp2p.pubsub import floodsub
from libp2p.typing import TProtocol from libp2p.typing import TProtocol
from libp2p.utils import encode_varint_prefixed
from .mcache import MessageCache from .mcache import MessageCache
from .pb import rpc_pb2 from .pb import rpc_pb2
from .pubsub import Pubsub from .pubsub import Pubsub
from .pubsub_router_interface import IPubsubRouter from .pubsub_router_interface import IPubsubRouter
PROTOCOL_ID = TProtocol("/meshsub/1.0.0")
class GossipSub(IPubsubRouter): class GossipSub(IPubsubRouter):
@ -104,16 +108,19 @@ class GossipSub(IPubsubRouter):
:param peer_id: id of peer to add :param peer_id: id of peer to add
:param protocol_id: router protocol the peer speaks, e.g., floodsub, gossipsub :param protocol_id: router protocol the peer speaks, e.g., floodsub, gossipsub
""" """
# Add peer to the correct peer list
peer_type = GossipSub.get_peer_type(protocol_id)
self.peers_to_protocol[peer_id] = protocol_id self.peers_to_protocol[peer_id] = protocol_id
if peer_type == "gossip": if protocol_id == PROTOCOL_ID:
self.peers_gossipsub.append(peer_id) self.peers_gossipsub.append(peer_id)
elif peer_type == "flood": elif protocol_id == floodsub.PROTOCOL_ID:
self.peers_floodsub.append(peer_id) self.peers_floodsub.append(peer_id)
else:
# We should never enter here. Becuase the `protocol_id` is registered by your pubsub
# instance in multistream-select, but it is not the protocol that gossipsub supports.
# In this case, probably we registered gossipsub to a wrong `protocol_id`
# in multistream-select, or wrong versions.
# TODO: Better handling
raise Exception(f"protocol is not supported: protocol_id={protocol_id}")
def remove_peer(self, peer_id: ID) -> None: def remove_peer(self, peer_id: ID) -> None:
""" """
@ -167,7 +174,7 @@ class GossipSub(IPubsubRouter):
# FIXME: We should add a `WriteMsg` similar to write delimited messages. # FIXME: We should add a `WriteMsg` similar to write delimited messages.
# Ref: https://github.com/libp2p/go-libp2p-pubsub/blob/master/comm.go#L107 # Ref: https://github.com/libp2p/go-libp2p-pubsub/blob/master/comm.go#L107
# TODO: Go use `sendRPC`, which possibly piggybacks gossip/control messages. # TODO: Go use `sendRPC`, which possibly piggybacks gossip/control messages.
await stream.write(rpc_msg.SerializeToString()) await stream.write(encode_varint_prefixed(rpc_msg.SerializeToString()))
def _get_peers_to_send( def _get_peers_to_send(
self, topic_ids: Iterable[str], msg_forwarder: ID, origin: ID self, topic_ids: Iterable[str], msg_forwarder: ID, origin: ID
@ -264,29 +271,6 @@ class GossipSub(IPubsubRouter):
# Forget mesh[topic] # Forget mesh[topic]
self.mesh.pop(topic, None) self.mesh.pop(topic, None)
# Interface Helper Functions
@staticmethod
def get_peer_type(protocol_id: str) -> str:
# TODO: Do this in a better, more efficient way
if "gossipsub" in protocol_id:
return "gossip"
if "floodsub" in protocol_id:
return "flood"
return "unknown"
async def deliver_messages_to_peers(
self, peers: List[ID], msg_sender: ID, origin_id: ID, serialized_packet: bytes
) -> None:
for peer_id_in_topic in peers:
# Forward to all peers that are not the
# message sender and are not the message origin
if peer_id_in_topic not in (msg_sender, origin_id):
stream = self.pubsub.peers[peer_id_in_topic]
# Publish the packet
await stream.write(serialized_packet)
# Heartbeat # Heartbeat
async def heartbeat(self) -> None: async def heartbeat(self) -> None:
""" """
@ -509,7 +493,7 @@ class GossipSub(IPubsubRouter):
peer_stream = self.pubsub.peers[sender_peer_id] peer_stream = self.pubsub.peers[sender_peer_id]
# 4) And write the packet to the stream # 4) And write the packet to the stream
await peer_stream.write(rpc_msg) await peer_stream.write(encode_varint_prefixed(rpc_msg))
async def handle_graft( async def handle_graft(
self, graft_msg: rpc_pb2.ControlGraft, sender_peer_id: ID self, graft_msg: rpc_pb2.ControlGraft, sender_peer_id: ID
@ -601,4 +585,4 @@ class GossipSub(IPubsubRouter):
peer_stream = self.pubsub.peers[to_peer] peer_stream = self.pubsub.peers[to_peer]
# Write rpc to stream # Write rpc to stream
await peer_stream.write(rpc_msg) await peer_stream.write(encode_varint_prefixed(rpc_msg))

View File

@ -21,6 +21,7 @@ from libp2p.host.host_interface import IHost
from libp2p.network.stream.net_stream_interface import INetStream from libp2p.network.stream.net_stream_interface import INetStream
from libp2p.peer.id import ID from libp2p.peer.id import ID
from libp2p.typing import TProtocol from libp2p.typing import TProtocol
from libp2p.utils import encode_varint_prefixed, read_varint_prefixed_bytes
from .pb import rpc_pb2 from .pb import rpc_pb2
from .pubsub_notifee import PubsubNotifee from .pubsub_notifee import PubsubNotifee
@ -71,7 +72,7 @@ class Pubsub:
topic_validators: Dict[str, TopicValidator] topic_validators: Dict[str, TopicValidator]
# NOTE: Be sure it is increased atomically everytime. # TODO: Be sure it is increased atomically everytime.
counter: int # uint64 counter: int # uint64
def __init__( def __init__(
@ -131,7 +132,7 @@ class Pubsub:
# Call handle peer to keep waiting for updates to peer queue # Call handle peer to keep waiting for updates to peer queue
asyncio.ensure_future(self.handle_peer_queue()) asyncio.ensure_future(self.handle_peer_queue())
def get_hello_packet(self) -> bytes: def get_hello_packet(self) -> rpc_pb2.RPC:
""" """
Generate subscription message with all topics we are subscribed to Generate subscription message with all topics we are subscribed to
only send hello packet if we have subscribed topics only send hello packet if we have subscribed topics
@ -141,7 +142,7 @@ class Pubsub:
packet.subscriptions.extend( packet.subscriptions.extend(
[rpc_pb2.RPC.SubOpts(subscribe=True, topicid=topic_id)] [rpc_pb2.RPC.SubOpts(subscribe=True, topicid=topic_id)]
) )
return packet.SerializeToString() return packet
async def continuously_read_stream(self, stream: INetStream) -> None: async def continuously_read_stream(self, stream: INetStream) -> None:
""" """
@ -152,17 +153,14 @@ class Pubsub:
peer_id = stream.mplex_conn.peer_id peer_id = stream.mplex_conn.peer_id
while True: while True:
incoming: bytes = (await stream.read()) incoming: bytes = await read_varint_prefixed_bytes(stream)
rpc_incoming: rpc_pb2.RPC = rpc_pb2.RPC() rpc_incoming: rpc_pb2.RPC = rpc_pb2.RPC()
rpc_incoming.ParseFromString(incoming) rpc_incoming.ParseFromString(incoming)
if rpc_incoming.publish: if rpc_incoming.publish:
# deal with RPC.publish # deal with RPC.publish
for msg in rpc_incoming.publish: for msg in rpc_incoming.publish:
if not self._is_subscribed_to_msg(msg): if not self._is_subscribed_to_msg(msg):
continue continue
# TODO(mhchia): This will block this read_stream loop until all data are pushed.
# Should investigate further if this is an issue.
asyncio.ensure_future(self.push_msg(msg_forwarder=peer_id, msg=msg)) asyncio.ensure_future(self.push_msg(msg_forwarder=peer_id, msg=msg))
if rpc_incoming.subscriptions: if rpc_incoming.subscriptions:
@ -220,20 +218,19 @@ class Pubsub:
on one of the supported pubsub protocols. on one of the supported pubsub protocols.
:param stream: newly created stream :param stream: newly created stream
""" """
# Add peer await self.continuously_read_stream(stream)
# Map peer to stream
peer_id: ID = stream.mplex_conn.peer_id async def _handle_new_peer(self, peer_id: ID) -> None:
stream: INetStream = await self.host.new_stream(peer_id, self.protocols)
self.peers[peer_id] = stream self.peers[peer_id] = stream
self.router.add_peer(peer_id, stream.get_protocol())
# Send hello packet # Send hello packet
hello: bytes = self.get_hello_packet() hello = self.get_hello_packet()
await stream.write(encode_varint_prefixed(hello.SerializeToString()))
await stream.write(hello) # TODO: Check EOF of this stream.
# Pass stream off to stream reader # TODO: Check if the peer in black list.
asyncio.ensure_future(self.continuously_read_stream(stream)) self.router.add_peer(peer_id, stream.get_protocol())
# Force context switch
await asyncio.sleep(0)
async def handle_peer_queue(self) -> None: async def handle_peer_queue(self) -> None:
""" """
@ -246,25 +243,9 @@ class Pubsub:
peer_id: ID = await self.peer_queue.get() peer_id: ID = await self.peer_queue.get()
# Open a stream to peer on existing connection
# (we know connection exists since that's the only way
# an element gets added to peer_queue)
stream: INetStream = await self.host.new_stream(peer_id, self.protocols)
# Add Peer # Add Peer
# Map peer to stream
self.peers[peer_id] = stream
self.router.add_peer(peer_id, stream.get_protocol())
# Send hello packet
hello: bytes = self.get_hello_packet()
await stream.write(hello)
# TODO: Investigate whether this should be replaced by `handlePeerEOF`
# Ref: https://github.com/libp2p/go-libp2p-pubsub/blob/49274b0e8aecdf6cad59d768e5702ff00aa48488/comm.go#L80 # noqa: E501
# Pass stream off to stream reader
asyncio.ensure_future(self.continuously_read_stream(stream))
asyncio.ensure_future(self._handle_new_peer(peer_id))
# Force context switch # Force context switch
await asyncio.sleep(0) await asyncio.sleep(0)
@ -365,7 +346,7 @@ class Pubsub:
# Broadcast message # Broadcast message
for stream in self.peers.values(): for stream in self.peers.values():
# Write message to stream # Write message to stream
await stream.write(raw_msg) await stream.write(encode_varint_prefixed(raw_msg))
async def publish(self, topic_id: str, data: bytes) -> None: async def publish(self, topic_id: str, data: bytes) -> None:
""" """

View File

@ -36,11 +36,7 @@ class PubsubNotifee(INotifee):
:param network: network the connection was opened on :param network: network the connection was opened on
:param conn: connection that was opened :param conn: connection that was opened
""" """
await self.initiator_peers_queue.put(conn.peer_id)
# Only add peer_id if we are initiator (otherwise we would end up
# with two pubsub streams between us and the peer)
if conn.initiator:
await self.initiator_peers_queue.put(conn.peer_id)
async def disconnected(self, network: INetwork, conn: IMuxedConn) -> None: async def disconnected(self, network: INetwork, conn: IMuxedConn) -> None:
pass pass

View File

@ -9,5 +9,4 @@ if TYPE_CHECKING:
TProtocol = NewType("TProtocol", str) TProtocol = NewType("TProtocol", str)
StreamHandlerFn = Callable[["INetStream"], Awaitable[None]] StreamHandlerFn = Callable[["INetStream"], Awaitable[None]]
StreamReader = Union["IMuxedStream", "INetStream", IRawConnection]
StreamReader = Union["IMuxedStream", IRawConnection]

View File

@ -9,6 +9,8 @@ extras_require = {
"pytest>=4.6.3,<5.0.0", "pytest>=4.6.3,<5.0.0",
"pytest-asyncio>=0.10.0,<1.0.0", "pytest-asyncio>=0.10.0,<1.0.0",
"pexpect>=4.6,<5", "pexpect>=4.6,<5",
# FIXME: Master branch. Use PyPI instead after it is released.
"p2pclient @ git+https://git@github.com/mhchia/py-libp2p-daemon-bindings@628266f",
], ],
"lint": [ "lint": [
"mypy>=0.701,<1.0", "mypy>=0.701,<1.0",

View File

@ -1,8 +1,15 @@
import asyncio
import sys import sys
from typing import Union
import pexpect import pexpect
import pytest import pytest
from tests.factories import FloodsubFactory, GossipsubFactory, PubsubFactory
from tests.pubsub.configs import GOSSIPSUB_PARAMS
from .daemon import Daemon, make_p2pd
@pytest.fixture @pytest.fixture
def proc_factory(): def proc_factory():
@ -22,3 +29,52 @@ def proc_factory():
finally: finally:
for proc in procs: for proc in procs:
proc.close() proc.close()
@pytest.fixture
def num_p2pds():
return 1
@pytest.fixture
def is_gossipsub():
return True
@pytest.fixture
async def p2pds(num_p2pds, is_host_secure, is_gossipsub, unused_tcp_port_factory):
p2pds: Union[Daemon, Exception] = await asyncio.gather(
*[
make_p2pd(
unused_tcp_port_factory(),
unused_tcp_port_factory(),
is_host_secure,
is_gossipsub=is_gossipsub,
)
for _ in range(num_p2pds)
],
return_exceptions=True,
)
p2pds_succeeded = tuple(p2pd for p2pd in p2pds if isinstance(p2pd, Daemon))
if len(p2pds_succeeded) != len(p2pds):
# Not all succeeded. Close the succeeded ones and print the failed ones(exceptions).
await asyncio.gather(*[p2pd.close() for p2pd in p2pds_succeeded])
exceptions = tuple(p2pd for p2pd in p2pds if isinstance(p2pd, Exception))
raise Exception(f"not all p2pds succeed: first exception={exceptions[0]}")
try:
yield p2pds
finally:
await asyncio.gather(*[p2pd.close() for p2pd in p2pds])
@pytest.fixture
def pubsubs(num_hosts, hosts, is_gossipsub):
if is_gossipsub:
routers = GossipsubFactory.create_batch(num_hosts, **GOSSIPSUB_PARAMS._asdict())
else:
routers = FloodsubFactory.create_batch(num_hosts)
_pubsubs = tuple(
PubsubFactory(host=host, router=router) for host, router in zip(hosts, routers)
)
yield _pubsubs
# TODO: Clean up

View File

@ -1 +1,2 @@
LOCALHOST_IP = "127.0.0.1"
PEXPECT_NEW_LINE = "\r\n" PEXPECT_NEW_LINE = "\r\n"

200
tests/interop/daemon.py Normal file
View File

@ -0,0 +1,200 @@
import asyncio
import time
from typing import Any, List
import multiaddr
from multiaddr import Multiaddr
from p2pclient import Client
from libp2p.peer.id import ID
from libp2p.peer.peerinfo import PeerInfo, info_from_p2p_addr
from .constants import LOCALHOST_IP
from .envs import GO_BIN_PATH
P2PD_PATH = GO_BIN_PATH / "p2pd"
TIMEOUT_DURATION = 30
async def try_until_success(coro_func, timeout=TIMEOUT_DURATION):
"""
Keep running ``coro_func`` until either it succeed or time is up.
All arguments of ``coro_func`` should be filled, i.e. it should be called without arguments.
"""
t_start = time.monotonic()
while True:
result = await coro_func()
if result:
break
if (time.monotonic() - t_start) >= timeout:
# timeout
assert False, f"{coro_func} is still failing after `{timeout}` seconds"
await asyncio.sleep(0.01)
class P2PDProcess:
proc: asyncio.subprocess.Process
cmd: str = str(P2PD_PATH)
args: List[Any]
is_proc_running: bool
_tasks: List["asyncio.Future[Any]"]
def __init__(
self,
control_maddr: Multiaddr,
is_secure: bool,
is_pubsub_enabled: bool = True,
is_gossipsub: bool = True,
is_pubsub_signing: bool = False,
is_pubsub_signing_strict: bool = False,
) -> None:
args = [f"-listen={str(control_maddr)}"]
# NOTE: To support `-insecure`, we need to hack `go-libp2p-daemon`.
if not is_secure:
args.append("-insecure=true")
if is_pubsub_enabled:
args.append("-pubsub")
if is_gossipsub:
args.append("-pubsubRouter=gossipsub")
else:
args.append("-pubsubRouter=floodsub")
if not is_pubsub_signing:
args.append("-pubsubSign=false")
if not is_pubsub_signing_strict:
args.append("-pubsubSignStrict=false")
# NOTE:
# Two other params are possibly what we want to configure:
# - gossipsubHeartbeatInterval: GossipSubHeartbeatInitialDelay = 100 * time.Millisecond # noqa: E501
# - gossipsubHeartbeatInitialDelay: GossipSubHeartbeatInterval = 1 * time.Second
# Referece: https://github.com/libp2p/go-libp2p-daemon/blob/b95e77dbfcd186ccf817f51e95f73f9fd5982600/p2pd/main.go#L348-L353 # noqa: E501
self.args = args
self.is_proc_running = False
self._tasks = []
async def wait_until_ready(self):
lines_head_pattern = (b"Control socket:", b"Peer ID:", b"Peer Addrs:")
lines_head_occurred = {line: False for line in lines_head_pattern}
async def read_from_daemon_and_check():
line = await self.proc.stdout.readline()
for head_pattern in lines_head_occurred:
if line.startswith(head_pattern):
lines_head_occurred[head_pattern] = True
return all([value for value in lines_head_occurred.values()])
await try_until_success(read_from_daemon_and_check)
# Sleep a little bit to ensure the listener is up after logs are emitted.
await asyncio.sleep(0.01)
async def start_printing_logs(self) -> None:
async def _print_from_stream(
src_name: str, reader: asyncio.StreamReader
) -> None:
while True:
line = await reader.readline()
if line != b"":
print(f"{src_name}\t: {line.rstrip().decode()}")
await asyncio.sleep(0.01)
self._tasks.append(
asyncio.ensure_future(_print_from_stream("out", self.proc.stdout))
)
self._tasks.append(
asyncio.ensure_future(_print_from_stream("err", self.proc.stderr))
)
await asyncio.sleep(0)
async def start(self) -> None:
if self.is_proc_running:
return
self.proc = await asyncio.subprocess.create_subprocess_exec(
self.cmd,
*self.args,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
bufsize=0,
)
self.is_proc_running = True
await self.wait_until_ready()
await self.start_printing_logs()
async def close(self) -> None:
if self.is_proc_running:
self.proc.terminate()
await self.proc.wait()
self.is_proc_running = False
for task in self._tasks:
task.cancel()
class Daemon:
p2pd_proc: P2PDProcess
control: Client
peer_info: PeerInfo
def __init__(
self, p2pd_proc: P2PDProcess, control: Client, peer_info: PeerInfo
) -> None:
self.p2pd_proc = p2pd_proc
self.control = control
self.peer_info = peer_info
def __repr__(self) -> str:
return f"<Daemon {self.peer_id.to_string()[2:8]}>"
@property
def peer_id(self) -> ID:
return self.peer_info.peer_id
@property
def listen_maddr(self) -> Multiaddr:
return self.peer_info.addrs[0]
async def close(self) -> None:
await self.p2pd_proc.close()
await self.control.close()
async def make_p2pd(
daemon_control_port: int,
client_callback_port: int,
is_secure: bool,
is_pubsub_enabled=True,
is_gossipsub=True,
is_pubsub_signing=False,
is_pubsub_signing_strict=False,
) -> Daemon:
control_maddr = Multiaddr(f"/ip4/{LOCALHOST_IP}/tcp/{daemon_control_port}")
p2pd_proc = P2PDProcess(
control_maddr,
is_secure,
is_pubsub_enabled,
is_gossipsub,
is_pubsub_signing,
is_pubsub_signing_strict,
)
await p2pd_proc.start()
client_callback_maddr = Multiaddr(f"/ip4/{LOCALHOST_IP}/tcp/{client_callback_port}")
p2pc = Client(control_maddr, client_callback_maddr)
await p2pc.listen()
peer_id, maddrs = await p2pc.identify()
listen_maddr: Multiaddr = None
for maddr in maddrs:
try:
ip = maddr.value_for_protocol(multiaddr.protocols.P_IP4)
# NOTE: Check if this `maddr` uses `tcp`.
maddr.value_for_protocol(multiaddr.protocols.P_TCP)
except multiaddr.exceptions.ProtocolLookupError:
continue
if ip == LOCALHOST_IP:
listen_maddr = maddr
break
assert listen_maddr is not None, "no loopback maddr is found"
peer_info = info_from_p2p_addr(
listen_maddr.encapsulate(Multiaddr(f"/p2p/{peer_id.to_string()}"))
)
return Daemon(p2pd_proc, p2pc, peer_info)

4
tests/interop/envs.py Normal file
View File

@ -0,0 +1,4 @@
import os
import pathlib
GO_BIN_PATH = pathlib.Path(os.environ["GOPATH"]) / "bin"

View File

@ -3,17 +3,13 @@ package main
import ( import (
"bufio" "bufio"
"context" "context"
"crypto/rand"
"flag" "flag"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"log" "log"
mrand "math/rand"
"github.com/libp2p/go-libp2p" utils "interop/utils"
"github.com/libp2p/go-libp2p-core/crypto"
"github.com/libp2p/go-libp2p-core/host"
"github.com/libp2p/go-libp2p-core/network" "github.com/libp2p/go-libp2p-core/network"
"github.com/libp2p/go-libp2p-core/peer" "github.com/libp2p/go-libp2p-core/peer"
"github.com/libp2p/go-libp2p-core/peerstore" "github.com/libp2p/go-libp2p-core/peerstore"
@ -23,59 +19,6 @@ import (
gologging "github.com/whyrusleeping/go-logging" gologging "github.com/whyrusleeping/go-logging"
) )
// makeBasicHost creates a LibP2P host with a random peer ID listening on the
// given multiaddress. It won't encrypt the connection if insecure is true.
func makeBasicHost(listenPort int, insecure bool, randseed int64) (host.Host, error) {
// If the seed is zero, use real cryptographic randomness. Otherwise, use a
// deterministic randomness source to make generated keys stay the same
// across multiple runs
var r io.Reader
if randseed == 0 {
r = rand.Reader
} else {
r = mrand.New(mrand.NewSource(randseed))
}
// Generate a key pair for this host. We will use it at least
// to obtain a valid host ID.
priv, _, err := crypto.GenerateKeyPairWithReader(crypto.RSA, 2048, r)
if err != nil {
return nil, err
}
opts := []libp2p.Option{
libp2p.ListenAddrStrings(fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", listenPort)),
libp2p.Identity(priv),
libp2p.DisableRelay(),
}
if insecure {
opts = append(opts, libp2p.NoSecurity)
}
basicHost, err := libp2p.New(context.Background(), opts...)
if err != nil {
return nil, err
}
// Build host multiaddress
hostAddr, _ := ma.NewMultiaddr(fmt.Sprintf("/ipfs/%s", basicHost.ID().Pretty()))
// Now we can build a full multiaddress to reach this host
// by encapsulating both addresses:
addr := basicHost.Addrs()[0]
fullAddr := addr.Encapsulate(hostAddr)
log.Printf("I am %s\n", fullAddr)
if insecure {
log.Printf("Now run \"./echo -l %d -d %s -insecure\" on a different terminal\n", listenPort+1, fullAddr)
} else {
log.Printf("Now run \"./echo -l %d -d %s\" on a different terminal\n", listenPort+1, fullAddr)
}
return basicHost, nil
}
func main() { func main() {
// LibP2P code uses golog to log messages. They log with different // LibP2P code uses golog to log messages. They log with different
// string IDs (i.e. "swarm"). We can control the verbosity level for // string IDs (i.e. "swarm"). We can control the verbosity level for
@ -94,7 +37,7 @@ func main() {
} }
// Make a host that listens on the given multiaddress // Make a host that listens on the given multiaddress
ha, err := makeBasicHost(*listenF, *insecure, *seed) ha, err := utils.MakeBasicHost(*listenF, *insecure, *seed)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@ -0,0 +1,69 @@
package utils
import (
"context"
"crypto/rand"
"fmt"
"io"
"log"
mrand "math/rand"
"github.com/libp2p/go-libp2p"
"github.com/libp2p/go-libp2p-core/crypto"
"github.com/libp2p/go-libp2p-core/host"
ma "github.com/multiformats/go-multiaddr"
)
// MakeBasicHost creates a LibP2P host with a random peer ID listening on the
// given multiaddress. It won't encrypt the connection if insecure is true.
func MakeBasicHost(listenPort int, insecure bool, randseed int64) (host.Host, error) {
// If the seed is zero, use real cryptographic randomness. Otherwise, use a
// deterministic randomness source to make generated keys stay the same
// across multiple runs
var r io.Reader
if randseed == 0 {
r = rand.Reader
} else {
r = mrand.New(mrand.NewSource(randseed))
}
// Generate a key pair for this host. We will use it at least
// to obtain a valid host ID.
priv, _, err := crypto.GenerateKeyPairWithReader(crypto.RSA, 2048, r)
if err != nil {
return nil, err
}
opts := []libp2p.Option{
libp2p.ListenAddrStrings(fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", listenPort)),
libp2p.Identity(priv),
libp2p.DisableRelay(),
}
if insecure {
opts = append(opts, libp2p.NoSecurity)
}
basicHost, err := libp2p.New(context.Background(), opts...)
if err != nil {
return nil, err
}
// Build host multiaddress
hostAddr, _ := ma.NewMultiaddr(fmt.Sprintf("/ipfs/%s", basicHost.ID().Pretty()))
// Now we can build a full multiaddress to reach this host
// by encapsulating both addresses:
addr := basicHost.Addrs()[0]
fullAddr := addr.Encapsulate(hostAddr)
log.Printf("I am %s\n", fullAddr)
if insecure {
log.Printf("Now run \"./echo -l %d -d %s -insecure\" on a different terminal\n", listenPort+1, fullAddr)
} else {
log.Printf("Now run \"./echo -l %d -d %s\" on a different terminal\n", listenPort+1, fullAddr)
}
return basicHost, nil
}

View File

@ -0,0 +1,31 @@
#!/bin/bash
SCRIPT_RELATIVE_PATH=`dirname $0`
GO_PKGS_PATH=$SCRIPT_RELATIVE_PATH
DAEMON_REPO=go-libp2p-daemon
DAEMON_PATH=$GO_PKGS_PATH/$DAEMON_REPO
EXAMPLES_PATHS=$GO_PKGS_PATH/examples
go version
# Install `p2pd`
# FIXME: Use the canonical repo in libp2p, when we don't need `insecure`.
if [ ! -e "$DAEMON_PATH" ]; then
git clone https://github.com/mhchia/$DAEMON_REPO.git --branch test/add-options $DAEMON_PATH
if [ "$?" != 0 ]; then
echo "Failed to clone the daemon repo"
exit 1
fi
fi
cd $DAEMON_PATH && go install ./...
cd -
# Install example modeuls
cd $EXAMPLES_PATHS && go install ./...
echo "Finish installing go modules for interop."

View File

@ -0,0 +1,24 @@
import pytest
from .utils import connect
@pytest.mark.parametrize("num_hosts", (1,))
@pytest.mark.asyncio
async def test_connect(hosts, p2pds):
p2pd = p2pds[0]
host = hosts[0]
assert len(await p2pd.control.list_peers()) == 0
# Test: connect from Py
await connect(host, p2pd)
assert len(await p2pd.control.list_peers()) == 1
# Test: `disconnect` from Py
await host.disconnect(p2pd.peer_id)
assert len(await p2pd.control.list_peers()) == 0
# Test: connect from Go
await connect(p2pd, host)
assert len(host.get_network().connections) == 1
# Test: `disconnect` from Go
await p2pd.control.disconnect(host.get_id())
# FIXME: Failed to handle disconnect
# assert len(host.get_network().connections) == 0

View File

@ -1,6 +1,4 @@
import asyncio import asyncio
import os
import pathlib
from multiaddr import Multiaddr from multiaddr import Multiaddr
import pytest import pytest
@ -9,9 +7,9 @@ from libp2p.peer.peerinfo import info_from_p2p_addr
from libp2p.typing import TProtocol from libp2p.typing import TProtocol
from .constants import PEXPECT_NEW_LINE from .constants import PEXPECT_NEW_LINE
from .envs import GO_BIN_PATH
GOPATH = pathlib.Path(os.environ["GOPATH"]) ECHO_PATH = GO_BIN_PATH / "echo"
ECHO_PATH = GOPATH / "bin" / "echo"
ECHO_PROTOCOL_ID = TProtocol("/echo/1.0.0") ECHO_PROTOCOL_ID = TProtocol("/echo/1.0.0")

View File

@ -0,0 +1,164 @@
import asyncio
import functools
from p2pclient.pb import p2pd_pb2
import pytest
from libp2p.peer.id import ID
from libp2p.pubsub.pb import rpc_pb2
from libp2p.utils import read_varint_prefixed_bytes
from .utils import connect
TOPIC_0 = "ABALA"
TOPIC_1 = "YOOOO"
async def p2pd_subscribe(p2pd, topic) -> "asyncio.Queue[rpc_pb2.Message]":
reader, writer = await p2pd.control.pubsub_subscribe(topic)
queue = asyncio.Queue()
async def _read_pubsub_msg() -> None:
writer_closed_task = asyncio.ensure_future(writer.wait_closed())
while True:
done, pending = await asyncio.wait(
[read_varint_prefixed_bytes(reader), writer_closed_task],
return_when=asyncio.FIRST_COMPLETED,
)
done_tasks = tuple(done)
if writer.is_closing():
return
read_task = done_tasks[0]
# Sanity check
assert read_task._coro.__name__ == "read_varint_prefixed_bytes"
msg_bytes = read_task.result()
ps_msg = p2pd_pb2.PSMessage()
ps_msg.ParseFromString(msg_bytes)
# Fill in the message used in py-libp2p
msg = rpc_pb2.Message(
from_id=ps_msg.from_field,
data=ps_msg.data,
seqno=ps_msg.seqno,
topicIDs=ps_msg.topicIDs,
signature=ps_msg.signature,
key=ps_msg.key,
)
queue.put_nowait(msg)
asyncio.ensure_future(_read_pubsub_msg())
await asyncio.sleep(0)
return queue
def validate_pubsub_msg(msg: rpc_pb2.Message, data: bytes, from_peer_id: ID) -> None:
assert msg.data == data and msg.from_id == from_peer_id
@pytest.mark.parametrize("is_gossipsub", (True, False))
@pytest.mark.parametrize("num_hosts, num_p2pds", ((1, 2),))
@pytest.mark.asyncio
async def test_pubsub(pubsubs, p2pds):
#
# Test: Recognize pubsub peers on connection.
#
py_pubsub = pubsubs[0]
# go0 <-> py <-> go1
await connect(p2pds[0], py_pubsub.host)
await connect(py_pubsub.host, p2pds[1])
py_peer_id = py_pubsub.host.get_id()
# Check pubsub peers
pubsub_peers_0 = await p2pds[0].control.pubsub_list_peers("")
assert len(pubsub_peers_0) == 1 and pubsub_peers_0[0] == py_peer_id
pubsub_peers_1 = await p2pds[1].control.pubsub_list_peers("")
assert len(pubsub_peers_1) == 1 and pubsub_peers_1[0] == py_peer_id
assert (
len(py_pubsub.peers) == 2
and p2pds[0].peer_id in py_pubsub.peers
and p2pds[1].peer_id in py_pubsub.peers
)
#
# Test: `subscribe`.
#
# (name, topics)
# (go_0, [0, 1]) <-> (py, [0, 1]) <-> (go_1, [1])
sub_py_topic_0 = await py_pubsub.subscribe(TOPIC_0)
sub_py_topic_1 = await py_pubsub.subscribe(TOPIC_1)
sub_go_0_topic_0 = await p2pd_subscribe(p2pds[0], TOPIC_0)
sub_go_0_topic_1 = await p2pd_subscribe(p2pds[0], TOPIC_1)
sub_go_1_topic_1 = await p2pd_subscribe(p2pds[1], TOPIC_1)
# Check topic peers
await asyncio.sleep(0.1)
# go_0
go_0_topic_0_peers = await p2pds[0].control.pubsub_list_peers(TOPIC_0)
assert len(go_0_topic_0_peers) == 1 and py_peer_id == go_0_topic_0_peers[0]
go_0_topic_1_peers = await p2pds[0].control.pubsub_list_peers(TOPIC_1)
assert len(go_0_topic_1_peers) == 1 and py_peer_id == go_0_topic_1_peers[0]
# py
py_topic_0_peers = py_pubsub.peer_topics[TOPIC_0]
assert len(py_topic_0_peers) == 1 and p2pds[0].peer_id == py_topic_0_peers[0]
# go_1
go_1_topic_1_peers = await p2pds[1].control.pubsub_list_peers(TOPIC_1)
assert len(go_1_topic_1_peers) == 1 and py_peer_id == go_1_topic_1_peers[0]
#
# Test: `publish`
#
# 1. py publishes
# - 1.1. py publishes data_11 to topic_0, py and go_0 receives.
# - 1.2. py publishes data_12 to topic_1, all receive.
# 2. go publishes
# - 2.1. go_0 publishes data_21 to topic_0, py and go_0 receive.
# - 2.2. go_1 publishes data_22 to topic_1, all receive.
# 1.1. py publishes data_11 to topic_0, py and go_0 receives.
data_11 = b"data_11"
await py_pubsub.publish(TOPIC_0, data_11)
validate_11 = functools.partial(
validate_pubsub_msg, data=data_11, from_peer_id=py_peer_id
)
validate_11(await sub_py_topic_0.get())
validate_11(await sub_go_0_topic_0.get())
# 1.2. py publishes data_12 to topic_1, all receive.
data_12 = b"data_12"
validate_12 = functools.partial(
validate_pubsub_msg, data=data_12, from_peer_id=py_peer_id
)
await py_pubsub.publish(TOPIC_1, data_12)
validate_12(await sub_py_topic_1.get())
validate_12(await sub_go_0_topic_1.get())
validate_12(await sub_go_1_topic_1.get())
# 2.1. go_0 publishes data_21 to topic_0, py and go_0 receive.
data_21 = b"data_21"
validate_21 = functools.partial(
validate_pubsub_msg, data=data_21, from_peer_id=p2pds[0].peer_id
)
await p2pds[0].control.pubsub_publish(TOPIC_0, data_21)
validate_21(await sub_py_topic_0.get())
validate_21(await sub_go_0_topic_0.get())
# 2.2. go_1 publishes data_22 to topic_1, all receive.
data_22 = b"data_22"
validate_22 = functools.partial(
validate_pubsub_msg, data=data_22, from_peer_id=p2pds[1].peer_id
)
await p2pds[1].control.pubsub_publish(TOPIC_1, data_22)
validate_22(await sub_py_topic_1.get())
validate_22(await sub_go_0_topic_1.get())
validate_22(await sub_go_1_topic_1.get())
#
# Test: `unsubscribe` and re`subscribe`
#
await py_pubsub.unsubscribe(TOPIC_0)
await asyncio.sleep(0.1)
assert py_peer_id not in (await p2pds[0].control.pubsub_list_peers(TOPIC_0))
assert py_peer_id not in (await p2pds[1].control.pubsub_list_peers(TOPIC_0))
await py_pubsub.subscribe(TOPIC_0)
await asyncio.sleep(0.1)
assert py_peer_id in (await p2pds[0].control.pubsub_list_peers(TOPIC_0))
assert py_peer_id in (await p2pds[1].control.pubsub_list_peers(TOPIC_0))

58
tests/interop/utils.py Normal file
View File

@ -0,0 +1,58 @@
import asyncio
from typing import Union
from multiaddr import Multiaddr
from libp2p.host.host_interface import IHost
from libp2p.peer.id import ID
from libp2p.peer.peerinfo import PeerInfo
from .daemon import Daemon
TDaemonOrHost = Union[IHost, Daemon]
def _get_peer_info(node: TDaemonOrHost) -> PeerInfo:
peer_info: PeerInfo
if isinstance(node, Daemon):
peer_info = node.peer_info
else: # isinstance(node, IHost)
peer_id = node.get_id()
maddrs = [
node.get_addrs()[0].decapsulate(Multiaddr(f"/p2p/{peer_id.to_string()}"))
]
peer_info = PeerInfo(peer_id, maddrs)
return peer_info
async def _is_peer(peer_id: ID, node: TDaemonOrHost) -> bool:
if isinstance(node, Daemon):
pinfos = await node.control.list_peers()
peers = tuple([pinfo.peer_id for pinfo in pinfos])
return peer_id in peers
else: # isinstance(node, IHost)
return peer_id in node.get_network().connections
async def connect(a: TDaemonOrHost, b: TDaemonOrHost) -> None:
# Type check
err_msg = (
f"Type of a={type(a)} or type of b={type(b)} is wrong."
"Should be either `IHost` or `Daemon`"
)
assert all(
[isinstance(node, IHost) or isinstance(node, Daemon) for node in (a, b)]
), err_msg
b_peer_info = _get_peer_info(b)
if isinstance(a, Daemon):
await a.control.connect(b_peer_info.peer_id, b_peer_info.addrs)
else: # isinstance(b, IHost)
await a.connect(b_peer_info)
# Allow additional sleep for both side to establish the connection.
await asyncio.sleep(0.1)
a_peer_info = _get_peer_info(a)
assert await _is_peer(b_peer_info.peer_id, a)
assert await _is_peer(a_peer_info.peer_id, b)

View File

@ -4,7 +4,6 @@ import multiaddr
import pytest import pytest
from libp2p.peer.id import ID from libp2p.peer.id import ID
from libp2p.peer.peerdata import PeerData
from libp2p.peer.peerinfo import InvalidAddrError, PeerInfo, info_from_p2p_addr from libp2p.peer.peerinfo import InvalidAddrError, PeerInfo, info_from_p2p_addr
ALPHABETS = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" ALPHABETS = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
@ -12,24 +11,17 @@ VALID_MULTI_ADDR_STR = "/ip4/127.0.0.1/tcp/8000/p2p/3YgLAeMKSAPcGqZkAt8mREqhQXmJ
def test_init_(): def test_init_():
peer_data = PeerData()
random_addrs = [random.randint(0, 255) for r in range(4)] random_addrs = [random.randint(0, 255) for r in range(4)]
peer_data.add_addrs(random_addrs)
random_id_string = "" random_id_string = ""
for _ in range(10): for _ in range(10):
random_id_string += random.SystemRandom().choice(ALPHABETS) random_id_string += random.SystemRandom().choice(ALPHABETS)
peer_id = ID(random_id_string.encode()) peer_id = ID(random_id_string.encode())
peer_info = PeerInfo(peer_id, peer_data) peer_info = PeerInfo(peer_id, random_addrs)
assert peer_info.peer_id == peer_id assert peer_info.peer_id == peer_id
assert peer_info.addrs == random_addrs assert peer_info.addrs == random_addrs
def test_init_no_value():
with pytest.raises(Exception):
PeerInfo()
@pytest.mark.parametrize( @pytest.mark.parametrize(
"addr", "addr",
( (

View File

@ -1,7 +1,9 @@
from typing import NamedTuple from typing import NamedTuple
FLOODSUB_PROTOCOL_ID = "/floodsub/1.0.0" from libp2p.pubsub import floodsub, gossipsub
GOSSIPSUB_PROTOCOL_ID = "/gossipsub/1.0.0"
FLOODSUB_PROTOCOL_ID = floodsub.PROTOCOL_ID
GOSSIPSUB_PROTOCOL_ID = gossipsub.PROTOCOL_ID
class GossipsubParams(NamedTuple): class GossipsubParams(NamedTuple):

View File

@ -1,23 +1,8 @@
import pytest import pytest
from tests.factories import FloodsubFactory, GossipsubFactory, PubsubFactory from tests.factories import FloodsubFactory, GossipsubFactory, PubsubFactory
from tests.pubsub.configs import GOSSIPSUB_PARAMS
from .configs import GOSSIPSUB_PARAMS
@pytest.fixture
def floodsubs(num_hosts):
return FloodsubFactory.create_batch(num_hosts)
@pytest.fixture
def gossipsub_params():
return GOSSIPSUB_PARAMS
@pytest.fixture
def gossipsubs(num_hosts, gossipsub_params):
yield GossipsubFactory.create_batch(num_hosts, **gossipsub_params._asdict())
# TODO: Clean up
def _make_pubsubs(hosts, pubsub_routers, cache_size): def _make_pubsubs(hosts, pubsub_routers, cache_size):
@ -38,14 +23,21 @@ def pubsub_cache_size():
@pytest.fixture @pytest.fixture
def pubsubs_fsub(hosts, floodsubs, pubsub_cache_size): def gossipsub_params():
return GOSSIPSUB_PARAMS
@pytest.fixture
def pubsubs_fsub(num_hosts, hosts, pubsub_cache_size):
floodsubs = FloodsubFactory.create_batch(num_hosts)
_pubsubs_fsub = _make_pubsubs(hosts, floodsubs, pubsub_cache_size) _pubsubs_fsub = _make_pubsubs(hosts, floodsubs, pubsub_cache_size)
yield _pubsubs_fsub yield _pubsubs_fsub
# TODO: Clean up # TODO: Clean up
@pytest.fixture @pytest.fixture
def pubsubs_gsub(hosts, gossipsubs, pubsub_cache_size): def pubsubs_gsub(num_hosts, hosts, pubsub_cache_size, gossipsub_params):
gossipsubs = GossipsubFactory.create_batch(num_hosts, **gossipsub_params._asdict())
_pubsubs_gsub = _make_pubsubs(hosts, gossipsubs, pubsub_cache_size) _pubsubs_gsub = _make_pubsubs(hosts, gossipsubs, pubsub_cache_size)
yield _pubsubs_gsub yield _pubsubs_gsub
# TODO: Clean up # TODO: Clean up

View File

@ -14,7 +14,8 @@ from .utils import dense_connect, one_to_all_connect
((4, GossipsubParams(degree=4, degree_low=3, degree_high=5)),), ((4, GossipsubParams(degree=4, degree_low=3, degree_high=5)),),
) )
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_join(num_hosts, hosts, gossipsubs, pubsubs_gsub): async def test_join(num_hosts, hosts, pubsubs_gsub):
gossipsubs = tuple(pubsub.router for pubsub in pubsubs_gsub)
hosts_indices = list(range(num_hosts)) hosts_indices = list(range(num_hosts))
topic = "test_join" topic = "test_join"
@ -85,7 +86,9 @@ async def test_leave(pubsubs_gsub):
@pytest.mark.parametrize("num_hosts", (2,)) @pytest.mark.parametrize("num_hosts", (2,))
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_handle_graft(pubsubs_gsub, hosts, gossipsubs, event_loop, monkeypatch): async def test_handle_graft(pubsubs_gsub, hosts, event_loop, monkeypatch):
gossipsubs = tuple(pubsub.router for pubsub in pubsubs_gsub)
index_alice = 0 index_alice = 0
id_alice = hosts[index_alice].get_id() id_alice = hosts[index_alice].get_id()
index_bob = 1 index_bob = 1
@ -137,7 +140,9 @@ async def test_handle_graft(pubsubs_gsub, hosts, gossipsubs, event_loop, monkeyp
"num_hosts, gossipsub_params", ((2, GossipsubParams(heartbeat_interval=3)),) "num_hosts, gossipsub_params", ((2, GossipsubParams(heartbeat_interval=3)),)
) )
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_handle_prune(pubsubs_gsub, hosts, gossipsubs): async def test_handle_prune(pubsubs_gsub, hosts):
gossipsubs = tuple(pubsub.router for pubsub in pubsubs_gsub)
index_alice = 0 index_alice = 0
id_alice = hosts[index_alice].get_id() id_alice = hosts[index_alice].get_id()
index_bob = 1 index_bob = 1

View File

@ -1,5 +1,4 @@
import asyncio import asyncio
import io
from typing import NamedTuple from typing import NamedTuple
import pytest import pytest
@ -7,6 +6,7 @@ import pytest
from libp2p.exceptions import ValidationError from libp2p.exceptions import ValidationError
from libp2p.peer.id import ID from libp2p.peer.id import ID
from libp2p.pubsub.pb import rpc_pb2 from libp2p.pubsub.pb import rpc_pb2
from libp2p.utils import encode_varint_prefixed
from tests.utils import connect from tests.utils import connect
from .utils import make_pubsub_msg from .utils import make_pubsub_msg
@ -70,8 +70,7 @@ async def test_peers_subscribe(pubsubs_fsub):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_hello_packet(pubsubs_fsub): async def test_get_hello_packet(pubsubs_fsub):
def _get_hello_packet_topic_ids(): def _get_hello_packet_topic_ids():
packet = rpc_pb2.RPC() packet = pubsubs_fsub[0].get_hello_packet()
packet.ParseFromString(pubsubs_fsub[0].get_hello_packet())
return tuple(sub.topicid for sub in packet.subscriptions) return tuple(sub.topicid for sub in packet.subscriptions)
# Test: No subscription, so there should not be any topic ids in the hello packet. # Test: No subscription, so there should not be any topic ids in the hello packet.
@ -239,11 +238,19 @@ class FakeNetStream:
def __init__(self) -> None: def __init__(self) -> None:
self._queue = asyncio.Queue() self._queue = asyncio.Queue()
async def read(self) -> bytes: async def read(self, n: int = -1) -> bytes:
buf = io.BytesIO() buf = bytearray()
while not self._queue.empty(): # Force to blocking wait if no data available now.
buf.write(await self._queue.get()) if self._queue.empty():
return buf.getvalue() first_byte = await self._queue.get()
buf.extend(first_byte)
# If `n == -1`, read until no data is in the buffer(_queue).
# Else, read until no data is in the buffer(_queue) or we have read `n` bytes.
while (n == -1) or (len(buf) < n):
if self._queue.empty():
break
buf.extend(await self._queue.get())
return bytes(buf)
async def write(self, data: bytes) -> int: async def write(self, data: bytes) -> int:
for i in data: for i in data:
@ -279,7 +286,7 @@ async def test_continuously_read_stream(pubsubs_fsub, monkeypatch):
async def wait_for_event_occurring(event): async def wait_for_event_occurring(event):
try: try:
await asyncio.wait_for(event.wait(), timeout=0.01) await asyncio.wait_for(event.wait(), timeout=1)
except asyncio.TimeoutError as error: except asyncio.TimeoutError as error:
event.clear() event.clear()
raise asyncio.TimeoutError( raise asyncio.TimeoutError(
@ -296,7 +303,9 @@ async def test_continuously_read_stream(pubsubs_fsub, monkeypatch):
publish_subscribed_topic = rpc_pb2.RPC( publish_subscribed_topic = rpc_pb2.RPC(
publish=[rpc_pb2.Message(topicIDs=[TESTING_TOPIC])] publish=[rpc_pb2.Message(topicIDs=[TESTING_TOPIC])]
) )
await stream.write(publish_subscribed_topic.SerializeToString()) await stream.write(
encode_varint_prefixed(publish_subscribed_topic.SerializeToString())
)
await wait_for_event_occurring(event_push_msg) await wait_for_event_occurring(event_push_msg)
# Make sure the other events are not emitted. # Make sure the other events are not emitted.
with pytest.raises(asyncio.TimeoutError): with pytest.raises(asyncio.TimeoutError):
@ -308,13 +317,15 @@ async def test_continuously_read_stream(pubsubs_fsub, monkeypatch):
publish_not_subscribed_topic = rpc_pb2.RPC( publish_not_subscribed_topic = rpc_pb2.RPC(
publish=[rpc_pb2.Message(topicIDs=["NOT_SUBSCRIBED"])] publish=[rpc_pb2.Message(topicIDs=["NOT_SUBSCRIBED"])]
) )
await stream.write(publish_not_subscribed_topic.SerializeToString()) await stream.write(
encode_varint_prefixed(publish_not_subscribed_topic.SerializeToString())
)
with pytest.raises(asyncio.TimeoutError): with pytest.raises(asyncio.TimeoutError):
await wait_for_event_occurring(event_push_msg) await wait_for_event_occurring(event_push_msg)
# Test: `handle_subscription` is called when a subscription message is received. # Test: `handle_subscription` is called when a subscription message is received.
subscription_msg = rpc_pb2.RPC(subscriptions=[rpc_pb2.RPC.SubOpts()]) subscription_msg = rpc_pb2.RPC(subscriptions=[rpc_pb2.RPC.SubOpts()])
await stream.write(subscription_msg.SerializeToString()) await stream.write(encode_varint_prefixed(subscription_msg.SerializeToString()))
await wait_for_event_occurring(event_handle_subscription) await wait_for_event_occurring(event_handle_subscription)
# Make sure the other events are not emitted. # Make sure the other events are not emitted.
with pytest.raises(asyncio.TimeoutError): with pytest.raises(asyncio.TimeoutError):
@ -324,7 +335,7 @@ async def test_continuously_read_stream(pubsubs_fsub, monkeypatch):
# Test: `handle_rpc` is called when a control message is received. # Test: `handle_rpc` is called when a control message is received.
control_msg = rpc_pb2.RPC(control=rpc_pb2.ControlMessage()) control_msg = rpc_pb2.RPC(control=rpc_pb2.ControlMessage())
await stream.write(control_msg.SerializeToString()) await stream.write(encode_varint_prefixed(control_msg.SerializeToString()))
await wait_for_event_occurring(event_handle_rpc) await wait_for_event_occurring(event_handle_rpc)
# Make sure the other events are not emitted. # Make sure the other events are not emitted.
with pytest.raises(asyncio.TimeoutError): with pytest.raises(asyncio.TimeoutError):
@ -406,9 +417,11 @@ async def test_message_all_peers(pubsubs_fsub, monkeypatch):
monkeypatch.setattr(pubsubs_fsub[0], "peers", mock_peers) monkeypatch.setattr(pubsubs_fsub[0], "peers", mock_peers)
empty_rpc = rpc_pb2.RPC() empty_rpc = rpc_pb2.RPC()
await pubsubs_fsub[0].message_all_peers(empty_rpc.SerializeToString()) empty_rpc_bytes = empty_rpc.SerializeToString()
empty_rpc_bytes_len_prefixed = encode_varint_prefixed(empty_rpc_bytes)
await pubsubs_fsub[0].message_all_peers(empty_rpc_bytes)
for stream in mock_peers.values(): for stream in mock_peers.values():
assert (await stream.read()) == empty_rpc.SerializeToString() assert (await stream.read()) == empty_rpc_bytes_len_prefixed
@pytest.mark.parametrize("num_hosts", (1,)) @pytest.mark.parametrize("num_hosts", (1,))

View File

@ -15,7 +15,7 @@ select = B,C,E,F,W,T4,B9
[isort] [isort]
force_sort_within_sections=True force_sort_within_sections=True
known_third_party=hypothesis,pytest,async_generator,cytoolz,trio_typing,pytest_trio known_third_party=pytest,p2pclient
multi_line_output=3 multi_line_output=3
include_trailing_comma=True include_trailing_comma=True
force_grid_wrap=0 force_grid_wrap=0