From 1e6bf9c11b73f792cab1e92f06226d80c8189ead Mon Sep 17 00:00:00 2001 From: Karl Bartel Date: Tue, 9 Mar 2021 11:34:43 +0100 Subject: [PATCH] Request user_id from PFS before direct transfer See https://github.com/raiden-network/raiden/issues/6841 --- raiden/network/pathfinding.py | 31 ++++++++++ raiden/routing.py | 10 ++-- .../tests/integration/fixtures/transport.py | 8 +++ .../integration/network/test_pathfinding.py | 58 ++++++++++++++++++- raiden/tests/unit/test_pfs_integration.py | 11 ++-- 5 files changed, 107 insertions(+), 11 deletions(-) diff --git a/raiden/network/pathfinding.py b/raiden/network/pathfinding.py index 50d2d9abec..4b04d68ce2 100644 --- a/raiden/network/pathfinding.py +++ b/raiden/network/pathfinding.py @@ -2,6 +2,8 @@ import random from dataclasses import dataclass from datetime import datetime +from json import JSONDecodeError +from typing import Union from urllib.parse import urlparse from uuid import UUID @@ -539,6 +541,35 @@ def post_pfs_paths( ) +def query_user( + pfs_config: PFSConfig, + user_address: Union[Address, TargetAddress], +) -> str: + """ Get the matrix user_id for the given address from the PFS """ + try: + response = session.get( + f"{pfs_config.info.url}/api/v1/user/{to_checksum_address(user_address)}", + ) + except RequestException as e: + raise ServiceRequestFailed( + f"Could not connect to Pathfinding Service ({str(e)})", + dict(exc_info=True), + ) + + try: + response_json = get_response_json(response) + except (ValueError, JSONDecodeError): + raise ServiceRequestFailed( + "Pathfinding Service returned malformed json in response", + {"http_code": response.status_code}, + ) + + if response.status_code != 200: + raise PFSReturnedError.from_response(response_json) + + return response_json["user_id"] + + def query_paths( pfs_config: PFSConfig, our_address: Address, diff --git a/raiden/routing.py b/raiden/routing.py index 74b33142fe..c271853146 100644 --- a/raiden/routing.py +++ b/raiden/routing.py @@ -5,7 +5,7 @@ from raiden.exceptions import ServiceRequestFailed from raiden.messages.metadata import RouteMetadata -from raiden.network.pathfinding import PFSConfig, query_paths +from raiden.network.pathfinding import PFSConfig, query_paths, query_user from raiden.transfer import channel, views from raiden.transfer.state import ChainState, ChannelState, RouteState from raiden.utils.formatting import to_checksum_address @@ -43,6 +43,10 @@ def get_best_routes( token_network = views.get_token_network_by_address(chain_state, token_network_address) assert token_network, "The token network must be validated and exist." + if pfs_config is None or one_to_n_address is None: + log.warning("Pathfinding Service could not be used.") + return "Pathfinding Service could not be used.", list(), None + # Always use a direct channel if available: # - There are no race conditions and the capacity is guaranteed to be # available. @@ -69,10 +73,6 @@ def get_best_routes( ) return None, [direct_route], None - if pfs_config is None or one_to_n_address is None: - log.warning("Pathfinding Service could not be used.") - return "Pathfinding Service could not be used.", list(), None - # Does any channel have sufficient capacity for the payment? channels = [ token_network.channelidentifiers_to_channels[channel_id] diff --git a/raiden/tests/integration/fixtures/transport.py b/raiden/tests/integration/fixtures/transport.py index 70d27b1b7a..29c46ebdb0 100644 --- a/raiden/tests/integration/fixtures/transport.py +++ b/raiden/tests/integration/fixtures/transport.py @@ -22,6 +22,14 @@ def synapse_config_generator(): yield generator +@pytest.fixture(autouse=True) +def query_user_mock(local_matrix_servers, monkeypatch): + def query_user(_pfs_config, user_address): + return f"@0x{user_address.hex()}:{local_matrix_servers[0]}" + + monkeypatch.setattr("raiden.routing.query_user", query_user) + + @pytest.fixture def matrix_server_count() -> int: return 1 diff --git a/raiden/tests/integration/network/test_pathfinding.py b/raiden/tests/integration/network/test_pathfinding.py index 2d7ec16e19..712fbab212 100644 --- a/raiden/tests/integration/network/test_pathfinding.py +++ b/raiden/tests/integration/network/test_pathfinding.py @@ -1,3 +1,4 @@ +import json from unittest.mock import patch import pytest @@ -10,18 +11,27 @@ from requests.exceptions import RequestException from raiden.constants import MATRIX_AUTO_SELECT_SERVER, RoutingMode -from raiden.exceptions import RaidenError +from raiden.exceptions import PFSReturnedError, RaidenError, ServiceRequestFailed from raiden.network.pathfinding import ( + PFSConfig, PFSInfo, check_pfs_for_production, configure_pfs_or_exit, + query_user, session, ) from raiden.settings import DEFAULT_PATHFINDING_MAX_FEE +from raiden.tests.utils.factories import UNIT_CHAIN_ID, UNIT_OUR_ADDRESS, make_address from raiden.tests.utils.mocks import mocked_json_response from raiden.tests.utils.smartcontracts import deploy_service_registry_and_set_urls from raiden.utils.keys import privatekey_to_address -from raiden.utils.typing import BlockNumber, ChainID, TokenAmount, TokenNetworkRegistryAddress +from raiden.utils.typing import ( + BlockNumber, + BlockTimeout, + ChainID, + TokenAmount, + TokenNetworkRegistryAddress, +) token_network_registry_address_test_default = TokenNetworkRegistryAddress( to_canonical_address("0xB9633dd9a9a71F22C933bF121d7a22008f66B908") @@ -184,3 +194,47 @@ def test_check_pfs_for_production( ) with pytest.raises(RaidenError): check_pfs_for_production(service_registry=service_registry, pfs_info=pfs_info) + + +def test_query_user(): + matrix_user_id = "0x12345678901234567890@homeserver" + pfs_config = PFSConfig( + info=PFSInfo( + url="mock-address", + chain_id=UNIT_CHAIN_ID, + token_network_registry_address=make_address(), + user_deposit_address=make_address(), + payment_address=make_address(), + confirmed_block_number=BlockNumber(100), + message="", + operator="", + version="", + price=TokenAmount(0), + matrix_server="http://matrix.example.com", + ), + maximum_fee=TokenAmount(100), + iou_timeout=BlockTimeout(100), + max_paths=5, + ) + + with patch("raiden.network.pathfinding.session") as session_mock: + # success + response = session_mock.get.return_value + response.status_code = 200 + response.content = json.dumps({"user_id": matrix_user_id}) + assert query_user(pfs_config, UNIT_OUR_ADDRESS) == matrix_user_id + + # malformed response + response = session_mock.get.return_value + response.status_code = 200 + response.content = "{wrong" + with pytest.raises(ServiceRequestFailed): + query_user(pfs_config, UNIT_OUR_ADDRESS) + + # error response + response = session_mock.get.return_value + response.status_code = 400 + response.content = json.dumps({"error_code": 123}) + with pytest.raises(PFSReturnedError) as exc_info: + query_user(pfs_config, UNIT_OUR_ADDRESS) + assert exc_info.value["error_code"] == 123 diff --git a/raiden/tests/unit/test_pfs_integration.py b/raiden/tests/unit/test_pfs_integration.py index 27d0e706a3..18febd7295 100644 --- a/raiden/tests/unit/test_pfs_integration.py +++ b/raiden/tests/unit/test_pfs_integration.py @@ -659,9 +659,11 @@ def test_routing_in_direct_channel(happy_path_fixture, our_address, one_to_n_add address1, _, _, _ = addresses # with the transfer of 50 the direct channel should be returned, - # so there must be not a pfs call - with patch("raiden.routing.get_best_routes_pfs") as pfs_request: - pfs_request.return_value = None, [], "feedback_token" + # so there must be not a route request to the pfs + with patch("raiden.routing.get_best_routes_pfs") as pfs_route_request, patch( + "raiden.routing.query_user" + ) as pfs_user_request: + pfs_route_request.return_value = None, [], "feedback_token" _, routes, _ = get_best_routes( chain_state=chain_state, token_network_address=token_network_state.address, @@ -674,7 +676,8 @@ def test_routing_in_direct_channel(happy_path_fixture, our_address, one_to_n_add privkey=PRIVKEY, ) assert routes[0].next_hop_address == address1 - assert not pfs_request.called + assert not pfs_route_request.called + assert pfs_user_request.called # with the transfer of 51 the direct channel should not be returned, # so there must be a pfs call