-more general KYC logic

This commit is contained in:
Christian Grothoff 2022-08-04 11:36:05 +02:00
parent 266068c96c
commit 61f39f0941
No known key found for this signature in database
GPG Key ID: 939E6BE1E29FC3CC
3 changed files with 347 additions and 14 deletions

View File

@ -73,6 +73,11 @@ struct TEH_KycProvider
*/ */
struct TEH_KYCLOGIC_ProviderDetails *pd; struct TEH_KYCLOGIC_ProviderDetails *pd;
/**
* Cost of running this provider's KYC.
*/
unsigned long long cost;
/** /**
* Length of the @e checks array. * Length of the @e checks array.
*/ */
@ -156,7 +161,7 @@ static unsigned int num_kyc_triggers;
/** /**
* Array of configured providers. * Array of configured providers.
*/ */
static struct TEH_KycProvider *kyc_providers; static struct TEH_KycProvider **kyc_providers;
/** /**
* Length of the #kyc_providers array. * Length of the #kyc_providers array.
@ -419,6 +424,7 @@ add_provider (const char *section)
kp->provider_section_name = section; kp->provider_section_name = section;
kp->user_type = ut; kp->user_type = ut;
kp->logic = lp; kp->logic = lp;
kp->cost = cost;
add_checks (checks, add_checks (checks,
&kp->provided_checks, &kp->provided_checks,
&kp->num_checks); &kp->num_checks);
@ -574,6 +580,33 @@ handle_section (void *cls,
} }
/**
* Comparator for qsort. Compares two triggers
* by timeframe to sort triggers by time.
*
* @param p1 first trigger to compare
* @param p2 second trigger to compare
* @return -1 if p1 < p2, 0 if p1==p2, 1 if p1 > p2.
*/
static int
sort_by_timeframe (void *p1,
void *p2)
{
struct TEH_KycTrigger **t1 = p1;
struct TEH_KycTrigger **t2 = p2;
if (GNUNET_TIME_relative_cmp ((*t1)->timeframe,
<,
(*t2)->timeframe))
return -1;
if (GNUNET_TIME_relative_cmp ((*t1)->timeframe,
>,
(*t2)->timeframe))
return 1;
return 0;
}
enum GNUNET_GenericReturnValue enum GNUNET_GenericReturnValue
TEH_kyc_init (void) TEH_kyc_init (void)
{ {
@ -599,7 +632,10 @@ TEH_kyc_init (void)
TEH_kyc_done (); TEH_kyc_done ();
return GNUNET_SYSERR; return GNUNET_SYSERR;
} }
qsort (kyc_triggers,
num_kyc_triggers,
sizeof (struct TEH_KycTrigger *),
&sort_by_timeframe);
return GNUNET_OK; return GNUNET_OK;
} }
@ -654,22 +690,288 @@ TEH_kyc_done (void)
} }
/**
* Closure for the #eval_trigger().
*/
struct ThresholdTestContext
{
/**
* Total amount so far.
*/
struct TALER_Amount total;
/**
* Trigger event to evaluate triggers of.
*/
enum TEH_KycTriggerEvent event;
/**
* Offset in the triggers array where we need to start
* checking for triggers. All trigges below this
* offset were already hit.
*/
unsigned int start;
/**
* Array of checks needed so far.
*/
struct TEH_KycCheck **needed;
/**
* Pointer to number of entries used in @a needed.
*/
unsigned int *needed_cnt;
};
/**
* Function called on each @a amount that was found to
* be relevant for a KYC check.
*
* @param cls closure to allow the KYC module to
* total up amounts and evaluate rules
* @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
eval_trigger (void *cls,
const struct TALER_Amount *amount,
struct GNUNET_TIME_Absolute date)
{
struct ThresholdTestContext *ttc = cls;
struct GNUNET_TIME_Relative duration;
bool bump = true;
duration = GNUNET_TIME_absolute_get_duration (date);
if (0 >
TALER_amount_add (&ttc->total,
&ttc->total,
amount))
{
GNUNET_break (0);
return GNUNET_SYSERR;
}
for (unsigned int i = ttc->start; i<num_kyc_triggers; i++)
{
const struct TEH_KycTrigger *kt = kyc_triggers[i];
if (event != kt->trigger)
continue;
timeframe = GNUNET_TIME_relative_max (timeframe,
kt->timeframe);
if (GNUNET_TIME_relative_cmp (kt->timeframe,
>,
duration))
{
if (bump)
ttc->start = i;
return;
}
if (-1 ==
TALER_amount_cmp (&ttc->total,
&kt->threshold))
{
if (bump)
ttc->start = i;
bump = false;
continue; /* amount too low to trigger */
}
/* add check to list of required checks, unless
already present... */
for (unsigned int j = 0; j<kt->num_checks; j++)
{
struct TEH_KycCheck *rc = kt->required_checks[j];
bool found = false;
for (unsigned int k = 0; k<*ttc->needed_cnt; k++)
if (ttc->needed[k] == rc)
{
found = true;
break;
}
if (! found)
{
ttc->needed[*ttc->needed_cnt] = rc;
(*ttc->needed_cnt)++;
}
}
}
if (bump)
return GNUNET_NO; /* we hit all possible triggers! */
return GNUNET_OK;
}
/**
* Closure for the #remove_satisfied().
*/
struct RemoveContext
{
/**
* Array of checks needed so far.
*/
struct TEH_KycCheck **needed;
/**
* Pointer to number of entries used in @a needed.
*/
unsigned int *needed_cnt;
};
/**
* Remove all checks satisfied by @a provider_name from
* our list of checks.
*
* @param cls a `struct RemoveContext`
* @param provider_name section name of provider that was already run previously
*/
static void
remove_satisfied (void *cls,
const char *provider_name)
{
struct RemoveContext *rc = cls;
for (unsigned int i = 0; i<num_kyc_providers; i++)
{
const struct TEH_KycProvider *kp = kyc_providers[i];
if (0 != strcasecmp (provider_name,
kp->provider_section_name))
continue;
for (unsigned int j = 0; j<kp->num_checks; j++)
{
const struct TEH_KycCheck *kc = kp->provided_checks[j];
for (unsigned int k = 0; k<*rc->needed_cnt; k++)
if (kc == rc->needed[k])
{
rc->needed[k] = rc->needed[*rc->needed_cnt - 1];
(*rc->needed_cnt)--;
if (0 == *rc->needed_cnt)
return; /* for sure finished */
break;
}
}
break;
}
}
const char * const char *
TEH_kyc_test_required (enum TEH_KycTriggerEvent event, TEH_kyc_test_required (enum TEH_KycTriggerEvent event,
const struct TALER_PaytoHashP *h_payto, const struct TALER_PaytoHashP *h_payto,
TEH_KycAmountIterator ai, TEH_KycAmountIterator ai,
void *cls) void *cls)
{ {
// Check if event(s) may at all require KYC. struct TEH_KycCheck *needed[num_kyc_checks];
// If so, check what provider checks are unsigned int needed_cnt = 0;
// already satisfied for h_payto (with database) struct GNUNET_TIME_Relative timeframe;
// If unsatisfied checks are left, use 'ai' unsigned long long min_cost = ULONG_LONG_MAX;
// to check if amount is high enough to trigger them. unsigned int max_checks = 0;
// If it is, find cheapest provider that satisfies const struct TEH_KycProvider *kp_best = NULL;
// all of them (or, if multiple providers would be
// needed, return one of them). timeframe = GNUNET_TIME_UNIT_ZERO;
GNUNET_break (0); for (unsigned int i = 0; i<num_kyc_triggers; i++)
{
const struct TEH_KycTrigger *kt = kyc_triggers[i];
if (event != kt->trigger)
continue;
timeframe = GNUNET_TIME_relative_max (timeframe,
kt->timeframe);
}
{
struct GNUNET_TIME_Absolute now;
struct ThresholdTestContext ttc = {
.event = event,
.needed = needed,
.needed_cnt = &needed_cnt
};
TALER_amount_set_zero (TEH_currency,
&ttc.total);
now = GNUNET_TIME_absolute_get ();
ai (ai_cls,
GNUNET_TIME_absolute_subtract (now,
timeframe),
&eval_trigger,
&ttc);
}
if (0 == needed_cnt)
return NULL; return NULL;
{
struct RemoveContext rc = {
.needed = needed,
.needed_cnt = &needed_cnt
};
enum GNUNET_DB_QueryStatus qs;
/* Check what provider checks are already satisfied for h_payto (with
database), remove those from the 'needed' array. */
GNUNET_break (0);
qs = TEH_plugin->select_satisfied_kyc_processes (TEH_plugin->cls,
h_payto,
&remove_satisfied,
&rc);
GNUNET_break (qs >= 0); // FIXME: handle DB failure more nicely?
}
/* Count maximum number of remaining checks covered by any
provider */
for (unsigned int i = 0; i<num_kyc_providers; i++)
{
const struct TEH_KycProvider *kp = kyc_providers[i];
unsigned int matched = 0;
for (unsigned int j = 0; j<kp->num_checks; j++)
{
const struct TEH_KycCheck *kc = kp->provided_checks[j];
for (unsigned int k = 0; k<needed_cnt; k++)
if (kc == needed[k])
{
matched++;
break;
}
}
max_checks = GNUNET_MAX (max_checks,
matched);
}
/* Find min-cost provider covering max_checks. */
for (unsigned int i = 0; i<num_kyc_providers; i++)
{
const struct TEH_KycProvider *kp = kyc_providers[i];
unsigned int matched = 0;
for (unsigned int j = 0; j<kp->num_checks; j++)
{
const struct TEH_KycCheck *kc = kp->provided_checks[j];
for (unsigned int k = 0; k<needed_cnt; k++)
if (kc == needed[k])
{
matched++;
break;
}
}
if ( (max_checks == matched) &&
(kp->cost < min_cost) )
{
min_cost = kp->cost;
kp_best = kp;
}
}
GNUNET_assert (NULL != kp_best);
return kp_best->provider_section_name;
} }

