Skip to content

Commit 8189c11

Browse files
konradkonradkarlb
authored andcommitted
Backport Capabilities for release
This ports protocol handshake (#3283 / #6503) capabilities from #6482 / #6572 back to the release branch.
1 parent eb925cc commit 8189c11

File tree

11 files changed

+306
-14
lines changed

11 files changed

+306
-14
lines changed

raiden/constants.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,15 @@ class Networks(Enum):
8282
SMOKETEST = ChainID(627)
8383

8484

85+
class Capabilities(Enum):
86+
"""Capabilities allow for protocol handshake between nodes."""
87+
88+
RECEIVE = "Receive" # handle receiving transfers
89+
MEDIATE = "Mediate" # support for mediating transfers; mediating requires receiving
90+
DELIVERY = "Delivery" # expects and sends Delivery messages
91+
WEBRTC = "webRTC" # supports webRTC messaging
92+
93+
8594
# Set at 64 since parity's default is 64 and Geth's default is 128
8695
# TODO: Make this configurable. Since in parity this is also a configurable value
8796
STATE_PRUNING_AFTER_BLOCKS = 64

raiden/network/transport/matrix/transport.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
from raiden.transfer.identifiers import CANONICAL_IDENTIFIER_UNORDERED_QUEUE, QueueIdentifier
5555
from raiden.transfer.state import NetworkState, QueueIdsToQueues
5656
from raiden.transfer.state_change import ActionChangeNodeNetworkState
57+
from raiden.utils.capabilities import capconfig_to_dict
5758
from raiden.utils.formatting import to_checksum_address, to_hex_address
5859
from raiden.utils.logging import redact_secret
5960
from raiden.utils.notifying_queue import NotifyingQueue
@@ -72,6 +73,7 @@
7273
MessageID,
7374
NamedTuple,
7475
Optional,
76+
PeerCapabilities,
7577
RoomID,
7678
Set,
7779
Tuple,
@@ -443,10 +445,12 @@ def start( # type: ignore
443445
self._address_mgr.start()
444446

445447
try:
448+
capabilities = capconfig_to_dict(self._config.capabilities_config)
446449
login(
447450
client=self._client,
448451
signer=self._raiden_service.signer,
449452
prev_auth_data=prev_auth_data,
453+
capabilities=capabilities,
450454
)
451455
except ValueError:
452456
# `ValueError` may be raised if `get_user` provides invalid data to
@@ -1377,7 +1381,7 @@ def _user_presence_changed(self, user: User, _presence: UserPresence) -> None:
13771381
)
13781382

13791383
def _address_reachability_changed(
1380-
self, address: Address, reachability: AddressReachability
1384+
self, address: Address, reachability: AddressReachability, capabilities: PeerCapabilities
13811385
) -> None:
13821386
if reachability is AddressReachability.REACHABLE:
13831387
node_reachability = NetworkState.REACHABLE
@@ -1392,6 +1396,8 @@ def _address_reachability_changed(
13921396
else:
13931397
raise TypeError(f'Unexpected reachability state "{reachability}".')
13941398

1399+
log.debug("Peer capabilities", capabilities=capabilities, address=address)
1400+
13951401
assert self._raiden_service is not None, "_raiden_service not set"
13961402
state_change = ActionChangeNodeNetworkState(address, node_reachability)
13971403
self._raiden_service.handle_and_track_state_changes([state_change])

raiden/network/transport/matrix/utils.py

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,10 @@
5656
)
5757
from raiden.network.utils import get_average_http_response_time
5858
from raiden.storage.serialization.serializer import MessageSerializer
59+
from raiden.utils.capabilities import deserialize_capabilities, serialize_capabilities
5960
from raiden.utils.gevent import spawn_named
6061
from raiden.utils.signer import Signer, recover
61-
from raiden.utils.typing import Address, ChainID, MessageID, Signature
62+
from raiden.utils.typing import Address, ChainID, MessageID, PeerCapabilities, Signature
6263
from raiden_contracts.constants import ID_TO_CHAINNAME
6364

6465
log = structlog.get_logger(__name__)
@@ -186,7 +187,9 @@ def __init__(
186187
self,
187188
client: GMatrixClient,
188189
displayname_cache: DisplayNameCache,
189-
address_reachability_changed_callback: Callable[[Address, AddressReachability], None],
190+
address_reachability_changed_callback: Callable[
191+
[Address, AddressReachability, PeerCapabilities], None
192+
],
190193
user_presence_changed_callback: Optional[Callable[[User, UserPresence], None]] = None,
191194
_log_context: Optional[Dict[str, Any]] = None,
192195
) -> None:
@@ -269,6 +272,10 @@ def get_address_reachability_state(self, address: Address) -> ReachabilityState:
269272
""" Return the current reachability state for ``address``. """
270273
return self._address_to_reachabilitystate.get(address, UNKNOWN_REACHABILITY_STATE)
271274

275+
def get_address_capabilities(self, address: Address) -> PeerCapabilities:
276+
""" Return the protocol capabilities for ``address``. """
277+
return self._address_to_capabilities.get(address, PeerCapabilities({}))
278+
272279
def force_user_presence(self, user: User, presence: UserPresence) -> None:
273280
""" Forcibly set the ``user`` presence to ``presence``.
274281
@@ -331,6 +338,18 @@ def track_address_presence(
331338

332339
self._maybe_address_reachability_changed(address)
333340

341+
def query_capabilities_for_user_id(self, user_id: str) -> PeerCapabilities:
342+
""" This pulls the `avatar_url` for a given user/user_id and parses the capabilities. """
343+
try:
344+
user: User = self._client.get_user(user_id)
345+
except MatrixRequestError:
346+
return PeerCapabilities({})
347+
avatar_url = user.get_avatar_url()
348+
if avatar_url is not None:
349+
return PeerCapabilities(deserialize_capabilities(avatar_url))
350+
else:
351+
return PeerCapabilities({})
352+
334353
def get_reachability_from_matrix(self, user_ids: Iterable[str]) -> AddressReachability:
335354
""" Get the current reachability without any side effects
336355
@@ -347,11 +366,14 @@ def get_reachability_from_matrix(self, user_ids: Iterable[str]) -> AddressReacha
347366
def _maybe_address_reachability_changed(self, address: Address) -> None:
348367
# A Raiden node may have multiple Matrix users, this happens when
349368
# Raiden roams from a Matrix server to another. This loop goes over all
350-
# these users and uses the "best" presence. IOW, if there is a single
369+
# these users and uses the "best" presence. IOW, if there is at least one
351370
# Matrix user that is reachable, then the Raiden node is considered
352371
# reachable.
353372
userids = self._address_to_userids[address].copy()
354-
composite_presence = {self._userid_to_presence.get(uid) for uid in userids}
373+
presence_to_uid = defaultdict(list)
374+
for uid in userids:
375+
presence_to_uid[self._userid_to_presence.get(uid)].append(uid)
376+
composite_presence = set(presence_to_uid.keys())
355377

356378
new_presence = UserPresence.UNKNOWN
357379
for presence in UserPresence.__members__.values():
@@ -364,7 +386,9 @@ def _maybe_address_reachability_changed(self, address: Address) -> None:
364386
prev_reachability_state = self.get_address_reachability_state(address)
365387
if new_address_reachability == prev_reachability_state.reachability:
366388
return
367-
389+
# for capabilities, we get the "first" uid that showed the `new_presence`
390+
present_uid = presence_to_uid[new_presence].pop()
391+
capabilities = self.query_capabilities_for_user_id(present_uid)
368392
now = datetime.now()
369393

370394
self.log.debug(
@@ -379,8 +403,11 @@ def _maybe_address_reachability_changed(self, address: Address) -> None:
379403
self._address_to_reachabilitystate[address] = ReachabilityState(
380404
new_address_reachability, now
381405
)
406+
self._address_to_capabilities[address] = capabilities
382407

383-
self._address_reachability_changed_callback(address, new_address_reachability)
408+
self._address_reachability_changed_callback(
409+
address, new_address_reachability, capabilities
410+
)
384411

385412
def _presence_listener(self, event: Dict[str, Any], presence_update_id: int) -> None:
386413
"""
@@ -437,6 +464,7 @@ def _presence_listener(self, event: Dict[str, Any], presence_update_id: int) ->
437464
def _reset_state(self) -> None:
438465
self._address_to_userids: Dict[Address, Set[str]] = defaultdict(set)
439466
self._address_to_reachabilitystate: Dict[Address, ReachabilityState] = dict()
467+
self._address_to_capabilities: Dict[Address, PeerCapabilities] = dict()
440468
self._userid_to_presence: Dict[str, UserPresence] = dict()
441469
self._userid_to_presence_update_id: Dict[str, int] = dict()
442470

@@ -569,7 +597,7 @@ def join_broadcast_room(client: GMatrixClient, broadcast_room_alias: str) -> Roo
569597
)
570598

571599

572-
def first_login(client: GMatrixClient, signer: Signer, username: str) -> User:
600+
def first_login(client: GMatrixClient, signer: Signer, username: str, cap_str: str) -> User:
573601
"""Login within a server.
574602
575603
There are multiple cases where a previous auth token can become invalid and
@@ -635,6 +663,12 @@ def first_login(client: GMatrixClient, signer: Signer, username: str) -> User:
635663
if current_display_name != signature_hex:
636664
user.set_display_name(signature_hex)
637665

666+
current_capabilities = user.get_avatar_url() or ""
667+
668+
# Only set the capabilities if necessary.
669+
if current_capabilities != cap_str:
670+
user.set_avatar_url(cap_str)
671+
638672
log.debug(
639673
"Logged in",
640674
node=to_checksum_address(username),
@@ -679,14 +713,21 @@ def login_with_token(client: GMatrixClient, user_id: str, access_token: str) ->
679713
return client.get_user(client.user_id)
680714

681715

682-
def login(client: GMatrixClient, signer: Signer, prev_auth_data: Optional[str] = None) -> User:
683-
""" Login with a matrix server.
716+
def login(
717+
client: GMatrixClient,
718+
signer: Signer,
719+
prev_auth_data: Optional[str] = None,
720+
capabilities: Dict[str, Any] = None,
721+
) -> User:
722+
"""Login with a matrix server.
684723
685724
Params:
686725
client: GMatrixClient instance configured with desired homeserver.
687726
signer: Signer used to sign the password and displayname.
688727
prev_auth_data: Previously persisted authentication using the format "{user}/{password}".
689728
"""
729+
if capabilities is None:
730+
capabilities = {}
690731
server_url = client.api.base_url
691732
server_name = urlparse(server_url).netloc
692733

@@ -708,7 +749,11 @@ def login(client: GMatrixClient, signer: Signer, prev_auth_data: Optional[str] =
708749
server_name=server_name,
709750
)
710751

711-
return first_login(client, signer, username)
752+
try:
753+
capstr = serialize_capabilities(capabilities)
754+
except ValueError:
755+
raise Exception("error serializing")
756+
return first_login(client, signer, username, capstr)
712757

713758

714759
@cached(cache=LRUCache(128), key=attrgetter("user_id", "displayname"), lock=Semaphore())

raiden/settings.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,14 @@ def get_proportional_imbalance_fee(self, token_address: TokenAddress) -> Proport
127127
)
128128

129129

130+
@dataclass
131+
class CapabilitiesConfig:
132+
receive: bool = True
133+
mediate: bool = True
134+
delivery: bool = True
135+
web_rtc: bool = False
136+
137+
130138
@dataclass
131139
class MatrixTransportConfig:
132140
retries_before_backoff: int
@@ -137,6 +145,7 @@ class MatrixTransportConfig:
137145
available_servers: List[str]
138146
sync_timeout: int = DEFAULT_TRANSPORT_MATRIX_SYNC_TIMEOUT
139147
sync_latency: int = DEFAULT_TRANSPORT_MATRIX_SYNC_LATENCY
148+
capabilities_config: CapabilitiesConfig = CapabilitiesConfig()
140149

141150

142151
@dataclass
@@ -201,6 +210,7 @@ class RaidenConfig:
201210
retry_interval_max=DEFAULT_TRANSPORT_MATRIX_RETRY_INTERVAL_MAX,
202211
server=MATRIX_AUTO_SELECT_SERVER,
203212
sync_timeout=DEFAULT_TRANSPORT_MATRIX_SYNC_TIMEOUT,
213+
capabilities_config=CapabilitiesConfig(),
204214
)
205215

206216
rest_api: RestApiConfig = RestApiConfig()

raiden/tests/integration/fixtures/raiden_network.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from raiden.app import App
1010
from raiden.constants import Environment, RoutingMode
11+
from raiden.settings import CapabilitiesConfig
1112
from raiden.tests.utils.network import (
1213
CHAIN,
1314
BlockchainServices,
@@ -82,6 +83,7 @@ def raiden_chain(
8283
resolver_ports: List[Optional[int]],
8384
enable_rest_api: bool,
8485
port_generator: Iterator[Port],
86+
capabilities: CapabilitiesConfig,
8587
) -> Iterable[List[App]]:
8688

8789
if len(token_addresses) != 1:
@@ -122,6 +124,7 @@ def raiden_chain(
122124
resolver_ports=resolver_ports,
123125
enable_rest_api=enable_rest_api,
124126
port_generator=port_generator,
127+
capabilities_config=capabilities,
125128
)
126129

127130
confirmed_block = raiden_apps[0].raiden.confirmation_blocks + 1
@@ -192,6 +195,19 @@ def resolvers(resolver_ports):
192195
resolver.terminate()
193196

194197

198+
@pytest.fixture
199+
def adhoc_capability():
200+
return False
201+
202+
203+
@pytest.fixture
204+
def capabilities(adhoc_capability) -> CapabilitiesConfig:
205+
config = CapabilitiesConfig()
206+
if adhoc_capability:
207+
config.adhoc_capability = adhoc_capability # type: ignore
208+
return config
209+
210+
195211
@pytest.fixture
196212
def raiden_network(
197213
token_addresses: List[TokenAddress],
@@ -222,6 +238,7 @@ def raiden_network(
222238
resolver_ports: List[Optional[int]],
223239
enable_rest_api: bool,
224240
port_generator: Iterator[Port],
241+
capabilities: CapabilitiesConfig,
225242
) -> Iterable[List[App]]:
226243
service_registry_address = None
227244
if blockchain_services.service_registry:
@@ -254,6 +271,7 @@ def raiden_network(
254271
resolver_ports=resolver_ports,
255272
enable_rest_api=enable_rest_api,
256273
port_generator=port_generator,
274+
capabilities_config=capabilities,
257275
)
258276

259277
confirmed_block = raiden_apps[0].raiden.confirmation_blocks + 1

raiden/tests/integration/network/transport/test_matrix_transport.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,9 @@
5757
from raiden.transfer.identifiers import CANONICAL_IDENTIFIER_UNORDERED_QUEUE, QueueIdentifier
5858
from raiden.transfer.state import NetworkState
5959
from raiden.transfer.state_change import ActionChannelClose
60+
from raiden.utils.capabilities import capconfig_to_dict, deserialize_capabilities
6061
from raiden.utils.formatting import to_checksum_address
61-
from raiden.utils.typing import Address, Dict, List, TokenNetworkAddress, cast
62+
from raiden.utils.typing import Address, Dict, List, PeerCapabilities, TokenNetworkAddress, cast
6263
from raiden.waiting import wait_for_network_state
6364

6465
HOP1_BALANCE_PROOF = factories.BalanceProofSignedStateProperties(pkey=factories.HOP1_KEY)
@@ -1333,3 +1334,40 @@ def test_transport_presence_updates(raiden_network, restart_node, retry_timeout)
13331334
app2.raiden.transport.immediate_health_check_for(app1.raiden.address)
13341335
wait_for_network_state(app0.raiden, app2.raiden.address, NetworkState.REACHABLE, retry_timeout)
13351336
wait_for_network_state(app1.raiden, app2.raiden.address, NetworkState.REACHABLE, retry_timeout)
1337+
1338+
1339+
@raise_on_failure
1340+
@pytest.mark.parametrize("matrix_server_count", [1])
1341+
@pytest.mark.parametrize("number_of_nodes", [2])
1342+
@pytest.mark.parametrize("adhoc_capability", [True])
1343+
@pytest.mark.parametrize(
1344+
"broadcast_rooms", [[DISCOVERY_DEFAULT_ROOM, PATH_FINDING_BROADCASTING_ROOM]]
1345+
)
1346+
def test_transport_capabilities(raiden_network: List[App], capabilities, retry_timeout):
1347+
"""
1348+
Test that raiden matrix users have the `avatar_url` set in a format understood
1349+
by the capabilities parser.
1350+
"""
1351+
app0_outer, app1_outer = raiden_network
1352+
app0 = app0_outer.raiden
1353+
app1 = app1_outer.raiden
1354+
1355+
app0.transport.immediate_health_check_for(app1.address)
1356+
app1.transport.immediate_health_check_for(app0.address)
1357+
1358+
wait_for_network_state(app0, app1.address, NetworkState.REACHABLE, retry_timeout)
1359+
wait_for_network_state(app1, app0.address, NetworkState.REACHABLE, retry_timeout)
1360+
1361+
expected_capabilities = capconfig_to_dict(capabilities)
1362+
1363+
app1_user_ids = app0.transport.get_user_ids_for_address(app1.address)
1364+
assert len(app1_user_ids) == 1, "app1 should have exactly one user_id"
1365+
app1_user = app0.transport._client.get_user(app1_user_ids.pop())
1366+
app1_avatar_url = app1_user.get_avatar_url()
1367+
assert len(app1_avatar_url), "avatar_url not set for app1"
1368+
app1_capabilities = deserialize_capabilities(app1_avatar_url)
1369+
assert "adhoc_capability" in app1_capabilities, "capabilities could not be parsed correctly"
1370+
1371+
msg = "capabilities were not collected in transport client"
1372+
collected_capabilities = app0.transport._address_mgr.get_address_capabilities(app1.address)
1373+
assert collected_capabilities == PeerCapabilities(expected_capabilities), msg

0 commit comments

Comments
 (0)