-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; 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. * Our base URL.
*/ */
@ -1860,6 +1867,16 @@ exchange_serve_process_config (void)
"CURRENCY"); "CURRENCY");
return GNUNET_SYSERR; 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 != if (GNUNET_OK !=
GNUNET_CONFIGURATION_get_value_string (TEH_cfg, GNUNET_CONFIGURATION_get_value_string (TEH_cfg,
"exchange", "exchange",

View File

@ -97,6 +97,13 @@ extern struct TALER_EXCHANGEDB_Plugin *TEH_plugin;
*/ */
extern char *TEH_currency; 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. * Our (externally visible) base URL.
*/ */

View File

@ -1,6 +1,6 @@
/* /*
This file is part of TALER 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 TALER is free software; you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as it under the terms of the GNU Affero General Public License as
@ -108,6 +108,11 @@ struct BatchWithdrawContext
*/ */
unsigned int planchets_length; 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 * Function implementing withdraw transaction. Runs the
* transaction logic; IF it returns a non-error code, the transaction * 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 balance_ok = false;
bool found = false; bool found = false;
const char *kyc_required; const char *kyc_required;
struct TALER_PaytoHashP reserve_h_payto;
wc->now = GNUNET_TIME_timestamp_get (); 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, qs = TEH_plugin->reserves_get_origin (TEH_plugin->cls,
wc->reserve_pub, wc->reserve_pub,
&wc->h_payto); &wc->h_payto);
@ -352,6 +479,9 @@ generate_reply_success (const struct TEH_RequestContext *rc,
&wc->h_payto, &wc->h_payto,
&wc->kyc); &wc->kyc);
} }
if (TALER_AML_NORMAL != wc->aml_decision)
return TEH_RESPONSE_reply_aml_blocked (rc->connection,
wc->aml_decision);
sigs = json_array (); sigs = json_array ();
GNUNET_assert (NULL != sigs); 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 */ /* 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); 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 * Send assertion that the given denomination key hash
* is not usable (typically expired) at this time. * is not usable (typically expired) at this time.

View File

@ -61,16 +61,21 @@ struct WithdrawContext
struct TALER_EXCHANGEDB_KycStatus kyc; struct TALER_EXCHANGEDB_KycStatus kyc;
/** /**
* Hash of the payto-URI representing the reserve * Hash of the payto-URI representing the account
* from which we are withdrawing. * 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. * Current time for the DB transaction.
*/ */
struct GNUNET_TIME_Timestamp now; 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; return;
qs = TEH_plugin->select_withdraw_amounts_for_kyc_check ( qs = TEH_plugin->select_withdraw_amounts_for_kyc_check (
TEH_plugin->cls, TEH_plugin->cls,
&wc->h_payto, &wc->h_account_payto,
limit, limit,
cb, cb,
cb_cls); 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 * Function implementing withdraw transaction. Runs the
* transaction logic; IF it returns a non-error code, the transaction * transaction logic; IF it returns a non-error code, the transaction
@ -150,23 +183,116 @@ withdraw_transaction (void *cls,
uint64_t ruuid; uint64_t ruuid;
const struct TALER_CsNonce *nonce; const struct TALER_CsNonce *nonce;
const struct TALER_BlindedPlanchet *bp; const struct TALER_BlindedPlanchet *bp;
struct TALER_PaytoHashP reserve_h_payto;
wc->now = GNUNET_TIME_timestamp_get (); 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, qs = TEH_plugin->reserves_get_origin (TEH_plugin->cls,
&wc->collectable.reserve_pub, &wc->collectable.reserve_pub,
&wc->h_payto); &wc->h_account_payto);
if (qs < 0) if (qs < 0)
return qs; return qs;
/* If no results, reserve was created by merge, /* If no results, reserve was created by merge, in which case no KYC check
in which case no KYC check is required as the is required as the merge already did that. */
merge already did that. */
if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
{ {
const char *kyc_required; const char *kyc_required;
qs = TALER_KYCLOGIC_kyc_test_required ( qs = TALER_KYCLOGIC_kyc_test_required (
TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW, TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW,
&wc->h_payto, &wc->h_account_payto,
TEH_plugin->select_satisfied_kyc_processes, TEH_plugin->select_satisfied_kyc_processes,
TEH_plugin->cls, TEH_plugin->cls,
&withdraw_amount_cb, &withdraw_amount_cb,
@ -191,7 +317,7 @@ withdraw_transaction (void *cls,
return TEH_plugin->insert_kyc_requirement_for_account ( return TEH_plugin->insert_kyc_requirement_for_account (
TEH_plugin->cls, TEH_plugin->cls,
kyc_required, kyc_required,
&wc->h_payto, &wc->h_account_payto,
&wc->kyc.requirement_row); &wc->kyc.requirement_row);
} }
} }
@ -515,8 +641,13 @@ TEH_handler_withdraw (struct TEH_RequestContext *rc,
if (! wc.kyc.ok) if (! wc.kyc.ok)
return TEH_RESPONSE_reply_kyc_required (rc->connection, return TEH_RESPONSE_reply_kyc_required (rc->connection,
&wc.h_payto, &wc.h_account_payto,
&wc.kyc); &wc.kyc);
if (TALER_AML_NORMAL != wc.aml_decision)
return TEH_RESPONSE_reply_aml_blocked (rc->connection,
wc.aml_decision);
{ {
MHD_RESULT ret; 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 supported by the exchange (can only be one)
CURRENCY = EUR CURRENCY = EUR
CURRENCY_ROUND_UNIT = EUR:0.01 CURRENCY_ROUND_UNIT = EUR:0.01
AML_THRESHOLD = EUR:1000000
[auditor] [auditor]
TINY_AMOUNT = EUR:0.01 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 static enum GNUNET_DB_QueryStatus
insert1 (struct PostgresClosure *pg, insert1 (struct PostgresClosure *pg,
const struct TALER_EXCHANGEDB_ReserveInInfo reserves[1], const struct TALER_EXCHANGEDB_ReserveInInfo reserves[1],

View File

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