Ejemplo n.º 1
0
    def create_n_deposits_with_subtasks(self, n=1, amount=2):
        for _ in range(n):
            task_to_compute = self._get_deserialized_task_to_compute(
                provider_public_key=self._get_provider_hex_public_key(),
                requestor_public_key=self._get_requestor_hex_public_key(),
                price=amount,
            )
            store_subtask(
                task_id=task_to_compute.task_id,
                subtask_id=task_to_compute.subtask_id,
                provider_public_key=self.PROVIDER_PUBLIC_KEY,
                requestor_public_key=self.REQUESTOR_PUBLIC_KEY,
                state=Subtask.SubtaskState.ACCEPTED,
                next_deadline=None,
                task_to_compute=task_to_compute,
                report_computed_task=factories.tasks.ReportComputedTaskFactory(
                    task_to_compute=task_to_compute))

            deposit_claim = DepositClaim()
            deposit_claim.subtask_id = task_to_compute.subtask_id
            deposit_claim.payer_deposit_account = self.deposit_account
            deposit_claim.payee_ethereum_address = task_to_compute.provider_ethereum_address
            deposit_claim.concent_use_case = ConcentUseCase.FORCED_ACCEPTANCE
            deposit_claim.amount = amount
            deposit_claim.clean()
            deposit_claim.save()
        return deposit_claim, task_to_compute
Ejemplo n.º 2
0
 def create_deposit_claim(self, amount, tx_hash):
     deposit_claim = DepositClaim(
         payee_ethereum_address=self.task_to_compute.
         provider_ethereum_address,
         payer_deposit_account=self.requestor_deposit_account,
         amount=amount,
         concent_use_case=ConcentUseCase.FORCED_PAYMENT,
         tx_hash=tx_hash,
         closure_time=parse_timestamp_to_utc_datetime(
             get_current_utc_timestamp()),
     )
     deposit_claim.full_clean()
     deposit_claim.save()
Ejemplo n.º 3
0
class DiscardClaimBanksterTest(ConcentIntegrationTestCase):
    def setUp(self):
        super().setUp()
        self.task_to_compute = self._get_deserialized_task_to_compute()

        self.client = Client(public_key_bytes=self.PROVIDER_PUBLIC_KEY)
        self.client.clean()
        self.client.save()

        self.deposit_account = DepositAccount()
        self.deposit_account.client = self.client
        self.deposit_account.ethereum_address = self.task_to_compute.requestor_ethereum_address
        self.deposit_account.clean()
        self.deposit_account.save()

        self.deposit_claim = DepositClaim()
        self.deposit_claim.payer_deposit_account = self.deposit_account
        self.deposit_claim.payee_ethereum_address = self.task_to_compute.provider_ethereum_address
        self.deposit_claim.concent_use_case = ConcentUseCase.FORCED_PAYMENT
        self.deposit_claim.amount = 1
        self.deposit_claim.closure_time = parse_timestamp_to_utc_datetime(
            get_current_utc_timestamp())
        self.deposit_claim.full_clean()
        self.deposit_claim.save()

    def test_that_discard_claim_should_return_false_and_not_remove_deposit_claim_if_tx_hash_is_none(
            self):
        claim_removed = discard_claim(self.deposit_claim)

        self.assertFalse(claim_removed)
        self.assertTrue(
            DepositClaim.objects.filter(pk=self.deposit_claim.pk).exists())

    def test_that_discard_claim_should_return_true_and_remove_deposit_claim_if_tx_hash_is_set(
            self):
        self.deposit_claim.tx_hash = 64 * '0'
        self.deposit_claim.clean()
        self.deposit_claim.save()

        claim_removed = discard_claim(self.deposit_claim)

        self.assertTrue(claim_removed)
        self.assertFalse(
            DepositClaim.objects.filter(pk=self.deposit_claim.pk).exists())
