exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

commit b880b232b68fc8c6aa992ad94b5a672ff8354e3c
parent 1e7931392a5abd759afee2a18065ccbcdbf6176f
Author: Florian Dold <dold@taler.net>
Date:   Tue,  1 Sep 2026 14:48:41 +0200

KYC logic: trigger rules only above their thresholds

Treat configured thresholds as inclusive maximum amounts and add focused boundary coverage.\n\nFix #11416.

Diffstat:
Msrc/include/taler/taler_kyclogic_lib.h | 4+++-
Msrc/kyclogic/kyclogic_api.c | 15+++++++++++----
Msrc/kyclogic/meson.build | 19+++++++++++++++++++
Asrc/kyclogic/test_kyclogic.c | 188+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 221 insertions(+), 5 deletions(-)

diff --git a/src/include/taler/taler_kyclogic_lib.h b/src/include/taler/taler_kyclogic_lib.h @@ -274,7 +274,9 @@ TALER_KYCLOGIC_rules_free (struct TALER_KYCLOGIC_LegitimizationRuleSet *lrs); /** * Check if KYC is provided for a particular operation. Returns the set of - * checks that still need to be satisfied. + * checks that still need to be satisfied. Rule thresholds are inclusive + * limits: a total less than or equal to the threshold is permitted, and a + * rule is triggered only when the total exceeds its threshold. * * Called within a database transaction, so must * not start a new one. diff --git a/src/kyclogic/kyclogic_api.c b/src/kyclogic/kyclogic_api.c @@ -3944,14 +3944,21 @@ check_amount ( i); continue; /* out of time range for rule */ } - if (-1 == TALER_amount_cmp (&ktc->sum, - &rule->threshold)) + /* + * A KYC threshold is the inclusive maximum permitted by the rule: + * clients may transact up to and including this amount. The rule is + * triggered only once the accumulated total exceeds the threshold. + * Keeping equality on the permitted side is also important because + * exposed AccountLimit values are advertised as maximum contributions. + */ + if (0 >= TALER_amount_cmp (&ktc->sum, + &rule->threshold)) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Below threshold of %s for rule %u\n", + "At or below threshold of %s for rule %u\n", TALER_amount2s (&rule->threshold), i); - continue; /* sum < threshold */ + continue; /* sum <= threshold */ } if ( (NULL != ktc->triggered_rule) && (1 == TALER_amount_cmp (&ktc->triggered_rule->threshold, diff --git a/src/kyclogic/meson.build b/src/kyclogic/meson.build @@ -74,6 +74,25 @@ pkg.generate( ) +test_kyclogic = executable( + 'test_kyclogic', + ['test_kyclogic.c'], + dependencies: [ + libtalerkyclogic_dep, + libtalerutil_dep, + gnunetutil_dep, + ], + include_directories: [incdir, configuration_inc], + build_by_default: false, + install: false, +) +test( + 'test_kyclogic', + test_kyclogic, + suite: ['kyclogic'], +) + + executable( 'taler-exchange-helper-sanctions-dummy', diff --git a/src/kyclogic/test_kyclogic.c b/src/kyclogic/test_kyclogic.c @@ -0,0 +1,188 @@ +/* + This file is part of TALER + Copyright (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file kyclogic/test_kyclogic.c + * @brief Tests for KYC rule evaluation + * @author Florian Dold + */ +#include "platform.h" +#include "taler/taler_kyclogic_lib.h" +#include "taler/taler_util.h" + + +/** + * Amounts to return from the test iterator. + */ +struct TestAmounts +{ + /** + * Amounts, in reverse chronological order. + */ + const char *const *amounts; + + /** + * Number of entries in @e amounts. + */ + unsigned int num_amounts; +}; + + +/** + * Return the amounts for one boundary test. + * + * @param cls a `struct TestAmounts` + * @param limit earliest relevant transaction time + * @param cb callback to invoke for each amount + * @param cb_cls closure for @a cb + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +amount_iterator (void *cls, + struct GNUNET_TIME_Absolute limit, + TALER_KYCLOGIC_KycAmountCallback cb, + void *cb_cls) +{ + const struct TestAmounts *ta = cls; + struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get (); + + (void) limit; + for (unsigned int i=0; i<ta->num_amounts; i++) + { + struct TALER_Amount amount; + + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount (ta->amounts[i], + &amount)); + GNUNET_assert (GNUNET_OK == + cb (cb_cls, + &amount, + now)); + } + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; +} + + +/** + * Check whether the configured EUR:2 rule triggers for @a amounts. + * + * @param amounts amounts to add up + * @param num_amounts length of @a amounts + * @param expect_trigger true if the rule should trigger + */ +static void +check_threshold (const char *const amounts[], + unsigned int num_amounts, + bool expect_trigger) +{ + struct TestAmounts ta = { + .amounts = amounts, + .num_amounts = num_amounts + }; + const struct TALER_KYCLOGIC_KycRule *triggered_rule; + struct TALER_Amount next_threshold; + struct TALER_Amount expected_threshold; + + GNUNET_assert ( + GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + TALER_KYCLOGIC_kyc_test_required ( + TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT, + NULL, + &amount_iterator, + &ta, + &triggered_rule, + &next_threshold)); + GNUNET_assert (expect_trigger == (NULL != triggered_rule)); + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount ("EUR:2", + &expected_threshold)); + GNUNET_assert (0 == + TALER_amount_cmp (&expected_threshold, + &next_threshold)); +} + + +int +main (int argc, + const char *const argv[]) +{ + struct GNUNET_CONFIGURATION_Handle *cfg; + const char *const below[] = { "EUR:1.99999999" }; + const char *const equal[] = { "EUR:2" }; + const char *const cumulative_equal[] = { "EUR:1", "EUR:1" }; + const char *const cumulative_above[] = { "EUR:1", "EUR:1.00000001" }; + + (void) argc; + (void) argv; + GNUNET_log_setup ("test-kyclogic", + "WARNING", + NULL); + cfg = GNUNET_CONFIGURATION_create (TALER_EXCHANGE_project_data ()); + GNUNET_CONFIGURATION_set_value_string (cfg, + "exchange", + "CURRENCY", + "EUR"); + GNUNET_CONFIGURATION_set_value_string (cfg, + "kyc-rule-deposit-boundary", + "ENABLED", + "YES"); + GNUNET_CONFIGURATION_set_value_string (cfg, + "kyc-rule-deposit-boundary", + "EXPOSED", + "YES"); + GNUNET_CONFIGURATION_set_value_string (cfg, + "kyc-rule-deposit-boundary", + "IS_AND_COMBINATOR", + "YES"); + GNUNET_CONFIGURATION_set_value_string (cfg, + "kyc-rule-deposit-boundary", + "OPERATION_TYPE", + "DEPOSIT"); + GNUNET_CONFIGURATION_set_value_string (cfg, + "kyc-rule-deposit-boundary", + "THRESHOLD", + "EUR:2"); + GNUNET_CONFIGURATION_set_value_string (cfg, + "kyc-rule-deposit-boundary", + "TIMEFRAME", + "1 day"); + GNUNET_CONFIGURATION_set_value_string (cfg, + "kyc-rule-deposit-boundary", + "NEXT_MEASURES", + "verboten"); + GNUNET_assert (GNUNET_OK == + TALER_KYCLOGIC_kyc_init (cfg, + NULL)); + GNUNET_CONFIGURATION_destroy (cfg); + + /* The configured threshold is an inclusive maximum. */ + check_threshold (below, + sizeof (below) / sizeof (below[0]), + false); + check_threshold (equal, + sizeof (equal) / sizeof (equal[0]), + false); + check_threshold (cumulative_equal, + sizeof (cumulative_equal) / + sizeof (cumulative_equal[0]), + false); + check_threshold (cumulative_above, + sizeof (cumulative_above) / + sizeof (cumulative_above[0]), + true); + + TALER_KYCLOGIC_kyc_done (); + return 0; +}