Пример #1
0
    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
Пример #4
0
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)
Пример #5
0
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)
Пример #6
0
    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
Пример #7
0
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)
Пример #8
0
    def estimated_fee(self) -> FeeAmount:
        if self.fees:
            return FeeAmount(sum(self.fees))

        return FeeAmount(0)