-more work on AML triggers for P2P transfers

This commit is contained in:
Christian Grothoff 2023-02-12 22:02:45 +01:00
parent 174022907b
commit 6db4bdbe6e
No known key found for this signature in database
GPG Key ID: 939E6BE1E29FC3CC
10 changed files with 338 additions and 33 deletions

@ -1 +1 @@
Subproject commit 3a616a04f1cd946bf0641b54cd71f1b858174f74
Subproject commit a7abaa856abbd16994132c5596ce04f442b9f4b9

View File

@ -147,6 +147,13 @@ struct TALER_EXCHANGEDB_Plugin *TEH_plugin;
*/
char *TEH_currency;
/**
* What is the largest amount we allow a peer to
* merge into a reserve before always triggering
* an AML check?
*/
struct TALER_Amount TEH_aml_threshold;
/**
* Our base URL.
*/
@ -1860,6 +1867,16 @@ exchange_serve_process_config (void)
"CURRENCY");
return GNUNET_SYSERR;
}
if (GNUNET_OK !=
TALER_config_get_amount (TEH_cfg,
"taler",
"AML_THRESHOLD",
&TEH_aml_threshold))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Need amount in section `TALER' under `AML_THRESHOLD'\n");
return GNUNET_SYSERR;
}
if (GNUNET_OK !=
GNUNET_CONFIGURATION_get_value_string (TEH_cfg,
"exchange",

View File

@ -97,6 +97,13 @@ extern struct TALER_EXCHANGEDB_Plugin *TEH_plugin;
*/
extern char *TEH_currency;
/**
* What is the largest amount we allow a peer to
* merge into a reserve before always triggering
* an AML check?
*/
extern struct TALER_Amount TEH_aml_threshold;
/**
* Our (externally visible) base URL.
*/

View File

@ -1,6 +1,6 @@
/*
This file is part of TALER
Copyright (C) 2014-2022 Taler Systems SA
Copyright (C) 2014-2023 Taler Systems SA
TALER is free software; you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@ -108,6 +108,11 @@ struct BatchWithdrawContext
*/
unsigned int planchets_length;
/**
* AML decision, #TALER_AML_NORMAL if we may proceed.
*/
enum TALER_AmlDecisionState aml_decision;
};
@ -150,6 +155,34 @@ batch_withdraw_amount_cb (void *cls,
}
/**
* Function called on each @a amount that was found to
* be relevant for the AML check as it was merged into
* the reserve.
*
* @param cls `struct TALER_Amount *` to total up the amounts
* @param amount encountered transaction amount
* @param date when was the amount encountered
* @return #GNUNET_OK to continue to iterate,
* #GNUNET_NO to abort iteration
* #GNUNET_SYSERR on internal error (also abort itaration)
*/
static enum GNUNET_GenericReturnValue
aml_amount_cb (
void *cls,
const struct TALER_Amount *amount,
struct GNUNET_TIME_Absolute date)
{
struct TALER_Amount *total = cls;
GNUNET_assert (0 <=
TALER_amount_add (total,
total,
amount));
return GNUNET_OK;
}
/**
* Function implementing withdraw transaction. Runs the
* transaction logic; IF it returns a non-error code, the transaction
@ -178,8 +211,102 @@ batch_withdraw_transaction (void *cls,
bool balance_ok = false;
bool found = false;
const char *kyc_required;
struct TALER_PaytoHashP reserve_h_payto;
wc->now = GNUNET_TIME_timestamp_get ();
/* Do AML check: compute total merged amount and check
against applicable AML threshold */
{
char *reserve_payto;
reserve_payto = TALER_reserve_make_payto (TEH_base_url,
wc->reserve_pub);
TALER_payto_hash (reserve_payto,
&reserve_h_payto);
GNUNET_free (reserve_payto);
}
{
struct TALER_Amount merge_amount;
struct TALER_Amount threshold;
struct GNUNET_TIME_Absolute now_minus_one_month;
now_minus_one_month
= GNUNET_TIME_absolute_subtract (wc->now.abs_time,
GNUNET_TIME_UNIT_MONTHS);
GNUNET_assert (GNUNET_OK ==
TALER_amount_set_zero (TEH_currency,
&merge_amount));
qs = TEH_plugin->select_merge_amounts_for_kyc_check (TEH_plugin->cls,
&reserve_h_payto,
now_minus_one_month,
&aml_amount_cb,
&merge_amount);
if (qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
if (GNUNET_DB_STATUS_HARD_ERROR == qs)
*mhd_ret = TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"select_merge_amounts_for_kyc_check");
return qs;
}
qs = TEH_plugin->select_aml_threshold (TEH_plugin->cls,
&reserve_h_payto,
&wc->aml_decision,
&threshold);
if (qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
if (GNUNET_DB_STATUS_HARD_ERROR == qs)
*mhd_ret = TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"select_aml_threshold");
return qs;
}
if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
{
threshold = TEH_aml_threshold; /* use default */
wc->aml_decision = TALER_AML_NORMAL;
}
switch (wc->aml_decision)
{
case TALER_AML_NORMAL:
if (0 >= TALER_amount_cmp (&merge_amount,
&threshold))
{
/* merge_amount <= threshold, continue withdraw below */
break;
}
wc->aml_decision = TALER_AML_PENDING;
qs = TEH_plugin->trigger_aml_process (TEH_plugin->cls,
&reserve_h_payto,
&merge_amount);
if (qs <= 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
if (GNUNET_DB_STATUS_HARD_ERROR == qs)
*mhd_ret = TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_STORE_FAILED,
"trigger_aml_process");
return qs;
}
return qs;
case TALER_AML_PENDING:
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"AML already pending, doing nothing\n");
return qs;
case TALER_AML_FROZEN:
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Account frozen, doing nothing\n");
return qs;
}
}
/* Check if the money came from a wire transfer */
qs = TEH_plugin->reserves_get_origin (TEH_plugin->cls,
wc->reserve_pub,
&wc->h_payto);
@ -352,6 +479,9 @@ generate_reply_success (const struct TEH_RequestContext *rc,
&wc->h_payto,
&wc->kyc);
}
if (TALER_AML_NORMAL != wc->aml_decision)
return TEH_RESPONSE_reply_aml_blocked (rc->connection,
wc->aml_decision);
sigs = json_array ();
GNUNET_assert (NULL != sigs);