Ejemplo n.º 4
0
class FinalizePaymentBanksterTest(ConcentIntegrationTestCase):
    def setUp(self):
        super().setUp()
        self.task_to_compute = self._get_deserialized_task_to_compute()

        self.subtask = store_subtask(
            task_id=self.task_to_compute.task_id,
            subtask_id=self.task_to_compute.subtask_id,
            provider_public_key=self.PROVIDER_PUBLIC_KEY,
            requestor_public_key=self.REQUESTOR_PUBLIC_KEY,
            state=Subtask.SubtaskState.ACCEPTED,
            next_deadline=None,
            task_to_compute=self.task_to_compute,
            report_computed_task=factories.tasks.ReportComputedTaskFactory(
                task_to_compute=self.task_to_compute))
        self.subtask.full_clean()
        self.subtask.save()

        self.deposit_account = DepositAccount()
        self.deposit_account.client = self.subtask.requestor
        self.deposit_account.ethereum_address = self.task_to_compute.requestor_ethereum_address
        self.deposit_account.clean()
        self.deposit_account.save()

        self.deposit_claim = DepositClaim()
        self.deposit_claim.subtask_id = self.task_to_compute.subtask_id
        self.deposit_claim.payer_deposit_account = self.deposit_account
        self.deposit_claim.payee_ethereum_address = self.task_to_compute.provider_ethereum_address
        self.deposit_claim.concent_use_case = ConcentUseCase.FORCED_ACCEPTANCE
        self.deposit_claim.amount = 2
        self.deposit_claim.clean()
        self.deposit_claim.save()

    def test_that_when_available_funds_are_zero_finalize_payment_should_delete_deposit_claim_and_return_none(
            self):
        with mock.patch('core.payments.service.get_deposit_value',
                        return_value=0) as get_deposit_value:
            returned_value = finalize_payment(self.deposit_claim)

        self.assertIsNone(returned_value)
        self.assertFalse(
            DepositClaim.objects.filter(pk=self.deposit_claim.pk).exists())

        get_deposit_value.assert_called_with(
            client_eth_address=self.deposit_claim.payer_deposit_account.
            ethereum_address, )

    def test_that_when_deposit_claim_is_for_forced_acceptance_use_case_finalize_payment_should_call_force_subtask_payment(
            self):
        (v, r, s) = self.task_to_compute.promissory_note_sig
        with mock.patch('core.payments.service.get_deposit_value',
                        return_value=2) as get_deposit_value:
            with mock.patch('core.payments.service.force_subtask_payment',
                            return_value=MOCK_TRANSACTION_HASH
                            ) as force_subtask_payment:
                returned_value = finalize_payment(self.deposit_claim)
        self.assertEqual(returned_value, MOCK_TRANSACTION_HASH)

        get_deposit_value.assert_called_with(
            client_eth_address=self.deposit_claim.payer_deposit_account.
            ethereum_address, )
        force_subtask_payment.assert_called_once_with(
            requestor_eth_address=self.deposit_claim.payer_deposit_account.
            ethereum_address,
            provider_eth_address=self.deposit_claim.payee_ethereum_address,
            value=self.task_to_compute.price,
            subtask_id=self.deposit_claim.subtask_id,
            v=v,
            r=r,
            s=s,
            reimburse_amount=self.deposit_claim.amount)

    def test_that_when_deposit_claim_is_for_additional_verification_use_case_finalize_payment_should_call_cover_additional_verification_cost(
            self):
        self.deposit_claim.concent_use_case = ConcentUseCase.ADDITIONAL_VERIFICATION
        self.deposit_claim.clean()
        self.deposit_claim.save()
        (v, r, s) = self.task_to_compute.promissory_note_sig

        with mock.patch('core.payments.service.get_deposit_value',
                        return_value=2) as get_deposit_value:
            with mock.patch('core.payments.service.force_subtask_payment',
                            return_value=MOCK_TRANSACTION_HASH
                            ) as force_subtask_payment:
                returned_value = finalize_payment(self.deposit_claim)

        self.assertEqual(returned_value, MOCK_TRANSACTION_HASH)

        get_deposit_value.assert_called_with(
            client_eth_address=self.deposit_claim.payer_deposit_account.
            ethereum_address, )
        force_subtask_payment.assert_called_once_with(
            requestor_eth_address=self.deposit_claim.payer_deposit_account.
            ethereum_address,
            provider_eth_address=self.deposit_claim.payee_ethereum_address,
            value=self.task_to_compute.price,
            subtask_id=self.deposit_claim.subtask_id,
            v=v,
            r=r,
            s=s,
            reimburse_amount=self.deposit_claim.amount)

    def test_that_when_there_are_other_deposit_claims_finalize_payment_substract_them_from_currently_processed_claim(
            self):
        newest_deposit_claim, newest_task_to_compute = self.create_n_deposits_with_subtasks(
            2)
        (v, r, s) = newest_task_to_compute.promissory_note_sig

        with mock.patch('core.payments.service.get_deposit_value',
                        return_value=5) as get_deposit_value:
            with mock.patch('core.payments.service.force_subtask_payment',
                            return_value=MOCK_TRANSACTION_HASH
                            ) as force_subtask_payment:
                returned_value = finalize_payment(newest_deposit_claim)

        self.assertEqual(returned_value, MOCK_TRANSACTION_HASH)

        get_deposit_value.assert_called_with(
            client_eth_address=newest_deposit_claim.payer_deposit_account.
            ethereum_address, )
        force_subtask_payment.assert_called_once_with(
            requestor_eth_address=newest_deposit_claim.payer_deposit_account.
            ethereum_address,
            provider_eth_address=newest_deposit_claim.payee_ethereum_address,
            value=newest_task_to_compute.price,
            subtask_id=newest_deposit_claim.subtask_id,
            v=v,
            r=r,
            s=s,
            # 5 - (2 + 2) | deposit_value - sum_of_other_claims
            reimburse_amount=1,
        )

    def create_n_deposits_with_subtasks(self, n=1, amount=2):
        for _ in range(n):
            task_to_compute = self._get_deserialized_task_to_compute(
                provider_public_key=self._get_provider_hex_public_key(),
                requestor_public_key=self._get_requestor_hex_public_key(),
                price=amount,
            )
            store_subtask(
                task_id=task_to_compute.task_id,
                subtask_id=task_to_compute.subtask_id,
                provider_public_key=self.PROVIDER_PUBLIC_KEY,
                requestor_public_key=self.REQUESTOR_PUBLIC_KEY,
                state=Subtask.SubtaskState.ACCEPTED,
                next_deadline=None,
                task_to_compute=task_to_compute,
                report_computed_task=factories.tasks.ReportComputedTaskFactory(
                    task_to_compute=task_to_compute))

            deposit_claim = DepositClaim()
            deposit_claim.subtask_id = task_to_compute.subtask_id
            deposit_claim.payer_deposit_account = self.deposit_account
            deposit_claim.payee_ethereum_address = task_to_compute.provider_ethereum_address
            deposit_claim.concent_use_case = ConcentUseCase.FORCED_ACCEPTANCE
            deposit_claim.amount = amount
            deposit_claim.clean()
            deposit_claim.save()
        return deposit_claim, task_to_compute
