sync

Backup service to store encrypted wallet databases (experimental)
Log | Files | Refs | Submodules | README | LICENSE

commit c2b5ac518c05250e15ce720c131fb2814f7fa3eb
parent 666ef72f72439c9e608a67aa45ee13df749ac12d
Author: Christian Grothoff <christian@grothoff.org>
Date:   Sun, 22 Mar 2026 15:16:04 +0100

use stored procedures instead of manual transactions

Diffstat:
Msrc/syncdb/.gitignore | 1+
Msrc/syncdb/Makefile.am | 20++++++++++++++++++++
Asrc/syncdb/procedures.sql.in | 26++++++++++++++++++++++++++
Msrc/syncdb/syncdb_create_tables.c | 5++++-
Msrc/syncdb/syncdb_increment_lifetime_TR.c | 172++++++++++++-------------------------------------------------------------------
Asrc/syncdb/syncdb_increment_lifetime_TR.sql | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/syncdb/syncdb_lookup_account_TR.c | 90+++++++++++++++++++++++++------------------------------------------------------
Asrc/syncdb/syncdb_lookup_account_TR.sql | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/syncdb/syncdb_pg.c | 59-----------------------------------------------------------
Msrc/syncdb/syncdb_pg.h | 27---------------------------
Msrc/syncdb/syncdb_store_backup_TR.c | 167++++++++++++++++++++++---------------------------------------------------------
Asrc/syncdb/syncdb_store_backup_TR.sql | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/syncdb/syncdb_update_backup_TR.c | 182+++++++++++++++++++++++--------------------------------------------------------
Asrc/syncdb/syncdb_update_backup_TR.sql | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
14 files changed, 512 insertions(+), 547 deletions(-)

