donau

Donation authority for GNU Taler (experimental)
Log | Files | Refs | Submodules | README | LICENSE

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:
Msrc/donau/donau-httpd_patch-charities-CHARITY_ID.c | 9++++++++-
Msrc/donaudb/meson.build | 1+
Msrc/donaudb/test_donaudb.c | 61++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/donaudb/update_charity.c | 36++++++++++++++++++++++++++----------
Asrc/donaudb/update_charity.sql | 48++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/include/donau-database/update_charity.h | 10++++++++--
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