Ejemplo n.º 5
0
def settle_overdue_acceptances(
    requestor_ethereum_address: str,
    provider_ethereum_address: str,
    acceptances: List[SubtaskResultsAccepted],
    requestor_public_key: bytes,
) -> DepositClaim:
    """
    The purpose of this operation is to calculate the total amount that the requestor owes provider for completed
    computations and transfer that amount from requestor's deposit.
    The caller is responsible for making sure that the payment is legitimate and should be performed.
    Bankster simply calculates the amount and executes it.
    """

    assert isinstance(requestor_ethereum_address, str)
    assert isinstance(provider_ethereum_address, str)
    assert all([
        isinstance(acceptance, SubtaskResultsAccepted)
        for acceptance in acceptances
    ])

    assert len(requestor_ethereum_address) == ETHEREUM_ADDRESS_LENGTH
    assert len(provider_ethereum_address) == ETHEREUM_ADDRESS_LENGTH
    assert provider_ethereum_address != requestor_ethereum_address

    validate_list_of_transaction_timestamp(acceptances)

    requestor_client: Client = get_or_create_with_retry(
        Client, public_key=requestor_public_key)

    requestor_deposit_account: DepositAccount = get_or_create_with_retry(
        DepositAccount,
        client=requestor_client,
        ethereum_address=requestor_ethereum_address)

    # Bankster asks SCI about the amount of funds available in requestor's deposit.
    requestor_deposit_value = service.get_deposit_value(
        client_eth_address=requestor_ethereum_address)  # pylint: disable=no-value-for-parameter

    # Bankster begins a database transaction and puts a database lock on the DepositAccount object.
    with non_nesting_atomic(using='control'):
        DepositAccount.objects.select_for_update().get(
            pk=requestor_deposit_account.pk)

        # Bankster sums the amounts of all existing DepositClaims that have the same payer as the one being processed.
        sum_of_existing_requestor_claims = DepositClaim.objects.filter(
            payer_deposit_account=requestor_deposit_account).aggregate(
                sum_of_existing_claims=Coalesce(Sum('amount'), 0))

        # Concent defines time T0 equal to oldest payment_ts from passed SubtaskResultAccepted messages from
        # subtask_results_accepted_list.
        oldest_payments_ts = min(subtask_results_accepted.payment_ts
                                 for subtask_results_accepted in acceptances)

        # Concent gets list of forced payments from payment API where T0 <= payment_ts + PAYMENT_DUE_TIME.
        list_of_settlement_payments = service.get_list_of_payments(  # pylint: disable=no-value-for-parameter
            requestor_eth_address=requestor_ethereum_address,
            provider_eth_address=provider_ethereum_address,
            min_block_timestamp=oldest_payments_ts,
            transaction_type=TransactionType.SETTLEMENT,
        )

        already_satisfied_claims_without_duplicates = find_unconfirmed_settlement_payments(
            list_of_settlement_payments,
            requestor_deposit_account,
            provider_ethereum_address,
            oldest_payments_ts,
        )

        # Concent gets list of transactions from payment API where timestamp >= T0.
        list_of_transactions = service.get_list_of_payments(  # pylint: disable=no-value-for-parameter
            requestor_eth_address=requestor_ethereum_address,
            provider_eth_address=provider_ethereum_address,
            min_block_timestamp=oldest_payments_ts,
            transaction_type=TransactionType.BATCH,
        )

        (_amount_paid, amount_pending) = get_provider_payment_info(
            list_of_settlement_payments=list_of_settlement_payments,
            list_of_transactions=list_of_transactions,
            settlement_payment_claims=
            already_satisfied_claims_without_duplicates,
            subtask_results_accepted_list=acceptances,
        )
        if amount_pending <= 0:
            raise BanksterNoUnsettledTasksError()

        # Bankster compares the amount with the available deposit minus the existing claims against requestor's account.
        # If the whole amount can't be paid, Concent lowers it to pay as much as possible.
        requestor_payable_amount = min(
            amount_pending,
            requestor_deposit_value -
            sum_of_existing_requestor_claims['sum_of_existing_claims'],
        )

        logger.info(
            f'requestor_payable_amount is {requestor_payable_amount} for ethereum address {requestor_ethereum_address}.'
        )

        if requestor_payable_amount <= 0:
            raise BanksterTooSmallRequestorDepositError(
                f"Requestor payable amount is {requestor_payable_amount}")

        # This is time T2 (end time) equal to youngest payment_ts from passed SubtaskResultAccepted messages from
        # subtask_results_accepted_list.
        youngest_payment_ts = max(subtask_results_accepted.payment_ts
                                  for subtask_results_accepted in acceptances)

        # Deposit lock for requestor.
        claim_against_requestor = DepositClaim(
            payee_ethereum_address=provider_ethereum_address,
            payer_deposit_account=requestor_deposit_account,
            amount=requestor_payable_amount,
            concent_use_case=ConcentUseCase.FORCED_PAYMENT,
            tx_hash=None,
            closure_time=parse_timestamp_to_utc_datetime(youngest_payment_ts),
        )
        claim_against_requestor.full_clean()
        claim_against_requestor.save()

    v_list, r_list, s_list, values, subtask_id_list = [], [], [], [], []
    for subtask_results_accepted in acceptances:
        v, r, s = subtask_results_accepted.task_to_compute.promissory_note_sig
        v_list.append(v)
        r_list.append(r)
        s_list.append(s)
        values.append(subtask_results_accepted.task_to_compute.price)
        subtask_id_list.append(
            subtask_results_accepted.task_to_compute.subtask_id)

    transaction_hash = service.make_settlement_payment(  # pylint: disable=no-value-for-parameter
        requestor_eth_address=requestor_ethereum_address,
        provider_eth_address=provider_ethereum_address,
        value=values,
        subtask_ids=subtask_id_list,
        closure_time=youngest_payment_ts,
        v=v_list,
        r=r_list,
        s=s_list,
        reimburse_amount=claim_against_requestor.amount_as_int,
    )
    transaction_hash = adjust_transaction_hash(transaction_hash)

    with non_nesting_atomic(using='control'):
        claim_against_requestor.tx_hash = transaction_hash
        claim_against_requestor.full_clean()
        claim_against_requestor.save()

    return claim_against_requestor
