exchange

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

commit a320eddc25461a3a853c4d5c72d5dea8f66be4fe
parent a44553fb46d44a2f600b1859160000f3f32bcfbd
Author: Christian Grothoff <christian@grothoff.org>
Date:   Sat,  1 Aug 2026 20:27:27 +0200

detect reconcilation issues with reported KYCAUTH in transfers

Diffstat:
Msrc/auditor/meson.build | 1+
Msrc/auditor/taler-auditor-httpd.c | 27+++++++++++++++++++++++++++
Asrc/auditor/taler-auditor-httpd_get-monitoring-kycauth-in-inconsistency.c | 152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/auditor/taler-auditor-httpd_get-monitoring-kycauth-in-inconsistency.h | 44++++++++++++++++++++++++++++++++++++++++++++
Msrc/auditor/taler-helper-auditor-wire-credit.c | 626+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/auditor/test-kyc.sh | 189++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Asrc/auditordb/0003-auditor_kycauth_in_inconsistency.sql | 39+++++++++++++++++++++++++++++++++++++++
Asrc/auditordb/0003-preamble.sql | 21+++++++++++++++++++++
Msrc/auditordb/auditor_do_gc_auditor.sql | 3+++
Asrc/auditordb/delete_kycauth_in_inconsistency.c | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/auditordb/get_kycauth_in_inconsistency.c | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/auditordb/helper.c | 1+
Asrc/auditordb/insert_kycauth_in_inconsistency.c | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/auditordb/iterate_kycauth_in_inconsistencies.c | 187+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/auditordb/meson.build | 11+++++++++++
Msrc/auditordb/restart.sql | 1+
Asrc/exchangedb/iterate_kycauth_in_above_serial_id_by_account.c | 170+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/include/auditor-database/delete_kycauth_in_inconsistency.h | 39+++++++++++++++++++++++++++++++++++++++
Asrc/include/auditor-database/get_kycauth_in_inconsistency.h | 43+++++++++++++++++++++++++++++++++++++++++++
Msrc/include/auditor-database/get_reserve_in_inconsistency.h | 3++-
Asrc/include/auditor-database/insert_kycauth_in_inconsistency.h | 40++++++++++++++++++++++++++++++++++++++++
Asrc/include/auditor-database/iterate_kycauth_in_inconsistencies.h | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/include/auditordb_lib.h | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/include/exchange-database/iterate_kycauth_in_above_serial_id_by_account.h | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
24 files changed, 1966 insertions(+), 34 deletions(-)

