diff --git a/raiden/routing.py b/raiden/routing.py index 3d2811177b..d2b036fd4e 100644 --- a/raiden/routing.py +++ b/raiden/routing.py @@ -9,6 +9,7 @@ from raiden.exceptions import ServiceRequestFailed from raiden.messages.metadata import RouteMetadata from raiden.network.pathfinding import PFSConfig, query_paths +from raiden.settings import INTERNAL_ROUTING_DEFAULT_FEE_PERC from raiden.transfer import channel, views from raiden.transfer.state import ChainState, ChannelState, RouteState from raiden.utils.typing import ( @@ -209,12 +210,15 @@ def get_best_routes_internal( # The complete route includes the initiator, add it to the beginning complete_route = [Address(from_address)] + neighbour.route + # https://github.com/raiden-network/raiden/issues/4751 + # Internal routing doesn't know about fee so it should set a percentage per hop + estimated_fee = FeeAmount(round(INTERNAL_ROUTING_DEFAULT_FEE_PERC * amount)) + available_routes.append( RouteState( route=complete_route, forward_channel_id=neighbour.channelid, - # Internal routing doesn't know about fees, so set them to 0 - estimated_fee=FeeAmount(0), + estimated_fee=estimated_fee, ) ) diff --git a/raiden/settings.py b/raiden/settings.py index bf8bd30d17..3c4f900496 100644 --- a/raiden/settings.py +++ b/raiden/settings.py @@ -56,6 +56,8 @@ DEFAULT_MEDIATION_PROPORTIONAL_FEE = ProportionalFeeAmount(0) DEFAULT_MEDIATION_PROPORTIONAL_IMBALANCE_FEE = ProportionalFeeAmount(0) DEFAULT_MEDIATION_FEE_MARGIN: float = 0.05 +INTERNAL_ROUTING_DEFAULT_FEE_PERC: float = 0.02 +MAX_MEDIATION_FEE_PERC: float = 0.2 ORACLE_BLOCKNUMBER_DRIFT_TOLERANCE = 3 ETHERSCAN_API = "https://{network}.etherscan.io/api?module=proxy&action={action}" diff --git a/raiden/tests/integration/api/test_restapi.py b/raiden/tests/integration/api/test_restapi.py index b96db05d61..7532c139dc 100644 --- a/raiden/tests/integration/api/test_restapi.py +++ b/raiden/tests/integration/api/test_restapi.py @@ -30,6 +30,7 @@ from raiden.tests.utils.network import CHAIN from raiden.tests.utils.protocol import WaitForMessage from raiden.tests.utils.smartcontracts import deploy_contract_web3 +from raiden.tests.utils.transfer import calculate_fee_for_amount from raiden.transfer import views from raiden.transfer.state import ChannelState from raiden.waiting import ( @@ -840,7 +841,7 @@ def test_api_payments_target_error(api_server_test_instance, raiden_network, tok @pytest.mark.parametrize("number_of_nodes", [2]) def test_api_payments(api_server_test_instance, raiden_network, token_addresses): _, app1 = raiden_network - amount = 200 + amount = 100 identifier = 42 token_address = token_addresses[0] target_address = app1.raiden.address @@ -985,7 +986,7 @@ def test_api_payments_with_secret_no_hash( api_server_test_instance, raiden_network, token_addresses ): _, app1 = raiden_network - amount = 200 + amount = 100 identifier = 42 token_address = token_addresses[0] target_address = app1.raiden.address @@ -1058,7 +1059,7 @@ def test_api_payments_with_secret_and_hash( api_server_test_instance, raiden_network, token_addresses ): _, app1 = raiden_network - amount = 200 + amount = 100 identifier = 42 token_address = token_addresses[0] target_address = app1.raiden.address @@ -1556,7 +1557,7 @@ def test_api_deposit_limit(api_server_test_instance, token_addresses, reveal_tim @pytest.mark.parametrize("number_of_nodes", [3]) def test_payment_events_endpoints(api_server_test_instance, raiden_network, token_addresses): app0, app1, app2 = raiden_network - amount1 = 200 + amount1 = 10 identifier1 = 42 secret1, secrethash1 = factories.make_secret_with_hash() token_address = token_addresses[0] @@ -1582,7 +1583,7 @@ def test_payment_events_endpoints(api_server_test_instance, raiden_network, toke # app0 is sending some tokens to target 2 identifier2 = 43 - amount2 = 123 + amount2 = 10 secret2, secrethash2 = factories.make_secret_with_hash() request = grequests.post( api_url_for( @@ -1884,7 +1885,7 @@ def test_payment_events_endpoints(api_server_test_instance, raiden_network, toke @pytest.mark.parametrize("number_of_nodes", [2]) def test_channel_events_raiden(api_server_test_instance, raiden_network, token_addresses): _, app1 = raiden_network - amount = 200 + amount = 100 identifier = 42 token_address = token_addresses[0] target_address = app1.raiden.address @@ -1906,7 +1907,8 @@ def test_channel_events_raiden(api_server_test_instance, raiden_network, token_a @pytest.mark.parametrize("channels_per_node", [CHAIN]) def test_pending_transfers_endpoint(raiden_network, token_addresses): initiator, mediator, target = raiden_network - amount = 200 + amount = 150 + amount_with_fee = amount + calculate_fee_for_amount(amount) identifier = 42 token_address = token_addresses[0] @@ -1944,7 +1946,7 @@ def test_pending_transfers_endpoint(raiden_network, token_addresses): ) transfer_arrived = target_wait.wait_for_message(LockedTransfer, {"payment_identifier": 42}) - transfer_arrived.wait() + transfer_arrived.wait(timeout=30.0) for server in (initiator_server, mediator_server, target_server): request = grequests.get(api_url_for(server, "pending_transfers_resource")) @@ -1953,7 +1955,7 @@ def test_pending_transfers_endpoint(raiden_network, token_addresses): content = json.loads(response.content) assert len(content) == 1 assert content[0]["payment_identifier"] == str(identifier) - assert content[0]["locked_amount"] == str(amount) + assert content[0]["locked_amount"] == str(amount_with_fee) assert content[0]["token_address"] == to_checksum_address(token_address) assert content[0]["token_network_address"] == to_checksum_address(token_network_address) diff --git a/raiden/tests/integration/long_running/test_settlement.py b/raiden/tests/integration/long_running/test_settlement.py index fa0ce8c1d1..3dad66cbcb 100644 --- a/raiden/tests/integration/long_running/test_settlement.py +++ b/raiden/tests/integration/long_running/test_settlement.py @@ -20,7 +20,12 @@ from raiden.tests.utils.events import raiden_state_changes_search_for_item, search_for_item from raiden.tests.utils.network import CHAIN from raiden.tests.utils.protocol import WaitForMessage -from raiden.tests.utils.transfer import assert_synced_channel_state, get_channelstate, transfer +from raiden.tests.utils.transfer import ( + assert_synced_channel_state, + calculate_fee_for_amount, + get_channelstate, + transfer, +) from raiden.transfer import channel, views from raiden.transfer.events import SendWithdrawConfirmation from raiden.transfer.identifiers import CanonicalIdentifier @@ -655,6 +660,7 @@ def run_test_settled_lock(token_addresses, raiden_network, deposit): registry_address = app0.raiden.default_registry.address token_address = token_addresses[0] amount = PaymentAmount(30) + amount_with_fee = amount + calculate_fee_for_amount(amount) token_network_address = views.get_token_network_address_by_token_address( views.state_from_app(app0), app0.raiden.default_registry.address, token_address ) @@ -727,8 +733,8 @@ def run_test_settled_lock(token_addresses, raiden_network, deposit): given_block_identifier=current_block, ) - expected_balance0 = initial_balance0 + deposit0 - amount * 2 - expected_balance1 = initial_balance1 + deposit1 + amount * 2 + expected_balance0 = initial_balance0 + deposit0 - amount_with_fee * 2 + expected_balance1 = initial_balance1 + deposit1 + amount_with_fee * 2 assert token_proxy.balance_of(address0) == expected_balance0 assert token_proxy.balance_of(address1) == expected_balance1 diff --git a/raiden/tests/integration/test_regression.py b/raiden/tests/integration/test_regression.py index 0523e7c3ef..45d39dea4a 100644 --- a/raiden/tests/integration/test_regression.py +++ b/raiden/tests/integration/test_regression.py @@ -18,7 +18,12 @@ ) from raiden.tests.utils.factories import UNIT_CHAIN_ID from raiden.tests.utils.network import payment_channel_open_and_deposit -from raiden.tests.utils.transfer import get_channelstate, transfer, watch_for_unlock_failures +from raiden.tests.utils.transfer import ( + calculate_amount_to_drain_channel, + get_channelstate, + transfer, + watch_for_unlock_failures, +) from raiden.transfer import views from raiden.transfer.mediated_transfer.events import EventRouteFailed, SendSecretReveal from raiden.transfer.mediated_transfer.state_change import ReceiveTransferCancelRoute @@ -285,12 +290,13 @@ def run_regression_payment_complete_after_refund_to_the_initiator( app_channels = [(app0, app1), (app1, app2), (app0, app3), (app3, app4), (app4, app2)] open_and_wait_for_channels(app_channels, registry_address, token, deposit, settle_timeout) + exhaust_amount = calculate_amount_to_drain_channel(deposit) # Use all deposit from app1->app2 to force a refund transfer( initiator_app=app1, target_app=app2, token_address=token, - amount=deposit, + amount=exhaust_amount, identifier=PaymentID(1), ) @@ -299,7 +305,7 @@ def run_regression_payment_complete_after_refund_to_the_initiator( initiator_app=app0, target_app=app2, token_address=token, - amount=deposit, + amount=exhaust_amount, identifier=PaymentID(2), timeout=20, ) diff --git a/raiden/tests/integration/transfer/test_mediatedtransfer.py b/raiden/tests/integration/transfer/test_mediatedtransfer.py index bcc5bb5819..19c7f1a87e 100644 --- a/raiden/tests/integration/transfer/test_mediatedtransfer.py +++ b/raiden/tests/integration/transfer/test_mediatedtransfer.py @@ -21,6 +21,7 @@ assert_succeeding_transfer_invariants, assert_synced_channel_state, block_timeout_for_transfer_by_secrethash, + calculate_amount_to_drain_channel, transfer, transfer_and_assert_path, wait_assert, @@ -195,10 +196,12 @@ def run_test_mediated_transfer_with_entire_deposit( chain_state, token_network_registry_address, token_address ) + amount = calculate_amount_to_drain_channel(deposit) + secrethash = transfer_and_assert_path( path=raiden_network, token_address=token_address, - amount=deposit, + amount=amount, identifier=1, timeout=network_wait * number_of_nodes, ) @@ -207,7 +210,7 @@ def run_test_mediated_transfer_with_entire_deposit( transfer_and_assert_path( path=reverse_path, token_address=token_address, - amount=deposit * 2, + amount=amount * 2, identifier=2, timeout=network_wait * number_of_nodes, ) @@ -442,7 +445,7 @@ def run_test_mediated_transfer_with_node_consuming_more_than_allocated_fee( chain_state, token_network_registry_address, token_address ) fee = FeeAmount(5) - amount = PaymentAmount(10) + amount = PaymentAmount(100) app1_app2_channel_state = views.get_channelstate_by_token_network_and_partner( chain_state=views.state_from_raiden(app1.raiden), @@ -570,7 +573,13 @@ def assert_balances(expected_transferred_amounts=List[int]): fee_without_margin = FeeAmount(20) fee = round(fee_without_margin * (1 + DEFAULT_MEDIATION_FEE_MARGIN)) - amount = PaymentAmount(10) + amount = PaymentAmount(35) + msg = ( + "The chosen values will result in less than the amount reaching " + "the target after the 3rd hop" + ) + amount_at_end = amount + fee - (amount + fee) // 5 - (amount + fee - (amount + fee) // 5) // 5 + assert amount_at_end >= amount, msg cases = [ # The fee is added by the initiator, but no mediator deducts fees. As a # result, the target receives the fee. @@ -646,7 +655,13 @@ def assert_balances(expected_transferred_amounts=List[int]): for i, fee_schedule in enumerate(case.get("incoming_fee_schedules", [])): if fee_schedule: set_fee_schedule(apps[i + 1], apps[i], fee_schedule) - with patch("raiden.routing.get_best_routes_internal", get_best_routes_with_fees): + + route_patch = patch("raiden.routing.get_best_routes_internal", get_best_routes_with_fees) + disable_max_mediation_fee_patch = patch( + "raiden.transfer.mediated_transfer.initiator.MAX_MEDIATION_FEE_PERC", new=10000 + ) + + with route_patch, disable_max_mediation_fee_patch: transfer_and_assert_path( path=raiden_network, token_address=token_address, amount=amount, identifier=2 ) diff --git a/raiden/tests/integration/transfer/test_refundtransfer.py b/raiden/tests/integration/transfer/test_refundtransfer.py index 7e22388615..6ad7866b63 100644 --- a/raiden/tests/integration/transfer/test_refundtransfer.py +++ b/raiden/tests/integration/transfer/test_refundtransfer.py @@ -17,6 +17,8 @@ ) from raiden.tests.utils.transfer import ( assert_synced_channel_state, + calculate_amount_to_drain_channel, + calculate_fee_for_amount, get_channelstate, transfer, wait_assert, @@ -60,7 +62,7 @@ def run_test_refund_messages(raiden_chain, token_addresses, deposit, network_wai ) # Exhaust the channel App1 <-> App2 (to force the refund transfer) - exhaust_amount = deposit + exhaust_amount = calculate_amount_to_drain_channel(deposit) transfer( initiator_app=app1, target_app=app2, @@ -70,6 +72,8 @@ def run_test_refund_messages(raiden_chain, token_addresses, deposit, network_wai ) refund_amount = deposit // 2 + refund_fees = calculate_fee_for_amount(refund_amount) + refund_amount_with_fees = refund_amount + refund_fees identifier = 1 payment_status = app0.raiden.mediated_transfer_async( token_network_address, refund_amount, app2.raiden.address, identifier @@ -81,7 +85,9 @@ def run_test_refund_messages(raiden_chain, token_addresses, deposit, network_wai # Since the refund is not unlocked both channels have the corresponding # amount locked (issue #1091) send_lockedtransfer = raiden_events_search_for_item( - app0.raiden, SendLockedTransfer, {"transfer": {"lock": {"amount": refund_amount}}} + app0.raiden, + SendLockedTransfer, + {"transfer": {"lock": {"amount": refund_amount_with_fees}}}, ) assert send_lockedtransfer @@ -100,7 +106,7 @@ def run_test_refund_messages(raiden_chain, token_addresses, deposit, network_wai [send_refundtransfer.transfer.lock], ) - # This channel was exhausted to force the refund transfer + # This channel was exhausted to force the refund transfer except for the fees with gevent.Timeout(network_wait): wait_assert( assert_synced_channel_state, token_network_address, app1, 0, [], app2, deposit * 2, [] @@ -160,6 +166,7 @@ def run_test_refund_transfer( # drain the channel app1 -> app2 identifier_drain = 2 amount_drain = deposit * 8 // 10 + amount_drain_with_fees = amount_drain + calculate_fee_for_amount(amount_drain) transfer( initiator_app=app1, target_app=app2, @@ -185,10 +192,10 @@ def run_test_refund_transfer( assert_synced_channel_state, token_network_address, app1, - deposit - amount_path - amount_drain, + deposit - amount_path - amount_drain_with_fees, [], app2, - deposit + amount_path + amount_drain, + deposit + amount_path + amount_drain_with_fees, [], ) @@ -196,6 +203,7 @@ def run_test_refund_transfer( # app2 doesn't have capacity, so a refund will be sent on app1 -> app0 identifier_refund = 3 amount_refund = 50 + amount_refund_with_fees = amount_refund + calculate_fee_for_amount(amount_refund) payment_status = app0.raiden.mediated_transfer_async( token_network_address, amount_refund, app2.raiden.address, identifier_refund ) @@ -205,7 +213,9 @@ def run_test_refund_transfer( # A lock structure with the correct amount send_locked = raiden_events_search_for_item( - app0.raiden, SendLockedTransfer, {"transfer": {"lock": {"amount": amount_refund}}} + app0.raiden, + SendLockedTransfer, + {"transfer": {"lock": {"amount": amount_refund_with_fees}}}, ) assert send_locked secrethash = send_locked.transfer.lock.secrethash @@ -237,10 +247,10 @@ def run_test_refund_transfer( assert_synced_channel_state, token_network_address, app1, - deposit - amount_path - amount_drain, + deposit - amount_path - amount_drain_with_fees, [], app2, - deposit + amount_path + amount_drain, + deposit + amount_path + amount_drain_with_fees, [], ) @@ -370,6 +380,7 @@ def run_test_different_view_of_last_bp_during_unlock( # drain the channel app1 -> app2 identifier_drain = 2 amount_drain = deposit * 8 // 10 + fee_app1_app2 = calculate_fee_for_amount(amount_drain) transfer( initiator_app=app1, target_app=app2, @@ -395,10 +406,10 @@ def run_test_different_view_of_last_bp_during_unlock( assert_synced_channel_state, token_network_address, app1, - deposit - amount_path - amount_drain, + deposit - amount_path - amount_drain - fee_app1_app2, [], app2, - deposit + amount_path + amount_drain, + deposit + amount_path + amount_drain + fee_app1_app2, [], ) @@ -406,6 +417,7 @@ def run_test_different_view_of_last_bp_during_unlock( # app2 doesn't have capacity, so a refund will be sent on app1 -> app0 identifier_refund = 3 amount_refund = 50 + amount_refund_with_fees = amount_refund + calculate_fee_for_amount(50) payment_status = app0.raiden.mediated_transfer_async( token_network_address, amount_refund, app2.raiden.address, identifier_refund ) @@ -415,7 +427,9 @@ def run_test_different_view_of_last_bp_during_unlock( # A lock structure with the correct amount send_locked = raiden_events_search_for_item( - app0.raiden, SendLockedTransfer, {"transfer": {"lock": {"amount": amount_refund}}} + app0.raiden, + SendLockedTransfer, + {"transfer": {"lock": {"amount": amount_refund_with_fees}}}, ) assert send_locked secrethash = send_locked.transfer.lock.secrethash @@ -447,10 +461,10 @@ def run_test_different_view_of_last_bp_during_unlock( assert_synced_channel_state, token_network_address, app1, - deposit - amount_path - amount_drain, + deposit - amount_path - amount_drain - fee_app1_app2, [], app2, - deposit + amount_path + amount_drain, + deposit + amount_path + amount_drain + fee_app1_app2, [], ) @@ -518,7 +532,7 @@ def patched_on_raiden_event(raiden, chain_state, event): {"receiver": app0.raiden.address}, retry_timeout, ) - assert unlock_app0.returned_tokens == 50 + assert unlock_app0.returned_tokens == amount_refund_with_fees with gevent.Timeout(timeout): unlock_app1 = wait_for_state_change( app1.raiden, @@ -526,7 +540,7 @@ def patched_on_raiden_event(raiden, chain_state, event): {"receiver": app1.raiden.address}, retry_timeout, ) - assert unlock_app1.returned_tokens == 50 + assert unlock_app1.returned_tokens == amount_refund_with_fees final_balance0 = token_proxy.balance_of(app0.raiden.address) final_balance1 = token_proxy.balance_of(app1.raiden.address) @@ -582,6 +596,7 @@ def run_test_refund_transfer_after_2nd_hop( # drain the channel app2 -> app3 identifier_drain = 2 amount_drain = deposit * 8 // 10 + amount_drain_with_fees = amount_drain + calculate_fee_for_amount(amount_drain) transfer( initiator_app=app2, target_app=app3, @@ -618,10 +633,10 @@ def run_test_refund_transfer_after_2nd_hop( assert_synced_channel_state, token_network_address, app2, - deposit - amount_path - amount_drain, + deposit - amount_path - amount_drain_with_fees, [], app3, - deposit + amount_path + amount_drain, + deposit + amount_path + amount_drain_with_fees, [], ) @@ -630,6 +645,7 @@ def run_test_refund_transfer_after_2nd_hop( # app2 -> app1 -> app0 identifier_refund = 3 amount_refund = 50 + amount_refund_with_fees = amount_refund + calculate_fee_for_amount(amount_refund) payment_status = app0.raiden.mediated_transfer_async( token_network_address, amount_refund, app3.raiden.address, identifier_refund ) @@ -639,7 +655,9 @@ def run_test_refund_transfer_after_2nd_hop( # Lock structures with the correct amount send_locked1 = raiden_events_search_for_item( - app0.raiden, SendLockedTransfer, {"transfer": {"lock": {"amount": amount_refund}}} + app0.raiden, + SendLockedTransfer, + {"transfer": {"lock": {"amount": amount_refund_with_fees}}}, ) assert send_locked1 @@ -652,7 +670,9 @@ def run_test_refund_transfer_after_2nd_hop( assert lock1.secrethash == refund_lock1.secrethash send_locked2 = raiden_events_search_for_item( - app1.raiden, SendLockedTransfer, {"transfer": {"lock": {"amount": amount_refund}}} + app1.raiden, + SendLockedTransfer, + {"transfer": {"lock": {"amount": amount_refund_with_fees}}}, ) assert send_locked2 @@ -693,9 +713,9 @@ def run_test_refund_transfer_after_2nd_hop( assert_synced_channel_state, token_network_address, app2, - deposit - amount_path - amount_drain, + deposit - amount_path - amount_drain_with_fees, [], app3, - deposit + amount_path + amount_drain, + deposit + amount_path + amount_drain_with_fees, [], ) diff --git a/raiden/tests/unit/transfer/mediated_transfer/test_initiatorstate.py b/raiden/tests/unit/transfer/mediated_transfer/test_initiatorstate.py index bdfc88650a..b612640667 100644 --- a/raiden/tests/unit/transfer/mediated_transfer/test_initiatorstate.py +++ b/raiden/tests/unit/transfer/mediated_transfer/test_initiatorstate.py @@ -9,7 +9,7 @@ from raiden.constants import EMPTY_HASH, MAXIMUM_PENDING_TRANSFERS from raiden.raiden_service import initiator_init -from raiden.settings import DEFAULT_MEDIATION_FEE_MARGIN +from raiden.settings import DEFAULT_MEDIATION_FEE_MARGIN, MAX_MEDIATION_FEE_PERC from raiden.tests.utils import factories from raiden.tests.utils.events import search_for_item from raiden.tests.utils.factories import ( @@ -150,6 +150,7 @@ def setup_initiator_tests( ) initiator_state = get_transfer_at_index(current_state, 0) + assert initiator_state, "There should be an initial initiator state" lock = channel.get_lock(channels[0].our_state, initiator_state.transfer_description.secrethash) assert lock available_routes = channels.get_routes(estimated_fee=allocated_fee) @@ -243,6 +244,42 @@ def test_init_with_usable_routes(): assert send_mediated_transfer.recipient == channels[0].partner_state.address +def test_init_with_fees_more_than_max_limit(): + transfer_amount = TokenAmount(100) + flat_fee = FeeAmount(int(transfer_amount + MAX_MEDIATION_FEE_PERC * transfer_amount)) + expected_fee_margin = int(flat_fee * DEFAULT_MEDIATION_FEE_MARGIN) + properties = factories.NettingChannelStateProperties( + our_state=factories.NettingChannelEndStateProperties( + balance=TokenAmount(transfer_amount + flat_fee + expected_fee_margin) + ) + ) + channels = factories.make_channel_set([properties]) + pseudo_random_generator = random.Random() + + transfer_description = create( + TransferDescriptionProperties(secret=UNIT_SECRET, amount=transfer_amount) + ) + routes = channels.get_routes(estimated_fee=flat_fee) + init_state_change = ActionInitInitiator(transfer=transfer_description, routes=routes) + + block_number = BlockNumber(1) + transition = initiator_manager.state_transition( + payment_state=None, + state_change=init_state_change, + channelidentifiers_to_channels=channels.channel_map, + nodeaddresses_to_networkstates=channels.nodeaddresses_to_networkstates, + pseudo_random_generator=pseudo_random_generator, + block_number=block_number, + ) + assert transition.new_state is None + assert isinstance(transition.events[0], EventPaymentSentFailed) + reason_msg = ( + "none of the available routes could be used and at least one of them " + "exceeded the maximum fee limit" + ) + assert transition.events[0].reason == reason_msg + + def test_init_without_routes(): block_number = BlockNumber(1) routes = [] diff --git a/raiden/tests/unit/transfer/mediated_transfer/test_mediatorstate.py b/raiden/tests/unit/transfer/mediated_transfer/test_mediatorstate.py index 8a70bf82ea..dc61695c2b 100644 --- a/raiden/tests/unit/transfer/mediated_transfer/test_mediatorstate.py +++ b/raiden/tests/unit/transfer/mediated_transfer/test_mediatorstate.py @@ -439,7 +439,7 @@ def test_events_for_balanceproof(): """ pseudo_random_generator = random.Random() - setup = factories.make_transfers_pair(2, amount=10, block_number=1) + setup = factories.make_transfers_pair(2, amount=UNIT_TRANSFER_AMOUNT, block_number=1) last_pair = setup.transfers_pair[-1] last_pair.payee_state = "payee_secret_revealed" @@ -1553,14 +1553,14 @@ def test_mediator_lock_expired_with_receive_lock_expired(): "recipient": UNIT_TRANSFER_TARGET, "transfer": { "lock": { - "amount": 10, + "amount": UNIT_TRANSFER_AMOUNT, "expiration": expiration, "secrethash": transfer.lock.secrethash, }, "balance_proof": { "nonce": 1, "transferred_amount": 0, - "locked_amount": 10, + "locked_amount": UNIT_TRANSFER_AMOUNT, # pylint: disable=no-member "locksroot": transfer.balance_proof.locksroot, }, @@ -2091,7 +2091,9 @@ def test_receive_unlock(): assert search_for_item(iteration.events, EventInvalidReceivedUnlock, {}), msg sender_state = channels[0].partner_state - lock = HashTimeLockState(amount=10, expiration=10, secrethash=UNIT_SECRETHASH) + lock = HashTimeLockState( + amount=UNIT_TRANSFER_AMOUNT, expiration=10, secrethash=UNIT_SECRETHASH + ) sender_state.secrethashes_to_lockedlocks[factories.UNIT_SECRETHASH] = lock sender_state.pending_locks = factories.make_pending_locks([lock]) sender_state.balance_proof = factories.create( diff --git a/raiden/tests/unit/transfer/mediated_transfer/test_mediatorstate_regression.py b/raiden/tests/unit/transfer/mediated_transfer/test_mediatorstate_regression.py index 8366a9c77d..53e28f4074 100644 --- a/raiden/tests/unit/transfer/mediated_transfer/test_mediatorstate_regression.py +++ b/raiden/tests/unit/transfer/mediated_transfer/test_mediatorstate_regression.py @@ -13,6 +13,7 @@ UNIT_SECRET, UNIT_SECRETHASH, UNIT_TOKEN_ADDRESS, + UNIT_TRANSFER_AMOUNT, UNIT_TRANSFER_IDENTIFIER, UNIT_TRANSFER_INITIATOR, UNIT_TRANSFER_TARGET, @@ -192,7 +193,7 @@ def test_regression_send_refund(): "token": UNIT_TOKEN_ADDRESS, "balance_proof": { "transferred_amount": 0, - "locked_amount": 10, + "locked_amount": UNIT_TRANSFER_AMOUNT, "locksroot": keccak(lock.encoded), "token_network_address": token_network_address, "channel_identifier": first_payer_transfer.balance_proof.channel_identifier, @@ -296,7 +297,7 @@ def test_regression_mediator_task_no_routes(): NettingChannelStateProperties( our_state=NettingChannelEndStateProperties(balance=0), partner_state=NettingChannelEndStateProperties( - balance=10, address=HOP2, privatekey=HOP2_KEY + balance=UNIT_TRANSFER_AMOUNT, address=HOP2, privatekey=HOP2_KEY ), ) ] diff --git a/raiden/tests/unit/transfer/test_source_routing.py b/raiden/tests/unit/transfer/test_source_routing.py index 671a3a1bbf..dfdbce79f7 100644 --- a/raiden/tests/unit/transfer/test_source_routing.py +++ b/raiden/tests/unit/transfer/test_source_routing.py @@ -6,6 +6,7 @@ from raiden.storage.serialization import DictSerializer from raiden.tests.utils import factories from raiden.tests.utils.events import search_for_item +from raiden.tests.utils.factories import UNIT_TRANSFER_AMOUNT from raiden.transfer import views from raiden.transfer.architecture import TransitionResult from raiden.transfer.events import EventPaymentSentFailed @@ -323,7 +324,7 @@ def test_mediator_skips_used_routes(): block_number = 3 defaults = factories.NettingChannelStateProperties( our_state=factories.NettingChannelEndStateProperties.OUR_STATE, - partner_state=factories.NettingChannelEndStateProperties(balance=10), + partner_state=factories.NettingChannelEndStateProperties(balance=UNIT_TRANSFER_AMOUNT), open_transaction=factories.TransactionExecutionStatusProperties( started_block_number=1, finished_block_number=2, result="success" ), diff --git a/raiden/tests/utils/factories.py b/raiden/tests/utils/factories.py index f9bd0d4371..cdf950f151 100644 --- a/raiden/tests/utils/factories.py +++ b/raiden/tests/utils/factories.py @@ -307,8 +307,8 @@ def make_hop_to_channel(channel_state: NettingChannelState = EMPTY) -> HopState: # Prefixing with UNIT_ to differ from the default globals. UNIT_SETTLE_TIMEOUT = 50 UNIT_REVEAL_TIMEOUT = 5 -UNIT_TRANSFER_AMOUNT = 10 -UNIT_TRANSFER_FEE = 5 +UNIT_TRANSFER_AMOUNT = 50 +UNIT_TRANSFER_FEE = 2 UNIT_SECRET = Secret(b"secretsecretsecretsecretsecretse") UNIT_SECRETHASH = sha256_secrethash(UNIT_SECRET) UNIT_TOKEN_ADDRESS = b"tokentokentokentoken" diff --git a/raiden/tests/utils/transfer.py b/raiden/tests/utils/transfer.py index 58bf534367..f579907cdb 100644 --- a/raiden/tests/utils/transfer.py +++ b/raiden/tests/utils/transfer.py @@ -14,7 +14,11 @@ from raiden.messages.metadata import Metadata, RouteMetadata from raiden.messages.transfers import Lock, LockedTransfer, LockExpired, Unlock from raiden.raiden_service import RaidenService -from raiden.settings import DEFAULT_RETRY_TIMEOUT +from raiden.settings import ( + DEFAULT_MEDIATION_FEE_MARGIN, + DEFAULT_RETRY_TIMEOUT, + INTERNAL_ROUTING_DEFAULT_FEE_PERC, +) from raiden.storage.restore import ( get_event_with_balance_proof_by_balance_hash, get_state_change_with_balance_proof_by_locksroot, @@ -405,13 +409,15 @@ def transfer_and_assert_path( secret=secret, ) + msg = ( + f"transfer from {to_checksum_address(first_app.raiden.address)} " + f"to {to_checksum_address(last_app.raiden.address)} for amount " + f"{amount} failed" + ) + exception = RuntimeError(msg + " due to Timeout") with watch_for_unlock_failures(*path): - with Timeout(seconds=timeout): + with Timeout(seconds=timeout, exception=exception): gevent.wait(results) - msg = ( - f"transfer from {to_checksum_address(first_app.raiden.address)} " - f"to {to_checksum_address(last_app.raiden.address)} failed." - ) assert payment_status.payment_done.get(), msg return secrethash @@ -1068,3 +1074,14 @@ def block_timeout_for_transfer_by_secrethash( block_number=BlockNumber(expiration), retry_timeout=DEFAULT_RETRY_TIMEOUT, ) + + +def calculate_amount_to_drain_channel(deposit: int) -> int: + # calculate the amount to send so that including fees it covers the entire deposit + denominator = 1 + INTERNAL_ROUTING_DEFAULT_FEE_PERC * (1 + DEFAULT_MEDIATION_FEE_MARGIN) + amount = round(deposit / denominator) + return amount + + +def calculate_fee_for_amount(amount: int) -> int: + return round((amount * INTERNAL_ROUTING_DEFAULT_FEE_PERC) * (1 + DEFAULT_MEDIATION_FEE_MARGIN)) diff --git a/raiden/transfer/mediated_transfer/initiator.py b/raiden/transfer/mediated_transfer/initiator.py index 666bf8a29e..bf50b1bd96 100644 --- a/raiden/transfer/mediated_transfer/initiator.py +++ b/raiden/transfer/mediated_transfer/initiator.py @@ -1,7 +1,11 @@ import random from raiden.constants import ABSENT_SECRET -from raiden.settings import DEFAULT_MEDIATION_FEE_MARGIN, DEFAULT_WAIT_BEFORE_LOCK_REMOVAL +from raiden.settings import ( + DEFAULT_MEDIATION_FEE_MARGIN, + DEFAULT_WAIT_BEFORE_LOCK_REMOVAL, + MAX_MEDIATION_FEE_PERC, +) from raiden.transfer import channel, routes from raiden.transfer.architecture import Event, TransitionResult from raiden.transfer.events import EventPaymentSentFailed, EventPaymentSentSuccess @@ -199,6 +203,7 @@ def try_new_route( initiator_state = None events: List[Event] = list() + route_fee_exceeds_max = False channel_state = None route_state = None @@ -220,6 +225,15 @@ def try_new_route( payment_amount=transfer_description.amount, estimated_fee=reachable_route_state.estimated_fee, ) + # https://github.com/raiden-network/raiden/issues/4751 + # If the transfer amount + fees exceeds a percentage of the + # initial amount then don't use this route + max_amount_limit = transfer_description.amount + int( + transfer_description.amount * MAX_MEDIATION_FEE_PERC + ) + if amount_with_fee > max_amount_limit: + route_fee_exceeds_max = True + continue is_channel_usable = channel.is_channel_usable_for_new_transfer( channel_state=candidate_channel_state, transfer_amount=amount_with_fee @@ -235,6 +249,9 @@ def try_new_route( else: reason = "none of the available routes could be used" + if route_fee_exceeds_max: + reason += " and at least one of them exceeded the maximum fee limit" + transfer_failed = EventPaymentSentFailed( token_network_registry_address=transfer_description.token_network_registry_address, token_network_address=transfer_description.token_network_address,