Ejemplo n.º 6
0
def claim_deposit(
    subtask_id: str,
    concent_use_case: ConcentUseCase,
    requestor_ethereum_address: str,
    provider_ethereum_address: str,
    subtask_cost: int,
    requestor_public_key: bytes,
    provider_public_key: bytes,
) -> Tuple[Optional[DepositClaim], Optional[DepositClaim]]:
    """
    The purpose of this operation is to check whether the clients participating in a use case have enough funds in their
    deposits to cover all the costs associated with the use case in the pessimistic scenario.
    """

    assert isinstance(concent_use_case, ConcentUseCase)
    assert isinstance(requestor_ethereum_address, str)
    assert isinstance(provider_ethereum_address, str)
    assert isinstance(subtask_cost, int) and subtask_cost > 0

    assert concent_use_case in [
        ConcentUseCase.FORCED_ACCEPTANCE,
        ConcentUseCase.ADDITIONAL_VERIFICATION
    ]
    assert len(requestor_ethereum_address) == ETHEREUM_ADDRESS_LENGTH
    assert len(provider_ethereum_address) == ETHEREUM_ADDRESS_LENGTH
    assert provider_ethereum_address != requestor_ethereum_address

    validate_bytes_public_key(requestor_public_key, 'requestor_public_key')
    validate_bytes_public_key(provider_public_key, 'provider_public_key')
    validate_uuid(subtask_id)

    is_claim_against_provider: bool = (
        concent_use_case == ConcentUseCase.ADDITIONAL_VERIFICATION
        and settings.ADDITIONAL_VERIFICATION_COST > 0)
    # Bankster creates Client and DepositAccount objects (if they don't exist yet) for the requestor
    # and also for the provider if there's a non-zero claim against his account.
    # This is done in single database transaction.
    requestor_client: Client = get_or_create_with_retry(
        Client, public_key=requestor_public_key)
    requestor_deposit_account: DepositAccount = get_or_create_with_retry(
        DepositAccount,
        client=requestor_client,
        ethereum_address=requestor_ethereum_address,
    )
    if is_claim_against_provider:
        provider_client: Client = get_or_create_with_retry(
            Client, public_key=provider_public_key)
        provider_deposit_account: DepositAccount = get_or_create_with_retry(
            DepositAccount,
            client=provider_client,
            ethereum_address=provider_ethereum_address,
        )

    # Bankster asks SCI about the amount of funds available in requestor's deposit.
    requestor_deposit = service.get_deposit_value(
        client_eth_address=requestor_ethereum_address)  # pylint: disable=no-value-for-parameter

    # If the amount claimed from provider's deposit is non-zero,
    # Bankster asks SCI about the amount of funds available in his deposit.
    if is_claim_against_provider:
        provider_deposit = service.get_deposit_value(
            client_eth_address=provider_ethereum_address)  # pylint: disable=no-value-for-parameter

    # Bankster puts database locks on DepositAccount objects
    # that will be used as payers in newly created DepositClaims.
    with non_nesting_atomic(using='control'):
        DepositAccount.objects.select_for_update().filter(
            client=requestor_client,
            ethereum_address=requestor_ethereum_address,
        )
        if is_claim_against_provider:
            DepositAccount.objects.select_for_update().filter(
                client=provider_client,
                ethereum_address=provider_ethereum_address,
            )

        # Bankster sums the amounts of all existing DepositClaims that have the same payer as the one being processed.
        aggregated_claims_against_requestor = DepositClaim.objects.filter(
            payer_deposit_account=requestor_deposit_account).aggregate(
                sum_of_existing_claims=Coalesce(Sum('amount'), 0))

        # If the existing claims against requestor's deposit are greater or equal to his current deposit,
        # we can't add a new claim.
        if requestor_deposit <= aggregated_claims_against_requestor[
                'sum_of_existing_claims']:
            return (None, None)

        # Deposit lock for requestor.
        claim_against_requestor = DepositClaim(
            subtask_id=subtask_id,
            payee_ethereum_address=provider_ethereum_address,
            amount=subtask_cost,
            concent_use_case=concent_use_case,
            payer_deposit_account=requestor_deposit_account,
        )
        claim_against_requestor.full_clean()
        claim_against_requestor.save()

        if is_claim_against_provider:
            # Bankster sums the amounts of all existing DepositClaims where the provider is the payer.
            aggregated_claims_against_provider = DepositClaim.objects.filter(
                payer_deposit_account=provider_deposit_account).aggregate(
                    sum_of_existing_claims=Coalesce(Sum('amount'), 0))

            # If the total of existing claims and the current claim is greater or equal to the current deposit,
            # we can't add a new claim.
            provider_obligations = aggregated_claims_against_provider[
                'sum_of_existing_claims'] + settings.ADDITIONAL_VERIFICATION_COST
            if provider_deposit <= provider_obligations:
                claim_against_requestor.delete()
                raise BanksterTooSmallProviderDepositError(
                    f'Provider deposit is {provider_deposit} (required: {provider_obligations}'
                )

        # Deposit lock for provider.
        if is_claim_against_provider:
            claim_against_provider = DepositClaim(
                subtask_id=subtask_id,
                payee_ethereum_address=ethereum_public_key_to_address(
                    settings.CONCENT_ETHEREUM_PUBLIC_KEY),
                amount=settings.ADDITIONAL_VERIFICATION_COST,
                concent_use_case=concent_use_case,
                payer_deposit_account=provider_deposit_account,
            )
            claim_against_provider.full_clean()
            claim_against_provider.save()
        else:
            claim_against_provider = None  # type: ignore

    return (claim_against_requestor, claim_against_provider)
