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
def test_that_when_there_are_other_deposit_claims_finalize_payment_substract_them_from_currently_processed_claim(self): self.deposit_claim = DepositClaim() self.deposit_claim.subtask_id = self._get_uuid('1') 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() # Save twice because we want two claims. self.deposit_claim.save() self.deposit_claim.pk = None self.deposit_claim.subtask_id = generate_uuid() self.deposit_claim.save() 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(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, # 5 - (2 + 2) | deposit_value - sum_of_other_claims value=1, subtask_id=self.deposit_claim.subtask_id, )
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 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()
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.clean() self.deposit_claim.save()
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 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)
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
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