def _calculate_fees(self) -> Optional[List[FeeAmount]]: """Calcluates fees backwards for this path. Returns ``None``, if the fee calculation cannot be done. """ total = PaymentWithFeeAmount(self.value) fees: List[FeeAmount] = [] with opentracing.tracer.start_span("calculate_fees"): for prev_node, mediator, next_node in reversed( list(window(self.nodes, 3))): view_in: ChannelView = self.G[prev_node][mediator]["view"] view_out: ChannelView = self.G[mediator][next_node]["view"] log.debug( "Fee calculation", amount=total, view_out=view_out, view_in=view_in, amount_without_fees=total, balance_in=view_in.capacity_partner, balance_out=view_out.capacity, schedule_in=view_in.fee_schedule_receiver, schedule_out=view_out.fee_schedule_sender, receivable_amount=view_in.capacity, ) amount_with_fees = get_amount_with_fees( amount_without_fees=total, balance_in=Balance(view_in.capacity_partner), balance_out=Balance(view_out.capacity), schedule_in=view_in.fee_schedule_receiver, schedule_out=view_out.fee_schedule_sender, receivable_amount=view_in.capacity, ) if amount_with_fees is None: log.warning( "Invalid path because of invalid fee calculation", amount=total, view_out=view_out, view_in=view_in, amount_without_fees=total, balance_in=view_in.capacity_partner, balance_out=view_out.capacity, schedule_in=view_in.fee_schedule_receiver, schedule_out=view_out.fee_schedule_sender, receivable_amount=view_in.capacity, ) return None fee = PaymentWithFeeAmount(amount_with_fees - total) total += fee # type: ignore fees.append(FeeAmount(fee)) # The hop to the target does not incur mediation fees fees.append(FeeAmount(0)) return fees
def test_update_fee(order, pathfinding_service_mock, token_network_model): metrics_state = save_metrics_state(metrics.REGISTRY) pathfinding_service_mock.database.insert( "token_network", dict(address=token_network_model.address) ) if order == "normal": setup_channel(pathfinding_service_mock, token_network_model) exception_expected = False else: exception_expected = True fee_schedule = FeeScheduleState( flat=FeeAmount(1), proportional=ProportionalFeeAmount(int(0.1e9)), imbalance_penalty=[(TokenAmount(0), FeeAmount(0)), (TokenAmount(10), FeeAmount(10))], ) fee_update = PFSFeeUpdate( canonical_identifier=CanonicalIdentifier( chain_identifier=ChainID(61), token_network_address=token_network_model.address, channel_identifier=ChannelID(1), ), updating_participant=PARTICIPANT1, fee_schedule=fee_schedule, timestamp=datetime.utcnow(), signature=EMPTY_SIGNATURE, ) fee_update.sign(LocalSigner(PARTICIPANT1_PRIVKEY)) pathfinding_service_mock.handle_message(fee_update) # Test for metrics having seen the processing of the message assert ( metrics_state.get_delta( "messages_processing_duration_seconds_sum", labels={"message_type": "PFSFeeUpdate"}, ) > 0.0 ) assert metrics_state.get_delta( "messages_exceptions_total", labels={"message_type": "PFSFeeUpdate"} ) == float(exception_expected) if order == "fee_update_before_channel_open": setup_channel(pathfinding_service_mock, token_network_model) cv = token_network_model.G[PARTICIPANT1][PARTICIPANT2]["view"] for key in ("flat", "proportional", "imbalance_penalty"): assert getattr(cv.fee_schedule_sender, key) == getattr(fee_schedule, key)
def get_fee_update_message( # pylint: disable=too-many-arguments updating_participant: Address, chain_id=ChainID(61), channel_identifier=DEFAULT_CHANNEL_ID, token_network_address: TokenNetworkAddress = DEFAULT_TOKEN_NETWORK_ADDRESS, fee_schedule: FeeScheduleState = FeeScheduleState( cap_fees=True, flat=FeeAmount(1), proportional=ProportionalFeeAmount(1)), timestamp: datetime = datetime.utcnow(), privkey_signer: bytes = PRIVATE_KEY_1, ) -> PFSFeeUpdate: fee_message = PFSFeeUpdate( canonical_identifier=CanonicalIdentifier( chain_identifier=chain_id, channel_identifier=channel_identifier, token_network_address=token_network_address, ), updating_participant=updating_participant, fee_schedule=fee_schedule, timestamp=timestamp, signature=EMPTY_SIGNATURE, ) fee_message.sign(LocalSigner(privkey_signer)) return fee_message
def test_stats_endpoint( api_sut_with_debug: PFSApi, api_url: str, token_network_model: TokenNetwork ): database = api_sut_with_debug.pathfinding_service.database default_path = [Address(b"1" * 20), Address(b"2" * 20), Address(b"3" * 20)] feedback_token = FeedbackToken(token_network_model.address) estimated_fee = FeeAmount(0) def check_response(num_all: int, num_only_feedback: int, num_only_success: int) -> None: url = api_url + "/v1/_debug/stats" response = requests.get(url) assert response.status_code == 200 data = response.json() assert data["total_calculated_routes"] == num_all assert data["total_feedback_received"] == num_only_feedback assert data["total_successful_routes"] == num_only_success database.prepare_feedback(feedback_token, default_path, estimated_fee) check_response(1, 0, 0) database.update_feedback(feedback_token, default_path, False) check_response(1, 1, 0) default_path2 = default_path[1:] feedback_token2 = FeedbackToken(token_network_model.address) database.prepare_feedback(feedback_token2, default_path2, estimated_fee) check_response(2, 1, 0) database.update_feedback(feedback_token2, default_path2, True) check_response(2, 2, 1)
def test_feedback(api_sut: PFSApi, api_url: str, token_network_model: TokenNetwork): database = api_sut.pathfinding_service.database default_path_hex = ["0x" + "1" * 40, "0x" + "2" * 40, "0x" + "3" * 40] default_path = [to_canonical_address(e) for e in default_path_hex] estimated_fee = FeeAmount(0) def make_request( token_id: Optional[str] = None, success: bool = True, path: Optional[List[str]] = None ): url = api_url + f"/v1/{to_checksum_address(token_network_model.address)}/feedback" token_id = token_id or uuid4().hex path = path or default_path_hex data = {"token": token_id, "success": success, "path": path} return requests.post(url, json=data) # Request with invalid UUID response = make_request(token_id="abc") assert response.status_code == 400 assert response.json()["error_code"] == exceptions.InvalidRequest.error_code # Request with invalid path response = make_request(path=["abc"]) assert response.status_code == 400 assert response.json()["error_code"] == exceptions.InvalidRequest.error_code # Test valid token, which is not stored in PFS DB token = FeedbackToken(token_network_address=token_network_model.address) response = make_request(token_id=token.uuid.hex) assert response.status_code == 400 assert not db_has_feedback_for(database, token, default_path) # Test expired token old_token = FeedbackToken( creation_time=datetime.utcnow() - timedelta(hours=1), token_network_address=token_network_model.address, ) database.prepare_feedback(old_token, default_path, estimated_fee) response = make_request(token_id=old_token.uuid.hex) assert response.status_code == 400 assert not db_has_feedback_for(database, token, default_path) # Test valid token token = FeedbackToken(token_network_address=token_network_model.address) database.prepare_feedback(token, default_path, estimated_fee) response = make_request(token_id=token.uuid.hex) assert response.status_code == 200 assert db_has_feedback_for(database, token, default_path)
def edge_weight( visited: Dict[ChannelID, float], view: ChannelView, view_from_partner: ChannelView, amount: PaymentAmount, fee_penalty: float, ) -> float: diversity_weight = visited.get(view.channel_id, 0) # Fees for initiator and target are included here. This promotes routes # that are nice to the initiator's and target's capacities, but it's # inconsistent with the estimated total fee. # Enable fee apping for both fee schedules schedule_in = copy(view.fee_schedule_receiver) schedule_in.cap_fees = True schedule_out = copy(view.fee_schedule_sender) schedule_out.cap_fees = True amount_with_fees = get_amount_with_fees( amount_without_fees=PaymentWithFeeAmount(amount), balance_in=Balance(view.capacity), balance_out=Balance(view.capacity), schedule_in=schedule_in, schedule_out=schedule_out, receivable_amount=view.capacity, ) if amount_with_fees is None: return float("inf") fee = FeeAmount(amount_with_fees - amount) fee_weight = fee / 1e18 * fee_penalty no_refund_weight = 0 if view_from_partner.capacity < int(float(amount) * 1.1): no_refund_weight = 1 return 1 + diversity_weight + fee_weight + no_refund_weight
def test_edge_weight(addresses): # pylint: disable=assigning-non-slot channel_id = ChannelID(1) participant1 = addresses[0] participant2 = addresses[1] capacity = TokenAmount(int(20 * 1e18)) capacity_partner = TokenAmount(int(10 * 1e18)) channel = Channel( token_network_address=TokenNetworkAddress(bytes([1])), channel_id=channel_id, participant1=participant1, participant2=participant2, capacity1=capacity, capacity2=capacity_partner, ) view, view_partner = channel.views amount = PaymentAmount(int(1e18)) # one RDN # no penalty assert (TokenNetwork.edge_weight(visited={}, view=view, view_from_partner=view_partner, amount=amount, fee_penalty=0) == 1) # channel already used in a previous route assert (TokenNetwork.edge_weight( visited={channel_id: 2}, view=view, view_from_partner=view_partner, amount=amount, fee_penalty=0, ) == 3) # absolute fee view.fee_schedule_sender.flat = FeeAmount(int(0.03e18)) assert (TokenNetwork.edge_weight( visited={}, view=view, view_from_partner=view_partner, amount=amount, fee_penalty=100, ) == 4) # relative fee view.fee_schedule_sender.flat = FeeAmount(0) view.fee_schedule_sender.proportional = ProportionalFeeAmount(int(0.01e6)) assert (TokenNetwork.edge_weight( visited={}, view=view, view_from_partner=view_partner, amount=amount, fee_penalty=100, ) == 2) # partner has not enough capacity for refund (no_refund_weight) -> edge weight +1 view_partner.capacity = TokenAmount(0) assert (TokenNetwork.edge_weight( visited={}, view=view, view_from_partner=view_partner, amount=amount, fee_penalty=100, ) == 3)
def estimated_fee(self) -> FeeAmount: if self.fees: return FeeAmount(sum(self.fees)) return FeeAmount(0)