Ejemplo n.º 7
0
def finalize_payment(deposit_claim: DepositClaim) -> Optional[str]:
    """
    This operation tells Bankster to pay out funds from deposit.
    For each claim, Bankster uses SCI to submit an Ethereum transaction to the Ethereum client which then propagates it
    to the rest of the network.
    Hopefully the transaction is included in one of the upcoming blocks on the blockchain.

    IMPORTANT!: This function must never be called in parallel for the same DepositClaim - otherwise provider might get
    paid twice for the same thing. It's caller responsibility to ensure that.
    """

    assert isinstance(deposit_claim, DepositClaim)
    assert deposit_claim.tx_hash is None

    # Bankster asks SCI about the amount of funds available on the deposit account listed in the DepositClaim.
    available_funds = service.get_deposit_value(  # pylint: disable=no-value-for-parameter
        client_eth_address=deposit_claim.payer_deposit_account.ethereum_address
    )

    # Bankster begins a database transaction and puts a database lock on the DepositAccount object.
    with non_nesting_atomic(using='control'):
        DepositAccount.objects.select_for_update().get(
            pk=deposit_claim.payer_deposit_account_id)

        # Bankster sums the amounts of all existing DepositClaims that have the same payer as the one being processed.
        aggregated_client_claims = DepositClaim.objects.filter(
            payer_deposit_account=deposit_claim.payer_deposit_account).exclude(
                pk=deposit_claim.pk).aggregate(
                    sum_of_existing_claims=Coalesce(Sum('amount'), 0))

        # Bankster subtracts that value from the amount of funds available in the deposit.
        available_funds_without_claims = available_funds - aggregated_client_claims[
            'sum_of_existing_claims']

        # If the result is negative or zero, Bankster removes the DepositClaim object being processed.
        if available_funds_without_claims <= 0:
            deposit_claim.delete()
            return None

        # Otherwise if the result is lower than DepositAccount.amount,
        # Bankster sets this field to the amount that's actually available.
        elif available_funds_without_claims < deposit_claim.amount:
            deposit_claim.amount = available_funds_without_claims
            deposit_claim.save()

    # If the DepositClaim still exists at this point, Bankster uses SCI to create an Ethereum transaction.
    subtask = Subtask.objects.filter(
        subtask_id=deposit_claim.subtask_id).first()  # pylint: disable=no-member
    task_to_compute: TaskToCompute = deserialize_message(
        subtask.task_to_compute.data.tobytes())
    v, r, s = task_to_compute.promissory_note_sig
    if deposit_claim.concent_use_case == ConcentUseCase.FORCED_ACCEPTANCE:
        ethereum_transaction_hash = service.force_subtask_payment(  # pylint: disable=no-value-for-parameter
            requestor_eth_address=deposit_claim.payer_deposit_account.
            ethereum_address,
            provider_eth_address=deposit_claim.payee_ethereum_address,
            value=task_to_compute.price,
            subtask_id=deposit_claim.subtask_id,
            v=v,
            r=r,
            s=s,
            reimburse_amount=deposit_claim.amount_as_int,
        )
    elif deposit_claim.concent_use_case == ConcentUseCase.ADDITIONAL_VERIFICATION:
        if subtask is not None:
            if task_to_compute.requestor_ethereum_address == deposit_claim.payer_deposit_account.ethereum_address:
                ethereum_transaction_hash = service.force_subtask_payment(  # pylint: disable=no-value-for-parameter
                    requestor_eth_address=deposit_claim.payer_deposit_account.
                    ethereum_address,
                    provider_eth_address=deposit_claim.payee_ethereum_address,
                    value=task_to_compute.price,
                    subtask_id=deposit_claim.subtask_id,
                    v=v,
                    r=r,
                    s=s,
                    reimburse_amount=deposit_claim.amount_as_int,
                )
            elif task_to_compute.provider_ethereum_address == deposit_claim.payer_deposit_account.ethereum_address:
                subtask_results_verify: SubtaskResultsVerify = deserialize_message(
                    subtask.subtask_results_verify.data.tobytes())
                (v, r, s) = subtask_results_verify.concent_promissory_note_sig
                ethereum_transaction_hash = service.cover_additional_verification_cost(  # pylint: disable=no-value-for-parameter
                    provider_eth_address=deposit_claim.payer_deposit_account.
                    ethereum_address,
                    value=subtask_results_verify.task_to_compute.price,
                    subtask_id=deposit_claim.subtask_id,
                    v=v,
                    r=r,
                    s=s,
                    reimburse_amount=deposit_claim.amount_as_int,
                )
            else:
                assert False
    else:
        assert False

    with non_nesting_atomic(using='control'):
        # The code below is executed in another transaction, so - in theory - deposit_claim object could be modified in
        # the meantime. Here we are working under assumption that it's not the case and it is coder's responsibility to
        # ensure that.
        ethereum_transaction_hash = adjust_transaction_hash(
            ethereum_transaction_hash)
        deposit_claim.tx_hash = ethereum_transaction_hash
        deposit_claim.full_clean()
        deposit_claim.save()

    service.register_confirmed_transaction_handler(  # pylint: disable=no-value-for-parameter
        tx_hash=deposit_claim.tx_hash,
        callback=lambda _: discard_claim(deposit_claim),
    )

    return deposit_claim.tx_hash
