commit a44553fb46d44a2f600b1859160000f3f32bcfbd
parent 4a2f87e8e6a25adf70dc74bd230587d4f9647a4a
Author: Christian Grothoff <christian@grothoff.org>
Date: Sat, 1 Aug 2026 19:15:58 +0200
track KYC auth in revenue in auditor
Diffstat:
3 files changed, 145 insertions(+), 2 deletions(-)
diff --git a/src/auditor/taler-helper-auditor-wire-credit.c b/src/auditor/taler-helper-auditor-wire-credit.c
@@ -51,6 +51,8 @@
#include "exchange-database/preflight.h"
#include "exchange-database/rollback.h"
#include \
+ "exchange-database/iterate_kycauth_in_above_serial_id_by_account.h"
+#include \
"exchange-database/iterate_reserves_in_above_serial_id_by_account.h"
#include "exchange-database/start_read_only.h"
@@ -118,6 +120,11 @@ struct WireAccount
uint64_t start_reserve_in_serial_id;
/**
+ * Progress point for this account in the kycauths_in table.
+ */
+ uint64_t last_kycauth_in_serial_id;
+
+ /**
* Where we are in the inbound transaction history.
*/
uint64_t wire_off_in;
@@ -128,6 +135,11 @@ struct WireAccount
char *label_reserve_in_serial_id;
/**
+ * Label under which we store our pp's kycauth_in_serial_id.
+ */
+ char *label_kycauth_in_serial_id;
+
+ /**
* Label under which we store our wire_off_in.
*/
char *label_wire_off_in;
@@ -185,16 +197,28 @@ static TALER_ARL_DEF_AB (total_bad_amount_in_minus);
static TALER_ARL_DEF_AB (total_misattribution_in);
/**
- * Total amount credited to exchange accounts.
+ * Total amount credited to exchange accounts, as claimed by the exchange.
+ * Includes #total_kycauth_revenue, so that this is the credit side of the
+ * balance the exchange's bank account is expected to have.
*/
static TALER_ARL_DEF_AB (total_wire_in);
/**
- * Total amount credited to exchange accounts via KYCAUTH
+ * Total amount credited to exchange accounts via KYCAUTH, as reported by
+ * the bank.
*/
static TALER_ARL_DEF_AB (total_kycauth_in);
/**
+ * Total amount credited to exchange accounts via KYCAUTH, as booked by the
+ * exchange in its `kycauths_in` table. KYCAUTH funds are not credited to
+ * any reserve and are never paid back, so they are revenue of the exchange
+ * operator, like wire fees and closing fees, and may be drained together
+ * with those. Counted towards #total_wire_in for that reason.
+ */
+static TALER_ARL_DEF_AB (total_kycauth_revenue);
+
+/**
* Total wire credit fees charged to the exchange account.
*/
static TALER_ARL_DEF_AB (total_wire_credit_fees);
@@ -325,6 +349,7 @@ do_shutdown (void *cls)
wa_tail,
wa);
GNUNET_free (wa->label_reserve_in_serial_id);
+ GNUNET_free (wa->label_kycauth_in_serial_id);
GNUNET_free (wa->label_wire_off_in);
GNUNET_free (wa);
}
@@ -402,6 +427,7 @@ commit (enum GNUNET_DB_QueryStatus qs)
TALER_ARL_adb,
TALER_ARL_SET_AB (total_wire_in),
TALER_ARL_SET_AB (total_kycauth_in),
+ TALER_ARL_SET_AB (total_kycauth_revenue),
TALER_ARL_SET_AB (total_wire_credit_fees),
TALER_ARL_SET_AB (total_bad_amount_in_plus),
TALER_ARL_SET_AB (total_bad_amount_in_minus),
@@ -413,6 +439,7 @@ commit (enum GNUNET_DB_QueryStatus qs)
TALER_ARL_adb,
TALER_ARL_SET_AB (total_wire_in),
TALER_ARL_SET_AB (total_kycauth_in),
+ TALER_ARL_SET_AB (total_kycauth_revenue),
TALER_ARL_SET_AB (total_wire_credit_fees),
TALER_ARL_SET_AB (total_bad_amount_in_plus),
TALER_ARL_SET_AB (total_bad_amount_in_minus),
@@ -428,6 +455,8 @@ commit (enum GNUNET_DB_QueryStatus qs)
TALER_ARL_adb,
wa->label_reserve_in_serial_id,
wa->last_reserve_in_serial_id,
+ wa->label_kycauth_in_serial_id,
+ wa->last_kycauth_in_serial_id,
wa->label_wire_off_in,
wa->wire_off_in,
NULL);
@@ -437,6 +466,8 @@ commit (enum GNUNET_DB_QueryStatus qs)
TALER_ARL_adb,
wa->label_reserve_in_serial_id,
wa->last_reserve_in_serial_id,
+ wa->label_kycauth_in_serial_id,
+ wa->last_kycauth_in_serial_id,
wa->label_wire_off_in,
wa->wire_off_in,
NULL);
@@ -537,6 +568,55 @@ check_equality (const struct TALER_Amount *credit,
/**
+ * Function called with details about incoming wire transfers that
+ * established an account key for KYC authentication, as claimed by the
+ * exchange DB.
+ *
+ * Such a transfer is not credited to a reserve and is never paid back, so
+ * the money is simply revenue of the exchange operator. We book it as
+ * 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.
+ *
+ * @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 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
+ */
+static enum GNUNET_GenericReturnValue
+kycauth_in_cb (void *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)
+{
+ struct WireAccount *wa = cls;
+
+ (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));
+ 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),
+ credit);
+ TALER_ARL_amount_add (&TALER_ARL_USE_AB (total_wire_in),
+ &TALER_ARL_USE_AB (total_wire_in),
+ credit);
+ return GNUNET_OK;
+}
+
+
+/**
* Function called with details about incoming wire transfers
* as claimed by the exchange DB.
*
@@ -781,6 +861,11 @@ analyze_credit (
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;
case TALER_BANK_CT_WAD:
GNUNET_break (0); /* FIXME: Wad not yet supported */
@@ -1105,6 +1190,23 @@ process_credits (void *cls)
}
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Analyzing exchange's KYCAUTH IN table for account `%s'\n",
+ wa->ai->section_name);
+ qs = TALER_EXCHANGEDB_iterate_kycauth_in_above_serial_id_by_account (
+ TALER_ARL_edb,
+ wa->ai->section_name,
+ wa->last_kycauth_in_serial_id,
+ &kycauth_in_cb,
+ wa);
+ if (0 > qs)
+ {
+ GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
+ global_ret = EXIT_FAILURE;
+ GNUNET_SCHEDULER_shutdown ();
+ return;
+ }
+
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Starting bank CREDIT history of account `%s'\n",
wa->ai->section_name);
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
@@ -1185,6 +1287,7 @@ begin_transaction (void)
TALER_ARL_adb,
TALER_ARL_GET_AB (total_wire_in),
TALER_ARL_GET_AB (total_kycauth_in),
+ TALER_ARL_GET_AB (total_kycauth_revenue),
TALER_ARL_GET_AB (total_wire_credit_fees),
TALER_ARL_GET_AB (total_bad_amount_in_plus),
TALER_ARL_GET_AB (total_bad_amount_in_minus),
@@ -1206,10 +1309,19 @@ begin_transaction (void)
NULL != wa;
wa = wa->next)
{
+ /* we are called again by rollback_and_reset() after a serialization
+ failure, so the labels may already have been computed */
+ GNUNET_free (wa->label_reserve_in_serial_id);
+ GNUNET_free (wa->label_kycauth_in_serial_id);
+ GNUNET_free (wa->label_wire_off_in);
GNUNET_asprintf (&wa->label_reserve_in_serial_id,
"wire-%s-%s",
wa->ai->section_name,
"reserve_in_serial_id");
+ GNUNET_asprintf (&wa->label_kycauth_in_serial_id,
+ "wire-%s-%s",
+ wa->ai->section_name,
+ "kycauth_in_serial_id");
GNUNET_asprintf (&wa->label_wire_off_in,
"wire-%s-%s",
wa->ai->section_name,
@@ -1218,6 +1330,8 @@ begin_transaction (void)
TALER_ARL_adb,
wa->label_reserve_in_serial_id,
&wa->last_reserve_in_serial_id,
+ wa->label_kycauth_in_serial_id,
+ &wa->last_kycauth_in_serial_id,
wa->label_wire_off_in,
&wa->wire_off_in,
NULL);
diff --git a/src/auditor/test-kyc.sh b/src/auditor/test-kyc.sh
@@ -547,6 +547,11 @@ CLEARED_WIRED="TESTKUDOS:8.02"
WITHHELD="TESTKUDOS:8.03"
# Two KYCAUTH wire transfers of TESTKUDOS:0.1, one per merchant instance.
KYCAUTH_IN="TESTKUDOS:0.2"
+# Everything the exchange's bank account was credited with: TESTKUDOS:56 of
+# reserve top-ups plus the two KYCAUTH transfers above. The KYCAUTH part is
+# revenue rather than a liability, but it is money in the account all the
+# same, so it belongs in the balance the account is expected to have.
+WIRE_IN="TESTKUDOS:56.2"
# Assert that all of the auditor's loss, delta and irregularity balances
@@ -696,6 +701,20 @@ function check_kyc_state() {
"$KYCAUTH_IN" \
"Wrong total for KYCAUTH wire transfers"
+ # KYCAUTH money is not credited to a reserve and never paid back, so
+ # the auditor books it as revenue of the exchange operator and counts
+ # it towards the balance the bank account is expected to have.
+ echo -n "Checking the KYCAUTH credits were booked as revenue... "
+ check_balance \
+ "total_kycauth_revenue" \
+ "$KYCAUTH_IN" \
+ "Wrong KYCAUTH revenue"
+ echo -n "Checking the KYCAUTH credits reached the balance sheet... "
+ check_balance \
+ "total_wire_in" \
+ "$WIRE_IN" \
+ "Wrong total credited to the exchange"
+
echo -n "Checking that only the cleared merchant was paid... "
check_balance \
"total_wire_out" \
@@ -805,6 +824,15 @@ function test_2() {
"total_kycauth_in" \
"TESTKUDOS:0.1" \
"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" \
+ "$KYCAUTH_IN" \
+ "Wrong KYCAUTH revenue"
echo -n "Checking that the unbooked credit was flagged... "
check_report_any \
"reserve-in-inconsistency" \
diff --git a/src/exchangedb/meson.build b/src/exchangedb/meson.build
@@ -208,6 +208,7 @@ libtalerexchangedb = library(
'iterate_exchange_kycauth_transfers.c',
'iterate_kyc_accounts.c',
'iterate_kyc_attributes.c',
+ 'iterate_kycauth_in_above_serial_id_by_account.c',
'iterate_merge_amounts_for_kyc_check.c',
'get_purse_by_merge_pub.c',
'get_purse.c',