commit cd9ef9b8345e9485d6e40bad2785c7691f4d2ea0
parent 22089cd6a77323e95733da90d6e515e26a61cba1
Author: Christian Grothoff <christian@grothoff.org>
Date: Wed, 29 Jul 2026 18:45:22 +0200
make the recovery document payment insert atomic via a stored procedure
Diffstat:
10 files changed, 175 insertions(+), 207 deletions(-)
diff --git a/meson.build b/meson.build
@@ -313,7 +313,7 @@ if not get_option('only-doc')
['libanastasisutil', '1:0:0'],
['libanastasisauthorization', '0:0:0'],
['libanastasiseufin', '0:0:0'],
- ['libanastasisdb', '3:0:0'],
+ ['libanastasisdb', '4:0:0'],
['libanastasisrest', '0:0:0'],
['libanastasistesting', '0:0:0'],
['libanastasisredux', '0:0:0'],
diff --git a/src/include/anastasis/anastasis-database/do_insert_recdoc_payment.h b/src/include/anastasis/anastasis-database/do_insert_recdoc_payment.h
@@ -0,0 +1,47 @@
+/*
+ This file is part of Anastasis
+ Copyright (C) 2020-2022 Anastasis SARL
+
+ Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ Anastasis; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file include/anastasis/anastasis-database/do_insert_recdoc_payment.h
+ * @brief Anastasis database: insert recdoc payment
+ * @author Christian Grothoff
+ */
+#ifndef ANASTASIS_DATABASE_DO_INSERT_RECDOC_PAYMENT_H
+#define ANASTASIS_DATABASE_DO_INSERT_RECDOC_PAYMENT_H
+
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_db_lib.h>
+#include "anastasis_service.h"
+#include "anastasis/anastasis-database/common.h"
+
+/**
+ * Store payment. Used to begin a payment, not indicative
+ * that the payment actually was made. (That is done
+ * when we increment the account's lifetime.)
+ *
+ * @param account_pub anastasis's public key
+ * @param post_counter how many uploads does @a amount pay for
+ * @param payment_secret payment secret which the user must provide with every upload
+ * @param amount how much we asked for
+ * @return transaction status
+ */
+enum GNUNET_DB_QueryStatus
+ANASTASIS_DB_do_insert_recdoc_payment (
+ const struct ANASTASIS_CRYPTO_AccountPublicKeyP *account_pub,
+ uint32_t post_counter,
+ const struct ANASTASIS_PaymentSecretP *payment_secret,
+ const struct TALER_Amount *amount);
+
+#endif
diff --git a/src/include/anastasis/anastasis-database/insert_recdoc_payment.h b/src/include/anastasis/anastasis-database/insert_recdoc_payment.h
@@ -1,47 +0,0 @@
-/*
- This file is part of Anastasis
- Copyright (C) 2020-2022 Anastasis SARL
-
- Anastasis is free software; you can redistribute it and/or modify it under the
- terms of the GNU Affero General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- Anastasis 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 Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License along with
- Anastasis; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/>
-*/
-/**
- * @file include/anastasis/anastasis-database/insert_recdoc_payment.h
- * @brief Anastasis database: insert recdoc payment
- * @author Christian Grothoff
- */
-#ifndef ANASTASIS_DATABASE_INSERT_RECDOC_PAYMENT_H
-#define ANASTASIS_DATABASE_INSERT_RECDOC_PAYMENT_H
-
-#include <gnunet/gnunet_util_lib.h>
-#include <gnunet/gnunet_db_lib.h>
-#include "anastasis_service.h"
-#include "anastasis/anastasis-database/common.h"
-
-/**
- * Store payment. Used to begin a payment, not indicative
- * that the payment actually was made. (That is done
- * when we increment the account's lifetime.)
- *
- * @param account_pub anastasis's public key
- * @param post_counter how many uploads does @a amount pay for
- * @param payment_secret payment secret which the user must provide with every upload
- * @param amount how much we asked for
- * @return transaction status
- */
-enum GNUNET_DB_QueryStatus
-ANASTASIS_DB_insert_recdoc_payment (
- const struct ANASTASIS_CRYPTO_AccountPublicKeyP *account_pub,
- uint32_t post_counter,
- const struct ANASTASIS_PaymentSecretP *payment_secret,
- const struct TALER_Amount *amount);
-
-#endif
diff --git a/src/include/anastasis_database_lib.h b/src/include/anastasis_database_lib.h
@@ -43,7 +43,7 @@
#include "anastasis/anastasis-database/get_recdoc_payment.h"
#include "anastasis/anastasis-database/get_challenge_payment.h"
#include "anastasis/anastasis-database/do_update_account_lifetime.h"
-#include "anastasis/anastasis-database/insert_recdoc_payment.h"
+#include "anastasis/anastasis-database/do_insert_recdoc_payment.h"
#include "anastasis/anastasis-database/insert_truth_payment.h"
#include "anastasis/anastasis-database/get_truth_payment.h"
#include "anastasis/anastasis-database/do_verify_challenge_code.h"
diff --git a/src/include/meson.build b/src/include/meson.build
@@ -37,7 +37,7 @@ install_data(
'anastasis/anastasis-database/get_recdoc_payment.h',
'anastasis/anastasis-database/get_challenge_payment.h',
'anastasis/anastasis-database/do_update_account_lifetime.h',
- 'anastasis/anastasis-database/insert_recdoc_payment.h',
+ 'anastasis/anastasis-database/do_insert_recdoc_payment.h',
'anastasis/anastasis-database/insert_truth_payment.h',
'anastasis/anastasis-database/get_truth_payment.h',
'anastasis/anastasis-database/do_verify_challenge_code.h',
diff --git a/src/stasis/do_insert_recdoc_payment.c b/src/stasis/do_insert_recdoc_payment.c
@@ -0,0 +1,63 @@
+/*
+ This file is part of Anastasis
+ Copyright (C) 2020-2026 Anastasis SARL
+
+ Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ Anastasis; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file stasis/do_insert_recdoc_payment.c
+ * @brief Anastasis database: record the start of a recovery document payment
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include "anastasis-db_pg.h"
+#include "anastasis/anastasis-database/do_insert_recdoc_payment.h"
+#include "anastasis/anastasis-database/preflight.h"
+#include <taler/taler_pq_lib.h>
+
+
+enum GNUNET_DB_QueryStatus
+ANASTASIS_DB_do_insert_recdoc_payment (
+ const struct ANASTASIS_CRYPTO_AccountPublicKeyP *account_pub,
+ uint32_t post_counter,
+ const struct ANASTASIS_PaymentSecretP *payment_secret,
+ const struct TALER_Amount *amount)
+{
+ struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get ();
+ struct GNUNET_TIME_Timestamp transient_expiration
+ = GNUNET_TIME_relative_to_timestamp (TRANSIENT_LIFETIME);
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (account_pub),
+ GNUNET_PQ_query_param_uint32 (&post_counter),
+ TALER_PQ_query_param_amount (pg->conn,
+ amount),
+ GNUNET_PQ_query_param_auto_from_type (payment_secret),
+ GNUNET_PQ_query_param_timestamp (&now),
+ GNUNET_PQ_query_param_timestamp (&transient_expiration),
+ GNUNET_PQ_query_param_end
+ };
+
+ GNUNET_break (GNUNET_OK ==
+ ANASTASIS_DB_preflight ());
+ /* Creating the account and recording the payment happen in one statement:
+ they must not be separated, since the payment row references the account
+ and two concurrent first uploads would otherwise collide. */
+ PREPARE ("do_insert_recdoc_payment",
+ "SELECT anastasis_do_insert_recdoc_payment"
+ " ($1, $2, $3, $4, $5, $6);");
+ return GNUNET_PQ_eval_prepared_non_select (pg->conn,
+ "do_insert_recdoc_payment",
+ params);
+}
+
+
+/* end of do_insert_recdoc_payment.c */
diff --git a/src/stasis/do_insert_recdoc_payment.sql b/src/stasis/do_insert_recdoc_payment.sql
@@ -0,0 +1,59 @@
+--
+-- This file is part of Anastasis
+-- Copyright (C) 2026 Anastasis SARL
+--
+-- ANASTASIS 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.
+--
+-- ANASTASIS 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
+-- ANASTASIS; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+--
+
+DROP FUNCTION IF EXISTS anastasis_do_insert_recdoc_payment;
+CREATE FUNCTION anastasis_do_insert_recdoc_payment (
+ IN in_user_id BYTEA,
+ IN in_post_counter INT4,
+ IN in_amount taler_amount,
+ IN in_payment_identifier BYTEA,
+ IN in_creation_date INT8,
+ IN in_transient_expiration INT8) -- lifetime given to an account created here
+RETURNS void
+LANGUAGE plpgsql
+AS $$
+BEGIN
+
+-- anastasis_recdoc_payment references anastasis_user, so the account has to
+-- exist first. ON CONFLICT DO NOTHING settles the race between two first
+-- uploads for the same account inside the statement; the SELECT ... FOR
+-- UPDATE this replaces held no lock at all, because it ran in its own
+-- autocommit transaction and released the row before the inserts.
+INSERT INTO anastasis_user
+ (user_id
+ ,expiration_date)
+ VALUES
+ (in_user_id
+ ,in_transient_expiration)
+ON CONFLICT (user_id) DO NOTHING;
+
+INSERT INTO anastasis_recdoc_payment
+ (user_id
+ ,post_counter
+ ,amount
+ ,payment_identifier
+ ,creation_date)
+ VALUES
+ (in_user_id
+ ,in_post_counter
+ ,in_amount
+ ,in_payment_identifier
+ ,in_creation_date);
+
+END $$;
+
+COMMENT ON FUNCTION anastasis_do_insert_recdoc_payment
+ IS 'Records the start of a payment for a recovery document upload, creating the account with a transient lifetime if it does not exist yet. Atomic, so concurrent first uploads for one account cannot collide on the primary key.';
diff --git a/src/stasis/insert_recdoc_payment.c b/src/stasis/insert_recdoc_payment.c
@@ -1,155 +0,0 @@
-/*
- This file is part of Anastasis
- Copyright (C) 2020-2022 Anastasis SARL
-
- Anastasis is free software; you can redistribute it and/or modify it under the
- terms of the GNU Affero General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- Anastasis 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 Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License along with
- Anastasis; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/>
-*/
-/**
- * @file stasis/insert_recdoc_payment.c
- * @brief Anastasis database: insert recdoc payment
- * @author Christian Grothoff
- */
-#include "platform.h"
-#include "anastasis-db_pg.h"
-#include "anastasis/anastasis-database/insert_recdoc_payment.h"
-#include "anastasis/anastasis-database/transaction.h"
-#include "anastasis/anastasis-database/preflight.h"
-#include <taler/taler_pq_lib.h>
-
-
-/**
- * Store payment. Used to begin a payment, not indicative
- * that the payment actually was made. (That is done
- * when we increment the account's lifetime.)
- *
- * @param account_pub anastasis's public key
- * @param post_counter how many uploads does @a amount pay for
- * @param payment_secret payment secret which the user must provide with every upload
- * @param amount how much we asked for
- * @return transaction status
- */
-enum GNUNET_DB_QueryStatus
-ANASTASIS_DB_insert_recdoc_payment (
- const struct ANASTASIS_CRYPTO_AccountPublicKeyP *account_pub,
- uint32_t post_counter,
- const struct ANASTASIS_PaymentSecretP *payment_secret,
- const struct TALER_Amount *amount)
-{
- struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get ();
- struct GNUNET_TIME_Timestamp expiration;
- struct GNUNET_PQ_QueryParam params[] = {
- GNUNET_PQ_query_param_auto_from_type (account_pub),
- GNUNET_PQ_query_param_uint32 (&post_counter),
- TALER_PQ_query_param_amount (pg->conn,
- amount),
- GNUNET_PQ_query_param_auto_from_type (payment_secret),
- GNUNET_PQ_query_param_timestamp (&now),
- GNUNET_PQ_query_param_end
- };
- enum GNUNET_DB_QueryStatus qs;
-
- GNUNET_break (GNUNET_OK ==
- ANASTASIS_DB_preflight ());
- PREPARE ("insert_recdoc_payment_select_account",
- "SELECT"
- " expiration_date "
- "FROM anastasis_user"
- " WHERE user_id=$1"
- " FOR UPDATE;");
- PREPARE ("insert_recdoc_payment_insert_account",
- "INSERT INTO anastasis_user "
- "(user_id"
- ",expiration_date"
- ") VALUES "
- "($1, $2);");
- PREPARE ("insert_recdoc_payment_insert",
- "INSERT INTO anastasis_recdoc_payment "
- "(user_id"
- ",post_counter"
- ",amount"
- ",payment_identifier"
- ",creation_date"
- ") VALUES "
- "($1, $2, $3, $4, $5);");
-
- /* because of constraint at user_id, first we have to verify
- if user exists, and if not, create one */
- {
- struct GNUNET_PQ_QueryParam iparams[] = {
- GNUNET_PQ_query_param_auto_from_type (account_pub),
- GNUNET_PQ_query_param_end
- };
- struct GNUNET_PQ_ResultSpec rs[] = {
- GNUNET_PQ_result_spec_timestamp ("expiration_date",
- &expiration),
- GNUNET_PQ_result_spec_end
- };
-
- qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
- "insert_recdoc_payment_select_account",
- iparams,
- rs);
- }
- switch (qs)
- {
- case GNUNET_DB_STATUS_HARD_ERROR:
- return qs;
- case GNUNET_DB_STATUS_SOFT_ERROR:
- GNUNET_break (0);
- return GNUNET_DB_STATUS_HARD_ERROR;
- case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
- {
- /* create new user with short lifetime */
- struct GNUNET_TIME_Timestamp exp
- = GNUNET_TIME_relative_to_timestamp (TRANSIENT_LIFETIME);
- struct GNUNET_PQ_QueryParam iparams[] = {
- GNUNET_PQ_query_param_auto_from_type (account_pub),
- GNUNET_PQ_query_param_timestamp (&exp),
- GNUNET_PQ_query_param_end
- };
-
- qs = GNUNET_PQ_eval_prepared_non_select (pg->conn,
- "insert_recdoc_payment_insert_account",
- iparams);
- switch (qs)
- {
- case GNUNET_DB_STATUS_HARD_ERROR:
- return GNUNET_DB_STATUS_HARD_ERROR;
- case GNUNET_DB_STATUS_SOFT_ERROR:
- GNUNET_break (0);
- return GNUNET_DB_STATUS_HARD_ERROR;
- case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
- GNUNET_break (0);
- return GNUNET_DB_STATUS_HARD_ERROR;
- case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
- /* successful, continue below */
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Created new account %s with transient life until %s\n",
- TALER_B2S (account_pub),
- GNUNET_TIME_timestamp2s (exp));
- break;
- }
- }
- /* continue below */
- break;
- case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
- /* handle case below */
- break;
- }
-
- return GNUNET_PQ_eval_prepared_non_select (pg->conn,
- "insert_recdoc_payment_insert",
- params);
-}
-
-
-/* end of insert_recdoc_payment.c */
diff --git a/src/stasis/meson.build b/src/stasis/meson.build
@@ -17,6 +17,7 @@ install_data(
procedures_sql = [
'procedures-prelude.sql',
'do_gc.sql',
+ 'do_insert_recdoc_payment.sql',
'do_update_account_lifetime.sql',
'commit.sql',
]
@@ -57,7 +58,7 @@ libanastasisdb_SOURCES = [
'insert_auth_iban_in.c',
'insert_challenge_payment.c',
'update_to_challenge_payment_refunded.c',
- 'insert_recdoc_payment.c',
+ 'do_insert_recdoc_payment.c',
'insert_truth_payment.c',
'do_insert_recovery_document.c',
'insert_truth.c',
diff --git a/src/stasis/test_anastasis_db.c b/src/stasis/test_anastasis_db.c
@@ -140,7 +140,7 @@ run (void *cls)
memset (&accountPubP, 2, sizeof (accountPubP));
memset (&accountSig, 3, sizeof (accountSig));
FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
- ANASTASIS_DB_insert_recdoc_payment (
+ ANASTASIS_DB_do_insert_recdoc_payment (
&accountPubP,
post_counter,
&paymentSecretP,