commit f73e437486716e464a4afdf15bd2d4ccc7eac6dc
parent 4c62524511278ea4c1c75b17fef6e004a9d43511
Author: Christian Grothoff <christian@grothoff.org>
Date: Mon, 6 Apr 2026 19:41:19 +0200
fix #11337
Diffstat:
9 files changed, 352 insertions(+), 99 deletions(-)
diff --git a/src/backend/taler-merchant-httpd_post-private-accounts.c b/src/backend/taler-merchant-httpd_post-private-accounts.c
@@ -190,10 +190,11 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh,
MHD_RESULT mret;
GNUNET_break_op (0);
- mret = TALER_MHD_reply_with_error (connection,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_GENERIC_PAYTO_URI_MALFORMED,
- err);
+ mret = TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PAYTO_URI_MALFORMED,
+ err);
GNUNET_free (err);
return mret;
}
@@ -287,22 +288,10 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh,
TALER_MERCHANT_BANK_auth_free (&auth);
}
- TMH_db->preflight (TMH_db->cls);
- for (unsigned int retries = 0;
- retries < MAX_RETRIES;
- retries++)
{
enum GNUNET_DB_QueryStatus qs;
- struct TMH_WireMethod *wm;
- TMH_db->rollback (TMH_db->cls);
- if (GNUNET_OK !=
- TMH_db->start (TMH_db->cls,
- "post-account"))
- {
- GNUNET_break (0);
- break;
- }
+ TMH_db->preflight (TMH_db->cls);
qs = TMH_db->select_accounts (TMH_db->cls,
mi->settings.id,
&account_cb,
@@ -311,13 +300,16 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh,
{
case GNUNET_DB_STATUS_HARD_ERROR:
GNUNET_break (0);
- TMH_db->rollback (TMH_db->cls);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"select_accounts");
case GNUNET_DB_STATUS_SOFT_ERROR:
- continue;
+ GNUNET_break (0);
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "select_accounts");
case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
break;
case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
@@ -327,7 +319,6 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh,
if (pac.have_same_account)
{
/* Idempotent request */
- TMH_db->rollback (TMH_db->cls);
return TALER_MHD_REPLY_JSON_PACK (
connection,
MHD_HTTP_OK,
@@ -337,13 +328,11 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh,
GNUNET_JSON_pack_data_auto (
"h_wire",
&pac.h_wire));
-
}
if (pac.have_conflicting_account)
{
/* Conflict, refuse request */
- TMH_db->rollback (TMH_db->cls);
GNUNET_break_op (0);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_CONFLICT,
@@ -356,7 +345,6 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh,
/* MFA needed */
enum GNUNET_GenericReturnValue ret;
- TMH_db->rollback (TMH_db->cls);
ret = TMH_mfa_check_simple (hc,
TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION,
mi);
@@ -370,15 +358,20 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh,
: MHD_NO;
}
}
+ }
+
+ /* All pre-checks clear, now try to activate/setup the new account */
+ {
+ struct TMH_WireMethod *wm;
/* convert provided payto URI into internal data structure with salts */
wm = TMH_setup_wire_account (pac.uri,
pac.credit_facade_url,
pac.credit_facade_credentials);
+ GNUNET_assert (NULL != wm);
if (NULL != pac.extra_wire_subject_metadata)
wm->extra_wire_subject_metadata
= GNUNET_strdup (pac.extra_wire_subject_metadata);
- GNUNET_assert (NULL != wm);
{
struct TALER_MERCHANTDB_AccountDetails ad = {
.payto_uri = wm->payto_uri,
@@ -388,64 +381,79 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh,
.credit_facade_url = wm->credit_facade_url,
.credit_facade_credentials = wm->credit_facade_credentials,
.extra_wire_subject_metadata = wm->extra_wire_subject_metadata,
- .active = wm->active
+ .active = true
};
- struct GNUNET_DB_EventHeaderP es = {
- .size = htons (sizeof (es)),
- .type = htons (TALER_DBEVENT_MERCHANT_ACCOUNTS_CHANGED)
- };
-
- qs = TMH_db->insert_account (TMH_db->cls,
- &ad);
+ enum GNUNET_DB_QueryStatus qs;
+ struct TALER_MerchantWireHashP h_wire;
+ struct TALER_WireSaltP salt;
+ bool not_found;
+ bool conflict;
+
+ TMH_db->preflight (TMH_db->cls);
+ qs = TMH_db->activate_account (TMH_db->cls,
+ &ad,
+ &h_wire,
+ &salt,
+ ¬_found,
+ &conflict);
switch (qs)
{
case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
break;
case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
GNUNET_break (0);
- TMH_db->rollback (TMH_db->cls);
TMH_wire_method_free (wm);
return TALER_MHD_reply_with_error (
connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
- "insert_account");
+ "activate_account");
case GNUNET_DB_STATUS_SOFT_ERROR:
- continue;
+ GNUNET_break (0);
+ TMH_wire_method_free (wm);
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_STORE_FAILED,
+ "activate_account");
case GNUNET_DB_STATUS_HARD_ERROR:
GNUNET_break (0);
- TMH_db->rollback (TMH_db->cls);
TMH_wire_method_free (wm);
return TALER_MHD_reply_with_error (
connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_STORE_FAILED,
- "insert_account");
+ "activate_account");
}
-
- TMH_db->event_notify (TMH_db->cls,
- &es,
- NULL,
- 0);
- qs = TMH_db->commit (TMH_db->cls);
- switch (qs)
+ if (not_found)
{
- case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
- break;
- case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
- break;
- case GNUNET_DB_STATUS_SOFT_ERROR:
+ /* must have been concurrently deleted, rare! */
TMH_wire_method_free (wm);
- continue;
- case GNUNET_DB_STATUS_HARD_ERROR:
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_NOT_FOUND,
+ TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN,
+ mi->settings.id);
+ }
+ if (conflict)
+ {
+ /* Conflicting POST must have been done between our pre-check
+ and the actual transaction, rare! */
TMH_wire_method_free (wm);
- GNUNET_break (0);
+ GNUNET_break_op (0);
return TALER_MHD_reply_with_error (
connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_COMMIT_FAILED,
- "post-account");
+ MHD_HTTP_CONFLICT,
+ TALER_EC_MERCHANT_PRIVATE_ACCOUNT_EXISTS,
+ pac.uri.full_payto);
}
+
+ /* Update salt/h_wire in case we re-activated an
+ existing account and found a different salt
+ value already in the DB */
+ wm->wire_salt = salt;
+ wm->h_wire = h_wire;
+
/* Finally, also update our running process */
GNUNET_CONTAINER_DLL_insert (mi->wm_head,
mi->wm_tail,
@@ -461,12 +469,7 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh,
&wm->wire_salt),
GNUNET_JSON_pack_data_auto ("h_wire",
&wm->h_wire));
- } /* end retries */
- TMH_db->rollback (TMH_db->cls);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_SOFT_FAILURE,
- "post-accounts");
+ }
}
diff --git a/src/backend/taler-merchant-kyccheck.c b/src/backend/taler-merchant-kyccheck.c
@@ -1745,7 +1745,6 @@ run (void *cls,
GNUNET_CONFIGURATION_iterate_sections (cfg,
&accept_exchanges,
NULL);
- fprintf (stderr, "NOW\n");
{
struct GNUNET_DB_EventHeaderP es = {
.size = htons (sizeof (es)),
diff --git a/src/backenddb/pg_activate_account.c b/src/backenddb/pg_activate_account.c
@@ -16,7 +16,7 @@
/**
* @file backenddb/pg_activate_account.c
* @brief Implementation of the activate_account function for Postgres
- * @author Iván Ávalos
+ * @author Christian Grothoff
*/
#include "taler/platform.h"
#include <taler/taler_error_codes.h>
@@ -25,29 +25,59 @@
#include "pg_activate_account.h"
#include "pg_helper.h"
+
enum GNUNET_DB_QueryStatus
-TMH_PG_activate_account (void *cls,
- const char *merchant_id,
- const struct TALER_MerchantWireHashP *h_wire)
+TMH_PG_activate_account (
+ void *cls,
+ const struct TALER_MERCHANTDB_AccountDetails *account_details,
+ struct TALER_MerchantWireHashP *h_wire,
+ struct TALER_WireSaltP *salt,
+ bool *not_found,
+ bool *conflict)
{
struct PostgresClosure *pg = cls;
struct GNUNET_PQ_QueryParam params[] = {
- GNUNET_PQ_query_param_string (merchant_id),
- GNUNET_PQ_query_param_auto_from_type (h_wire),
+ GNUNET_PQ_query_param_string (account_details->instance_id),
+ GNUNET_PQ_query_param_auto_from_type (&account_details->h_wire),
+ GNUNET_PQ_query_param_auto_from_type (&account_details->salt),
+ GNUNET_PQ_query_param_string (account_details->payto_uri.full_payto),
+ NULL ==account_details->credit_facade_url
+ ? GNUNET_PQ_query_param_null ()
+ : GNUNET_PQ_query_param_string (account_details->credit_facade_url),
+ NULL == account_details->credit_facade_credentials
+ ? GNUNET_PQ_query_param_null ()
+ : TALER_PQ_query_param_json (account_details->credit_facade_credentials),
+ NULL == account_details->extra_wire_subject_metadata
+ ? GNUNET_PQ_query_param_null ()
+ : GNUNET_PQ_query_param_string (
+ account_details->extra_wire_subject_metadata),
GNUNET_PQ_query_param_end
};
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_bool ("not_found",
+ not_found),
+ GNUNET_PQ_result_spec_bool ("conflict",
+ conflict),
+ GNUNET_PQ_result_spec_auto_from_type ("h_wire",
+ h_wire),
+ GNUNET_PQ_result_spec_auto_from_type ("salt",
+ salt),
+ GNUNET_PQ_result_spec_end
+ };
+ GNUNET_assert (account_details->active);
check_connection (pg);
PREPARE (pg,
"activate_account",
- "UPDATE merchant_accounts SET"
- " active=TRUE"
- " WHERE h_wire=$2 AND"
- " merchant_serial="
- " (SELECT merchant_serial"
- " FROM merchant_instances"
- " WHERE merchant_id=$1)");
- return GNUNET_PQ_eval_prepared_non_select (pg->conn,
- "activate_account",
- params);
+ "SELECT "
+ " out_h_wire AS h_wire"
+ " ,out_salt AS salt"
+ " ,out_conflict AS conflict"
+ " ,out_not_found AS not_found"
+ " FROM merchant_do_activate_account"
+ " ($1,$2,$3,$4,$5,$6,$7);");
+ return GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "activate_account",
+ params,
+ rs);
}
diff --git a/src/backenddb/pg_activate_account.h b/src/backenddb/pg_activate_account.h
@@ -26,16 +26,24 @@
#include "taler/taler_merchantdb_plugin.h"
/**
- * Set an instance's account in our database to "active".
+ * Insert account or set an instance's
+ * existing account in our database to "active".
*
* @param cls closure
- * @param merchant_id merchant backend instance ID
- * @param h_wire hash of the wire account to set to active
+ * @param account_details details of the account to actiate
+ * @param[out] h_wire set to the hash of the salted payto URI
+ * @param[out] salt set to salt used for computing @a h_wire
+ * @param[out] not_found set to true if the instance was not found
+ * @param[out] conflict set to true if a conflicting account is active
* @return database result code
*/
enum GNUNET_DB_QueryStatus
-TMH_PG_activate_account (void *cls,
- const char *merchant_id,
- const struct TALER_MerchantWireHashP *h_wire);
+TMH_PG_activate_account (
+ void *cls,
+ const struct TALER_MERCHANTDB_AccountDetails *account_details,
+ struct TALER_MerchantWireHashP *h_wire,
+ struct TALER_WireSaltP *salt,
+ bool *not_found,
+ bool *conflict);
#endif
diff --git a/src/backenddb/pg_activate_account.sql b/src/backenddb/pg_activate_account.sql
@@ -0,0 +1,146 @@
+--
+-- 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 merchant_do_activate_account;
+CREATE FUNCTION merchant_do_activate_account(
+ IN in_instance_name TEXT
+ ,IN in_h_wire BYTEA
+ ,IN in_salt BYTEA
+ ,IN in_full_payto TEXT
+ ,IN in_credit_facade_url TEXT -- can be NULL
+ ,IN in_credit_facade_credentials TEXT -- can be NULL
+ ,IN in_extra_wire_subject_metadata TEXT -- can be NULL
+ ,OUT out_h_wire BYTEA
+ ,OUT out_salt BYTEA
+ ,OUT out_not_found BOOL
+ ,OUT out_conflict BOOL
+)
+LANGUAGE plpgsql
+AS $$
+DECLARE my_instance INT8;
+DECLARE my_active BOOL;
+DECLARE my_cfu TEXT;
+DECLARE my_cfc TEXT;
+DECLARE my_ewsm TEXT;
+DECLARE my_h_wire BYTEA;
+DECLARE my_salt BYTEA;
+BEGIN
+ out_not_found = FALSE;
+ out_conflict = FALSE;
+ out_h_wire = in_h_wire;
+ out_salt = in_salt;
+
+ SELECT merchant_serial
+ INTO my_instance
+ FROM merchant_instances
+ WHERE merchant_id=in_instance_name;
+ IF NOT FOUND
+ THEN
+ out_not_found = TRUE;
+ RETURN;
+ END IF;
+
+ INSERT INTO merchant_accounts
+ AS ma
+ (merchant_serial
+ ,h_wire
+ ,salt
+ ,payto_uri
+ ,credit_facade_url
+ ,credit_facade_credentials
+ ,active
+ ,extra_wire_subject_metadata
+ ) VALUES (
+ my_instance
+ ,in_h_wire
+ ,in_salt
+ ,in_full_payto
+ ,in_credit_facade_url
+ ,in_credit_facade_credentials::JSONB
+ ,TRUE
+ ,in_extra_wire_subject_metadata
+ ) ON CONFLICT DO NOTHING;
+ IF FOUND
+ THEN
+ -- Notify taler-merchant-kyccheck about the change in
+ -- accounts. (TALER_DBEVENT_MERCHANT_ACCOUNTS_CHANGED)
+ NOTIFY XDQM4Z4N0D3GX0H9JEXH70EBC2T3KY7HC0TJB0Z60D2H781RXR6AG;
+ RETURN;
+ END IF;
+
+ SELECT h_wire
+ ,salt
+ ,active
+ ,credit_facade_url
+ ,credit_facade_credentials::TEXT
+ ,extra_wire_subject_metadata
+ INTO my_h_wire
+ ,my_salt
+ ,my_active
+ ,my_cfu
+ ,my_cfc
+ ,my_ewsm
+ FROM merchant_accounts
+ WHERE merchant_serial=my_instance
+ AND payto_uri=in_full_payto;
+ IF NOT FOUND
+ THEN
+ -- This should never happen (we had a conflict!)
+ -- Still, safe way is to return not found.
+ out_not_found = TRUE;
+ RETURN;
+ END IF;
+
+ -- Check for conflict
+ IF (my_active AND
+ (ROW (my_cfu
+ ,my_cfc
+ ,my_ewsm)
+ IS DISTINCT FROM
+ ROW (in_credit_facade_url
+ ,in_credit_facade_credentials
+ ,in_extra_wire_subject_metadata)))
+ THEN
+ -- Active conflicting account, refuse!
+ out_conflict = TRUE;
+ RETURN;
+ END IF;
+
+ -- Equivalent account exists, use its salt instead of the new salt
+ -- and just set it to active!
+ out_salt = my_salt;
+ out_h_wire = my_h_wire;
+
+ -- Now check if existing account is already active
+ IF my_active
+ THEN
+ -- nothing to do
+ RETURN;
+ END IF;
+
+ UPDATE merchant_accounts
+ SET active=TRUE
+ ,credit_facade_url=in_credit_facade_url
+ ,credit_facade_credentials=in_credit_facade_credentials::JSONB
+ ,extra_wire_subject_metadata=in_extra_wire_subject_metadata
+ WHERE h_wire=out_h_wire
+ AND merchant_serial=my_instance;
+
+ -- Notify taler-merchant-kyccheck about the change in (active)
+ -- accounts. (TALER_DBEVENT_MERCHANT_ACCOUNTS_CHANGED)
+ NOTIFY XDQM4Z4N0D3GX0H9JEXH70EBC2T3KY7HC0TJB0Z60D2H781RXR6AG;
+
+END $$;
diff --git a/src/backenddb/pg_inactivate_account.c b/src/backenddb/pg_inactivate_account.c
@@ -36,18 +36,27 @@ TMH_PG_inactivate_account (void *cls,
GNUNET_PQ_query_param_auto_from_type (h_wire),
GNUNET_PQ_query_param_end
};
+ bool found;
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_bool ("found",
+ &found),
+ GNUNET_PQ_result_spec_end
+ };
+ enum GNUNET_DB_QueryStatus qs;
check_connection (pg);
PREPARE (pg,
"inactivate_account",
- "UPDATE merchant_accounts SET"
- " active=FALSE"
- " WHERE h_wire=$2"
- " AND merchant_serial="
- " (SELECT merchant_serial"
- " FROM merchant_instances"
- " WHERE merchant_id=$1)");
- return GNUNET_PQ_eval_prepared_non_select (pg->conn,
- "inactivate_account",
- params);
+ "SELECT out_found AS found"
+ " FROM merchant_do_inactivate_account"
+ " ($1,$2);");
+ qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "inactivate_account",
+ params,
+ rs);
+ if (qs < 0)
+ return qs;
+ if (! found)
+ return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS;
+ return qs;
}
diff --git a/src/backenddb/pg_inactivate_account.sql b/src/backenddb/pg_inactivate_account.sql
@@ -0,0 +1,49 @@
+--
+-- 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 merchant_do_inactivate_account;
+CREATE FUNCTION merchant_do_inactivate_account (
+ IN in_instance_name TEXT
+ ,IN in_h_wire BYTEA
+ ,OUT out_found BOOL
+)
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ my_instance INT8;
+BEGIN
+ SELECT merchant_serial
+ INTO my_instance
+ FROM merchant_instances
+ WHERE merchant_id=in_instance_name;
+ IF NOT FOUND
+ THEN
+ out_found = FALSE;
+ RETURN;
+ END IF;
+ UPDATE merchant_accounts
+ SET active=FALSE
+ WHERE h_wire=in_h_wire
+ AND merchant_serial=my_instance;
+ out_found = FOUND;
+ IF out_found
+ THEN
+ -- Notify taler-merchant-kyccheck about the change in
+ -- accounts. (TALER_DBEVENT_MERCHANT_ACCOUNTS_CHANGED)
+ NOTIFY XDQM4Z4N0D3GX0H9JEXH70EBC2T3KY7HC0TJB0Z60D2H781RXR6AG;
+ END IF;
+
+END $$;
diff --git a/src/backenddb/procedures.sql.in b/src/backenddb/procedures.sql.in
@@ -40,6 +40,8 @@ SET search_path TO merchant;
#include "pg_uri_escape.sql"
#include "pg_merchant_kyc_trigger.sql"
#include "pg_merchant_send_kyc_notification.sql"
+#include "pg_activate_account.sql"
+#include "pg_inactivate_account.sql"
DROP PROCEDURE IF EXISTS merchant_do_gc;
CREATE PROCEDURE merchant_do_gc(in_now INT8)
diff --git a/src/include/taler/taler_merchantdb_plugin.h b/src/include/taler/taler_merchantdb_plugin.h
@@ -2512,14 +2512,21 @@ struct TALER_MERCHANTDB_Plugin
* Set an instance's account in our database to "active".
*
* @param cls closure
- * @param merchant_id merchant backend instance ID
- * @param h_wire hash of the wire account to set to active
+ * @param account_details details of the account to actiate
+ * @param[out] h_wire set to the hash of the salted payto URI
+ * @param[out] salt set to salt used for computing @a h_wire
+ * @param[out] not_found set to true if the instance was not found
+ * @param[out] conflict set to true if a conflicting account is active
* @return database result code
*/
enum GNUNET_DB_QueryStatus
- (*activate_account)(void *cls,
- const char *merchant_id,
- const struct TALER_MerchantWireHashP *h_wire);
+ (*activate_account)(
+ void *cls,
+ const struct TALER_MERCHANTDB_AccountDetails *account_details,
+ struct TALER_MerchantWireHashP *h_wire,
+ struct TALER_WireSaltP *salt,
+ bool *not_found,
+ bool *conflict);
/**