Ejemplo n.º 8
0
def settle_overdue_acceptances(
    requestor_ethereum_address: str,
    provider_ethereum_address: str,
    acceptances: List[SubtaskResultsAccepted],
    requestor_public_key: bytes,
) -> Optional[DepositClaim]:
    """
    The purpose of this operation is to calculate the total amount that the requestor owes provider for completed
    computations and transfer that amount from requestor's deposit.
    The caller is responsible for making sure that the payment is legitimate and should be performed.
    Bankster simply calculates the amount and executes it.
    """

    assert isinstance(requestor_ethereum_address, str)
    assert isinstance(provider_ethereum_address, str)
    assert all([
        isinstance(acceptance, SubtaskResultsAccepted)
        for acceptance in acceptances
    ])

    assert len(requestor_ethereum_address) == ETHEREUM_ADDRESS_LENGTH
    assert len(provider_ethereum_address) == ETHEREUM_ADDRESS_LENGTH
    assert provider_ethereum_address != requestor_ethereum_address

    with transaction.atomic(using='control'):
        try:
            requestor_client = Client.objects.get_or_create_full_clean(
                public_key=requestor_public_key, )
        except IntegrityError as exception:
            if exception.pgcode == pg_errorcodes.UNIQUE_VIOLATION:
                requestor_client = Client.objects.get_or_create_full_clean(
                    public_key=requestor_public_key, )
            else:
                raise

    with transaction.atomic(using='control'):
        try:
            requestor_deposit_account = DepositAccount.objects.get_or_create_full_clean(
                client=requestor_client,
                ethereum_address=requestor_ethereum_address,
            )
        except IntegrityError as exception:
            if exception.pgcode == pg_errorcodes.UNIQUE_VIOLATION:
                requestor_deposit_account = DepositAccount.objects.get_or_create_full_clean(
                    client=requestor_client,
                    ethereum_address=requestor_ethereum_address,
                )
            else:
                raise

    # Bankster asks SCI about the amount of funds available in requestor's deposit.
    requestor_deposit_value = service.get_deposit_value(
        client_eth_address=requestor_ethereum_address)  # pylint: disable=no-value-for-parameter

    # Bankster begins a database transaction and puts a database lock on the DepositAccount object.
    with transaction.atomic(using='control'):
        DepositAccount.objects.select_for_update().get(
            pk=requestor_deposit_account.pk)

        # Bankster sums the amounts of all existing DepositClaims that have the same payer as the one being processed.
        sum_of_existing_requestor_claims = DepositClaim.objects.filter(
            payer_deposit_account=requestor_deposit_account).aggregate(
                sum_of_existing_claims=Coalesce(Sum('amount'), 0))

        assert sum_of_existing_requestor_claims['sum_of_existing_claims'] >= 0

        # If the existing claims against requestor's deposit are greater or equal to his current deposit,
        # we can't add a new claim.
        if requestor_deposit_value <= sum_of_existing_requestor_claims[
                'sum_of_existing_claims']:
            return None

        # Concent defines time T0 equal to oldest payment_ts from passed SubtaskResultAccepted messages from
        # subtask_results_accepted_list.
        oldest_payments_ts = min(subtask_results_accepted.payment_ts
                                 for subtask_results_accepted in acceptances)

        cut_off_time = get_current_utc_timestamp()

        # Concent gets list of transactions from payment API where timestamp >= T0.
        list_of_transactions = service.get_list_of_payments(  # pylint: disable=no-value-for-parameter
            requestor_eth_address=requestor_ethereum_address,
            provider_eth_address=provider_ethereum_address,
            payment_ts=oldest_payments_ts,
            transaction_type=TransactionType.BATCH,
        )

        # Concent defines time T1 equal to youngest timestamp from list of transactions.
        if len(list_of_transactions) == 0:
            t1_is_bigger_than_payments_ts = False
        else:
            youngest_transaction_timestamp = max(
                ethereum_transaction.closure_time
                for ethereum_transaction in list_of_transactions)

            assert youngest_transaction_timestamp <= cut_off_time

            # Concent checks if all passed SubtaskResultAccepted messages from subtask_results_accepted_list
            # have payment_ts < T1
            t1_is_bigger_than_payments_ts = any(
                youngest_transaction_timestamp >
                subtask_results_accepted.payment_ts
                for subtask_results_accepted in acceptances)

        # Any of the items from list of overdue acceptances
        # matches condition current_time < payment_ts + PAYMENT_DUE_TIME
        acceptance_time_overdue = any(
            cut_off_time < subtask_results_accepted.payment_ts +
            settings.PAYMENT_DUE_TIME
            for subtask_results_accepted in acceptances)

        if t1_is_bigger_than_payments_ts or acceptance_time_overdue:
            raise BanksterTimestampError

        # Concent gets list of forced payments from payment API where T0 <= payment_ts + PAYMENT_DUE_TIME.
        list_of_forced_payments = service.get_list_of_payments(  # pylint: disable=no-value-for-parameter
            requestor_eth_address=requestor_ethereum_address,
            provider_eth_address=provider_ethereum_address,
            payment_ts=oldest_payments_ts,
            transaction_type=TransactionType.FORCE,
        )

        (_amount_paid, amount_pending) = get_provider_payment_info(
            list_of_forced_payments=list_of_forced_payments,
            list_of_payments=list_of_transactions,
            subtask_results_accepted_list=acceptances,
        )

        # Bankster compares the amount with the available deposit minus the existing claims against requestor's account.
        # If the whole amount can't be paid, Concent lowers it to pay as much as possible.
        requestor_payable_amount = min(
            amount_pending,
            requestor_deposit_value -
            sum_of_existing_requestor_claims['sum_of_existing_claims'],
        )

        logger.info(
            f'requestor_payable_amount is {requestor_payable_amount} for ethereum address {requestor_ethereum_address}.'
        )

        if requestor_payable_amount <= 0:
            return None

        transaction_hash = service.make_force_payment_to_provider(  # pylint: disable=no-value-for-parameter
            requestor_eth_address=requestor_ethereum_address,
            provider_eth_address=provider_ethereum_address,
            value=requestor_payable_amount,
            payment_ts=cut_off_time,
        )
        transaction_hash = adjust_transaction_hash(transaction_hash)

        # Deposit lock for requestor.
        claim_against_requestor = DepositClaim(
            payee_ethereum_address=provider_ethereum_address,
            payer_deposit_account=requestor_deposit_account,
            amount=requestor_payable_amount,
            concent_use_case=ConcentUseCase.FORCED_PAYMENT,
            tx_hash=transaction_hash,
        )
        claim_against_requestor.full_clean()
        claim_against_requestor.save()

    return claim_against_requestor
Ejemplo n.º 9
0
def finalize_payment(deposit_claim: DepositClaim) -> Optional[str]:
    """
    This operation tells Bankster to pay out funds from deposit.
    For each claim, Bankster uses SCI to submit an Ethereum transaction to the Ethereum client which then propagates it
    to the rest of the network.
    Hopefully the transaction is included in one of the upcoming blocks on the blockchain.
    """

    assert isinstance(deposit_claim, DepositClaim)

    # Bankster asks SCI about the amount of funds available on the deposit account listed in the DepositClaim.
    available_funds = service.get_deposit_value(  # pylint: disable=no-value-for-parameter
        client_eth_address=deposit_claim.payer_deposit_account.ethereum_address
    )

    # Bankster begins a database transaction and puts a database lock on the DepositAccount object.
    with transaction.atomic(using='control'):
        DepositAccount.objects.select_for_update().get(
            pk=deposit_claim.payer_deposit_account_id)

        # Bankster sums the amounts of all existing DepositClaims that have the same payer as the one being processed.
        aggregated_client_claims = DepositClaim.objects.filter(
            payer_deposit_account=deposit_claim.payer_deposit_account).exclude(
                pk=deposit_claim.pk).aggregate(
                    sum_of_existing_claims=Coalesce(Sum('amount'), 0))

        # Bankster subtracts that value from the amount of funds available in the deposit.
        available_funds_without_claims = available_funds - aggregated_client_claims[
            'sum_of_existing_claims']

        # If the result is negative or zero, Bankster removes the DepositClaim object being processed.
        if available_funds_without_claims <= 0:
            deposit_claim.delete()
            return None

        # Otherwise if the result is lower than DepositAccount.amount,
        # Bankster sets this field to the amount that's actually available.
        elif available_funds_without_claims < deposit_claim.amount:
            deposit_claim.amount = available_funds_without_claims

        # If the DepositClaim still exists at this point, Bankster uses SCI to create an Ethereum transaction.
        if deposit_claim.concent_use_case == ConcentUseCase.FORCED_ACCEPTANCE:
            ethereum_transaction_hash = service.force_subtask_payment(  # pylint: disable=no-value-for-parameter
                requestor_eth_address=deposit_claim.payer_deposit_account.
                ethereum_address,
                provider_eth_address=deposit_claim.payee_ethereum_address,
                value=deposit_claim.amount,
                subtask_id=deposit_claim.subtask_id,
            )
        elif deposit_claim.concent_use_case == ConcentUseCase.ADDITIONAL_VERIFICATION:
            subtask = Subtask.objects.filter(
                subtask_id=deposit_claim.subtask_id).first()  # pylint: disable=no-member
            if subtask is not None:
                task_to_compute = deserialize_message(
                    subtask.task_to_compute.data.tobytes())
                if task_to_compute.requestor_ethereum_address == deposit_claim.payer_deposit_account.ethereum_address:
                    ethereum_transaction_hash = service.force_subtask_payment(  # pylint: disable=no-value-for-parameter
                        requestor_eth_address=deposit_claim.
                        payer_deposit_account.ethereum_address,
                        provider_eth_address=deposit_claim.
                        payee_ethereum_address,
                        value=deposit_claim.amount,
                        subtask_id=deposit_claim.subtask_id,
                    )
                elif task_to_compute.provider_ethereum_address == deposit_claim.payer_deposit_account.ethereum_address:
                    ethereum_transaction_hash = service.cover_additional_verification_cost(  # pylint: disable=no-value-for-parameter
                        provider_eth_address=deposit_claim.
                        payer_deposit_account.ethereum_address,
                        value=deposit_claim.amount,
                        subtask_id=deposit_claim.subtask_id,
                    )
                else:
                    assert False
        else:
            assert False

        # Bankster puts transaction ID in DepositClaim.tx_hash.
        ethereum_transaction_hash = adjust_transaction_hash(
            ethereum_transaction_hash)
        deposit_claim.tx_hash = ethereum_transaction_hash
        deposit_claim.full_clean()
        deposit_claim.save()

        service.call_on_confirmed_transaction(  # pylint: disable=no-value-for-parameter
            tx_hash=deposit_claim.tx_hash,
            callback=lambda _: discard_claim(deposit_claim),
        )

    return deposit_claim.tx_hash
