exchange

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

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:
Msrc/exchange/taler-exchange-httpd_common_kyc.c | 15+++++++++++++++
Msrc/exchange/taler-exchange-httpd_post-reserves-RESERVE_PUB-close.c | 11++++++++++-
Msrc/exchangedb/get_reserve_close_info.c | 11+++++++++--
Msrc/exchangedb/test_regressions.c | 37+++++++++++++++++++++++++++++++++++++
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 } };