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:
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