Ejemplo n.º 10
0
class TestDepositClaimValidation(ConcentIntegrationTestCase):
    def setUp(self):
        super().setUp()
        task_to_compute = self._get_deserialized_task_to_compute()
        self.payer_ethereum_address = task_to_compute.requestor_ethereum_address
        self.payee_ethereum_address = task_to_compute.provider_ethereum_address

        client = Client(public_key_bytes=self.REQUESTOR_PUBLIC_KEY)
        client.clean()
        client.save()

        self.payer_deposit_account = DepositAccount()
        self.payer_deposit_account.client = client
        self.payer_deposit_account.ethereum_address = task_to_compute.requestor_ethereum_address
        self.payer_deposit_account.clean()
        self.payer_deposit_account.save()

        self.deposit_claim = DepositClaim()
        self.deposit_claim.payer_deposit_account = self.payer_deposit_account
        self.deposit_claim.subtask_id = task_to_compute.subtask_id
        self.deposit_claim.payee_ethereum_address = self.payee_ethereum_address
        self.deposit_claim.concent_use_case = ConcentUseCase.FORCED_TASK_RESULT.value
        self.deposit_claim.amount = 1
        self.deposit_claim.tx_hash = encode_hex(MOCK_TRANSACTION.hash)

    def test_that_exception_is_raised_when_subtask_is_null_and_concent_use_case_is_not_forced_payment(self):
        self.deposit_claim.subtask_id = None
        with pytest.raises(ValidationError) as exception_info:
            self.deposit_claim.clean()
        self.assertIn('subtask_id', exception_info.value.error_dict)

    def test_that_exception_is_not_raised_when_subtask_is_null_and_concent_use_case_is_forced_payment(self):
        self.deposit_claim.subtask = None
        self.deposit_claim.concent_use_case = ConcentUseCase.FORCED_PAYMENT.value
        self.deposit_claim.clean()

    def test_that_exception_is_raised_when_payee_ethereum_address_is_the_same_as_payer_deposit_account_ethereum_address(self):
        self.deposit_claim.payee_ethereum_address = self.deposit_claim.payer_deposit_account.ethereum_address

        with pytest.raises(ValidationError) as exception_info:
            self.deposit_claim.clean()
        self.assertIn('payer_deposit_account', exception_info.value.error_dict)

    def test_that_exception_is_raised_when_payee_ethereum_address_has_wrong_length(self):
        self.deposit_claim.payee_ethereum_address = self.payee_ethereum_address + '1'
        with pytest.raises(ValidationError) as exception_info:
            self.deposit_claim.clean()
        self.assertIn('payee_ethereum_address', exception_info.value.error_dict)

    def test_that_exception_is_raised_when_payee_ethereum_address_has_wrong_type(self):
        self.deposit_claim.payee_ethereum_address = b'x' * ETHEREUM_ADDRESS_LENGTH
        with pytest.raises(ValidationError) as exception_info:
            self.deposit_claim.clean()
        self.assertIn('payee_ethereum_address', exception_info.value.error_dict)

    def test_that_exception_is_raised_when_amount_is_equal_to_zero(self):
        self.deposit_claim.amount = 0
        with pytest.raises(ValidationError) as exception_info:
            self.deposit_claim.clean()
        self.assertIn('amount', exception_info.value.error_dict)

    def test_that_exception_is_raised_when_amount_is_less_then_zero(self):
        self.deposit_claim.amount = -1
        with pytest.raises(ValidationError) as exception_info:
            self.deposit_claim.clean()
        self.assertIn('amount', exception_info.value.error_dict)

    def test_that_exception_is_raised_when_amount_is_of_wrong_type(self):
        self.deposit_claim.amount = 5.0
        with pytest.raises(ValidationError):
            self.deposit_claim.clean()

    def test_that_exception_is_raised_when_tx_hash_is_not_none_and_not_string(self):
        self.deposit_claim.tx_hash = b'x' * ETHEREUM_TRANSACTION_HASH_LENGTH
        with pytest.raises(ValidationError) as exception_info:
            self.deposit_claim.clean()
        self.assertIn('tx_hash', exception_info.value.error_dict)

    def test_that_exception_is_raised_when_tx_hash_has_wrong_length(self):
        self.deposit_claim.tx_hash = encode_hex(MOCK_TRANSACTION.hash) + 'a'
        with pytest.raises(ValidationError) as exception_info:
            self.deposit_claim.clean()
        self.assertIn('tx_hash', exception_info.value.error_dict)

    def test_that_no_exception_is_raised_when_tx_hash_is_none(self):
        self.deposit_claim.tx_hash = None
        self.deposit_claim.clean()
        self.deposit_claim.save()

    def test_that_deposit_account_is_not_removed_when_deposit_claim_is_deleted(self):
        self.deposit_claim.clean()
        self.deposit_claim.save()

        DepositClaim.objects.filter(pk=self.deposit_claim.pk).delete()

        self.assertTrue(
            DepositAccount.objects.filter(pk=self.payer_deposit_account.pk).exists()  # pylint: disable=no-member
        )

    def test_that_no_exception_is_raised_when_deposit_claim_is_valid(self):
        self.deposit_claim.clean()
        self.deposit_claim.save()