diff --git a/src/syncdb/.gitignore b/src/syncdb/.gitignore @@ -3,3 +3,4 @@ test_sync_db-postgres .libs test-suite.log sync-dbinit +procedures.sql diff --git a/src/syncdb/Makefile.am b/src/syncdb/Makefile.am @@ -13,11 +13,30 @@ endif sqldir = $(prefix)/share/sync/sql/ +sqlinputs = \ + syncdb_increment_lifetime_TR.sql \ + syncdb_lookup_account_TR.sql \ + syncdb_lookup_backup_TR.sql \ + syncdb_lookup_pending_payments_by_account_TR.sql \ + syncdb_store_backup_TR.sql \ + syncdb_store_payment_TR.sql \ + syncdb_update_backup_TR.sql \ + procedures.sql.in + sql_DATA = \ versioning.sql \ + procedures.sql \ sync-0001.sql \ drop.sql +BUILT_SOURCES = \ + procedures.sql + +procedures.sql: procedures.sql.in syncdb_*_TR.sql + chmod +w $@ || true + gcc -E -P -undef - < procedures.sql.in 2>/dev/null | sed -e "s/--.*//" | awk 'NF' - >$@ + chmod ugo-w $@ + bin_PROGRAMS = \ sync-dbinit @@ -81,4 +100,5 @@ TESTS = \ EXTRA_DIST = \ $(pkgcfg_DATA) \ $(sql_DATA) \ + $(sqlinputs) \ test_sync_db_postgres.conf diff --git a/src/syncdb/procedures.sql.in b/src/syncdb/procedures.sql.in @@ -0,0 +1,26 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2024 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/> +-- + +BEGIN; + +SET search_path TO sync; + +#include "syncdb_increment_lifetime_TR.sql" +#include "syncdb_lookup_account_TR.sql" +#include "syncdb_store_backup_TR.sql" +#include "syncdb_update_backup_TR.sql" + +COMMIT; diff --git a/src/syncdb/syncdb_create_tables.c b/src/syncdb/syncdb_create_tables.c @@ -29,6 +29,7 @@ SYNCDB_create_tables (void) GNUNET_PQ_make_execute ("SET search_path TO sync;"), GNUNET_PQ_EXECUTE_STATEMENT_END }; + enum GNUNET_GenericReturnValue ret; conn = GNUNET_PQ_connect_with_cfg (pg->cfg, "syncdb-postgres", @@ -37,8 +38,10 @@ SYNCDB_create_tables (void) NULL); if (NULL == conn) return GNUNET_SYSERR; + ret = GNUNET_PQ_exec_sql (conn, + "procedures"); GNUNET_PQ_disconnect (conn); - return GNUNET_OK; + return ret; } diff --git a/src/syncdb/syncdb_increment_lifetime_TR.c b/src/syncdb/syncdb_increment_lifetime_TR.c @@ -27,166 +27,44 @@ SYNCDB_increment_lifetime_TR ( const char *order_id, struct GNUNET_TIME_Relative lifetime) { - struct GNUNET_TIME_Timestamp expiration; + struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get (); + bool no_payment; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (account_pub), + GNUNET_PQ_query_param_string (order_id), + GNUNET_PQ_query_param_relative_time (&lifetime), + GNUNET_PQ_query_param_timestamp (&now), + GNUNET_PQ_query_param_end + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_bool ("out_no_payment", + &no_payment), + GNUNET_PQ_result_spec_end + }; enum GNUNET_DB_QueryStatus qs; - if (GNUNET_OK != - begin_transaction ("increment lifetime")) - { - GNUNET_break (0); - return SYNC_DB_HARD_ERROR; - } - - PREPARE ("increment_lifetime_payment_done", - "UPDATE payments " - "SET" - " paid=TRUE " - "WHERE" - " order_id=$1" - " AND" - " account_pub=$2" - " AND" - " paid=FALSE;"); - - { - struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (order_id), - GNUNET_PQ_query_param_auto_from_type (account_pub), - GNUNET_PQ_query_param_end - }; - - qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, - "increment_lifetime_payment_done", - params); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - rollback (); - return SYNC_DB_HARD_ERROR; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - rollback (); - return SYNC_DB_SOFT_ERROR; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - rollback (); - return SYNC_DB_NO_RESULTS; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - } - - PREPARE ("increment_lifetime_account_select", + SYNCDB_preflight (); + PREPARE ("do_increment_lifetime", "SELECT" - " expiration_date " - "FROM" - " accounts " - "WHERE" - " account_pub=$1;"); - - { - struct GNUNET_PQ_QueryParam params[] = { - 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, - "increment_lifetime_account_select", - params, - rs); - } - - PREPARE ("increment_lifetime_account_insert", - "INSERT INTO accounts " - "(account_pub" - ",expiration_date" - ") VALUES " - "($1,$2);"); - - PREPARE ("increment_lifetime_account_update", - "UPDATE accounts " - "SET" - " expiration_date=$1 " - "WHERE" - " account_pub=$2;"); - + " out_no_payment" + " FROM sync_do_increment_lifetime" + " ($1,$2,$3,$4);"); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "do_increment_lifetime", + params, + rs); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: - rollback (); return SYNC_DB_HARD_ERROR; case GNUNET_DB_STATUS_SOFT_ERROR: - rollback (); return SYNC_DB_SOFT_ERROR; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - { - struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_auto_from_type (account_pub), - GNUNET_PQ_query_param_timestamp (&expiration), - GNUNET_PQ_query_param_end - }; - - expiration = GNUNET_TIME_relative_to_timestamp (lifetime); - qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, - "increment_lifetime_account_insert", - params); - } - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - { - struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_timestamp (&expiration), - GNUNET_PQ_query_param_auto_from_type (account_pub), - GNUNET_PQ_query_param_end - }; - - expiration = GNUNET_TIME_absolute_to_timestamp ( - GNUNET_TIME_absolute_add (expiration.abs_time, - lifetime)); - qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, - "increment_lifetime_account_update", - params); - } - break; - default: GNUNET_break (0); return SYNC_DB_HARD_ERROR; - } - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - rollback (); - return SYNC_DB_HARD_ERROR; - case GNUNET_DB_STATUS_SOFT_ERROR: - rollback (); - GNUNET_break (0); - return SYNC_DB_SOFT_ERROR; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_break (0); - rollback (); - return SYNC_DB_NO_RESULTS; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - default: - GNUNET_break (0); - return SYNC_DB_HARD_ERROR; - } - qs = commit_transaction (); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - return SYNC_DB_HARD_ERROR; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return SYNC_DB_SOFT_ERROR; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return SYNC_DB_ONE_RESULT; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + if (no_payment) + return SYNC_DB_NO_RESULTS; return SYNC_DB_ONE_RESULT; default: GNUNET_break (0); diff --git a/src/syncdb/syncdb_increment_lifetime_TR.sql b/src/syncdb/syncdb_increment_lifetime_TR.sql @@ -0,0 +1,66 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2024 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 sync_do_increment_lifetime; +CREATE FUNCTION sync_do_increment_lifetime ( + IN in_account_pub BYTEA, + IN in_order_id TEXT, + IN in_lifetime_us INT8, + IN in_now_us INT8, + OUT out_no_payment BOOLEAN) +LANGUAGE plpgsql +AS $$ +DECLARE + my_expiration INT8; +BEGIN + out_no_payment = FALSE; + + -- Mark payment as done + UPDATE payments + SET paid=TRUE + WHERE order_id=in_order_id + AND account_pub=in_account_pub + AND paid=FALSE; + + IF NOT FOUND + THEN + out_no_payment = TRUE; + RETURN; + END IF; + + -- Get current expiration + SELECT expiration_date + INTO my_expiration + FROM accounts + WHERE account_pub=in_account_pub; + + IF NOT FOUND + THEN + -- No account yet, create one with expiration = now + lifetime + INSERT INTO accounts + (account_pub + ,expiration_date) + VALUES + (in_account_pub + ,in_now_us + in_lifetime_us); + ELSE + -- Account exists, extend expiration + UPDATE accounts + SET expiration_date=my_expiration + in_lifetime_us + WHERE account_pub=in_account_pub; + END IF; +END $$; diff --git a/src/syncdb/syncdb_lookup_account_TR.c b/src/syncdb/syncdb_lookup_account_TR.c @@ -26,34 +26,35 @@ SYNCDB_lookup_account_TR ( const struct SYNC_AccountPublicKeyP *account_pub, struct GNUNET_HashCode *backup_hash) { - enum GNUNET_DB_QueryStatus qs; + bool payment_required; + bool no_backup; struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_auto_from_type (account_pub), GNUNET_PQ_query_param_end }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_auto_from_type ("out_backup_hash", + backup_hash), + GNUNET_PQ_result_spec_bool ("out_payment_required", + &payment_required), + GNUNET_PQ_result_spec_bool ("out_no_backup", + &no_backup), + GNUNET_PQ_result_spec_end + }; + enum GNUNET_DB_QueryStatus qs; SYNCDB_preflight (); - - PREPARE ("lookup_account_backup_select_hash", - "SELECT " - " backup_hash " - "FROM" - " backups " - "WHERE" - " account_pub=$1;"); - - { - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_auto_from_type ("backup_hash", - backup_hash), - GNUNET_PQ_result_spec_end - }; - - qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_account_backup_select_hash", - params, - rs); - } + PREPARE ("do_lookup_account", + "SELECT" + " out_backup_hash" + ",out_payment_required" + ",out_no_backup" + " FROM sync_do_lookup_account" + " ($1);"); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "do_lookup_account", + params, + rs); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: @@ -62,49 +63,14 @@ SYNCDB_lookup_account_TR ( GNUNET_break (0); return SYNC_DB_SOFT_ERROR; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - break; /* handle interesting case below */ - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - return SYNC_DB_ONE_RESULT; - default: GNUNET_break (0); return SYNC_DB_HARD_ERROR; - } - - PREPARE ("lookup_account_account_select", - "SELECT" - " expiration_date " - "FROM" - " accounts " - "WHERE" - " account_pub=$1;"); - - /* check if account exists */ - { - struct GNUNET_TIME_Timestamp expiration; - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_auto_from_type ("expiration_date", - &expiration), - GNUNET_PQ_result_spec_end - }; - - qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_account_account_select", - params, - rs); - } - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - return SYNC_DB_HARD_ERROR; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return SYNC_DB_SOFT_ERROR; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* indicates: no account */ - return SYNC_DB_PAYMENT_REQUIRED; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* indicates: no backup */ - return SYNC_DB_NO_RESULTS; + if (payment_required) + return SYNC_DB_PAYMENT_REQUIRED; + if (no_backup) + return SYNC_DB_NO_RESULTS; + return SYNC_DB_ONE_RESULT; default: GNUNET_break (0); return SYNC_DB_HARD_ERROR; diff --git a/src/syncdb/syncdb_lookup_account_TR.sql b/src/syncdb/syncdb_lookup_account_TR.sql @@ -0,0 +1,54 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2024 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 sync_do_lookup_account; +CREATE FUNCTION sync_do_lookup_account ( + IN in_account_pub BYTEA, + OUT out_backup_hash BYTEA, + OUT out_payment_required BOOLEAN, + OUT out_no_backup BOOLEAN) +LANGUAGE plpgsql +AS $$ +BEGIN + out_payment_required = FALSE; + out_no_backup = FALSE; + + -- Check if backup exists + SELECT backup_hash + INTO out_backup_hash + FROM backups + WHERE account_pub=in_account_pub; + + IF FOUND + THEN + RETURN; + END IF; + + -- No backup; check if account exists + PERFORM 1 + FROM accounts + WHERE account_pub=in_account_pub; + + IF NOT FOUND + THEN + out_payment_required = TRUE; + RETURN; + END IF; + + -- Account exists but no backup + out_no_backup = TRUE; +END $$; diff --git a/src/syncdb/syncdb_pg.c b/src/syncdb/syncdb_pg.c @@ -29,65 +29,6 @@ struct PostgresClosure *pg; enum GNUNET_GenericReturnValue -begin_transaction (const char *name) -{ - struct GNUNET_PQ_ExecuteStatement es[] = { - GNUNET_PQ_make_execute ("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"), - GNUNET_PQ_EXECUTE_STATEMENT_END - }; - - SYNCDB_preflight (); - pg->transaction_name = name; - if (GNUNET_OK != - GNUNET_PQ_exec_statements (pg->conn, - es)) - { - TALER_LOG_ERROR ("Failed to start transaction\n"); - GNUNET_break (0); - return GNUNET_SYSERR; - } - return GNUNET_OK; -} - - -void -rollback (void) -{ - struct GNUNET_PQ_ExecuteStatement es[] = { - GNUNET_PQ_make_execute ("ROLLBACK"), - GNUNET_PQ_EXECUTE_STATEMENT_END - }; - - if (GNUNET_OK != - GNUNET_PQ_exec_statements (pg->conn, - es)) - { - TALER_LOG_ERROR ("Failed to rollback transaction\n"); - GNUNET_break (0); - } - pg->transaction_name = NULL; -} - - -enum GNUNET_DB_QueryStatus -commit_transaction (void) -{ - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_PQ_QueryParam no_params[] = { - GNUNET_PQ_query_param_end - }; - - PREPARE ("do_commit", - "COMMIT"); - qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, - "do_commit", - no_params); - pg->transaction_name = NULL; - return qs; -} - - -enum GNUNET_GenericReturnValue SYNCDB_init (const struct GNUNET_CONFIGURATION_Handle *cfg, bool skip_preflight) { diff --git a/src/syncdb/syncdb_pg.h b/src/syncdb/syncdb_pg.h @@ -74,33 +74,6 @@ extern struct PostgresClosure *pg; /** - * Start a transaction. - * - * @param name unique name identifying the transaction (for debugging), - * must point to a constant - * @return #GNUNET_OK on success - */ -enum GNUNET_GenericReturnValue -begin_transaction (const char *name); - - -/** - * Roll back the current transaction of a database connection. - */ -void -rollback (void); - - -/** - * Commit the current transaction of a database connection. - * - * @return transaction status code - */ -enum GNUNET_DB_QueryStatus -commit_transaction (void); - - -/** * Prepares SQL statement @a sql under @a name for * connection @a pg once. * Returns with #GNUNET_DB_STATUS_HARD_ERROR on failure. diff --git a/src/syncdb/syncdb_store_backup_TR.c b/src/syncdb/syncdb_store_backup_TR.c @@ -29,80 +29,46 @@ SYNCDB_store_backup_TR ( size_t backup_size, const void *backup) { + uint32_t bs = (uint32_t) backup_size; + bool payment_required; + bool old_backup_mismatch; + bool no_change; + bool inserted; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (account_pub), + GNUNET_PQ_query_param_auto_from_type (account_sig), + GNUNET_PQ_query_param_auto_from_type (backup_hash), + GNUNET_PQ_query_param_uint32 (&bs), + GNUNET_PQ_query_param_fixed_size (backup, + backup_size), + GNUNET_PQ_query_param_end + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_bool ("out_payment_required", + &payment_required), + GNUNET_PQ_result_spec_bool ("out_old_backup_mismatch", + &old_backup_mismatch), + GNUNET_PQ_result_spec_bool ("out_no_change", + &no_change), + GNUNET_PQ_result_spec_bool ("out_inserted", + &inserted), + GNUNET_PQ_result_spec_end + }; enum GNUNET_DB_QueryStatus qs; - struct GNUNET_HashCode bh; - static struct GNUNET_HashCode no_previous_hash; SYNCDB_preflight (); - - PREPARE ("store_backup_backup_insert", - "INSERT INTO backups " - "(account_pub" - ",account_sig" - ",prev_hash" - ",backup_hash" - ",data" - ") VALUES " - "($1,$2,$3,$4,$5);"); - - { - struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_auto_from_type (account_pub), - GNUNET_PQ_query_param_auto_from_type (account_sig), - GNUNET_PQ_query_param_auto_from_type (&no_previous_hash), - GNUNET_PQ_query_param_auto_from_type (backup_hash), - GNUNET_PQ_query_param_fixed_size (backup, - backup_size), - GNUNET_PQ_query_param_end - }; - - qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, - "store_backup_backup_insert", - params); - } - switch (qs) - { - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return SYNC_DB_SOFT_ERROR; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return SYNC_DB_NO_RESULTS; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - return SYNC_DB_ONE_RESULT; - case GNUNET_DB_STATUS_HARD_ERROR: - /* handle interesting case below */ - break; - default: - GNUNET_break (0); - return SYNC_DB_HARD_ERROR; - } - - PREPARE ("store_backup_account_select", + PREPARE ("do_store_backup", "SELECT" - " expiration_date " - "FROM" - " accounts " - "WHERE" - " account_pub=$1;"); - - /* First, check if account exists */ - { - struct GNUNET_TIME_Timestamp ed; - struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_auto_from_type (account_pub), - GNUNET_PQ_query_param_end - }; - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_auto_from_type ("expiration_date", - &ed), - GNUNET_PQ_result_spec_end - }; - - qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "store_backup_account_select", - params, - rs); - } + " out_payment_required" + ",out_old_backup_mismatch" + ",out_no_change" + ",out_inserted" + " FROM sync_do_store_backup" + " ($1,$2,$3,$4,$5);"); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "do_store_backup", + params, + rs); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: @@ -111,66 +77,23 @@ SYNCDB_store_backup_TR ( GNUNET_break (0); return SYNC_DB_SOFT_ERROR; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return SYNC_DB_PAYMENT_REQUIRED; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* handle interesting case below */ - break; - default: GNUNET_break (0); return SYNC_DB_HARD_ERROR; - } - - PREPARE ("store_backup_backup_select_hash", - "SELECT " - " backup_hash " - "FROM" - " backups " - "WHERE" - " account_pub=$1;"); - - - /* account exists, check if existing backup conflicts */ - { - struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_auto_from_type (account_pub), - GNUNET_PQ_query_param_end - }; - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_auto_from_type ("backup_hash", - &bh), - GNUNET_PQ_result_spec_end - }; - - qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "store_backup_backup_select_hash", - params, - rs); - } - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - return SYNC_DB_HARD_ERROR; - case GNUNET_DB_STATUS_SOFT_ERROR: + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + if (inserted) + return SYNC_DB_ONE_RESULT; + if (payment_required) + return SYNC_DB_PAYMENT_REQUIRED; + if (old_backup_mismatch) + return SYNC_DB_OLD_BACKUP_MISMATCH; + if (no_change) + return SYNC_DB_NO_RESULTS; GNUNET_break (0); - return SYNC_DB_SOFT_ERROR; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* original error must have been a hard error, oddly enough */ return SYNC_DB_HARD_ERROR; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* handle interesting case below */ - break; default: GNUNET_break (0); return SYNC_DB_HARD_ERROR; } - - /* had an existing backup, is it identical? */ - if (0 != GNUNET_memcmp (&bh, - backup_hash)) - /* previous conflicting backup exists */ - return SYNC_DB_OLD_BACKUP_MISMATCH; - /* backup identical to what was provided, no change */ - return SYNC_DB_NO_RESULTS; } diff --git a/src/syncdb/syncdb_store_backup_TR.sql b/src/syncdb/syncdb_store_backup_TR.sql @@ -0,0 +1,93 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2024 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 sync_do_store_backup; +CREATE FUNCTION sync_do_store_backup ( + IN in_account_pub BYTEA, + IN in_account_sig BYTEA, + IN in_backup_hash BYTEA, + IN in_backup_size INT4, + IN in_data BYTEA, + OUT out_payment_required BOOLEAN, + OUT out_old_backup_mismatch BOOLEAN, + OUT out_no_change BOOLEAN, + OUT out_inserted BOOLEAN) +LANGUAGE plpgsql +AS $$ +DECLARE + my_no_previous_hash BYTEA; + my_existing_hash BYTEA; +BEGIN + out_payment_required = FALSE; + out_old_backup_mismatch = FALSE; + out_no_change = FALSE; + out_inserted = FALSE; + my_no_previous_hash = '\x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'; + + -- Check if account exists + PERFORM 1 + FROM accounts + WHERE account_pub=in_account_pub; + + IF NOT FOUND + THEN + out_payment_required = TRUE; + RETURN; + END IF; + + -- Try to insert the backup + INSERT INTO backups + (account_pub + ,account_sig + ,prev_hash + ,backup_hash + ,data) + VALUES + (in_account_pub + ,in_account_sig + ,my_no_previous_hash + ,in_backup_hash + ,in_data) + ON CONFLICT DO NOTHING; + + IF FOUND + THEN + out_inserted = TRUE; + RETURN; + END IF; + + -- Check for existing backup hash + SELECT backup_hash + INTO my_existing_hash + FROM backups + WHERE account_pub=in_account_pub; + + IF NOT FOUND + THEN + -- Shouldn't happen: account exists but insert failed and no backup exists + RAISE EXCEPTION 'unexpected state: account exists but no backup and insert failed'; + END IF; + + IF my_existing_hash <> in_backup_hash + THEN + out_old_backup_mismatch = TRUE; + RETURN; + END IF; + + -- Backup identical to what was provided + out_no_change = TRUE; +END $$; diff --git a/src/syncdb/syncdb_update_backup_TR.c b/src/syncdb/syncdb_update_backup_TR.c @@ -30,81 +30,51 @@ SYNCDB_update_backup_TR ( size_t backup_size, const void *backup) { + uint32_t bs = (uint32_t) backup_size; + bool updated; + bool payment_required; + bool old_backup_missing; + bool old_backup_mismatch; + bool no_change; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (account_pub), + GNUNET_PQ_query_param_auto_from_type (old_backup_hash), + GNUNET_PQ_query_param_auto_from_type (account_sig), + GNUNET_PQ_query_param_auto_from_type (backup_hash), + GNUNET_PQ_query_param_uint32 (&bs), + GNUNET_PQ_query_param_fixed_size (backup, + backup_size), + GNUNET_PQ_query_param_end + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_bool ("out_updated", + &updated), + GNUNET_PQ_result_spec_bool ("out_payment_required", + &payment_required), + GNUNET_PQ_result_spec_bool ("out_old_backup_missing", + &old_backup_missing), + GNUNET_PQ_result_spec_bool ("out_old_backup_mismatch", + &old_backup_mismatch), + GNUNET_PQ_result_spec_bool ("out_no_change", + &no_change), + GNUNET_PQ_result_spec_end + }; enum GNUNET_DB_QueryStatus qs; - struct GNUNET_HashCode bh; SYNCDB_preflight (); - PREPARE ("backup_update", - "UPDATE backups " - " SET" - " backup_hash=$1" - ",account_sig=$2" - ",prev_hash=$3" - ",data=$4" - " WHERE" - " account_pub=$5" - " AND" - " backup_hash=$6;"); - { - struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_auto_from_type (backup_hash), - GNUNET_PQ_query_param_auto_from_type (account_sig), - GNUNET_PQ_query_param_auto_from_type (old_backup_hash), - GNUNET_PQ_query_param_fixed_size (backup, - backup_size), - GNUNET_PQ_query_param_auto_from_type (account_pub), - GNUNET_PQ_query_param_auto_from_type (old_backup_hash), - GNUNET_PQ_query_param_end - }; - - qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, - "backup_update", - params); - } - switch (qs) - { - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return SYNC_DB_SOFT_ERROR; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* handle interesting case below */ - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - return SYNC_DB_ONE_RESULT; - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - return SYNC_DB_HARD_ERROR; - default: - GNUNET_break (0); - return SYNC_DB_HARD_ERROR; - } - - PREPARE ("update_backup_account_select", + PREPARE ("do_update_backup", "SELECT" - " expiration_date " - "FROM" - " accounts " - "WHERE" - " account_pub=$1;"); - - /* First, check if account exists */ - { - struct GNUNET_TIME_Timestamp ed; - struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_auto_from_type (account_pub), - GNUNET_PQ_query_param_end - }; - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_auto_from_type ("expiration_date", - &ed), - GNUNET_PQ_result_spec_end - }; - - qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "update_backup_account_select", - params, - rs); - } + " out_updated" + ",out_payment_required" + ",out_old_backup_missing" + ",out_old_backup_mismatch" + ",out_no_change" + " FROM sync_do_update_backup" + " ($1,$2,$3,$4,$5,$6);"); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "do_update_backup", + params, + rs); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: @@ -113,71 +83,25 @@ SYNCDB_update_backup_TR ( GNUNET_break (0); return SYNC_DB_SOFT_ERROR; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return SYNC_DB_PAYMENT_REQUIRED; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* handle interesting case below */ - break; - default: GNUNET_break (0); return SYNC_DB_HARD_ERROR; - } - - PREPARE ("update_backup_backup_select_hash", - "SELECT " - " backup_hash " - "FROM" - " backups " - "WHERE" - " account_pub=$1;"); - - /* account exists, check if existing backup conflicts */ - { - struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_auto_from_type (account_pub), - GNUNET_PQ_query_param_end - }; - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_auto_from_type ("backup_hash", - &bh), - GNUNET_PQ_result_spec_end - }; - - qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "update_backup_backup_select_hash", - params, - rs); - } - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - return SYNC_DB_HARD_ERROR; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return SYNC_DB_SOFT_ERROR; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return SYNC_DB_OLD_BACKUP_MISSING; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* handle interesting case below */ - break; + if (updated) + return SYNC_DB_ONE_RESULT; + if (payment_required) + return SYNC_DB_PAYMENT_REQUIRED; + if (old_backup_missing) + return SYNC_DB_OLD_BACKUP_MISSING; + if (old_backup_mismatch) + return SYNC_DB_OLD_BACKUP_MISMATCH; + if (no_change) + return SYNC_DB_NO_RESULTS; + GNUNET_break (0); + return SYNC_DB_HARD_ERROR; default: GNUNET_break (0); return SYNC_DB_HARD_ERROR; } - - /* had an existing backup, is it identical? */ - if (0 == GNUNET_memcmp (&bh, - backup_hash)) - { - /* backup identical to what was provided, no change */ - return SYNC_DB_NO_RESULTS; - } - if (0 == GNUNET_memcmp (&bh, - old_backup_hash)) - /* all constraints seem satisfied, original error must - have been a hard error */ - return SYNC_DB_HARD_ERROR; - /* previous backup does not match old_backup_hash */ - return SYNC_DB_OLD_BACKUP_MISMATCH; } diff --git a/src/syncdb/syncdb_update_backup_TR.sql b/src/syncdb/syncdb_update_backup_TR.sql @@ -0,0 +1,97 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2024 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 sync_do_update_backup; +CREATE FUNCTION sync_do_update_backup ( + IN in_account_pub BYTEA, + IN in_old_backup_hash BYTEA, + IN in_account_sig BYTEA, + IN in_backup_hash BYTEA, + IN in_backup_size INT4, + IN in_data BYTEA, + OUT out_updated BOOLEAN, + OUT out_payment_required BOOLEAN, + OUT out_old_backup_missing BOOLEAN, + OUT out_old_backup_mismatch BOOLEAN, + OUT out_no_change BOOLEAN) +LANGUAGE plpgsql +AS $$ +DECLARE + my_existing_hash BYTEA; +BEGIN + out_updated = FALSE; + out_payment_required = FALSE; + out_old_backup_missing = FALSE; + out_old_backup_mismatch = FALSE; + out_no_change = FALSE; + + -- Try to update the backup (matches on account_pub AND old backup_hash) + UPDATE backups + SET backup_hash=in_backup_hash + ,account_sig=in_account_sig + ,prev_hash=in_old_backup_hash + ,data=in_data + WHERE account_pub=in_account_pub + AND backup_hash=in_old_backup_hash; + + IF FOUND + THEN + out_updated = TRUE; + RETURN; + END IF; + + -- Update failed; check if account exists + PERFORM 1 + FROM accounts + WHERE account_pub=in_account_pub; + + IF NOT FOUND + THEN + out_payment_required = TRUE; + RETURN; + END IF; + + -- Account exists; check existing backup hash + SELECT backup_hash + INTO my_existing_hash + FROM backups + WHERE account_pub=in_account_pub; + + IF NOT FOUND + THEN + out_old_backup_missing = TRUE; + RETURN; + END IF; + + -- Had an existing backup; is the new hash identical? + IF my_existing_hash = in_backup_hash + THEN + out_no_change = TRUE; + RETURN; + END IF; + + -- Check if existing hash matches expected old hash + IF my_existing_hash = in_old_backup_hash + THEN + -- All constraints satisfied but update failed => hard error + -- (this shouldn't normally happen) + RAISE EXCEPTION 'unexpected state: backup hash matches but update failed'; + END IF; + + -- Previous backup does not match old_backup_hash + out_old_backup_mismatch = TRUE; +END $$;