def discard_claim(deposit_claim: DepositClaim) -> bool: """ This operation tells Bankster to discard the claim. Claim is simply removed, freeing the funds. """ assert isinstance(deposit_claim, DepositClaim) with transaction.atomic(using='control'): DepositAccount.objects.select_for_update().get( pk=deposit_claim.payer_deposit_account_id) if deposit_claim.tx_hash is None: claim_removed = False else: try: deposit_claim.delete() claim_removed = True except DepositAccount.DoesNotExist: claim_removed = False log( logger, f'After removing DepositClaim. Was DepositClaim removed: {claim_removed}. Tx_hash: {deposit_claim.tx_hash}', subtask_id=deposit_claim.subtask_id, ) return claim_removed
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 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
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