diff --git a/src/auditor/meson.build b/src/auditor/meson.build @@ -223,6 +223,7 @@ taler_auditor_httpd_SOURCES = [ 'taler-auditor-httpd_get-monitoring-progress.c', 'taler-auditor-httpd_get-monitoring-early-aggregation.c', 'taler-auditor-httpd_get-monitoring-pending-deposits.c', + 'taler-auditor-httpd_get-monitoring-kycauth-in-inconsistency.c', 'taler-auditor-httpd_get-monitoring-reserve-in-inconsistency.c', 'taler-auditor-httpd_get-monitoring-reserve-not-closed-inconsistency.c', 'taler-auditor-httpd_get-monitoring-denominations-without-sigs.c', diff --git a/src/auditor/taler-auditor-httpd.c b/src/auditor/taler-auditor-httpd.c @@ -51,6 +51,7 @@ #include "taler-auditor-httpd.h" #include "taler-auditor-httpd_delete-generic.h" #include "taler-auditor-httpd_patch-generic-suppressed.h" +#include "taler-auditor-httpd_get-monitoring-kycauth-in-inconsistency.h" #include "taler-auditor-httpd_get-monitoring-reserve-in-inconsistency.h" #include "taler-auditor-httpd_get-monitoring-reserve-not-closed-inconsistency.h" #include "taler-auditor-httpd_get-monitoring-denominations-without-sigs.h" @@ -669,6 +670,32 @@ handle_mhd_request (void *cls, .response_code = MHD_HTTP_OK, .requires_auth = true, .table = TALER_AUDITORDB_RESERVE_IN_INCONSISTENCY }, + { .url = "/monitoring/kycauth-in-inconsistency", + .method = MHD_HTTP_METHOD_GET, + .mime_type = "application/json", + .data = NULL, + .data_size = 0, + .handler = &TAH_get_monitoring_kycauth_in_inconsistency, + .response_code = MHD_HTTP_OK, + .requires_auth = true }, + { .url = "/monitoring/kycauth-in-inconsistency", + .method = MHD_HTTP_METHOD_DELETE, + .mime_type = "application/json", + .data = NULL, + .data_size = 0, + .handler = &TAH_delete_generic, + .response_code = MHD_HTTP_OK, + .requires_auth = true, + .table = TALER_AUDITORDB_KYCAUTH_IN_INCONSISTENCY }, + { .url = "/monitoring/kycauth-in-inconsistency", + .method = MHD_HTTP_METHOD_PATCH, + .mime_type = "application/json", + .data = NULL, + .data_size = 0, + .handler = &TAH_patch_generic_suppressed, + .response_code = MHD_HTTP_OK, + .requires_auth = true, + .table = TALER_AUDITORDB_KYCAUTH_IN_INCONSISTENCY }, { .url = "/monitoring/reserve-not-closed-inconsistency", .method = MHD_HTTP_METHOD_GET, .mime_type = "application/json", diff --git a/src/auditor/taler-auditor-httpd_get-monitoring-kycauth-in-inconsistency.c b/src/auditor/taler-auditor-httpd_get-monitoring-kycauth-in-inconsistency.c @@ -0,0 +1,152 @@ +/* + 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 src/auditor/taler-auditor-httpd_get-monitoring-kycauth-in-inconsistency.c + * @brief return list of KYCAUTH incoming wire transfer inconsistencies + * @author Christian Grothoff + */ +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <pthread.h> +#include "taler/taler_json_lib.h" +#include "taler/taler_mhd_lib.h" +#include "taler-auditor-httpd.h" +#include "taler-auditor-httpd_get-monitoring-kycauth-in-inconsistency.h" +#define TALER_AUDITORDB_KYCAUTH_IN_INCONSISTENCY_RESULT_CLOSURE json_t +#include "auditor-database/iterate_kycauth_in_inconsistencies.h" +#include "auditor-database/preflight.h" + + +/** + * Add kycauth-in-inconsistency to the list. + * + * @param[in,out] list a `json_t *` array to extend + * @param dc struct of inconsistencies + * @return #GNUNET_OK to continue to iterate, #GNUNET_SYSERR to stop iterating + */ +static enum GNUNET_GenericReturnValue +process_kycauth_in_inconsistency ( + json_t *list, + const struct TALER_AUDITORDB_KycauthInInconsistency *dc) +{ + json_t *obj; + + obj = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("row_id", + dc->serial_id), + GNUNET_JSON_pack_uint64 ("bank_row_id", + dc->bank_row_id), + TALER_JSON_pack_amount ("amount_exchange_expected", + &dc->amount_exchange_expected), + TALER_JSON_pack_amount ("amount_wired", + &dc->amount_wired), + GNUNET_JSON_pack_data_auto ("account_pub", + &dc->account_pub), + TALER_JSON_pack_time_abs_human ("timestamp", + dc->timestamp), + TALER_JSON_pack_full_payto ("account", + dc->account), + GNUNET_JSON_pack_string ("diagnostic", + dc->diagnostic), + GNUNET_JSON_pack_bool ("suppressed", + dc->suppressed) + ); + GNUNET_break (0 == + json_array_append_new (list, + obj)); + return GNUNET_OK; +} + + +enum MHD_Result +TAH_get_monitoring_kycauth_in_inconsistency ( + struct TAH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + const char *const args[]) +{ + json_t *ja; + enum GNUNET_DB_QueryStatus qs; + int64_t limit = -20; + uint64_t offset; + bool return_suppressed = false; + + (void) rh; + (void) connection_cls; + (void) upload_data; + (void) upload_data_size; + if (GNUNET_SYSERR == + TALER_AUDITORDB_preflight (TAH_apg)) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SETUP_FAILED, + NULL); + } + TALER_MHD_parse_request_snumber (connection, + "limit", + &limit); + if (limit < 0) + offset = INT64_MAX; + else + offset = 0; + TALER_MHD_parse_request_number (connection, + "offset", + &offset); + { + const char *ret_s + = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "return_suppressed"); + if ( (NULL != ret_s) && + (0 == strcmp (ret_s, + "true")) ) + { + return_suppressed = true; + } + } + ja = json_array (); + GNUNET_break (NULL != ja); + qs = TALER_AUDITORDB_iterate_kycauth_in_inconsistencies ( + TAH_apg, + limit, + offset, + return_suppressed, + &process_kycauth_in_inconsistency, + ja); + if (0 > qs) + { + GNUNET_break (0); + json_decref (ja); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "kycauth-in-inconsistency"); + } + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("kycauth_in_inconsistency", + ja)); +} + + +/* end of taler-auditor-httpd_get-monitoring-kycauth-in-inconsistency.c */ diff --git a/src/auditor/taler-auditor-httpd_get-monitoring-kycauth-in-inconsistency.h b/src/auditor/taler-auditor-httpd_get-monitoring-kycauth-in-inconsistency.h @@ -0,0 +1,44 @@ +/* + 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/> + */ +#ifndef TALER_AUDITOR_HTTPD_GET_MONITORING_KYCAUTH_IN_INCONSISTENCY_H +#define TALER_AUDITOR_HTTPD_GET_MONITORING_KYCAUTH_IN_INCONSISTENCY_H + +#include <gnunet/gnunet_util_lib.h> +#include <microhttpd.h> +#include "taler-auditor-httpd.h" + +/** + * Handle a "/monitoring/kycauth-in-inconsistency" request. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] connection_cls the connection's closure (can be updated) + * @param upload_data upload data + * @param[in,out] upload_data_size number of bytes (left) in @a upload_data + * @param args NULL-terminated array of remaining parts of the URI broken up at '/' + * @return MHD result code + */ +enum MHD_Result +TAH_get_monitoring_kycauth_in_inconsistency ( + struct TAH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + const char *const args[]); + + +#endif diff --git a/src/auditor/taler-helper-auditor-wire-credit.c b/src/auditor/taler-helper-auditor-wire-credit.c @@ -39,6 +39,9 @@ #include "auditor-database/get_balance.h" #include "auditor-database/insert_auditor_progress.h" #include "auditor-database/insert_balance.h" +#include "auditor-database/delete_kycauth_in_inconsistency.h" +#include "auditor-database/get_kycauth_in_inconsistency.h" +#include "auditor-database/insert_kycauth_in_inconsistency.h" #include "auditor-database/insert_misattribution_in_inconsistency.h" #include "auditor-database/insert_reserve_in_inconsistency.h" #include "auditor-database/insert_row_inconsistency.h" @@ -165,6 +168,12 @@ static enum GNUNET_DB_QueryStatus global_qs; static struct GNUNET_CONTAINER_MultiHashMap *in_map; /** + * Map with information about incoming KYCAUTH wire transfers. + * Maps hashes of the wire offsets to `struct KycauthInInfo`s. + */ +static struct GNUNET_CONTAINER_MultiHashMap *kycauth_map; + +/** * Head of list of wire accounts we still need to look at. */ static struct WireAccount *wa_head; @@ -289,6 +298,48 @@ struct ReserveInInfo /** + * Entry in #kycauth_map with the KYCAUTH transfer the exchange claims to + * have received, waiting to be matched against the bank's credit history. + */ +struct KycauthInInfo +{ + + /** + * Hash of expected row offset. + */ + struct GNUNET_HashCode row_off_hash; + + /** + * RowID in the kycauths_in table. + */ + uint64_t rowid; + + /** + * Public key the transfer associated with the debited account. + */ + union TALER_AccountPublicKeyP account_pub; + + /** + * Amount the exchange says it received. + */ + struct TALER_Amount credit; + + /** + * When the exchange says it received the funds. + */ + struct GNUNET_TIME_Timestamp execution_date; + + /** + * payto://-URL of the debited account, allocated at the end of this + * struct. NULL if the exchange has no `wire_targets` row for the + * account, which it only creates the first time it sees one. + */ + struct TALER_FullPayto sender_account_details; + +}; + + +/** * Free entry in #in_map. * * @param cls NULL @@ -314,6 +365,31 @@ free_rii (void *cls, /** + * Free entry in #kycauth_map. + * + * @param cls NULL + * @param key unused key + * @param value the `struct KycauthInInfo` to free + * @return #GNUNET_OK + */ +static enum GNUNET_GenericReturnValue +free_kii (void *cls, + const struct GNUNET_HashCode *key, + void *value) +{ + struct KycauthInInfo *kii = value; + + (void) cls; + GNUNET_assert (GNUNET_YES == + GNUNET_CONTAINER_multihashmap_remove (kycauth_map, + key, + kii)); + GNUNET_free (kii); + return GNUNET_OK; +} + + +/** * Task run on shutdown. * * @param cls NULL @@ -338,6 +414,14 @@ do_shutdown (void *cls) GNUNET_CONTAINER_multihashmap_destroy (in_map); in_map = NULL; } + if (NULL != kycauth_map) + { + GNUNET_CONTAINER_multihashmap_iterate (kycauth_map, + &free_kii, + NULL); + GNUNET_CONTAINER_multihashmap_destroy (kycauth_map); + kycauth_map = NULL; + } while (NULL != (wa = wa_head)) { if (NULL != wa->chh) @@ -399,6 +483,14 @@ rollback_and_reset (void) GNUNET_CONTAINER_multihashmap_destroy (in_map); in_map = NULL; } + if (NULL != kycauth_map) + { + GNUNET_CONTAINER_multihashmap_iterate (kycauth_map, + &free_kii, + NULL); + GNUNET_CONTAINER_multihashmap_destroy (kycauth_map); + kycauth_map = NULL; + } qs = begin_transaction (); if (GNUNET_DB_STATUS_HARD_ERROR == qs) break; @@ -510,6 +602,13 @@ conclude_credit_history (void) GNUNET_CONTAINER_multihashmap_destroy (in_map); in_map = NULL; } + if (NULL != kycauth_map) + { + GNUNET_assert (0 == + GNUNET_CONTAINER_multihashmap_size (kycauth_map)); + GNUNET_CONTAINER_multihashmap_destroy (kycauth_map); + kycauth_map = NULL; + } commit (global_qs); } @@ -568,6 +667,74 @@ check_equality (const struct TALER_Amount *credit, /** + * Check if two payto://-URLs designate the same bank account. + * + * @param a first account, may be NULL + * @param b second account, may be NULL + * @return true if they match, or if either side is unknown + */ +static bool +same_account (const struct TALER_FullPayto a, + const struct TALER_FullPayto b) +{ + struct TALER_NormalizedPayto na; + struct TALER_NormalizedPayto nb; + bool ret; + + /* The exchange only creates the wire_targets row for an account the + first time it sees one, so it legitimately may not know the debited + account of a KYCAUTH transfer. Do not turn that into a finding. */ + if ( (NULL == a.full_payto) || + (NULL == b.full_payto) ) + return true; + na = TALER_payto_normalize (a); + nb = TALER_payto_normalize (b); + ret = (0 == TALER_normalized_payto_cmp (na, + nb)); + GNUNET_free (na.normalized_payto); + GNUNET_free (nb.normalized_payto); + return ret; +} + + +/** + * Check if the two views of a KYCAUTH wire transfer agree in every + * respect. + * + * @param credit amount of the first view + * @param credit2 amount of the second view + * @param account_pub account key of the first view + * @param account_pub2 account key of the second view + * @param sender_account_details debited account of the first view + * @param sender_account_details2 debited account of the second view + * @param execution_date execution time of the first view + * @param execution_date2 execution time of the second view + * @return true if the two agree + */ +static bool +check_kycauth_equality ( + const struct TALER_Amount *credit, + const struct TALER_Amount *credit2, + const union TALER_AccountPublicKeyP *account_pub, + const union TALER_AccountPublicKeyP *account_pub2, + const struct TALER_FullPayto sender_account_details, + const struct TALER_FullPayto sender_account_details2, + struct GNUNET_TIME_Timestamp execution_date, + struct GNUNET_TIME_Timestamp execution_date2) +{ + return (0 == TALER_amount_cmp (credit, + credit2)) && + (0 == GNUNET_memcmp (account_pub, + account_pub2)) && + same_account (sender_account_details, + sender_account_details2) && + GNUNET_TIME_timestamp_cmp (execution_date, + ==, + execution_date2); +} + + +/** * Function called with details about incoming wire transfers that * established an account key for KYC authentication, as claimed by the * exchange DB. @@ -577,11 +744,17 @@ check_equality (const struct TALER_Amount *credit, * such: it is part of the balance the exchange's bank account is expected * to have, and the exchange may drain it together with its fee income. * + * The row is then remembered in #kycauth_map, so that the walk over the + * bank's credit history that follows can confirm the transfer actually + * happened; whatever is left in that map afterwards is money the exchange + * claims to have received and the bank knows nothing about. + * * @param cls a `struct WireAccount` we are processing * @param rowid unique serial ID for the entry in our DB * @param account_pub public key the transfer associated with the account * @param credit amount that was received - * @param sender_account_details payto://-URL of the sender's bank account + * @param sender_account_details payto://-URL of the sender's bank account, + * NULL if the exchange has no `wire_targets` row for it * @param wire_reference unique identifier for the wire transfer * @param execution_date when did we receive the funds * @return #GNUNET_OK to continue to iterate, #GNUNET_SYSERR to stop @@ -596,15 +769,16 @@ kycauth_in_cb (void *cls, struct GNUNET_TIME_Timestamp execution_date) { struct WireAccount *wa = cls; + struct KycauthInInfo *kii; + size_t slen; - (void) account_pub; - (void) sender_account_details; - (void) wire_reference; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Analyzing exchange KYCAUTH IN (%llu) at %s of %s\n", (unsigned long long) rowid, GNUNET_TIME_timestamp2s (execution_date), TALER_amount2s (credit)); + /* Advance the cursor up front: every path below has fully accounted for + this row, and reading it a second time would count the revenue twice. */ wa->last_kycauth_in_serial_id = rowid + 1; TALER_ARL_amount_add (&TALER_ARL_USE_AB (total_kycauth_revenue), &TALER_ARL_USE_AB (total_kycauth_revenue), @@ -612,6 +786,115 @@ kycauth_in_cb (void *cls, TALER_ARL_amount_add (&TALER_ARL_USE_AB (total_wire_in), &TALER_ARL_USE_AB (total_wire_in), credit); + { + enum GNUNET_DB_QueryStatus qs; + struct TALER_AUDITORDB_KycauthInInconsistency dc; + + qs = TALER_AUDITORDB_get_kycauth_in_inconsistency ( + TALER_ARL_adb, + wire_reference, + &dc); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + global_qs = qs; + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return GNUNET_SYSERR; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + { + bool healed = false; + + if (TALER_amount_is_zero (&dc.amount_exchange_expected)) + { + /* An earlier run saw this transfer at the bank while the + exchange had not booked it yet. If the two now agree, that + finding was a transient and we retract it. */ + healed = check_kycauth_equality (&dc.amount_wired, + credit, + &dc.account_pub, + account_pub, + dc.account, + sender_account_details, + GNUNET_TIME_absolute_to_timestamp ( + dc.timestamp), + execution_date); + } + GNUNET_free (dc.account.full_payto); + GNUNET_free (dc.diagnostic); + if (healed) + { + qs = TALER_AUDITORDB_delete_kycauth_in_inconsistency ( + TALER_ARL_adb, + dc.serial_id); + if (qs < 0) + { + global_qs = qs; + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return GNUNET_SYSERR; + } + /* We are retracting the finding, so the excess credit we booked + when we made it has to go as well -- otherwise a transfer the + exchange merely booked late inflates this balance forever. */ + TALER_ARL_amount_subtract ( + &TALER_ARL_USE_AB (total_bad_amount_in_plus), + &TALER_ARL_USE_AB (total_bad_amount_in_plus), + &dc.amount_wired); + /* The bank side of this transfer was accounted for when the + finding was made, so do not expect it again. */ + return GNUNET_OK; + } + break; + } + } + } + slen = (NULL == sender_account_details.full_payto) + ? 0 + : strlen (sender_account_details.full_payto) + 1; + kii = GNUNET_malloc (sizeof (struct KycauthInInfo) + slen); + kii->rowid = rowid; + kii->account_pub = *account_pub; + kii->credit = *credit; + kii->execution_date = execution_date; + if (0 != slen) + { + kii->sender_account_details.full_payto = (char *) &kii[1]; + GNUNET_memcpy (&kii[1], + sender_account_details.full_payto, + slen); + } + GNUNET_CRYPTO_hash (&wire_reference, + sizeof (uint64_t), + &kii->row_off_hash); + if (GNUNET_OK != + GNUNET_CONTAINER_multihashmap_put ( + kycauth_map, + &kii->row_off_hash, + kii, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)) + { + struct TALER_AUDITORDB_RowInconsistency ri = { + .row_id = rowid, + .row_table = (char *) "kycauths_in", + .diagnostic = (char *) "duplicate wire offset" + }; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Duplicate wire offset\n"); + qs = TALER_AUDITORDB_insert_row_inconsistency ( + TALER_ARL_adb, + &ri); + GNUNET_free (kii); + if (qs < 0) + { + global_qs = qs; + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return GNUNET_SYSERR; + } + } return GNUNET_OK; } @@ -648,6 +931,9 @@ reserve_in_cb (void *cls, GNUNET_TIME_timestamp2s (execution_date), TALER_amount2s (credit), TALER_B2S (reserve_pub)); + /* Advance the cursor up front: every path below has fully accounted for + this row, and reading it a second time would count the credit twice. */ + wa->last_reserve_in_serial_id = rowid + 1; TALER_ARL_amount_add (&TALER_ARL_USE_AB (total_wire_in), &TALER_ARL_USE_AB (total_wire_in), credit); @@ -669,19 +955,24 @@ reserve_in_cb (void *cls, case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: break; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - if (TALER_amount_is_zero (&dc.amount_exchange_expected)) { - /* database entry indicates unmatched transaction */ - enum GNUNET_GenericReturnValue ret; - - ret = check_equality (&dc.amount_wired, - credit, - &dc.reserve_pub, - reserve_pub, - dc.account, - sender_account_details, - GNUNET_TIME_absolute_to_timestamp (dc.timestamp), - execution_date); + enum GNUNET_GenericReturnValue ret = GNUNET_NO; + + if (TALER_amount_is_zero (&dc.amount_exchange_expected)) + { + /* database entry indicates unmatched transaction */ + ret = check_equality (&dc.amount_wired, + credit, + &dc.reserve_pub, + reserve_pub, + dc.account, + sender_account_details, + GNUNET_TIME_absolute_to_timestamp (dc.timestamp) + , + execution_date); + } + GNUNET_free (dc.account.full_payto); + GNUNET_free (dc.diagnostic); if (GNUNET_SYSERR == ret) return GNUNET_SYSERR; if (GNUNET_YES == ret) @@ -695,10 +986,17 @@ reserve_in_cb (void *cls, GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); return GNUNET_SYSERR; } + /* We are retracting the finding, so the excess credit we booked + when we made it has to go as well -- otherwise a transfer the + exchange merely booked late inflates this balance forever. */ + TALER_ARL_amount_subtract ( + &TALER_ARL_USE_AB (total_bad_amount_in_plus), + &TALER_ARL_USE_AB (total_bad_amount_in_plus), + &dc.amount_wired); return GNUNET_OK; } + break; } - break; } } slen = strlen (sender_account_details.full_payto) + 1; @@ -742,7 +1040,6 @@ reserve_in_cb (void *cls, } return GNUNET_OK; } - wa->last_reserve_in_serial_id = rowid + 1; return GNUNET_OK; } @@ -797,6 +1094,58 @@ complain_in_not_found (void *cls, /** + * Complain that we failed to match an entry from #kycauth_map: the + * exchange booked a KYCAUTH transfer the bank never made. Beyond the + * missing money, this means the exchange let an account authenticate + * without anyone ever having proven that they control it. + * + * @param cls a `struct WireAccount` + * @param key unused key + * @param value the `struct KycauthInInfo` we could not match + * @return #GNUNET_OK + */ +static enum GNUNET_GenericReturnValue +complain_kycauth_in_not_found (void *cls, + const struct GNUNET_HashCode *key, + void *value) +{ + struct WireAccount *wa = cls; + struct KycauthInInfo *kii = value; + enum GNUNET_DB_QueryStatus qs; + struct TALER_AUDITORDB_KycauthInInconsistency kiiDb = { + .bank_row_id = kii->rowid, + .diagnostic = (char *) + "KYCAUTH wire transfer claimed by exchange not found", + .account = (NULL == kii->sender_account_details.full_payto) + ? wa->ai->payto_uri + : kii->sender_account_details, + .amount_exchange_expected = kii->credit, + .amount_wired = zero, + .account_pub = kii->account_pub, + .timestamp = kii->execution_date.abs_time + }; + + (void) key; + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "KYCAUTH wire transfer #%llu claimed by exchange not found\n", + (unsigned long long) kii->rowid); + qs = TALER_AUDITORDB_insert_kycauth_in_inconsistency ( + TALER_ARL_adb, + &kiiDb); + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + global_qs = qs; + return GNUNET_SYSERR; + } + TALER_ARL_amount_add (&TALER_ARL_USE_AB (total_bad_amount_in_minus), + &TALER_ARL_USE_AB (total_bad_amount_in_minus), + &kii->credit); + return GNUNET_OK; +} + + +/** * Start processing the next wire account. * Shuts down if we are done. * @@ -834,11 +1183,234 @@ conclude_account (struct WireAccount *wa) return; } } + if (NULL != kycauth_map) + { + GNUNET_CONTAINER_multihashmap_iterate (kycauth_map, + &complain_kycauth_in_not_found, + wa); + /* clean up before 2nd phase */ + GNUNET_CONTAINER_multihashmap_iterate (kycauth_map, + &free_kii, + NULL); + if (global_qs < 0) + { + commit (global_qs); + return; + } + } process_credits (wa->next); } /** + * Analyze a KYCAUTH credit the bank reports against what the exchange + * booked in its `kycauths_in` table (collected in #kycauth_map). + * + * @param[in,out] wa account that received the transfer + * @param credit_details transfer details as reported by the bank + * @return true on success, false to stop loop at this point + */ +static bool +analyze_kycauth_credit ( + struct WireAccount *wa, + const struct TALER_BANK_CreditDetails *credit_details) +{ + struct KycauthInInfo *kii; + struct GNUNET_HashCode key; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Analyzing bank KYCAUTH CREDIT #%llu at %s of %s\n", + (unsigned long long) credit_details->serial_id, + GNUNET_TIME_timestamp2s (credit_details->execution_date), + TALER_amount2s (&credit_details->amount)); + TALER_ARL_amount_add (&TALER_ARL_USE_AB (total_kycauth_in), + &TALER_ARL_USE_AB (total_kycauth_in), + &credit_details->amount); + /* the bank charges its credit fee on this transfer just like on + any other, so it must be part of the total either way */ + TALER_ARL_amount_add (&TALER_ARL_USE_AB (total_wire_credit_fees), + &TALER_ARL_USE_AB (total_wire_credit_fees), + &credit_details->credit_fee); + GNUNET_CRYPTO_hash (&credit_details->serial_id, + sizeof (credit_details->serial_id), + &key); + kii = GNUNET_CONTAINER_multihashmap_get (kycauth_map, + &key); + if (NULL == kii) + { + struct TALER_AUDITORDB_KycauthInInconsistency dc = { + .bank_row_id = credit_details->serial_id, + .amount_exchange_expected = zero, + .amount_wired = credit_details->amount, + .account_pub = credit_details->details.kycauth.account_pub, + .timestamp = credit_details->execution_date.abs_time, + .account = credit_details->debit_account_uri, + .diagnostic = (char *) "unknown to exchange" + }; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to find KYCAUTH transfer at `%s' in exchange database.\n", + GNUNET_TIME_timestamp2s (credit_details->execution_date)); + qs = TALER_AUDITORDB_insert_kycauth_in_inconsistency (TALER_ARL_adb, + &dc); + if (qs <= 0) + { + global_qs = qs; + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return false; + } + TALER_ARL_amount_add (&TALER_ARL_USE_AB (total_bad_amount_in_plus), + &TALER_ARL_USE_AB (total_bad_amount_in_plus), + &credit_details->amount); + /* We have fully processed this credit: the finding is on file, and + kycauth_in_cb() retracts it if the exchange books the transfer + later. Looking at it again would only duplicate the finding. */ + wa->wire_off_in = credit_details->serial_id; + return true; + } + + /* Update offset */ + wa->wire_off_in = credit_details->serial_id; + if (0 != TALER_amount_cmp (&kii->credit, + &credit_details->amount)) + { + struct TALER_AUDITORDB_KycauthInInconsistency dc = { + .bank_row_id = credit_details->serial_id, + .amount_exchange_expected = kii->credit, + .amount_wired = credit_details->amount, + .account_pub = kii->account_pub, + .timestamp = kii->execution_date.abs_time, + .account = wa->ai->payto_uri, + .diagnostic = (char *) "wire amount does not match" + }; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "KYCAUTH transfer amount differs\n"); + qs = TALER_AUDITORDB_insert_kycauth_in_inconsistency ( + TALER_ARL_adb, + &dc); + if (qs <= 0) + { + global_qs = qs; + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return false; + } + if (0 < TALER_amount_cmp (&credit_details->amount, + &kii->credit)) + { + struct TALER_Amount delta; + + TALER_ARL_amount_subtract (&delta, + &credit_details->amount, + &kii->credit); + TALER_ARL_amount_add (&TALER_ARL_USE_AB (total_bad_amount_in_plus), + &TALER_ARL_USE_AB (total_bad_amount_in_plus), + &delta); + } + else + { + struct TALER_Amount delta; + + TALER_ARL_amount_subtract (&delta, + &kii->credit, + &credit_details->amount); + TALER_ARL_amount_add (&TALER_ARL_USE_AB (total_bad_amount_in_minus), + &TALER_ARL_USE_AB (total_bad_amount_in_minus), + &delta); + } + } + if (0 != GNUNET_memcmp (&kii->account_pub, + &credit_details->details.kycauth.account_pub)) + { + /* No money is missing, but the exchange associated a different key + with the account than the one the sender asked for: the wrong party + can now authenticate as the owner of that bank account. */ + struct TALER_AUDITORDB_KycauthInInconsistency dc = { + .bank_row_id = credit_details->serial_id, + .amount_exchange_expected = kii->credit, + .amount_wired = credit_details->amount, + .account_pub = kii->account_pub, + .timestamp = kii->execution_date.abs_time, + .account = wa->ai->payto_uri, + .diagnostic = (char *) "account public key does not match" + }; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "KYCAUTH account public key differs\n"); + qs = TALER_AUDITORDB_insert_kycauth_in_inconsistency ( + TALER_ARL_adb, + &dc); + if (qs <= 0) + { + global_qs = qs; + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return false; + } + } + if (! same_account (kii->sender_account_details, + credit_details->debit_account_uri)) + { + /* Again no money is missing: the exchange authorised the wrong bank + account. There is no reserve to close towards it either, so this + does not feed into #total_misattribution_in. */ + struct TALER_AUDITORDB_KycauthInInconsistency dc = { + .bank_row_id = credit_details->serial_id, + .amount_exchange_expected = kii->credit, + .amount_wired = credit_details->amount, + .account_pub = kii->account_pub, + .timestamp = kii->execution_date.abs_time, + .account = credit_details->debit_account_uri, + .diagnostic = (char *) "debited account does not match" + }; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Origin bank account of KYCAUTH transfer differs\n"); + qs = TALER_AUDITORDB_insert_kycauth_in_inconsistency ( + TALER_ARL_adb, + &dc); + if (qs <= 0) + { + global_qs = qs; + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return false; + } + } + if (GNUNET_TIME_timestamp_cmp (credit_details->execution_date, + !=, + kii->execution_date)) + { + struct TALER_AUDITORDB_RowMinorInconsistencies rmi = { + .problem_row = kii->rowid, + .diagnostic = (char *) "execution date mismatch", + .row_table = (char *) "kycauths_in" + }; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Execution date of KYCAUTH transfer differs\n"); + qs = TALER_AUDITORDB_insert_row_minor_inconsistencies ( + TALER_ARL_adb, + &rmi); + if (qs < 0) + { + global_qs = qs; + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return false; + } + } + GNUNET_assert (GNUNET_OK == + free_kii (NULL, + &key, + kii)); + return true; +} + + +/** * Analyze credit transaction @a details into @a wa. * * @param[in,out] wa account that received the transfer @@ -858,15 +1430,8 @@ analyze_credit ( case TALER_BANK_CT_RESERVE: break; case TALER_BANK_CT_KYCAUTH: - TALER_ARL_amount_add (&TALER_ARL_USE_AB (total_kycauth_in), - &TALER_ARL_USE_AB (total_kycauth_in), - &credit_details->amount); - /* the bank charges its credit fee on this transfer just like on - any other, so it must be part of the total either way */ - TALER_ARL_amount_add (&TALER_ARL_USE_AB (total_wire_credit_fees), - &TALER_ARL_USE_AB (total_wire_credit_fees), - &credit_details->credit_fee); - return true; + return analyze_kycauth_credit (wa, + credit_details); case TALER_BANK_CT_WAD: GNUNET_break (0); /* FIXME: Wad not yet supported */ return false; @@ -912,6 +1477,10 @@ analyze_credit ( TALER_ARL_amount_add (&TALER_ARL_USE_AB (total_bad_amount_in_plus), &TALER_ARL_USE_AB (total_bad_amount_in_plus), &credit_details->amount); + /* We have fully processed this credit: the finding is on file, and + reserve_in_cb() retracts it if the exchange books the transfer + later. Looking at it again would only duplicate the finding. */ + wa->wire_off_in = credit_details->serial_id; return true; } @@ -1244,6 +1813,9 @@ begin_credit_audit (void) GNUNET_assert (NULL == in_map); in_map = GNUNET_CONTAINER_multihashmap_create (1024, GNUNET_YES); + GNUNET_assert (NULL == kycauth_map); + kycauth_map = GNUNET_CONTAINER_multihashmap_create (1024, + GNUNET_YES); /* now go over all bank accounts and check delta with in_map */ process_credits (wa_head); } diff --git a/src/auditor/test-kyc.sh b/src/auditor/test-kyc.sh @@ -34,11 +34,14 @@ # # 1. the KYC artefacts (KYCAUTH credits, legitimization measures and # outcomes, an aggregation the exchange lawfully withheld) must not -# make the auditor invent findings, and the one KYC-specific balance it -# does track, total_kycauth_in, must be exactly right; +# make the auditor invent findings, and the KYCAUTH totals it does +# track -- total_kycauth_in from the bank, total_kycauth_revenue from +# the exchange -- must be exactly right; # 2. fault detection must still work on a KYC-enabled exchange, so the # later tests inject the same kinds of faults test-auditor.sh does and -# require the auditor to flag them. +# require the auditor to flag them. Tests 2, 8 and 9 pull the two +# KYCAUTH totals apart in each of the ways they can disagree, which is +# what the reconciliation between them exists to catch. # # Requires 'jq' tool and Postgres superuser rights! set -eu @@ -46,7 +49,7 @@ set -eu # Set of numbers for all the testcases. # When adding new tests, increase the last number: -ALL_TESTS=$(seq 0 7) +ALL_TESTS=$(seq 0 10) # $TESTS determines which tests we should run. # This construction is used to make it easy to @@ -571,6 +574,8 @@ function check_all_clean() { check_no_report "wire-out-inconsistency" echo -n "Test for reserve in inconsistencies... " check_no_report "reserve-in-inconsistency" + echo -n "Test for KYCAUTH in inconsistencies... " + check_no_report "kycauth-in-inconsistency" echo -n "Test for misattribution inconsistencies... " check_no_report "misattribution-in-inconsistency" echo -n "Test for row inconsistencies... " @@ -826,8 +831,6 @@ function test_2() { "Wrong total for KYCAUTH wire transfers" # Only the bank's books were touched, so the exchange still claims the # KYCAUTH revenue for a transfer the bank no longer reports as one. - # The auditor does not (yet) complain about that disagreement; see - # issue 1 in issues.txt. echo -n "Checking that the exchange still claims the revenue... " check_balance \ "total_kycauth_revenue" \ @@ -843,6 +846,23 @@ function test_2() { "TESTKUDOS:0.1" \ "Wrong total_bad_amount_in_plus" + # ... and the other side of the same disagreement: the exchange has a + # kycauths_in row for a transfer the bank now says was something else, + # so the auditor must not take the exchange's word for it. + echo -n "Checking that the unconfirmed KYCAUTH was flagged... " + check_report_any \ + "kycauth-in-inconsistency" \ + "diagnostic" "KYCAUTH wire transfer claimed by exchange not found" + echo -n "Checking the amount the exchange claims... " + check_report_any \ + "kycauth-in-inconsistency" \ + "amount_exchange_expected" "TESTKUDOS:0.1" + echo -n "Checking that it was counted as a missing credit... " + check_balance \ + "total_bad_amount_in_minus" \ + "TESTKUDOS:0.1" \ + "Wrong total_bad_amount_in_minus" + post_audit full_reload cleanup @@ -1021,6 +1041,163 @@ function test_7() { } +# The reverse of test 2: the bank reports a KYCAUTH transfer that the +# exchange has no record of. An exchange that could make such a row +# disappear could also make the account authorisation it justified +# disappear, so this must be flagged even though the money involved is +# small. +function test_8() { + + echo "===========8: KYCAUTH credit missing from the exchange===========" + echo -n "Modifying database: " + echo "DELETE FROM exchange.kycauths_in WHERE kycauth_in_serial_id=1" \ + | psql -Aqt "$DB" + echo "DONE" + + run_audit + check_auditor_running + + echo -n "Checking that the bank's view is unchanged... " + check_balance \ + "total_kycauth_in" \ + "$KYCAUTH_IN" \ + "Wrong total for KYCAUTH wire transfers" + echo -n "Checking that the exchange claims less revenue... " + check_balance \ + "total_kycauth_revenue" \ + "TESTKUDOS:0.1" \ + "Wrong KYCAUTH revenue" + echo -n "Checking that the unbooked KYCAUTH was flagged... " + check_report_any \ + "kycauth-in-inconsistency" \ + "diagnostic" "unknown to exchange" + echo -n "Checking the amount the bank reports... " + check_report_any \ + "kycauth-in-inconsistency" \ + "amount_wired" "TESTKUDOS:0.1" + echo -n "Checking that it was counted as an excess credit... " + check_balance \ + "total_bad_amount_in_plus" \ + "TESTKUDOS:0.1" \ + "Wrong total_bad_amount_in_plus" + + full_reload + cleanup +} + + +# The exchange booked a KYCAUTH transfer, but for a different amount than +# the bank actually moved. +function test_9() { + + echo "===========9: KYCAUTH amount inconsistency===========" + echo -n "Modifying database: " + echo "UPDATE exchange.kycauths_in SET credit.val=5 WHERE kycauth_in_serial_id=1" \ + | psql -Aqt "$DB" + echo "DONE" + + run_audit + check_auditor_running + + echo -n "Checking that the mismatch was flagged... " + check_report_any \ + "kycauth-in-inconsistency" \ + "diagnostic" "wire amount does not match" + echo -n "Checking the amount the exchange claims... " + check_report_any \ + "kycauth-in-inconsistency" \ + "amount_exchange_expected" "TESTKUDOS:5.1" + echo -n "Checking the amount the bank reports... " + check_report_any \ + "kycauth-in-inconsistency" \ + "amount_wired" "TESTKUDOS:0.1" + echo -n "Checking that the shortfall was counted... " + check_balance \ + "total_bad_amount_in_minus" \ + "TESTKUDOS:5" \ + "Wrong total_bad_amount_in_minus" + echo -n "Checking that the inflated revenue reached the balance sheet... " + check_balance \ + "total_kycauth_revenue" \ + "TESTKUDOS:5.2" \ + "Wrong KYCAUTH revenue" + + full_reload + cleanup +} + + +# The auditor may well see a KYCAUTH transfer at the bank before the +# exchange has booked it. It reports that, and must take the report back +# -- together with the excess credit it booked alongside -- once the +# exchange catches up. Without the retraction of the balance, a purely +# transient disagreement would inflate total_bad_amount_in_plus forever. +function test_10() { + + echo "===========10: KYCAUTH finding retracted when the exchange catches up===========" + echo -n "Modifying database: " + # Hide the exchange's record of the *last* KYCAUTH transfer, keeping a + # copy so it can be restored byte for byte. It has to be the last one: + # kycauth_in_cb() resumes from a progress point, so a row reinstated + # below that point would never be looked at again. + echo "CREATE TABLE stashed_kycauth AS + SELECT * FROM exchange.kycauths_in + WHERE kycauth_in_serial_id + = (SELECT MAX(kycauth_in_serial_id) FROM exchange.kycauths_in); + DELETE FROM exchange.kycauths_in + WHERE kycauth_in_serial_id + = (SELECT MAX(kycauth_in_serial_id) FROM exchange.kycauths_in);" \ + | psql -Aqt "$DB" + echo "DONE" + + pre_audit + audit_only + check_auditor_running + + echo -n "Checking that the unbooked transfer was reported... " + check_report_any \ + "kycauth-in-inconsistency" \ + "diagnostic" "unknown to exchange" + echo -n "Checking that it was counted as an excess credit... " + check_balance \ + "total_bad_amount_in_plus" \ + "TESTKUDOS:0.1" \ + "Wrong total_bad_amount_in_plus" + + # The exchange catches up. No progress points are touched: the credit + # audit is simply run once more, exactly as a resident helper would on + # its next wake-up. + echo -n "Letting the exchange catch up: " + echo "INSERT INTO exchange.kycauths_in SELECT * FROM stashed_kycauth; + DROP TABLE stashed_kycauth;" \ + | psql -Aqt "$DB" + echo "DONE" + + echo -n "Re-running the credit audit ..." + $VALGRIND taler-helper-auditor-wire-credit \ + -i \ + -L DEBUG \ + -c "$CONF" \ + -t \ + > "${MY_TMP_DIR}/test-audit-wire-credit-retract.out" \ + 2> "${MY_TMP_DIR}/test-audit-wire-credit-retract.err" \ + || exit_fail "wire credit re-audit failed (see ${MY_TMP_DIR}/test-audit-wire-credit-retract.*)" + echo " DONE" + + echo -n "Checking that the report was retracted... " + check_no_report "kycauth-in-inconsistency" + echo -n "Checking that the excess credit was retracted too... " + check_balance \ + "total_bad_amount_in_plus" \ + "TESTKUDOS:0" \ + "Retracted finding left total_bad_amount_in_plus inflated" + + post_audit + full_reload + cleanup +} + + # *************** Main test loop starts here ************** diff --git a/src/auditordb/0003-auditor_kycauth_in_inconsistency.sql b/src/auditordb/0003-auditor_kycauth_in_inconsistency.sql @@ -0,0 +1,39 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2014--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/> +-- + +SET search_path TO auditor; +CREATE TABLE IF NOT EXISTS auditor_kycauth_in_inconsistency +( + row_id BIGINT GENERATED BY DEFAULT AS IDENTITY UNIQUE PRIMARY KEY, + bank_row_id INT8 NOT NULL, + amount_exchange_expected taler_amount NOT NULL, + amount_wired taler_amount NOT NULL, + account_pub BYTEA NOT NULL CHECK (LENGTH(account_pub)=32), + timestamp BIGINT NOT NULL, + account TEXT NOT NULL, + diagnostic TEXT NOT NULL, + suppressed BOOLEAN NOT NULL DEFAULT FALSE, + creation_date INT8 NOT NULL + DEFAULT (EXTRACT(EPOCH FROM NOW()) * 1000 * 1000)::BIGINT +); +COMMENT ON TABLE auditor_kycauth_in_inconsistency + IS 'Report a KYCAUTH wire transfer where the bank and the exchange disagree.'; +COMMENT ON COLUMN auditor_kycauth_in_inconsistency.account_pub + IS 'account public key the transfer established, as known to whichever side did report the transfer'; +COMMENT ON COLUMN auditor_kycauth_in_inconsistency.account + IS 'bank account that was debited, as known to whichever side did report the transfer'; +COMMENT ON COLUMN auditor_kycauth_in_inconsistency.creation_date + IS 'for garbage collection'; diff --git a/src/auditordb/0003-preamble.sql b/src/auditordb/0003-preamble.sql @@ -0,0 +1,21 @@ +-- +-- 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/> +-- + +BEGIN; + +SELECT _v.register_patch('auditor-0003', NULL, NULL); + +SET search_path TO auditor; diff --git a/src/auditordb/auditor_do_gc_auditor.sql b/src/auditordb/auditor_do_gc_auditor.sql @@ -65,6 +65,9 @@ BEGIN DELETE FROM auditor_reserve_balance_summary_wrong_inconsistency WHERE suppressed AND creation_date < cutoff; + DELETE FROM auditor_kycauth_in_inconsistency + WHERE suppressed + AND creation_date < cutoff; DELETE FROM auditor_reserve_in_inconsistency WHERE suppressed AND creation_date < cutoff; diff --git a/src/auditordb/delete_kycauth_in_inconsistency.c b/src/auditordb/delete_kycauth_in_inconsistency.c @@ -0,0 +1,44 @@ +/* + 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 src/auditordb/delete_kycauth_in_inconsistency.c + * @brief Implementation of the delete_kycauth_in_inconsistency function for Postgres + * @author Christian Grothoff + */ +#include "taler/taler_pq_lib.h" +#include "auditor-database/delete_kycauth_in_inconsistency.h" +#include "pg_helper.h" + + +enum GNUNET_DB_QueryStatus +TALER_AUDITORDB_delete_kycauth_in_inconsistency ( + struct TALER_AUDITORDB_PostgresContext *pg, + uint64_t row_id) +{ + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_uint64 (&row_id), + GNUNET_PQ_query_param_end + }; + + PREPARE (pg, + "delete_kycauth_in_inconsistency", + "DELETE FROM auditor_kycauth_in_inconsistency " + " WHERE row_id=$1"); + return GNUNET_PQ_eval_prepared_non_select ( + pg->conn, + "delete_kycauth_in_inconsistency", + params); +} diff --git a/src/auditordb/get_kycauth_in_inconsistency.c b/src/auditordb/get_kycauth_in_inconsistency.c @@ -0,0 +1,78 @@ +/* + 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 src/auditordb/get_kycauth_in_inconsistency.c + * @brief Implementation of the get_kycauth_in_inconsistency function for Postgres + * @author Christian Grothoff + */ +#include "taler/taler_error_codes.h" +#include "taler/taler_dbevents.h" +#include "taler/taler_pq_lib.h" +#include "auditor-database/get_kycauth_in_inconsistency.h" +#include "pg_helper.h" + + +enum GNUNET_DB_QueryStatus +TALER_AUDITORDB_get_kycauth_in_inconsistency ( + struct TALER_AUDITORDB_PostgresContext *pg, + uint64_t bank_row_id, + struct TALER_AUDITORDB_KycauthInInconsistency *dc) +{ + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_uint64 (&bank_row_id), + GNUNET_PQ_query_param_end + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_uint64 ("row_id", + &dc->serial_id), + TALER_PQ_RESULT_SPEC_AMOUNT ("amount_exchange_expected", + &dc->amount_exchange_expected), + TALER_PQ_RESULT_SPEC_AMOUNT ("amount_wired", + &dc->amount_wired), + GNUNET_PQ_result_spec_auto_from_type ("account_pub", + &dc->account_pub), + GNUNET_PQ_result_spec_absolute_time ("timestamp", + &dc->timestamp), + GNUNET_PQ_result_spec_string ("account", + &dc->account.full_payto), + GNUNET_PQ_result_spec_string ("diagnostic", + &dc->diagnostic), + GNUNET_PQ_result_spec_bool ("suppressed", + &dc->suppressed), + GNUNET_PQ_result_spec_end + }; + + dc->bank_row_id = bank_row_id; + PREPARE (pg, + "get_kycauth_in_inconsistency", + "SELECT" + " row_id" + ",amount_exchange_expected" + ",amount_wired" + ",account_pub" + ",timestamp" + ",account" + ",diagnostic" + ",suppressed" + " FROM auditor_kycauth_in_inconsistency" + " WHERE (bank_row_id = $1)" + ); + return GNUNET_PQ_eval_prepared_singleton_select ( + pg->conn, + "get_kycauth_in_inconsistency", + params, + rs); +} diff --git a/src/auditordb/helper.c b/src/auditordb/helper.c @@ -51,6 +51,7 @@ TALER_AUDITORDB_get_deletable_suppressable_table_name ( "auditor_row_minor_inconsistencies", "auditor_wire_format_inconsistency", "auditor_wire_out_inconsistency", + "auditor_kycauth_in_inconsistency", NULL, }; diff --git a/src/auditordb/insert_kycauth_in_inconsistency.c b/src/auditordb/insert_kycauth_in_inconsistency.c @@ -0,0 +1,60 @@ +/* + 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 src/auditordb/insert_kycauth_in_inconsistency.c + * @brief Implementation of the insert_kycauth_in_inconsistency function for Postgres + * @author Christian Grothoff + */ +#include "taler/taler_pq_lib.h" +#include "pg_helper.h" +#include "auditor-database/insert_kycauth_in_inconsistency.h" + + +enum GNUNET_DB_QueryStatus +TALER_AUDITORDB_insert_kycauth_in_inconsistency ( + struct TALER_AUDITORDB_PostgresContext *pg, + const struct TALER_AUDITORDB_KycauthInInconsistency *dc) +{ + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_uint64 (&dc->bank_row_id), + TALER_PQ_query_param_amount (pg->conn, + &dc->amount_exchange_expected), + TALER_PQ_query_param_amount (pg->conn, + &dc->amount_wired), + GNUNET_PQ_query_param_auto_from_type (&dc->account_pub), + GNUNET_PQ_query_param_absolute_time (&dc->timestamp), + GNUNET_PQ_query_param_string (dc->account.full_payto), + GNUNET_PQ_query_param_string (dc->diagnostic), + GNUNET_PQ_query_param_end + }; + + PREPARE (pg, + "insert_kycauth_in_inconsistency", + "INSERT INTO auditor_kycauth_in_inconsistency " + "(bank_row_id," + " amount_exchange_expected," + " amount_wired," + " account_pub," + " timestamp," + " account," + " diagnostic" + ") VALUES ($1,$2,$3,$4,$5,$6,$7);" + ); + return GNUNET_PQ_eval_prepared_non_select ( + pg->conn, + "insert_kycauth_in_inconsistency", + params); +} diff --git a/src/auditordb/iterate_kycauth_in_inconsistencies.c b/src/auditordb/iterate_kycauth_in_inconsistencies.c @@ -0,0 +1,187 @@ +/* + 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 src/auditordb/iterate_kycauth_in_inconsistencies.c + * @brief Implementation of the iterate_kycauth_in_inconsistencies function for Postgres + * @author Christian Grothoff + */ +#include "taler/taler_error_codes.h" +#include "taler/taler_dbevents.h" +#include "taler/taler_pq_lib.h" +#include "pg_helper.h" +#include "auditor-database/iterate_kycauth_in_inconsistencies.h" + + +struct KycauthInInconsistencyContext +{ + + /** + * Function to call for each inconsistency. + */ + TALER_AUDITORDB_KycauthInInconsistencyCallback cb; + + /** + * Closure for @e cb + */ + void *cb_cls; + + /** + * Plugin context. + */ + struct TALER_AUDITORDB_PostgresContext *pg; + + /** + * Query status to return. + */ + enum GNUNET_DB_QueryStatus qs; +}; + + +/** + * Helper function for #TALER_AUDITORDB_iterate_kycauth_in_inconsistencies(). + * To be called with the results of a SELECT statement + * that has returned @a num_results results. + * + * @param cls closure of type `struct KycauthInInconsistencyContext *` + * @param result the postgres result + * @param num_results the number of results in @a result + */ +static void +kycauth_in_inconsistency_cb (void *cls, + PGresult *result, + unsigned int num_results) +{ + struct KycauthInInconsistencyContext *dcc = cls; + struct TALER_AUDITORDB_PostgresContext *pg = dcc->pg; + + for (unsigned int i = 0; i < num_results; i++) + { + struct TALER_AUDITORDB_KycauthInInconsistency dc; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_uint64 ("row_id", + &dc.serial_id), + GNUNET_PQ_result_spec_uint64 ("bank_row_id", + &dc.bank_row_id), + TALER_PQ_RESULT_SPEC_AMOUNT ("amount_exchange_expected", + &dc.amount_exchange_expected), + TALER_PQ_RESULT_SPEC_AMOUNT ("amount_wired", + &dc.amount_wired), + GNUNET_PQ_result_spec_auto_from_type ("account_pub", + &dc.account_pub), + GNUNET_PQ_result_spec_absolute_time ("timestamp", + &dc.timestamp), + GNUNET_PQ_result_spec_string ("account", + &dc.account.full_payto), + GNUNET_PQ_result_spec_string ("diagnostic", + &dc.diagnostic), + GNUNET_PQ_result_spec_bool ("suppressed", + &dc.suppressed), + GNUNET_PQ_result_spec_end + }; + enum GNUNET_GenericReturnValue rval; + + if (GNUNET_OK != + GNUNET_PQ_extract_result (result, + rs, + i)) + { + GNUNET_break (0); + dcc->qs = GNUNET_DB_STATUS_HARD_ERROR; + return; + } + + dcc->qs = i + 1; + rval = dcc->cb (dcc->cb_cls, + &dc); + GNUNET_PQ_cleanup_result (rs); + if (GNUNET_OK != rval) + break; + } +} + + +enum GNUNET_DB_QueryStatus +TALER_AUDITORDB_iterate_kycauth_in_inconsistencies ( + struct TALER_AUDITORDB_PostgresContext *pg, + int64_t limit, + uint64_t offset, + bool return_suppressed, + TALER_AUDITORDB_KycauthInInconsistencyCallback cb, + void *cb_cls) +{ + uint64_t plimit = (uint64_t) ((limit < 0) ? -limit : limit); + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_uint64 (&offset), + GNUNET_PQ_query_param_bool (return_suppressed), + GNUNET_PQ_query_param_uint64 (&plimit), + GNUNET_PQ_query_param_end + }; + struct KycauthInInconsistencyContext dcc = { + .cb = cb, + .cb_cls = cb_cls, + .pg = pg + }; + enum GNUNET_DB_QueryStatus qs; + + PREPARE (pg, + "iterate_kycauth_in_inconsistencies_get_desc", + "SELECT" + " row_id" + ",bank_row_id" + ",amount_exchange_expected" + ",amount_wired" + ",account_pub" + ",timestamp" + ",account" + ",diagnostic" + ",suppressed" + " FROM auditor_kycauth_in_inconsistency" + " WHERE (row_id < $1)" + " AND ($2 OR suppressed is false)" + " ORDER BY row_id DESC" + " LIMIT $3" + ); + PREPARE (pg, + "iterate_kycauth_in_inconsistencies_get_asc", + "SELECT" + " row_id" + ",bank_row_id" + ",amount_exchange_expected" + ",amount_wired" + ",account_pub" + ",timestamp" + ",account" + ",diagnostic" + ",suppressed" + " FROM auditor_kycauth_in_inconsistency" + " WHERE (row_id > $1)" + " AND ($2 OR suppressed is false)" + " ORDER BY row_id ASC" + " LIMIT $3" + ); + qs = GNUNET_PQ_eval_prepared_multi_select ( + pg->conn, + (limit > 0) + ? "iterate_kycauth_in_inconsistencies_get_asc" + : "iterate_kycauth_in_inconsistencies_get_desc", + params, + &kycauth_in_inconsistency_cb, + &dcc); + if (qs > 0) + return dcc.qs; + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR != qs); + return qs; +} diff --git a/src/auditordb/meson.build b/src/auditordb/meson.build @@ -50,6 +50,12 @@ auditor_0002_sql = [ 'commit.sql', ] +auditor_0003_sql = [ + '0003-preamble.sql', + '0003-auditor_kycauth_in_inconsistency.sql', + 'commit.sql', +] + generated_sql = [ ['versioning.sql', ['versioning.sql']], ['drop.sql', ['drop.sql']], @@ -57,6 +63,7 @@ generated_sql = [ ['procedures.sql', procedures_sql], ['auditor-0001.sql', ['auditor-0001.sql']], ['auditor-0002.sql', auditor_0002_sql], + ['auditor-0003.sql', auditor_0003_sql], ] foreach g : generated_sql @@ -81,6 +88,7 @@ libtalerauditordb = library( 'delete_generic.c', 'delete_pending_deposit.c', 'delete_purse_info.c', + 'delete_kycauth_in_inconsistency.c', 'delete_reserve_in_inconsistency.c', 'delete_wire_out_inconsistency_if_matching.c', 'delete_reserve_info.c', @@ -104,6 +112,8 @@ libtalerauditordb = library( 'iterate_emergencies.c', 'iterate_exchange_signkeys.c', 'iterate_fee_time_inconsistencies.c', + 'get_kycauth_in_inconsistency.c', + 'iterate_kycauth_in_inconsistencies.c', 'iterate_misattribution_in_inconsistencies.c', 'iterate_progress_points.c', 'get_purse_info.c', @@ -137,6 +147,7 @@ libtalerauditordb = library( 'insert_emergency.c', 'insert_exchange_signkey.c', 'insert_fee_time_inconsistency.c', + 'insert_kycauth_in_inconsistency.c', 'insert_historic_denom_revenue.c', 'insert_historic_reserve_revenue.c', 'insert_misattribution_in_inconsistency.c', diff --git a/src/auditordb/restart.sql b/src/auditordb/restart.sql @@ -45,6 +45,7 @@ DELETE FROM auditor_emergency_by_count; DELETE FROM auditor_fee_time_inconsistency; DELETE FROM auditor_historic_denomination_revenue; DELETE FROM auditor_historic_reserve_summary; +DELETE FROM auditor_kycauth_in_inconsistency; DELETE FROM auditor_misattribution_in_inconsistency; DELETE FROM auditor_pending_deposits; DELETE FROM auditor_progress; diff --git a/src/exchangedb/iterate_kycauth_in_above_serial_id_by_account.c b/src/exchangedb/iterate_kycauth_in_above_serial_id_by_account.c @@ -0,0 +1,170 @@ +/* + This file is part of TALER + Copyright (C) 2025 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 exchangedb/iterate_kycauth_in_above_serial_id_by_account.c + * @brief Implementation of the iterate_kycauth_in_above_serial_id_by_account function for Postgres + * @author Christian Grothoff + */ +#include "taler/taler_pq_lib.h" +#include \ + "exchange-database/iterate_kycauth_in_above_serial_id_by_account.h" +#include "helper.h" + + +/** + * Closure for #kycauth_in_serial_helper_cb(). + */ +struct KycauthInSerialContext +{ + + /** + * Callback to call. + */ + TALER_EXCHANGEDB_KycauthInCallback cb; + + /** + * Closure for @e cb. + */ + void *cb_cls; + + /** + * Plugin context. + */ + struct TALER_EXCHANGEDB_PostgresContext *pg; + + /** + * Status code, set to #GNUNET_SYSERR on hard errors. + */ + enum GNUNET_GenericReturnValue status; +}; + + +/** + * Helper function to be called with the results of a SELECT statement + * that has returned @a num_results results. + * + * @param cls closure of type `struct KycauthInSerialContext` + * @param result the postgres result + * @param num_results the number of results in @a result + */ +static void +kycauth_in_serial_helper_cb (void *cls, + PGresult *result, + unsigned int num_results) +{ + struct KycauthInSerialContext *kisc = cls; + struct TALER_EXCHANGEDB_PostgresContext *pg = kisc->pg; + + for (unsigned int i = 0; i<num_results; i++) + { + union TALER_AccountPublicKeyP account_pub; + struct TALER_Amount credit; + struct TALER_FullPayto sender_account_details = { NULL }; + struct GNUNET_TIME_Timestamp execution_date; + uint64_t rowid; + uint64_t wire_reference; + bool no_sender_account_details; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_auto_from_type ("account_pub", + &account_pub), + GNUNET_PQ_result_spec_uint64 ("wire_reference", + &wire_reference), + TALER_PQ_RESULT_SPEC_AMOUNT ("credit", + &credit), + GNUNET_PQ_result_spec_timestamp ("execution_date", + &execution_date), + /* the exchange only creates the wire_targets row the first time it + sees the account, so we must not drop a transfer just because + that row is missing */ + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_string ("sender_account_details", + &sender_account_details.full_payto), + &no_sender_account_details), + GNUNET_PQ_result_spec_uint64 ("kycauth_in_serial_id", + &rowid), + GNUNET_PQ_result_spec_end + }; + enum GNUNET_GenericReturnValue ret; + + if (GNUNET_OK != + GNUNET_PQ_extract_result (result, + rs, + i)) + { + GNUNET_break (0); + kisc->status = GNUNET_SYSERR; + return; + } + ret = kisc->cb (kisc->cb_cls, + rowid, + &account_pub, + &credit, + sender_account_details, + wire_reference, + execution_date); + GNUNET_PQ_cleanup_result (rs); + if (GNUNET_OK != ret) + break; + } +} + + +enum GNUNET_DB_QueryStatus +TALER_EXCHANGEDB_iterate_kycauth_in_above_serial_id_by_account ( + struct TALER_EXCHANGEDB_PostgresContext *pg, + const char *account_name, + uint64_t serial_id, + TALER_EXCHANGEDB_KycauthInCallback cb, + void *cb_cls) +{ + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_uint64 (&serial_id), + GNUNET_PQ_query_param_string (account_name), + GNUNET_PQ_query_param_end + }; + struct KycauthInSerialContext kisc = { + .cb = cb, + .cb_cls = cb_cls, + .pg = pg, + .status = GNUNET_OK + }; + enum GNUNET_DB_QueryStatus qs; + + PREPARE (pg, + "iterate_kycauth_in_above_serial_id_by_account", + "SELECT" + " account_pub" + ",wire_reference" + ",credit" + ",execution_date" + ",wt.payto_uri AS sender_account_details" + ",kycauth_in_serial_id" + " FROM kycauths_in" + " LEFT JOIN wire_targets wt" + " ON (wire_source_h_payto = wire_target_h_payto)" + " WHERE kycauth_in_serial_id>=$1" + " AND exchange_account_section=$2" + " ORDER BY kycauth_in_serial_id ASC;"); + qs = GNUNET_PQ_eval_prepared_multi_select ( + pg->conn, + "iterate_kycauth_in_above_serial_id_by_account", + params, + &kycauth_in_serial_helper_cb, + &kisc); + if (GNUNET_OK != kisc.status) + return GNUNET_DB_STATUS_HARD_ERROR; + return qs; +} diff --git a/src/include/auditor-database/delete_kycauth_in_inconsistency.h b/src/include/auditor-database/delete_kycauth_in_inconsistency.h @@ -0,0 +1,39 @@ +/* + 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 src/include/auditor-database/delete_kycauth_in_inconsistency.h + * @brief implementation of the delete_kycauth_in_inconsistency function for Postgres + * @author Christian Grothoff + */ +#ifndef AUDITOR_DATABASE_DELETE_KYCAUTH_IN_INCONSISTENCY_H +#define AUDITOR_DATABASE_DELETE_KYCAUTH_IN_INCONSISTENCY_H + +#include "auditordb_lib.h" + + +/** + * Delete information about a KYCAUTH-in-inconsistency from the database. + * + * @param pg the database context + * @param row_id row of the inconsistency in our table + * @return query result status + */ +enum GNUNET_DB_QueryStatus +TALER_AUDITORDB_delete_kycauth_in_inconsistency ( + struct TALER_AUDITORDB_PostgresContext *pg, + uint64_t row_id); + +#endif diff --git a/src/include/auditor-database/get_kycauth_in_inconsistency.h b/src/include/auditor-database/get_kycauth_in_inconsistency.h @@ -0,0 +1,43 @@ +/* + 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 src/include/auditor-database/get_kycauth_in_inconsistency.h + * @brief implementation of the get_kycauth_in_inconsistency function for Postgres + * @author Christian Grothoff + */ +#ifndef AUDITOR_DATABASE_GET_KYCAUTH_IN_INCONSISTENCY_H +#define AUDITOR_DATABASE_GET_KYCAUTH_IN_INCONSISTENCY_H + +#include "auditordb_lib.h" + + +/** + * Return any KYCAUTH incoming inconsistency associated with the given + * @a bank_row_id. + * + * @param pg the database context + * @param bank_row_id row to select by + * @param[out] dc details to return; on success the caller must + * `GNUNET_free()` the @e account.full_payto and @e diagnostic strings + * @return query result status + */ +enum GNUNET_DB_QueryStatus +TALER_AUDITORDB_get_kycauth_in_inconsistency ( + struct TALER_AUDITORDB_PostgresContext *pg, + uint64_t bank_row_id, + struct TALER_AUDITORDB_KycauthInInconsistency *dc); + +#endif diff --git a/src/include/auditor-database/get_reserve_in_inconsistency.h b/src/include/auditor-database/get_reserve_in_inconsistency.h @@ -30,7 +30,8 @@ * * @param pg the database context * @param bank_row_id row to select by - * @param[out] dc details to return + * @param[out] dc details to return; on success the caller must + * `GNUNET_free()` the @e account.full_payto and @e diagnostic strings */ enum GNUNET_DB_QueryStatus TALER_AUDITORDB_get_reserve_in_inconsistency (struct diff --git a/src/include/auditor-database/insert_kycauth_in_inconsistency.h b/src/include/auditor-database/insert_kycauth_in_inconsistency.h @@ -0,0 +1,40 @@ +/* + 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 src/include/auditor-database/insert_kycauth_in_inconsistency.h + * @brief implementation of the insert_kycauth_in_inconsistency function for Postgres + * @author Christian Grothoff + */ +#ifndef AUDITOR_DATABASE_INSERT_KYCAUTH_IN_INCONSISTENCY_H +#define AUDITOR_DATABASE_INSERT_KYCAUTH_IN_INCONSISTENCY_H + +#include "auditordb_lib.h" + + +/** + * Insert information about a KYCAUTH wire transfer where the bank and the + * exchange disagree into the database. + * + * @param pg the database context + * @param dc inconsistency to store + * @return query result status + */ +enum GNUNET_DB_QueryStatus +TALER_AUDITORDB_insert_kycauth_in_inconsistency ( + struct TALER_AUDITORDB_PostgresContext *pg, + const struct TALER_AUDITORDB_KycauthInInconsistency *dc); + +#endif diff --git a/src/include/auditor-database/iterate_kycauth_in_inconsistencies.h b/src/include/auditor-database/iterate_kycauth_in_inconsistencies.h @@ -0,0 +1,70 @@ +/* + 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 src/include/auditor-database/iterate_kycauth_in_inconsistencies.h + * @brief implementation of the iterate_kycauth_in_inconsistencies function for Postgres + * @author Christian Grothoff + */ +#ifndef AUDITOR_DATABASE_ITERATE_KYCAUTH_IN_INCONSISTENCIES_H +#define AUDITOR_DATABASE_ITERATE_KYCAUTH_IN_INCONSISTENCIES_H + +#include "taler/taler_util.h" +#include "taler/taler_json_lib.h" +#include "auditordb_lib.h" + + +#ifndef TALER_AUDITORDB_KYCAUTH_IN_INCONSISTENCY_RESULT_CLOSURE +/** + * Type of the closure for #TALER_AUDITORDB_KycauthInInconsistencyCallback. + */ +#define TALER_AUDITORDB_KYCAUTH_IN_INCONSISTENCY_RESULT_CLOSURE void +#endif + +/* Callback typedefs */ +/** + * Function called with information about a KYCAUTH incoming inconsistency. + * + * @param cls closure + * @param dc the inconsistency + * @return #GNUNET_OK to continue to iterate, #GNUNET_SYSERR to stop + */ +typedef enum GNUNET_GenericReturnValue +(*TALER_AUDITORDB_KycauthInInconsistencyCallback)( + TALER_AUDITORDB_KYCAUTH_IN_INCONSISTENCY_RESULT_CLOSURE *cls, + const struct TALER_AUDITORDB_KycauthInInconsistency *dc); + + +/** + * Get information about kycauth-in-inconsistency from the database. + * + * @param pg the database context + * @param limit number of records to return, negative for descending + * @param offset table row to start from, exclusive, direction determined by @a limit + * @param return_suppressed should suppressed rows be returned anyway? + * @param cb function to call with results + * @param cb_cls closure for @a cb + * @return query result status + */ +enum GNUNET_DB_QueryStatus +TALER_AUDITORDB_iterate_kycauth_in_inconsistencies ( + struct TALER_AUDITORDB_PostgresContext *pg, + int64_t limit, + uint64_t offset, + bool return_suppressed, + TALER_AUDITORDB_KycauthInInconsistencyCallback cb, + TALER_AUDITORDB_KYCAUTH_IN_INCONSISTENCY_RESULT_CLOSURE *cb_cls); + +#endif diff --git a/src/include/auditordb_lib.h b/src/include/auditordb_lib.h @@ -290,6 +290,63 @@ struct TALER_AUDITORDB_ReserveInInconsistency }; /** + * Information about a disagreement between the bank and the exchange + * about an incoming wire transfer that established an account key for + * KYC authentication. + */ +struct TALER_AUDITORDB_KycauthInInconsistency +{ + /** + * Row of the finding in our own table. + */ + uint64_t serial_id; + + /** + * Row of the wire transfer in the bank's credit history. This is what + * the two sides are matched on: the exchange stores it in + * `kycauths_in.wire_reference`. + */ + uint64_t bank_row_id; + + /** + * Amount the exchange claims to have received, zero if the exchange + * does not know the transfer at all. + */ + struct TALER_Amount amount_exchange_expected; + + /** + * Amount the bank says it transferred, zero if the bank does not + * report the transfer at all. + */ + struct TALER_Amount amount_wired; + + /** + * Account public key the transfer established, as known to whichever + * side did report the transfer. + */ + union TALER_AccountPublicKeyP account_pub; + + /** + * When the transfer was executed. + */ + struct GNUNET_TIME_Absolute timestamp; + + /** + * Bank account that was debited, as known to whichever side did report + * the transfer. + */ + struct TALER_FullPayto account; + + /** + * Human-readable description of what does not match. + */ + char *diagnostic; + + bool suppressed; + +}; + +/** * Balance values for a reserve (or all reserves). */ struct TALER_AUDITORDB_ReserveFeeBalance @@ -581,6 +638,14 @@ enum TALER_AUDITORDB_DeletableSuppressableTables TALER_AUDITORDB_WIRE_OUT_INCONSISTENCY, /** + * For auditor_kycauth_in_inconsistency table. Note that new values + * must be appended here rather than inserted alphabetically: the + * numbering is what `TALER_AUDITORDB_get_deletable_suppressable_table_name()` + * indexes its table with. + */ + TALER_AUDITORDB_KYCAUTH_IN_INCONSISTENCY, + + /** * Terminal. */ TALER_AUDITORDB_DELETABLESUPPRESSABLE_TABLES_MAX diff --git a/src/include/exchange-database/iterate_kycauth_in_above_serial_id_by_account.h b/src/include/exchange-database/iterate_kycauth_in_above_serial_id_by_account.h @@ -0,0 +1,86 @@ +/* + This file is part of TALER + Copyright (C) 2025 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 src/include/exchange-database/iterate_kycauth_in_above_serial_id_by_account.h + * @brief implementation of the iterate_kycauth_in_above_serial_id_by_account function for Postgres + * @author Christian Grothoff + */ +#ifndef EXCHANGE_DATABASE_ITERATE_KYCAUTH_IN_ABOVE_SERIAL_ID_BY_ACCOUNT_H +#define EXCHANGE_DATABASE_ITERATE_KYCAUTH_IN_ABOVE_SERIAL_ID_BY_ACCOUNT_H + +#include "taler/taler_util.h" +#include "taler/taler_json_lib.h" +#include "exchangedb_lib.h" + + +/* Callback typedefs */ +/** + * Function called with details about incoming wire transfers that + * established an account key for KYC authentication. + * + * @param cls closure + * @param rowid unique serial ID of the entry in kycauths_in + * @param account_pub public key the transfer associated with the account + * @param credit amount that was received + * @param sender_account_details information about the sender's bank account, + * in payto://-format; NULL if the exchange has no wire_targets row + * for the debited account + * @param wire_reference unique identifier for the wire transfer + * @param execution_date when did we receive the funds + * @return #GNUNET_OK to continue to iterate, #GNUNET_SYSERR to stop + */ +#ifndef TALER_EXCHANGEDB_KYCAUTH_IN_RESULT_CLOSURE +/** + * Type of the closure for #TALER_EXCHANGEDB_KycauthInCallback. + */ +#define TALER_EXCHANGEDB_KYCAUTH_IN_RESULT_CLOSURE void +#endif +typedef enum GNUNET_GenericReturnValue +(*TALER_EXCHANGEDB_KycauthInCallback)( + TALER_EXCHANGEDB_KYCAUTH_IN_RESULT_CLOSURE *cls, + uint64_t rowid, + const union TALER_AccountPublicKeyP *account_pub, + const struct TALER_Amount *credit, + const struct TALER_FullPayto sender_account_details, + uint64_t wire_reference, + struct GNUNET_TIME_Timestamp execution_date); + +/** + * Select inbound wire transfers into kycauths_in above @a serial_id + * in monotonically increasing order by account. + * + * @param pg the database context + * @param account_name name of the account to select by + * @param serial_id lowest serial ID to include (the row itself is returned, + * so callers resuming from a progress point pass last seen + 1) + * @param cb function to call on each result + * @param cb_cls closure for @a cb + * @return transaction status code + */ +enum GNUNET_DB_QueryStatus +TALER_EXCHANGEDB_iterate_kycauth_in_above_serial_id_by_account (struct + TALER_EXCHANGEDB_PostgresContext + *pg, + const char * + account_name, + uint64_t + serial_id, + TALER_EXCHANGEDB_KycauthInCallback + cb, + TALER_EXCHANGEDB_KYCAUTH_IN_RESULT_CLOSURE + *cb_cls); + +#endif