View File

@ -203,8 +203,7 @@ TEH_kyc_test_required (enum TEH_KycTriggerEvent event,
/** /**
* Obtain the provider logic for a given * Obtain the provider logic for a given @a provider_section_name.
* @a provider_section_name.
* *
* @param provider_section_name identifies a KYC provider process * @param provider_section_name identifies a KYC provider process
* @param[out] plugin set to the KYC logic API * @param[out] plugin set to the KYC logic API

View File

@ -865,6 +865,20 @@ typedef void
const struct TALER_MasterSignatureP *master_sig); const struct TALER_MasterSignatureP *master_sig);
/**
* Function called on all KYC process names that the given
* account has already passed.
*
* @param cls closure
* @param kyc_provider_section_name configuration section
* of the respective KYC process
*/
typedef void
(*TALER_EXCHANGEDB_SatisfiedProviderCallback)(
void *cls,
const char *kyc_provider_section_name);
/** /**
* Function called with information about the exchange's auditors. * Function called with information about the exchange's auditors.
* *
@ -5607,6 +5621,24 @@ struct TALER_EXCHANGEDB_Plugin
uint64_t serial); uint64_t serial);
/**
* Call us on KYC processes satisfied for the given
* account.
*
* @param cls the @e cls of this struct with the plugin-specific state
* @param h_payto account identifier
* @param spc function to call for each satisfied KYC process
* @param spc_cls closure for @a spc
* @return transaction status code
*/
enum GNUNET_DB_QueryStatus
(*select_satisfied_kyc_processes)(
void *cls,
const struct TALER_PaytoHashP *h_payto,
TALER_EXCHANGEDB_SatisfiedProviderCallback spc,
void *spc_cls);
}; };
#endif /* _TALER_EXCHANGE_DB_H */ #endif /* _TALER_EXCHANGE_DB_H */