Ejemplo n.º 11
0
class TestDepositClaimValidation(ConcentIntegrationTestCase):
    def setUp(self):
        super().setUp()
        task_to_compute = self._get_deserialized_task_to_compute()
        self.payer_ethereum_address = task_to_compute.requestor_ethereum_address
        self.payee_ethereum_address = task_to_compute.provider_ethereum_address

        client = Client(public_key_bytes=self.REQUESTOR_PUBLIC_KEY)
        client.clean()
        client.save()

        self.payer_deposit_account = DepositAccount()
        self.payer_deposit_account.client = client
        self.payer_deposit_account.ethereum_address = task_to_compute.requestor_ethereum_address
        self.payer_deposit_account.clean()
        self.payer_deposit_account.save()

        self.deposit_claim = DepositClaim()
        self.deposit_claim.payer_deposit_account = self.payer_deposit_account
        self.deposit_claim.subtask_id = task_to_compute.subtask_id
        self.deposit_claim.payee_ethereum_address = self.payee_ethereum_address
        self.deposit_claim.concent_use_case = ConcentUseCase.FORCED_TASK_RESULT.value
        self.deposit_claim.amount = 1
        self.deposit_claim.tx_hash = encode_hex(MOCK_TRANSACTION.hash)

    def test_that_exception_is_raised_when_subtask_is_null_and_concent_use_case_is_not_forced_payment(
            self):
        self.deposit_claim.subtask_id = None
        with pytest.raises(ValidationError) as exception_info:
            self.deposit_claim.clean()
        self.assertIn('subtask_id', exception_info.value.error_dict)

    def test_that_exception_is_not_raised_when_subtask_is_null_and_concent_use_case_is_forced_payment(
            self):
        self.deposit_claim.subtask = None
        self.deposit_claim.concent_use_case = ConcentUseCase.FORCED_PAYMENT.value
        self.deposit_claim.closure_time = parse_timestamp_to_utc_datetime(
            get_current_utc_timestamp())
        self.deposit_claim.clean()

    def test_that_exception_is_raised_when_payee_ethereum_address_is_the_same_as_payer_deposit_account_ethereum_address(
            self):
        self.deposit_claim.payee_ethereum_address = self.deposit_claim.payer_deposit_account.ethereum_address

        with pytest.raises(ValidationError) as exception_info:
            self.deposit_claim.clean()
        self.assertIn('payer_deposit_account', exception_info.value.error_dict)

    def test_that_exception_is_raised_when_amount_is_equal_to_zero(self):
        self.deposit_claim.amount = 0
        with pytest.raises(ValidationError) as exception_info:
            self.deposit_claim.clean()
        self.assertIn('amount', exception_info.value.error_dict)

    def test_that_exception_is_raised_when_amount_is_less_then_zero(self):
        self.deposit_claim.amount = -1
        with pytest.raises(ValidationError) as exception_info:
            self.deposit_claim.clean()
        self.assertIn('amount', exception_info.value.error_dict)

    def test_that_exception_is_not_raised_when_amount_is_at_max_length(self):
        self.deposit_claim.amount = int('1' * BIG_ENDIAN_INT_MAX_DIGITS)
        self.deposit_claim.clean()
        self.deposit_claim.save()

    def test_that_exception_is_raised_when_tx_hash_is_not_none_and_not_string(
            self):
        self.deposit_claim.tx_hash = '11'
        self.deposit_claim.clean()
        self.deposit_claim.save()

    def test_that_exception_is_raised_when_closure_time_is_none_in_use_case_forced_payment(
            self):
        self.deposit_claim.concent_use_case = ConcentUseCase.FORCED_PAYMENT.value
        self.deposit_claim.closure_time = None
        with pytest.raises(ValidationError) as exception_info:
            self.deposit_claim.clean()
        self.assertIn('closure_time', exception_info.value.error_dict)

    def test_that_exception_is_raised_when_closure_time_is_set_in_use_case_other_than_forced_payment(
            self):
        self.deposit_claim.concent_use_case = ConcentUseCase.ADDITIONAL_VERIFICATION.value
        self.deposit_claim.closure_time = parse_timestamp_to_utc_datetime(
            get_current_utc_timestamp())
        with pytest.raises(ValidationError) as exception_info:
            self.deposit_claim.clean()
        self.assertIn('closure_time', exception_info.value.error_dict)

    def test_that_no_exception_is_raised_when_tx_hash_is_none(self):
        self.deposit_claim.tx_hash = None
        self.deposit_claim.clean()
        self.deposit_claim.save()

    def test_that_deposit_account_is_not_removed_when_deposit_claim_is_deleted(
            self):
        self.deposit_claim.clean()
        self.deposit_claim.save()

        DepositClaim.objects.filter(pk=self.deposit_claim.pk).delete()

        self.assertTrue(
            DepositAccount.objects.filter(
                pk=self.payer_deposit_account.pk).exists()  # pylint: disable=no-member
        )

    def test_that_no_exception_is_raised_when_deposit_claim_is_valid(self):
        self.deposit_claim.clean()
        self.deposit_claim.save()