View File

@ -1142,4 +1142,29 @@ TEH_RESPONSE_reply_kyc_required (struct MHD_Connection *connection,
}
MHD_RESULT
TEH_RESPONSE_reply_aml_blocked (struct MHD_Connection *connection,
enum TALER_AmlDecisionState status)
{
enum TALER_ErrorCode ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
switch (status)
{
case TALER_AML_NORMAL:
GNUNET_break (0);
return MHD_NO;
case TALER_AML_PENDING:
ec = TALER_EC_EXCHANGE_GENERIC_AML_PENDING;
break;
case TALER_AML_FROZEN:
ec = TALER_EC_EXCHANGE_GENERIC_AML_FROZEN;
break;
}
return TALER_MHD_REPLY_JSON_PACK (
connection,
MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS,
TALER_JSON_pack_ec (ec));
}
/* end of taler-exchange-httpd_responses.c */

View File

@ -91,6 +91,19 @@ TEH_RESPONSE_reply_kyc_required (struct MHD_Connection *connection,
const struct TALER_EXCHANGEDB_KycStatus *kyc);
/**
* Send information that an AML process is blocking
* the operation right now.
*
* @param connection connection to the client
* @param status current AML status
* @return MHD result code
*/
MHD_RESULT
TEH_RESPONSE_reply_aml_blocked (struct MHD_Connection *connection,
enum TALER_AmlDecisionState status);
/**
* Send assertion that the given denomination key hash
* is not usable (typically expired) at this time.

View File

@ -61,16 +61,21 @@ struct WithdrawContext
struct TALER_EXCHANGEDB_KycStatus kyc;
/**
* Hash of the payto-URI representing the reserve
* from which we are withdrawing.
* Hash of the payto-URI representing the account
* from which the money was put into the reserve.
*/
struct TALER_PaytoHashP h_payto;
struct TALER_PaytoHashP h_account_payto;
/**
* Current time for the DB transaction.
*/
struct GNUNET_TIME_Timestamp now;
/**
* AML decision, #TALER_AML_NORMAL if we may proceed.
*/
enum TALER_AmlDecisionState aml_decision;
};
@ -108,7 +113,7 @@ withdraw_amount_cb (void *cls,
return;
qs = TEH_plugin->select_withdraw_amounts_for_kyc_check (
TEH_plugin->cls,
&wc->h_payto,
&wc->h_account_payto,
limit,
cb,
cb_cls);
@ -120,6 +125,34 @@ withdraw_amount_cb (void *cls,
}
/**
* Function called on each @a amount that was found to
* be relevant for the AML check as it was merged into
* the reserve.
*
* @param cls `struct TALER_Amount *` to total up the amounts
* @param amount encountered transaction amount
* @param date when was the amount encountered
* @return #GNUNET_OK to continue to iterate,
* #GNUNET_NO to abort iteration
* #GNUNET_SYSERR on internal error (also abort itaration)
*/
static enum GNUNET_GenericReturnValue
aml_amount_cb (
void *cls,
const struct TALER_Amount *amount,
struct GNUNET_TIME_Absolute date)
{
struct TALER_Amount *total = cls;
GNUNET_assert (0 <=
TALER_amount_add (total,
total,
amount));
return GNUNET_OK;
}
/**
* Function implementing withdraw transaction. Runs the
* transaction logic; IF it returns a non-error code, the transaction
@ -150,23 +183,116 @@ withdraw_transaction (void *cls,
uint64_t ruuid;
const struct TALER_CsNonce *nonce;
const struct TALER_BlindedPlanchet *bp;
struct TALER_PaytoHashP reserve_h_payto;
wc->now = GNUNET_TIME_timestamp_get ();
/* Do AML check: compute total merged amount and check
against applicable AML threshold */
{
char *reserve_payto;
reserve_payto = TALER_reserve_make_payto (TEH_base_url,
&wc->collectable.reserve_pub);
TALER_payto_hash (reserve_payto,
&reserve_h_payto);
GNUNET_free (reserve_payto);
}
{
struct TALER_Amount merge_amount;
struct TALER_Amount threshold;
struct GNUNET_TIME_Absolute now_minus_one_month;
now_minus_one_month
= GNUNET_TIME_absolute_subtract (wc->now.abs_time,
GNUNET_TIME_UNIT_MONTHS);
GNUNET_assert (GNUNET_OK ==
TALER_amount_set_zero (TEH_currency,
&merge_amount));
qs = TEH_plugin->select_merge_amounts_for_kyc_check (TEH_plugin->cls,
&reserve_h_payto,
now_minus_one_month,
&aml_amount_cb,
&merge_amount);
if (qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
if (GNUNET_DB_STATUS_HARD_ERROR == qs)
*mhd_ret = TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"select_merge_amounts_for_kyc_check");
return qs;
}
qs = TEH_plugin->select_aml_threshold (TEH_plugin->cls,
&reserve_h_payto,
&wc->aml_decision,
&threshold);
if (qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
if (GNUNET_DB_STATUS_HARD_ERROR == qs)
*mhd_ret = TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"select_aml_threshold");
return qs;
}
if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
{
threshold = TEH_aml_threshold; /* use default */
wc->aml_decision = TALER_AML_NORMAL;
}
switch (wc->aml_decision)
{
case TALER_AML_NORMAL:
if (0 >= TALER_amount_cmp (&merge_amount,
&threshold))
{
/* merge_amount <= threshold, continue withdraw below */
break;
}
wc->aml_decision = TALER_AML_PENDING;
qs = TEH_plugin->trigger_aml_process (TEH_plugin->cls,
&reserve_h_payto,
&merge_amount);
if (qs <= 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
if (GNUNET_DB_STATUS_HARD_ERROR == qs)
*mhd_ret = TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_STORE_FAILED,
"trigger_aml_process");
return qs;
}
return qs;
case TALER_AML_PENDING:
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"AML already pending, doing nothing\n");
return qs;
case TALER_AML_FROZEN:
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Account frozen, doing nothing\n");
return qs;
}
}
/* Check if the money came from a wire transfer */
qs = TEH_plugin->reserves_get_origin (TEH_plugin->cls,
&wc->collectable.reserve_pub,
&wc->h_payto);
&wc->h_account_payto);
if (qs < 0)
return qs;
/* If no results, reserve was created by merge,
in which case no KYC check is required as the
merge already did that. */
/* If no results, reserve was created by merge, in which case no KYC check
is required as the merge already did that. */
if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
{
const char *kyc_required;
qs = TALER_KYCLOGIC_kyc_test_required (
TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW,
&wc->h_payto,
&wc->h_account_payto,
TEH_plugin->select_satisfied_kyc_processes,
TEH_plugin->cls,
&withdraw_amount_cb,
@ -191,7 +317,7 @@ withdraw_transaction (void *cls,
return TEH_plugin->insert_kyc_requirement_for_account (
TEH_plugin->cls,
kyc_required,
&wc->h_payto,
&wc->h_account_payto,
&wc->kyc.requirement_row);
}
}
@ -515,8 +641,13 @@ TEH_handler_withdraw (struct TEH_RequestContext *rc,
if (! wc.kyc.ok)
return TEH_RESPONSE_reply_kyc_required (rc->connection,
&wc.h_payto,
&wc.h_account_payto,
&wc.kyc);
if (TALER_AML_NORMAL != wc.aml_decision)
return TEH_RESPONSE_reply_aml_blocked (rc->connection,
wc.aml_decision);
{
MHD_RESULT ret;

View File

@ -7,6 +7,7 @@ TALER_RUNTIME_DIR = ${TMPDIR:-${TMP:-/tmp}}/${USER:-}/taler-system-runtime/
# Currency supported by the exchange (can only be one)
CURRENCY = EUR
CURRENCY_ROUND_UNIT = EUR:0.01
AML_THRESHOLD = EUR:1000000
[auditor]
TINY_AMOUNT = EUR:0.01

View File

@ -54,25 +54,6 @@ compute_notify_on_reserve (const struct TALER_ReservePublicKeyP *reserve_pub)
}
static void
notify_on_reserve (struct PostgresClosure *pg,
const struct TALER_ReservePublicKeyP *reserve_pub)
{
struct TALER_ReserveEventP rep = {
.header.size = htons (sizeof (rep)),
.header.type = htons (TALER_DBEVENT_EXCHANGE_RESERVE_INCOMING),
.reserve_pub = *reserve_pub
};
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Notifying on reserve!\n");
TEH_PG_event_notify (pg,
&rep.header,
NULL,
0);
}
static enum GNUNET_DB_QueryStatus
insert1 (struct PostgresClosure *pg,
const struct TALER_EXCHANGEDB_ReserveInInfo reserves[1],

View File

@ -41,7 +41,7 @@ TEH_PG_select_aml_threshold (
uint32_t status32 = TALER_AML_NORMAL;
struct GNUNET_PQ_ResultSpec rs[] = {
TALER_PQ_RESULT_SPEC_AMOUNT ("threshold",
&threshold),
threshold),
GNUNET_PQ_result_spec_uint32 ("status",
&status32),
GNUNET_PQ_result_spec_end