commit b82f0ef5b50227008e319785c616194ab3497b8e
parent c601d1cc47eea447d15c7dec6c62d99a0be89cc9
Author: Christian Grothoff <christian@grothoff.org>
Date: Fri, 7 Aug 2026 00:19:51 +0200
handle multiple charities using same public key nicely
Diffstat:
6 files changed, 151 insertions(+), 14 deletions(-)
diff --git a/src/donau/donau-httpd_patch-charities-CHARITY_ID.c b/src/donau/donau-httpd_patch-charities-CHARITY_ID.c
@@ -87,6 +87,7 @@ DH_handler_patch_charities (struct DH_RequestContext *rc,
{
struct DONAUDB_CharityMetaData meta;
enum GNUNET_DB_QueryStatus qs;
+ bool conflict = false;
qs = DONAUDB_get_charity (DH_context,
charity_id,
@@ -126,10 +127,16 @@ DH_handler_patch_charities (struct DH_RequestContext *rc,
&charity_pub,
charity_name,
charity_url,
- &max_per_year);
+ &max_per_year,
+ &conflict);
GNUNET_free (meta.charity_name);
GNUNET_free (meta.charity_url);
+ if (conflict)
+ return TALER_MHD_reply_with_error (rc->connection,
+ MHD_HTTP_CONFLICT,
+ TALER_EC_DONAU_CHARITY_PUB_EXISTS,
+ NULL);
switch (qs)
{
case GNUNET_DB_STATUS_HARD_ERROR:
diff --git a/src/donaudb/meson.build b/src/donaudb/meson.build
@@ -21,6 +21,7 @@ procedures_sql = [
'insert_receipts_submitted.sql',
'do_insert_receipt_issued.sql',
'insert_charity.sql',
+ 'update_charity.sql',
'commit.sql',
]
diff --git a/src/donaudb/test_donaudb.c b/src/donaudb/test_donaudb.c
@@ -327,6 +327,7 @@ run (void *cls)
struct TALER_Amount updated_max;
struct DONAU_CharityPrivateKeyP updated_charity_priv;
struct DONAU_CharityPublicKeyP updated_charity_pub;
+ bool conflict = true;
GNUNET_CRYPTO_eddsa_key_create (&updated_charity_priv.eddsa_priv);
GNUNET_CRYPTO_eddsa_key_get_public (&updated_charity_priv.eddsa_priv,
@@ -341,7 +342,9 @@ run (void *cls)
&updated_charity_pub,
updated_charity_name,
updated_charity_url,
- &updated_max));
+ &updated_max,
+ &conflict));
+ FAILIF (conflict);
ZR_BLK (&charity_meta);
FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
@@ -619,6 +622,62 @@ run (void *cls)
GNUNET_free (rollover_meta.charity_url);
}
+ /* D-6: updating a charity to a charity_pub that another charity
+ already owns must be reported as a conflict, not as "no such
+ charity" -- and it must not abort the caller's transaction. */
+ {
+ uint64_t first_id;
+ uint64_t second_id;
+ struct DONAUDB_CharityMetaData first_meta;
+ struct DONAUDB_CharityMetaData second_meta;
+ struct TALER_Amount some_max;
+ bool conflict = false;
+
+ FAILIF (GNUNET_OK !=
+ make_charity (CURRENCY ":100",
+ &first_id));
+ FAILIF (GNUNET_OK !=
+ make_charity (CURRENCY ":100",
+ &second_id));
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ DONAUDB_get_charity (ctx,
+ first_id,
+ &first_meta));
+ GNUNET_assert (GNUNET_OK ==
+ TALER_string_to_amount (CURRENCY ":100",
+ &some_max));
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ DONAUDB_update_charity (ctx,
+ second_id,
+ &first_meta.charity_pub,
+ "stolen",
+ "https://stolen.example.com/",
+ &some_max,
+ &conflict));
+ FAILIF (! conflict);
+ /* The second charity must be untouched ... */
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ DONAUDB_get_charity (ctx,
+ second_id,
+ &second_meta));
+ FAILIF (0 == GNUNET_memcmp (&second_meta.charity_pub,
+ &first_meta.charity_pub));
+ /* ... and an unknown charity_id must still be "no results". */
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
+ DONAUDB_update_charity (ctx,
+ second_id + 424242,
+ &second_meta.charity_pub,
+ "ghost",
+ "https://ghost.example.com/",
+ &some_max,
+ &conflict));
+ FAILIF (conflict);
+ GNUNET_free (first_meta.charity_name);
+ GNUNET_free (first_meta.charity_url);
+ GNUNET_free (second_meta.charity_name);
+ GNUNET_free (second_meta.charity_url);
+ }
+
result = 0;
drop:
diff --git a/src/donaudb/update_charity.c b/src/donaudb/update_charity.c
@@ -32,7 +32,8 @@ DONAUDB_update_charity (struct DONAUDB_PostgresContext *ctx,
const struct DONAU_CharityPublicKeyP *charity_pub,
const char *charity_name,
const char *charity_url,
- const struct TALER_Amount *max_per_year)
+ const struct TALER_Amount *max_per_year,
+ bool *conflict)
{
struct GNUNET_PQ_QueryParam params[] = {
GNUNET_PQ_query_param_uint64 (&charity_id),
@@ -43,18 +44,33 @@ DONAUDB_update_charity (struct DONAUDB_PostgresContext *ctx,
max_per_year),
GNUNET_PQ_query_param_end
};
+ bool found;
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_bool ("found",
+ &found),
+ GNUNET_PQ_result_spec_bool ("conflict",
+ conflict),
+ GNUNET_PQ_result_spec_end
+ };
+ enum GNUNET_DB_QueryStatus qs;
+ *conflict = false;
PREPARE (ctx,
"update_charity",
- "UPDATE charities"
- " SET charity_pub = $2"
- " ,charity_name = $3"
- " ,charity_url = $4"
- " ,max_per_year = $5"
- " WHERE charity_id = $1;");
- return GNUNET_PQ_eval_prepared_non_select (ctx->conn,
- "update_charity",
- params);
+ "SELECT "
+ " out_found AS found"
+ ",out_conflict AS conflict"
+ " FROM do_update_charity"
+ " ($1, $2, $3, $4, $5);");
+ qs = GNUNET_PQ_eval_prepared_singleton_select (ctx->conn,
+ "update_charity",
+ params,
+ rs);
+ if (qs <= 0)
+ return qs;
+ if (! found)
+ return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS;
+ return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
}
diff --git a/src/donaudb/update_charity.sql b/src/donaudb/update_charity.sql
@@ -0,0 +1,48 @@
+--
+-- This file is part of TALER
+-- Copyright (C) 2026 Taler Systems SA
+--
+-- TALER is free software; you can redistribute it and/or modify it under the
+-- terms of the GNU General Public License as published by the Free Software
+-- Foundation; either version 3, or (at your option) any later version.
+--
+-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+-- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License along with
+-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+--
+
+DROP FUNCTION IF EXISTS do_update_charity;
+CREATE FUNCTION do_update_charity (
+ IN in_charity_id INT8
+ ,IN in_charity_pub BYTEA
+ ,IN in_charity_name TEXT
+ ,IN in_charity_url TEXT
+ ,IN in_max_per_year taler_amount
+ ,OUT out_found BOOLEAN
+ ,OUT out_conflict BOOLEAN
+)
+LANGUAGE plpgsql
+AS $$
+BEGIN
+ out_conflict = FALSE;
+ UPDATE charities
+ SET charity_pub=in_charity_pub
+ ,charity_name=in_charity_name
+ ,charity_url=in_charity_url
+ ,max_per_year=in_max_per_year
+ WHERE charity_id=in_charity_id;
+ out_found = FOUND;
+EXCEPTION
+ WHEN unique_violation THEN
+ -- charity_pub is the primary key of `charities' and is entirely
+ -- client-supplied; a collision means "this key belongs to a different
+ -- charity", NOT "no such charity_id".
+ out_found = TRUE;
+ out_conflict = TRUE;
+END $$;
+
+COMMENT ON FUNCTION do_update_charity
+ IS 'Updates a charity. out_found is FALSE if no charity with the given charity_id exists; out_conflict is TRUE if the requested charity_pub is already used by another charity.';
diff --git a/src/include/donau-database/update_charity.h b/src/include/donau-database/update_charity.h
@@ -33,7 +33,12 @@
* @param charity_name new name
* @param charity_url new landing page URL
* @param max_per_year yearly donation limit
- * @return transaction status code
+ * @param[out] conflict set to true if @a charity_pub is already used by
+ * a *different* charity; in that case nothing was updated
+ * even though the status is #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT
+ * @return transaction status code;
+ * #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if there is no charity
+ * with @a charity_id
*/
enum GNUNET_DB_QueryStatus
DONAUDB_update_charity (
@@ -42,6 +47,7 @@ DONAUDB_update_charity (
const struct DONAU_CharityPublicKeyP *charity_pub,
const char *charity_name,
const char *charity_url,
- const struct TALER_Amount *max_per_year);
+ const struct TALER_Amount *max_per_year,
+ bool *conflict);
#endif