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