commit ec87b4d4bc88f04328951065f369194d9f1a3dcd
parent 97666f86c43df5573e278aa64f6476a419fbd0d1
Author: Christian Grothoff <christian@grothoff.org>
Date: Thu, 6 Aug 2026 17:50:15 +0200
handle serialization failure in legi check and retry nicely
Diffstat:
4 files changed, 71 insertions(+), 3 deletions(-)
diff --git a/src/exchange/taler-exchange-httpd_common_kyc.c b/src/exchange/taler-exchange-httpd_common_kyc.c
@@ -1540,6 +1540,21 @@ current_rules_cb (
"trigger_kyc_rule_for_account");
goto cleanup;
}
+ if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
+ {
+ /* Serialization failure or a lost connection. This used to fall
+ through to the commit below, which PostgreSQL happily accepted on the
+ already-aborted transaction, so the client was told its legitimization
+ measure had been registered when nothing had been written. There is
+ no retry loop in this file (the transaction is hand-rolled rather than
+ run through TEH_DB_run_transaction), so all we can do here is fail
+ visibly and let the client repeat the request. */
+ TALER_EXCHANGEDB_rollback (TEH_pg);
+ legi_fail (lch,
+ TALER_EC_GENERIC_DB_SOFT_FAILURE,
+ "trigger_kyc_rule_for_account");
+ goto cleanup;
+ }
qs = TALER_EXCHANGEDB_commit (TEH_pg);
if (0 > qs)
{
diff --git a/src/exchange/taler-exchange-httpd_post-reserves-RESERVE_PUB-close.c b/src/exchange/taler-exchange-httpd_post-reserves-RESERVE_PUB-close.c
@@ -399,10 +399,19 @@ reserve_close_transaction (
return GNUNET_DB_STATUS_HARD_ERROR;
}
+ /* Store the account the balance is to be wired to, which is what the
+ client asked for and falls back to the reserve's origin account above.
+ Passing @e payto_uri here instead would dereference NULL for a reserve
+ that has no origin account at all (funded by a purse merge): the 409
+ ruled that case out only in combination with rcc->payto_uri, and on the
+ KYC-resumed pass we get here with payto_uri still NULL. It would also
+ record the origin account rather than the requested one whenever the two
+ differ, which is precisely the case that had to pass a KYC check. */
+ GNUNET_assert (NULL != rcc->payto_uri.full_payto);
qs = TALER_EXCHANGEDB_insert_close_request (
TEH_pg,
&rcc->reserve_pub,
- payto_uri,
+ rcc->payto_uri,
&rcc->reserve_sig,
rcc->timestamp,
&rcc->balance,
diff --git a/src/exchangedb/get_reserve_close_info.c b/src/exchangedb/get_reserve_close_info.c
@@ -38,11 +38,18 @@ TALER_EXCHANGEDB_get_reserve_close_info (
TALER_PQ_result_spec_amount ("current_balance",
pg->currency,
balance),
- GNUNET_PQ_result_spec_string ("payto_uri",
- &payto_uri->full_payto),
+ GNUNET_PQ_result_spec_allow_null (
+ GNUNET_PQ_result_spec_string ("payto_uri",
+ &payto_uri->full_payto),
+ NULL),
GNUNET_PQ_result_spec_end
};
+ /* A reserve that was funded peer-to-peer (purse merge or wad) has no
+ 'reserves_in' row, so the LEFT JOIN yields a NULL payto_uri. The caller
+ checks for that and has a 409 ready; reading it without allow_null made
+ the extraction fail and turned that case into an HTTP 500. */
+ payto_uri->full_payto = NULL;
PREPARE (pg,
"get_reserve_close_info",
"SELECT "
diff --git a/src/exchangedb/test_regressions.c b/src/exchangedb/test_regressions.c
@@ -33,6 +33,7 @@
#include "exchange-database/rollback.h"
#include "exchange-database/do_reserve_open.h"
#include "exchange-database/get_purse_deposit.h"
+#include "exchange-database/get_reserve_close_info.h"
/**
@@ -575,6 +576,40 @@ check_purse_deposit_without_age_commitment (void)
/**
+ * E-6: a reserve funded by a purse merge has no `reserves_in` row, so the
+ * LEFT JOIN in get_reserve_close_info() returns a NULL payto_uri. Without
+ * allow_null the extraction failed and POST /reserves/$RP/close answered 500
+ * instead of the 409 the handler already implements.
+ */
+static int
+check_reserve_close_info_without_origin (void)
+{
+ struct TALER_ReservePublicKeyP reserve_pub;
+ struct TALER_Amount balance;
+ struct TALER_FullPayto payto_uri;
+ char *sql;
+ enum GNUNET_GenericReturnValue ok;
+
+ memset (&reserve_pub,
+ 0x36,
+ sizeof (reserve_pub));
+ sql = hex_insert_reserve (&reserve_pub);
+ ok = exec_sql (sql);
+ GNUNET_free (sql);
+ FAILIF (GNUNET_OK != ok);
+
+ /* Before the fix: HARD_ERROR from the failed NULL extraction. */
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ TALER_EXCHANGEDB_get_reserve_close_info (pg,
+ &reserve_pub,
+ &balance,
+ &payto_uri));
+ FAILIF (NULL != payto_uri.full_payto);
+ return 0;
+}
+
+
+/**
* All checks we know about.
*/
static const struct
@@ -592,6 +627,8 @@ static const struct
&check_reserve_open_int4_overflows },
{ "purse-deposit-without-age-commitment",
&check_purse_deposit_without_age_commitment },
+ { "reserve-close-info-without-origin",
+ &check_reserve_close_info_without_origin },
{ NULL, NULL }
};