merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

commit 5845596472d3a265d16836f5b0e401f34b74531c
parent b63506baec3d85ca5db57b00cbedd3f522c80caa
Author: Christian Grothoff <christian@grothoff.org>
Date:   Wed,  5 Aug 2026 22:40:54 +0200

ensure depositcheck survives instance deletion with unchecked (and then uncheckable) deposits

Diffstat:
Msrc/backenddb/iterate_pending_deposits.c | 20++++++++++++++++++--
Msrc/backenddb/iterate_pending_deposits.sql | 6+++++-
Msrc/backenddb/test_merchantdb.c | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 117 insertions(+), 3 deletions(-)

diff --git a/src/backenddb/iterate_pending_deposits.c b/src/backenddb/iterate_pending_deposits.c @@ -79,13 +79,16 @@ lookup_deposits_cb (void *cls, struct TALER_Amount amount_with_fee; struct TALER_Amount deposit_fee; struct TALER_CoinSpendPublicKeyP coin_pub; + bool no_priv; struct GNUNET_PQ_ResultSpec rs[] = { GNUNET_PQ_result_spec_uint64 ("out_deposit_serial", &deposit_serial), GNUNET_PQ_result_spec_auto_from_type ("out_h_contract_terms", &h_contract_terms), - GNUNET_PQ_result_spec_auto_from_type ("out_merchant_priv", - &merchant_priv), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_auto_from_type ("out_merchant_priv", + &merchant_priv), + &no_priv), GNUNET_PQ_result_spec_string ("out_merchant_id", &instance_id), GNUNET_PQ_result_spec_absolute_time ("out_wire_transfer_deadline", @@ -112,6 +115,19 @@ lookup_deposits_cb (void *cls, ldc->qs = GNUNET_DB_STATUS_HARD_ERROR; return; } + if (no_priv) + { + /* Instance was deleted (private key purged); we can no longer + sign the tracking request, so there is nothing to be done for + this deposit. The SQL already filters those out, this is + merely belt-and-braces. */ + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Skipping deposit %llu of instance `%s': private key was deleted\n", + (unsigned long long) deposit_serial, + instance_id); + GNUNET_PQ_cleanup_result (rs); + continue; + } ldc->cb (ldc->cb_cls, deposit_serial, wire_deadline, diff --git a/src/backenddb/iterate_pending_deposits.sql b/src/backenddb/iterate_pending_deposits.sql @@ -46,6 +46,9 @@ BEGIN FROM merchant.merchant_instances LOOP EXIT WHEN remaining <= 0; + -- Instance was deleted: without its private key we cannot sign the + -- tracking request, so its deposits can never be settled. + CONTINUE WHEN rec.merchant_priv IS NULL; s := 'merchant_instance_' || rec.merchant_serial::TEXT; BEGIN FOR inner_rec IN @@ -105,4 +108,5 @@ $FN$; COMMENT ON FUNCTION merchant.lookup_pending_deposits(TEXT, INT8, INT8, BOOLEAN) IS 'Returns up to p_limit pending-settlement deposit rows for the given exchange' ' across all instance schemas, with merchant_priv read from' - ' merchant.merchant_instances (NULL if absent).'; + ' merchant.merchant_instances; instances whose private key was deleted' + ' are skipped, as their deposits can no longer be settled.'; diff --git a/src/backenddb/test_merchantdb.c b/src/backenddb/test_merchantdb.c @@ -76,6 +76,7 @@ #include "merchant-database/get_order_by_fulfillment.h" #include "merchant-database/get_order_status.h" #include "merchant-database/iterate_orders.h" +#include "merchant-database/iterate_pending_deposits.h" #include "merchant-database/iterate_pending_webhooks_above_serial_id.h" #include "merchant-database/iterate_pending_webhooks.h" #include "merchant-database/get_product.h" @@ -4264,6 +4265,95 @@ post_test_deposits (struct TestDeposits_Closure *cls) /** + * Counts the pending deposits we are called for. + * + * @param cls a `unsigned int *` counter + */ +static void +count_pending_deposits_cb ( + void *cls, + uint64_t deposit_serial, + struct GNUNET_TIME_Absolute wire_deadline, + struct GNUNET_TIME_Absolute retry_time, + const struct TALER_PrivateContractHashP *h_contract_terms, + const struct TALER_MerchantPrivateKeyP *merchant_priv, + const char *instance_id, + const struct TALER_MerchantWireHashP *h_wire, + const struct TALER_Amount *amount_with_fee, + const struct TALER_Amount *deposit_fee, + const struct TALER_CoinSpendPublicKeyP *coin_pub) +{ + unsigned int *count = cls; + + (void) deposit_serial; + (void) wire_deadline; + (void) retry_time; + (void) h_contract_terms; + (void) merchant_priv; + (void) instance_id; + (void) h_wire; + (void) amount_with_fee; + (void) deposit_fee; + (void) coin_pub; + (*count)++; +} + + +/** + * Tests that pending deposits of an instance whose private key was + * deleted do not break the iteration. + * + * Regression test: 'merchant_priv' is nullable and DELETE + * /management/instances/$ID sets it to NULL, but the result spec did + * not allow NULL. Extraction then failed with a hard error, which + * makes taler-merchant-depositcheck shut down merchant-wide and + * crash-loop on the very same row. + * + * @param instance the instance whose private key gets deleted. + * @param exchange_url the exchange the deposits were made to. + * @return 0 on success, 1 otherwise. + */ +static int +test_pending_deposits_without_private_key (const struct InstanceData *instance, + const char *exchange_url) +{ + unsigned int count = 0; + + /* Before the deletion the deposits are pending. */ + TEST_COND_RET_ON_FAIL (0 < + TALER_MERCHANTDB_iterate_pending_deposits ( + pg, + exchange_url, + 1024, + true, + &count_pending_deposits_cb, + &count), + "Iterating over pending deposits failed\n"); + TEST_COND_RET_ON_FAIL (0 < count, + "No pending deposits found\n"); + TEST_RET_ON_FAIL (test_delete_instance_private_key ( + instance, + GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); + /* Now they are unsettleable, but that must not be an error. */ + count = 0; + TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == + TALER_MERCHANTDB_iterate_pending_deposits ( + pg, + exchange_url, + 1024, + true, + &count_pending_deposits_cb, + &count), + "Pending deposits of an instance without a private key" + " were not skipped\n"); + TEST_COND_RET_ON_FAIL (0 == count, + "Deposit of an instance without a private key" + " was returned\n"); + return 0; +} + + +/** * Runs tests for deposits. * * @param cls the closure containing test data. @@ -4408,6 +4498,10 @@ run_test_deposits (struct TestDeposits_Closure *cls) 1, &cls->deposits[2])); } + /* Must be last: deletes the private key of the instance. */ + TEST_RET_ON_FAIL (test_pending_deposits_without_private_key ( + &cls->instance, + cls->deposits[0].exchange_url)); return 0; }