commit e838c2881998fe7dd9b186b5fa06af433541bc32
parent f33f0927a2cb0ce0b2e7843fe1664cfc895a4ec0
Author: Iván Ávalos <avalos@disroot.org>
Date: Tue, 26 May 2026 17:35:13 +0200
new sync server implementation (DD92)
Diffstat:
133 files changed, 14195 insertions(+), 7178 deletions(-)
diff --git a/meson.build b/meson.build
@@ -7,7 +7,11 @@ project(
)
cc = meson.get_compiler('c')
-incdir = include_directories('src/include', 'src/include/sync', 'src/include/sync/sync-database')
+incdir = include_directories(
+ 'src/include',
+ 'src/include/sync',
+ 'src/include/sync/sync-database',
+)
# Used to populate gnunet_private_config.h
private_config = configuration_data()
@@ -69,9 +73,10 @@ if not get_option('only-doc')
mhd_dep = cc.find_library('microhttpd', required: true)
endif
+ # sync-httpd2 is the only daemon we build, so MHD2 is mandatory.
mhd2_dep = dependency('libmicrohttpd2', required: false)
if not mhd2_dep.found()
- mhd2_dep = cc.find_library('microhttpd2', required: false)
+ mhd2_dep = cc.find_library('microhttpd2', required: true)
endif
json_dep = dependency('jansson', required: false)
@@ -256,10 +261,7 @@ if not get_option('only-doc')
'taler/taler_pq_lib.h',
'TALER_PQ_query_param_array_blinded_denom_sig',
required: true,
- dependencies: [
- talerpq_dep,
- pq_dep,
- ],
+ dependencies: [talerpq_dep, pq_dep],
)
private_config.set10('HAVE_TALERPQ', talerpq_dep.found())
@@ -346,11 +348,16 @@ if not get_option('only-doc')
subdir('doc')
endif
- taler_prefix = get_option('prefix') / get_option('libdir')
+ # The variable name must match GNUNET_OS_ProjectData.env_varname in
+ # src/util/os_installation.c. Otherwise GNUnet falls back to locating
+ # $DATADIR relative to libsyncutil, which under the build-tree
+ # LD_LIBRARY_PATH that meson injects resolves into the build directory,
+ # and the tests fail to find the installed SQL files.
+ sync_prefix = get_option('prefix') / get_option('libdir')
add_test_setup(
'default',
- env: ['TALER_SYNC_PREFIX=' + taler_prefix],
+ env: ['SYNC_PREFIX=' + sync_prefix],
exclude_suites: ['perf', 'installcheck', 'integrationtests'],
is_default: true,
)
diff --git a/src/include/sync/meson.build b/src/include/sync/meson.build
@@ -3,6 +3,7 @@ subdir('sync-database')
install_data(
'sync_database_lib.h',
'sync_service.h',
+ 'sync_signatures.h',
'sync_testing_lib.h',
'sync_util.h',
install_dir: get_option('includedir') / 'sync',
diff --git a/src/include/sync/sync-database/append_block_TR.h b/src/include/sync/sync-database/append_block_TR.h
@@ -0,0 +1,73 @@
+/*
+ This file is part of TALER
+ (C) 2026 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Lesser 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/>
+*/
+/**
+ * @file include/sync/sync-database/append_block_TR.h
+ * @brief append block at the end of DLL
+ * @author Iván Ávalos
+ */
+#ifndef SYNC_DATABASE_APPEND_BLOCK_TR_H
+#define SYNC_DATABASE_APPEND_BLOCK_TR_H
+
+#include <gnunet/gnunet_util_lib.h>
+#include "sync/sync_service.h"
+
+/**
+ * Append block at the end of DLL.
+ *
+ * The append relinks the previous tail, so @a prev_upload_sig is the
+ * tail's new signature (covering its new next link). That signature was
+ * verified against the tail as it was read outside the transaction, so
+ * the transaction re-checks the stored tail against @a prev_hash and
+ * @a prev_prev_nonce before storing it.
+ *
+ * @param account_pub account to store block under
+ * @param block_nonce nonce of the block to store
+ * @param prev_nonce nonce of the last block in the backup
+ * @param data_hash hash of @a data
+ * @param data_size number of bytes in @a data
+ * @param data raw data of the block to store
+ * @param refs_length number of entries in @a object_uids and @a object_incs
+ * @param object_uids array of object UIDs referenced by the block
+ * @param object_incs array of refcount increments for @a object_uids
+ * @param upload_sig verified signature of the new block
+ * @param refs_hash hash over the object references
+ * @param prev_upload_sig new signature of the tail, NULL if the block is
+ * the first in the backup
+ * @param prev_hash current hash of the tail, NULL if the block is the
+ * first in the backup
+ * @param prev_prev_nonce nonce preceding the tail as covered by
+ * @a prev_upload_sig, NULL if the tail is the first block or if
+ * the block being appended is
+ * @return transaction status
+ */
+enum SYNC_DB_QueryStatus
+SYNCDB_append_block_TR (
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const struct SYNC_BlockNonce *block_nonce,
+ const struct SYNC_BlockNonce *prev_nonce,
+ const struct GNUNET_HashCode *data_hash,
+ size_t data_size,
+ const void *data,
+ unsigned int refs_length,
+ const struct SYNC_ObjectUID *object_uids,
+ const int16_t *object_incs,
+ const struct SYNC_AccountSignatureP *upload_sig,
+ const struct GNUNET_HashCode *refs_hash,
+ const struct SYNC_AccountSignatureP *prev_upload_sig,
+ const struct GNUNET_HashCode *prev_hash,
+ const struct SYNC_BlockNonce *prev_prev_nonce);
+
+#endif
diff --git a/src/include/sync/sync-database/common.h b/src/include/sync/sync-database/common.h
@@ -14,7 +14,7 @@
Taler; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/>
*/
/**
- * @file include/sync_database_common.h
+ * @file include/sync/sync-database/common.h
* @brief common sync database definitions
* @author Christian Grothoff
*/
@@ -34,14 +34,19 @@
enum SYNC_DB_QueryStatus
{
/**
- * Client claimed to be updating an existing backup, but we have none.
+ * Backup is in an inconsistent state.
*/
- SYNC_DB_OLD_BACKUP_MISSING = -5,
+ SYNC_DB_INCONSISTENT_BACKUP = -7,
/**
- * Update failed because the old backup hash does not match what we previously had in the DB.
+ * Client attempted to write an outdated copy of the backup.
*/
- SYNC_DB_OLD_BACKUP_MISMATCH = -4,
+ SYNC_DB_OUTDATED_WRITE = -6,
+
+ /**
+ * Client attempted to write a resource that does not exist on the server.
+ */
+ SYNC_DB_TARGET_MISSING = -5,
/**
* Account is unpaid / does not exist.
diff --git a/src/include/sync/sync-database/create_tables.h b/src/include/sync/sync-database/create_tables.h
@@ -14,7 +14,7 @@
Taler; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/>
*/
/**
- * @file include/sync_database_create_tables.h
+ * @file include/sync/sync-database/create_tables.h
* @brief create sync database tables
* @author Christian Grothoff
*/
diff --git a/src/include/sync/sync-database/delete_block_TR.h b/src/include/sync/sync-database/delete_block_TR.h
@@ -0,0 +1,72 @@
+/*
+ This file is part of TALER
+ (C) 2026 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Lesser 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/>
+*/
+/**
+ * @file include/sync/sync-database/delete_block_TR.h
+ * @brief delete a block in the DLL
+ * @author Iván Ávalos
+ */
+#ifndef SYNC_DATABASE_DELETE_BLOCK_TR_H
+#define SYNC_DATABASE_DELETE_BLOCK_TR_H
+
+#include <gnunet/gnunet_util_lib.h>
+#include "sync/sync_service.h"
+
+/**
+ * Delete a block in the DLL.
+ *
+ * The deletion relinks both neighbours, so @a prev_upload_sig and
+ * @a next_upload_sig are the neighbours' new signatures (covering their
+ * new links). Those signatures were verified against the neighbours as
+ * they were read outside the transaction, so the transaction re-checks
+ * the stored neighbours against @a prev_hash and @a prev_prev_nonce,
+ * resp. @a next_hash and @a next_next_nonce, before storing them.
+ *
+ * @param account_pub account to store block under
+ * @param block_nonce nonce of the block to delete
+ * @param prev_nonce nonce of the preceding block to @a block_nonce
+ * @param next_nonce nonce of the succeeding block to @a block_nonce
+ * @param block_hash hash of the block to delete
+ * @param refs_length number of entries in @a object_uids and @a object_incs
+ * @param object_uids array of object UIDs referenced by the block
+ * @param object_incs array of refcount increments for @a object_uids
+ * @param prev_upload_sig new signature of the preceding block, NULL if none
+ * @param prev_hash current hash of the preceding block, NULL if none
+ * @param prev_prev_nonce nonce preceding the preceding block as covered
+ * by @a prev_upload_sig, NULL if there is none
+ * @param next_upload_sig new signature of the succeeding block, NULL if none
+ * @param next_hash current hash of the succeeding block, NULL if none
+ * @param next_next_nonce nonce succeeding the succeeding block as covered
+ * by @a next_upload_sig, NULL if there is none
+ * @return transaction status
+ */
+enum SYNC_DB_QueryStatus
+SYNCDB_delete_block_TR (
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const struct SYNC_BlockNonce *block_nonce,
+ const struct SYNC_BlockNonce *prev_nonce,
+ const struct SYNC_BlockNonce *next_nonce,
+ const struct GNUNET_HashCode *block_hash,
+ unsigned int refs_length,
+ const struct SYNC_ObjectUID *object_uids,
+ const int16_t *object_incs,
+ const struct SYNC_AccountSignatureP *prev_upload_sig,
+ const struct GNUNET_HashCode *prev_hash,
+ const struct SYNC_BlockNonce *prev_prev_nonce,
+ const struct SYNC_AccountSignatureP *next_upload_sig,
+ const struct GNUNET_HashCode *next_hash,
+ const struct SYNC_BlockNonce *next_next_nonce);
+
+#endif
diff --git a/src/include/sync/sync-database/drop_tables.h b/src/include/sync/sync-database/drop_tables.h
@@ -14,7 +14,7 @@
Taler; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/>
*/
/**
- * @file include/sync_database_drop_tables.h
+ * @file include/sync/sync-database/drop_tables.h
* @brief drop sync tables
* @author Christian Grothoff
*/
diff --git a/src/include/sync/sync-database/gc.h b/src/include/sync/sync-database/gc.h
@@ -14,7 +14,7 @@
Taler; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/>
*/
/**
- * @file include/sync_database_gc.h
+ * @file include/sync/sync-database/gc.h
* @brief garbage collection for sync database
* @author Christian Grothoff
*/
@@ -27,17 +27,21 @@
/**
* Function called to perform "garbage collection" on the
* database, expiring records we no longer require. Deletes
- * all user records that are not paid up (and by cascade deletes
- * the associated recovery documents). Also deletes expired
- * truth and financial records older than @a fin_expire.
+ * accounts that are no longer paid up (and by cascade their
+ * blocks and objects), stale pending payments and unreferenced
+ * expired objects.
*
- * @param expire_backups backups older than the given time stamp should be garbage collected
+ * @param expire_backups backups older than the given time stamp
+ * should be garbage collected
* @param expire_pending_payments payments still pending from since before
- * this value should be garbage collected
- * @return transaction status
+ * this value should be garbage collected
+ * @param expire_objects unreferenced objects that expired before this
+ * value should be garbage collected
+ * @return #GNUNET_OK on success, #GNUNET_SYSERR on failure
*/
-enum GNUNET_DB_QueryStatus
-SYNCDB_gc (struct GNUNET_TIME_Absolute expire,
- struct GNUNET_TIME_Absolute expire_pending_payments);
+enum GNUNET_GenericReturnValue
+SYNCDB_gc (struct GNUNET_TIME_Absolute expire_backups,
+ struct GNUNET_TIME_Absolute expire_pending_payments,
+ struct GNUNET_TIME_Absolute expire_objects);
#endif
diff --git a/src/include/sync/sync-database/increment_lifetime_TR.h b/src/include/sync/sync-database/increment_lifetime_TR.h
@@ -14,7 +14,7 @@
Taler; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/>
*/
/**
- * @file include/sync_database_increment_lifetime_TR.h
+ * @file include/sync/sync-database/increment_lifetime_TR.h
* @brief increment account lifetime after payment
* @author Christian Grothoff
*/
diff --git a/src/include/sync/sync-database/lookup_account_TR.h b/src/include/sync/sync-database/lookup_account_TR.h
@@ -1,21 +1,21 @@
/*
- This file is part of GNU Taler
- Copyright (C) 2019, 2021 Taler Systems SA
+ This file is part of TALER
+ (C) 2026 Taler Systems SA
- Taler is free software; you can redistribute it and/or modify it under the
+ TALER is free software; you can redistribute it and/or modify it under the
terms of the GNU Lesser 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
+ 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.GPL. If not, see <http://www.gnu.org/licenses/>
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
- * @file include/sync_database_lookup_account_TR.h
- * @brief lookup account and backup metadata
+ * @file include/sync/sync-database/lookup_account_TR.h
+ * @brief look up the status of an account
* @author Christian Grothoff
*/
#ifndef SYNC_DATABASE_LOOKUP_ACCOUNT_TR_H
@@ -23,18 +23,27 @@
#include <gnunet/gnunet_util_lib.h>
#include "sync/sync_service.h"
-#include "sync/sync-database/common.h"
/**
- * Lookup an account and associated backup meta data.
+ * Look up the status of an account.
+ *
+ * Unlike every other entry point, this one does not fail an expired
+ * account: telling the client *when* the account expired is the whole
+ * point, so the caller gets the stored date and decides. Only an
+ * account that does not exist at all is #SYNC_DB_TARGET_MISSING.
*
* @param account_pub account to look up
- * @param[out] backup_hash set to hash of backup
- * @return transaction status
+ * @param[out] expiration_date when the account expires (or expired)
+ * @param[out] storage_used_bytes total size of the account's blocks
+ * @param[out] block_count number of blocks in the account's backup
+ * @return transaction status, #SYNC_DB_TARGET_MISSING if there is no
+ * such account
*/
enum SYNC_DB_QueryStatus
SYNCDB_lookup_account_TR (
const struct SYNC_AccountPublicKeyP *account_pub,
- struct GNUNET_HashCode *backup_hash);
+ struct GNUNET_TIME_Timestamp *expiration_date,
+ uint64_t *storage_used_bytes,
+ uint64_t *block_count);
#endif
diff --git a/src/include/sync/sync-database/lookup_backup_TR.h b/src/include/sync/sync-database/lookup_backup_TR.h
@@ -1,48 +0,0 @@
-/*
- This file is part of GNU Taler
- Copyright (C) 2019, 2021 Taler Systems SA
-
- Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU Lesser 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.GPL. If not, see <http://www.gnu.org/licenses/>
-*/
-/**
- * @file include/sync_database_lookup_backup_TR.h
- * @brief obtain backup data
- * @author Christian Grothoff
- */
-#ifndef SYNC_DATABASE_LOOKUP_BACKUP_TR_H
-#define SYNC_DATABASE_LOOKUP_BACKUP_TR_H
-
-#include <gnunet/gnunet_util_lib.h>
-#include "sync/sync_service.h"
-#include "sync/sync-database/common.h"
-
-/**
- * Obtain backup.
- *
- * @param account_pub account to obtain backup for
- * @param[out] account_sig set to signature affirming storage request
- * @param[out] prev_hash set to hash of the previous backup (all zeros if none)
- * @param[out] backup_hash set to hash of backup
- * @param[out] backup_size set to number of bytes in backup
- * @param[out] backup set to raw data to backup, caller MUST FREE
- * @return transaction status
- */
-enum SYNC_DB_QueryStatus
-SYNCDB_lookup_backup_TR (
- const struct SYNC_AccountPublicKeyP *account_pub,
- struct SYNC_AccountSignatureP *account_sig,
- struct GNUNET_HashCode *prev_hash,
- struct GNUNET_HashCode *backup_hash,
- size_t *backup_size,
- void **backup);
-
-#endif
diff --git a/src/include/sync/sync-database/lookup_block_TR.h b/src/include/sync/sync-database/lookup_block_TR.h
@@ -0,0 +1,52 @@
+/*
+ This file is part of GNU 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 Lesser 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.GPL. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file include/sync/sync-database/lookup_block_TR.h
+ * @brief lookup a single block by nonce
+ * @author Iván Ávalos
+ */
+#ifndef SYNC_DATABASE_LOOKUP_BLOCK_TR_H
+#define SYNC_DATABASE_LOOKUP_BLOCK_TR_H
+
+#include "sync/sync_service.h"
+
+
+/**
+ * Lookup a single block by @a nonce.
+ *
+ * @param account_pub account to look up in
+ * @param nonce nonce of the block to fetch
+ * @param[out] block_hash set to the block hash (may be NULL)
+ * @param[out] prev_nonce set to the previous block nonce (may be NULL)
+ * @param[out] next_nonce set to the next block nonce (may be NULL)
+ * @param[out] data_size set to the number of bytes in the data (may be NULL)
+ * @param[out] is_first set to true if this block is the first in the backup
+ * @param[out] is_last set to true if this block is the last in the backup
+ * @return query status (see GNUNET_DB_QueryStatus)
+ */
+enum SYNC_DB_QueryStatus
+SYNCDB_lookup_block_TR (
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const struct SYNC_BlockNonce *nonce,
+ struct GNUNET_HashCode *block_hash,
+ struct SYNC_BlockNonce *prev_nonce,
+ struct SYNC_BlockNonce *next_nonce,
+ size_t *data_size,
+ struct GNUNET_HashCode *refs_hash,
+ bool *is_first,
+ bool *is_last);
+
+#endif
diff --git a/src/include/sync/sync-database/lookup_block_range_TR.h b/src/include/sync/sync-database/lookup_block_range_TR.h
@@ -0,0 +1,68 @@
+/*
+ This file is part of GNU 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 Lesser 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.GPL. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file include/sync/sync-database/lookup_block_range_TR.h
+ * @brief lookup a limited range of blocks starting at a nonce
+ * @author Iván Ávalos
+ */
+#ifndef SYNC_DATABASE_LOOKUP_BLOCK_RANGE_TR_H
+#define SYNC_DATABASE_LOOKUP_BLOCK_RANGE_TR_H
+
+#include "sync/sync_service.h"
+
+
+/**
+ * Iterator called for every block returned by the query.
+ *
+ * @param cls closure pointer
+ * @param nonce nonce identifying the block
+ * @param block_hash hash of the block data
+ * @param prev_nonce nonce of the block preceding @a nonce
+ * @param next_nonce nonce of the block succeeding @a nonce
+ * @param data_size number of bytes in @a data
+ * @param data pointer to block data (may be NULL)
+ */
+typedef void
+(*SYNC_DB_BlockIterator)(void *cls,
+ const struct SYNC_BlockNonce *nonce,
+ const struct GNUNET_HashCode *block_hash,
+ const struct SYNC_BlockNonce *prev_nonce,
+ const struct SYNC_BlockNonce *next_nonce,
+ size_t data_size,
+ const void *data,
+ const struct SYNC_AccountSignatureP *upload_sig,
+ const struct GNUNET_HashCode *old_hash,
+ const struct GNUNET_HashCode *refs_hash);
+
+
+/**
+ * Lookup a (limited) range of blocks starting at @a start_nonce.
+ *
+ * @param start_nonce start of the range (inclusive)
+ * @param limit maximum number of blocks to return
+ * @param it iterator to call for each returned block
+ * @param it_cls closure for @a it
+ * @return query status (see GNUNET_DB_QueryStatus)
+ */
+enum SYNC_DB_QueryStatus
+SYNCDB_lookup_block_range_TR (
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const struct SYNC_BlockNonce *start_nonce,
+ const int16_t limit,
+ SYNC_DB_BlockIterator it,
+ void *it_cls);
+
+#endif
diff --git a/src/include/sync/sync-database/lookup_object.h b/src/include/sync/sync-database/lookup_object.h
@@ -0,0 +1,47 @@
+/*
+ This file is part of TALER
+ (C) 2026 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Lesser 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/>
+*/
+/**
+ * @file include/sync/sync-database/lookup_object.h
+ * @brief lookup object procedure wrapper
+ * @author Iván Ávalos
+ */
+#ifndef SYNC_DATABASE_LOOKUP_OBJECT_TR_H
+#define SYNC_DATABASE_LOOKUP_OBJECT_TR_H
+
+#include <gnunet/gnunet_util_lib.h>
+#include "sync/sync_service.h"
+
+/**
+ * Lookup an object blob for an account.
+ *
+ * @param account_pub account to look up
+ * @param object_hash object identifier hash
+ * @param[out] refcount object reference count
+ * @param[out] expiration_date expiration timestamp
+ * @param[out] data_size size of object data
+ * @param[out] data buffer to receive object data
+ * @return transaction status
+ */
+enum SYNC_DB_QueryStatus
+SYNCDB_lookup_object (
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const struct GNUNET_HashCode *object_hash,
+ int64_t *refcount,
+ struct GNUNET_TIME_Timestamp *expiration_date,
+ size_t *data_size,
+ void *data);
+
+#endif
diff --git a/src/include/sync/sync-database/lookup_object_uid.h b/src/include/sync/sync-database/lookup_object_uid.h
@@ -0,0 +1,47 @@
+/*
+ This file is part of TALER
+ (C) 2026 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Lesser 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/>
+*/
+/**
+ * @file include/sync/sync-database/lookup_object_uid.h
+ * @brief lookup object by UID procedure wrapper
+ * @author Iván Ávalos
+ */
+#ifndef SYNC_DATABASE_LOOKUP_OBJECT_UID_H
+#define SYNC_DATABASE_LOOKUP_OBJECT_UID_H
+
+#include <gnunet/gnunet_util_lib.h>
+#include "sync/sync_service.h"
+
+/**
+ * Lookup an object blob for an account by its UID.
+ *
+ * @param account_pub account to look up
+ * @param uid object UID to look up
+ * @param[out] object_hash hash of the object data
+ * @param[out] refcount object reference count
+ * @param[out] data_size size of object data
+ * @param[out] data buffer to receive object data (allocated by callee)
+ * @return transaction status
+ */
+enum SYNC_DB_QueryStatus
+SYNCDB_lookup_object_uid (
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const struct SYNC_ObjectUID *uid,
+ struct GNUNET_HashCode *object_hash,
+ int64_t *refcount,
+ size_t *data_size,
+ void *data);
+
+#endif
diff --git a/src/include/sync/sync-database/lookup_pending_payments_by_account.h b/src/include/sync/sync-database/lookup_pending_payments_by_account.h
@@ -0,0 +1,62 @@
+/*
+ This file is part of GNU Taler
+ Copyright (C) 2019, 2021 Taler Systems SA
+
+ Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Lesser 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.GPL. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file include/sync/sync-database/lookup_pending_payments_by_account.h
+ * @brief lookup pending payments by account
+ * @author Christian Grothoff
+ */
+#ifndef SYNC_DATABASE_LOOKUP_PENDING_PAYMENTS_BY_ACCOUNT_TR_H
+#define SYNC_DATABASE_LOOKUP_PENDING_PAYMENTS_BY_ACCOUNT_TR_H
+
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_db_lib.h>
+#include <taler/taler_util.h>
+#include "sync/sync_service.h"
+
+
+/**
+ * Function called on all pending payments for an account.
+ *
+ * @param cls closure
+ * @param timestamp for how long have we been waiting
+ * @param order_id order id in the backend
+ * @param token claim token, or NULL for none
+ * @param amount how much is the order for
+ */
+typedef void
+(*SYNC_DB_PaymentPendingIterator)(void *cls,
+ struct GNUNET_TIME_Timestamp timestamp,
+ const char *order_id,
+ const struct TALER_ClaimTokenP *token,
+ const struct TALER_Amount *amount);
+
+
+/**
+ * Lookup pending payments by account.
+ *
+ * @param account_pub account to look for pending payments under
+ * @param it iterator to call on all pending payments
+ * @param it_cls closure for @a it
+ * @return transaction status, on success the number of
+ * pending payments found
+ */
+enum SYNC_DB_QueryStatus
+SYNCDB_lookup_pending_payments_by_account_TR (
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ SYNC_DB_PaymentPendingIterator it,
+ void *it_cls);
+
+#endif
diff --git a/src/include/sync/sync-database/lookup_pending_payments_by_account_TR.h b/src/include/sync/sync-database/lookup_pending_payments_by_account_TR.h
@@ -1,60 +0,0 @@
-/*
- This file is part of GNU Taler
- Copyright (C) 2019, 2021 Taler Systems SA
-
- Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU Lesser 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.GPL. If not, see <http://www.gnu.org/licenses/>
-*/
-/**
- * @file include/sync_database_lookup_pending_payments_by_account_TR.h
- * @brief lookup pending payments by account
- * @author Christian Grothoff
- */
-#ifndef SYNC_DATABASE_LOOKUP_PENDING_PAYMENTS_BY_ACCOUNT_TR_H
-#define SYNC_DATABASE_LOOKUP_PENDING_PAYMENTS_BY_ACCOUNT_TR_H
-
-#include <gnunet/gnunet_util_lib.h>
-#include <gnunet/gnunet_db_lib.h>
-#include "sync/sync_service.h"
-
-
-/**
- * Function called on all pending payments for an account.
- *
- * @param cls closure
- * @param timestamp for how long have we been waiting
- * @param order_id order id in the backend
- * @param token claim token, or NULL for none
- * @param amount how much is the order for
- */
-typedef void
-(*SYNC_DB_PaymentPendingIterator)(void *cls,
- struct GNUNET_TIME_Timestamp timestamp,
- const char *order_id,
- const struct TALER_ClaimTokenP *token,
- const struct TALER_Amount *amount);
-
-
-/**
- * Lookup pending payments by account.
- *
- * @param account_pub account to look for pending payments under
- * @param it iterator to call on all pending payments
- * @param it_cls closure for @a it
- * @return transaction status
- */
-enum GNUNET_DB_QueryStatus
-SYNCDB_lookup_pending_payments_by_account_TR (
- const struct SYNC_AccountPublicKeyP *account_pub,
- SYNC_DB_PaymentPendingIterator it,
- void *it_cls);
-
-#endif
diff --git a/src/include/sync/sync-database/meson.build b/src/include/sync/sync-database/meson.build
@@ -1,15 +1,19 @@
install_data(
- 'create_tables.h',
'common.h',
'drop_tables.h',
+ 'create_tables.h',
+ 'preflight.h',
'gc.h',
+ 'append_block_TR.h',
+ 'delete_block_TR.h',
+ 'update_block_TR.h',
'increment_lifetime_TR.h',
- 'lookup_account_TR.h',
- 'lookup_backup_TR.h',
- 'lookup_pending_payments_by_account_TR.h',
- 'preflight.h',
- 'store_backup_TR.h',
+ 'lookup_block_range_TR.h',
+ 'lookup_block_TR.h',
+ 'lookup_object.h',
+ 'lookup_object_uid.h',
+ 'lookup_pending_payments_by_account.h',
'store_payment_TR.h',
- 'update_backup_TR.h',
+ 'store_object_TR.h',
install_dir: get_option('includedir') / 'sync' / 'sync-database',
)
diff --git a/src/include/sync/sync-database/preflight.h b/src/include/sync/sync-database/preflight.h
@@ -14,7 +14,7 @@
Taler; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/>
*/
/**
- * @file include/sync_database_preflight.h
+ * @file include/sync/sync-database/preflight.h
* @brief preflight check for sync database
* @author Christian Grothoff
*/
diff --git a/src/include/sync/sync-database/store_backup_TR.h b/src/include/sync/sync-database/store_backup_TR.h
@@ -1,48 +0,0 @@
-/*
- This file is part of GNU Taler
- Copyright (C) 2019, 2021 Taler Systems SA
-
- Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU Lesser 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.GPL. If not, see <http://www.gnu.org/licenses/>
-*/
-/**
- * @file include/sync_database_store_backup_TR.h
- * @brief store first backup
- * @author Christian Grothoff
- */
-#ifndef SYNC_DATABASE_STORE_BACKUP_TR_H
-#define SYNC_DATABASE_STORE_BACKUP_TR_H
-
-#include <gnunet/gnunet_util_lib.h>
-#include "sync/sync_service.h"
-#include "sync/sync-database/common.h"
-
-/**
- * Store backup. Only applicable for the FIRST backup under
- * an @a account_pub. Use @e SYNCDB_update_backup_TR to update an
- * existing backup.
- *
- * @param account_pub account to store @a backup under
- * @param account_sig signature affirming storage request
- * @param backup_hash hash of @a backup
- * @param backup_size number of bytes in @a backup
- * @param backup raw data to backup
- * @return transaction status
- */
-enum SYNC_DB_QueryStatus
-SYNCDB_store_backup_TR (
- const struct SYNC_AccountPublicKeyP *account_pub,
- const struct SYNC_AccountSignatureP *account_sig,
- const struct GNUNET_HashCode *backup_hash,
- size_t backup_size,
- const void *backup);
-
-#endif
diff --git a/src/include/sync/sync-database/store_object_TR.h b/src/include/sync/sync-database/store_object_TR.h
@@ -0,0 +1,47 @@
+/*
+ This file is part of TALER
+ (C) 2026 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Lesser 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/>
+*/
+/**
+ * @file include/sync/sync-database/store_object_TR.h
+ * @brief store object procedure wrapper
+ * @author Iván Ávalos
+ */
+#ifndef SYNC_DATABASE_STORE_OBJECT_TR_H
+#define SYNC_DATABASE_STORE_OBJECT_TR_H
+
+#include <gnunet/gnunet_util_lib.h>
+#include "sync/sync_service.h"
+
+/**
+ * Store an object blob for an account.
+ *
+ * @param uid unique identifier for the object
+ * @param account_pub account to store object under
+ * @param object_hash hash of @a data
+ * @param lifetime relative lifetime for the object
+ * @param data_size size of object data
+ * @param data pointer to object data
+ * @return transaction status
+ */
+enum SYNC_DB_QueryStatus
+SYNCDB_store_object_TR (
+ const struct SYNC_ObjectUID *uid,
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const struct GNUNET_HashCode *object_hash,
+ struct GNUNET_TIME_Relative lifetime,
+ size_t data_size,
+ const void *data);
+
+#endif
diff --git a/src/include/sync/sync-database/store_payment_TR.h b/src/include/sync/sync-database/store_payment_TR.h
@@ -14,7 +14,7 @@
Taler; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/>
*/
/**
- * @file include/sync_database_store_payment_TR.h
+ * @file include/sync/sync-database/store_payment_TR.h
* @brief store payment for sync database
* @author Christian Grothoff
*/
@@ -35,6 +35,7 @@
* @param order_id order we created
* @param token claim token, or NULL for none
* @param amount how much we asked for
+ * @param lifetime how long the account will be valid for
* @return transaction status
*/
enum SYNC_DB_QueryStatus
@@ -42,6 +43,7 @@ SYNCDB_store_payment_TR (
const struct SYNC_AccountPublicKeyP *account_pub,
const char *order_id,
const struct TALER_ClaimTokenP *token,
- const struct TALER_Amount *amount);
+ const struct TALER_Amount *amount,
+ struct GNUNET_TIME_Relative lifetime);
#endif
diff --git a/src/include/sync/sync-database/update_backup_TR.h b/src/include/sync/sync-database/update_backup_TR.h
@@ -1,48 +0,0 @@
-/*
- This file is part of GNU Taler
- Copyright (C) 2019, 2021 Taler Systems SA
-
- Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU Lesser 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.GPL. If not, see <http://www.gnu.org/licenses/>
-*/
-/**
- * @file include/sync_database_update_backup_TR.h
- * @brief update existing backup
- * @author Christian Grothoff
- */
-#ifndef SYNC_DATABASE_UPDATE_BACKUP_TR_H
-#define SYNC_DATABASE_UPDATE_BACKUP_TR_H
-
-#include <gnunet/gnunet_util_lib.h>
-#include "sync/sync_service.h"
-#include "sync/sync-database/common.h"
-
-/**
- * Update backup.
- *
- * @param account_pub account to store @a backup under
- * @param old_backup_hash hash of the previous backup (must match)
- * @param account_sig signature affirming storage request
- * @param backup_hash hash of @a backup
- * @param backup_size number of bytes in @a backup
- * @param backup raw data to backup
- * @return transaction status
- */
-enum SYNC_DB_QueryStatus
-SYNCDB_update_backup_TR (
- const struct SYNC_AccountPublicKeyP *account_pub,
- const struct GNUNET_HashCode *old_backup_hash,
- const struct SYNC_AccountSignatureP *account_sig,
- const struct GNUNET_HashCode *backup_hash,
- size_t backup_size,
- const void *backup);
-
-#endif
diff --git a/src/include/sync/sync-database/update_block_TR.h b/src/include/sync/sync-database/update_block_TR.h
@@ -0,0 +1,61 @@
+/*
+ This file is part of TALER
+ (C) 2026 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Lesser 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/>
+*/
+/**
+ * @file include/sync/sync-database/update_block_TR.h
+ * @brief update contents of a block in the DLL
+ * @author Iván Ávalos
+ */
+#ifndef SYNC_DATABASE_UPDATE_BLOCK_TR_H
+#define SYNC_DATABASE_UPDATE_BLOCK_TR_H
+
+#include <gnunet/gnunet_util_lib.h>
+#include "sync/sync_service.h"
+
+/**
+ * Update the contents of a block in the DLL.
+ *
+ * @param account_pub account under which block is stored
+ * @param block_nonce nonce of the block to update
+ * @param prev_nonce nonce of the preceding block to @a block_nonce
+ * @param next_nonce nonce of the succeeding block to @a block_nonce
+ * @param old_data_hash hash of the previous raw data
+ * @param new_data_hash hash of @a data
+ * @param data_size number of bytes in @a data
+ * @param data raw data of the block to update
+ * @param refs_length number of entries in @a object_uids and @a object_incs
+ * @param object_uids array of object UIDs referenced by the block
+ * @param object_incs array of refcount increments for @a object_uids
+ * @param upload_sig verified signature of the update
+ * @param refs_hash hash over the object references
+ * @return transaction status
+ */
+enum SYNC_DB_QueryStatus
+SYNCDB_update_block_TR (
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const struct SYNC_BlockNonce *block_nonce,
+ const struct SYNC_BlockNonce *prev_nonce,
+ const struct SYNC_BlockNonce *next_nonce,
+ const struct GNUNET_HashCode *old_data_hash,
+ const struct GNUNET_HashCode *new_data_hash,
+ size_t data_size,
+ const void *data,
+ unsigned int refs_length,
+ const struct SYNC_ObjectUID *object_uids,
+ const int16_t *object_incs,
+ const struct SYNC_AccountSignatureP *upload_sig,
+ const struct GNUNET_HashCode *refs_hash);
+
+#endif
diff --git a/src/include/sync/sync_database_lib.h b/src/include/sync/sync_database_lib.h
@@ -14,7 +14,7 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
- * @file include/sync_database_lib.h
+ * @file include/sync/sync_database_lib.h
* @brief sync database library interface
*/
#ifndef SYNC_DB_LIB_H
@@ -25,26 +25,40 @@
#include <sync/sync-database/create_tables.h>
#include <sync/sync-database/preflight.h>
#include <sync/sync-database/gc.h>
-#include <sync/sync-database/store_payment_TR.h>
-#include <sync/sync-database/lookup_pending_payments_by_account_TR.h>
-#include <sync/sync-database/store_backup_TR.h>
-#include <sync/sync-database/update_backup_TR.h>
-#include <sync/sync-database/lookup_account_TR.h>
-#include <sync/sync-database/lookup_backup_TR.h>
+#include <sync/sync-database/append_block_TR.h>
+#include <sync/sync-database/delete_block_TR.h>
+#include <sync/sync-database/update_block_TR.h>
#include <sync/sync-database/increment_lifetime_TR.h>
+#include <sync/sync-database/lookup_block_range_TR.h>
+#include <sync/sync-database/lookup_account_TR.h>
+#include <sync/sync-database/lookup_block_TR.h>
+#include <sync/sync-database/lookup_object.h>
+#include <sync/sync-database/lookup_object_uid.h>
+#include <sync/sync-database/lookup_pending_payments_by_account.h>
+#include <sync/sync-database/store_payment_TR.h>
+#include <sync/sync-database/store_object_TR.h>
/**
* Initialize the sync database subsystem.
*
* @param cfg configuration to use
- * @param skip_preflight true if we should skip the usual
- * preflight check which assures us that the DB is actually
- * operational; only sync-dbinit should use true here.
* @return #GNUNET_OK on success, #GNUNET_SYSERR on failure
*/
enum GNUNET_GenericReturnValue
-SYNCDB_init (const struct GNUNET_CONFIGURATION_Handle *cfg,
- bool skip_preflight);
+SYNCDB_init (
+ const struct GNUNET_CONFIGURATION_Handle *cfg);
+
+
+/**
+ * Initialize the sync database subsystem for administration.
+ * Disables the check that the database schema is current.
+ *
+ * @param cfg configuration to use
+ * @return #GNUNET_OK on success, #GNUNET_SYSERR on failure
+ */
+enum GNUNET_GenericReturnValue
+SYNCDB_init_admin (
+ const struct GNUNET_CONFIGURATION_Handle *cfg);
/**
diff --git a/src/include/sync/sync_service.h b/src/include/sync/sync_service.h
@@ -14,7 +14,7 @@
Anastasis; see the file COPYING.LIB. If not, see <http://www.gnu.org/licenses/>
*/
/**
- * @file include/sync_service.h
+ * @file include/sync/sync_service.h
* @brief C interface of libsync, a C library to use sync's HTTP API
* @author Christian Grothoff
*/
@@ -27,9 +27,6 @@
#include <jansson.h>
-GNUNET_NETWORK_STRUCT_BEGIN
-
-
/**
* Private key identifying an account.
*/
@@ -53,356 +50,502 @@ struct SYNC_AccountPublicKeyP
struct GNUNET_CRYPTO_EddsaPublicKey eddsa_pub;
};
-
/**
- * Data signed by the account public key of a sync client to
- * authorize the upload of the backup.
+ * Signature made with an account's public key.
*/
-struct SYNC_UploadSignaturePS
+struct SYNC_AccountSignatureP
{
/**
- * Set to #TALER_SIGNATURE_SYNC_BACKUP_UPLOAD.
- */
- struct GNUNET_CRYPTO_SignaturePurpose purpose;
-
- /**
- * Hash of the previous backup, all zeros for none.
- */
- struct GNUNET_HashCode old_backup_hash GNUNET_PACKED;
-
- /**
- * Hash of the new backup.
+ * We use EdDSA.
*/
- struct GNUNET_HashCode new_backup_hash GNUNET_PACKED;
-
+ struct GNUNET_CRYPTO_EddsaSignature eddsa_sig;
};
+/**
+ * Nonce used to identify a block and derive its encryption key.
+ */
+struct SYNC_BlockNonce
+{
+ unsigned char nonce[24];
+};
/**
- * Signature made with an account's public key.
+ * Hash of the encryption key used to encrypt the object.
*/
-struct SYNC_AccountSignatureP
+struct SYNC_ObjectUID
{
- /**
- * We use EdDSA.
- */
- struct GNUNET_CRYPTO_EddsaSignature eddsa_sig;
+ unsigned char uid[64];
};
-GNUNET_NETWORK_STRUCT_END
+
+/* ********************* Block list API *********************** */
/**
- * High-level ways how an upload may conclude.
+ * One entry returned by the block list.
*/
-enum SYNC_UploadStatus
+struct SYNC_BlockListEntry
{
+
+ /**
+ * Nonce identifying this block.
+ */
+ struct SYNC_BlockNonce nonce;
+
+ /**
+ * Hash of the block data.
+ */
+ struct GNUNET_HashCode hash;
+
+ /**
+ * Number of bytes in @e data.
+ */
+ size_t data_size;
+
/**
- * Backup was successfully made.
+ * Block data, allocated by the parser.
*/
- SYNC_US_SUCCESS = 0,
+ void *data;
/**
- * Account expired or payment was explicitly requested
- * by the client.
+ * Nonce of the preceding block (may be all zeros if
+ * @e have_prev_nonce is false).
*/
- SYNC_US_PAYMENT_REQUIRED = 1,
+ struct SYNC_BlockNonce prev_nonce;
/**
- * Conflicting backup existed on server. Client should
- * reconcile and try again with (using the provided
- * recovered backup as the previous backup).
+ * True if @e prev_nonce is set.
*/
- SYNC_US_CONFLICTING_BACKUP = 2,
+ bool have_prev_nonce;
/**
- * HTTP interaction failed, see HTTP status.
+ * Nonce of the succeeding block (may be all zeros if
+ * @e have_next_nonce is false).
*/
- SYNC_US_HTTP_ERROR = 3,
+ struct SYNC_BlockNonce next_nonce;
/**
- * We had an internal error (not sure this can happen,
- * but reserved for HTTP 400 status codes).
+ * True if @e next_nonce is set.
*/
- SYNC_US_CLIENT_ERROR = 4,
+ bool have_next_nonce;
/**
- * Server had an internal error.
+ * Upload signature stored with the block; the wallet verifies the
+ * backup against it before applying.
*/
- SYNC_US_SERVER_ERROR = 5
+ struct SYNC_AccountSignatureP upload_sig;
+
+ /**
+ * Hash of the block data this block replaced, part of the signed
+ * context (all zeros for the first upload of the block).
+ */
+ struct GNUNET_HashCode old_hash;
+
+ /**
+ * Hash over the object references covered by the stored signature.
+ */
+ struct GNUNET_HashCode refs_hash;
+
};
/**
- * Result of an upload.
+ * Result of a block list operation.
*/
-struct SYNC_UploadDetails
+struct SYNC_BlockListDetails
{
/**
+ * HTTP status code.
+ */
+ unsigned int http_status;
+
+ /**
* Taler error code.
*/
enum TALER_ErrorCode ec;
/**
- * HTTP status of the request.
+ * Number of entries in @e entries.
*/
- unsigned int http_status;
+ unsigned int num_entries;
+
+ /**
+ * Array of block list entries (may be freed by the
+ * caller after copying).
+ */
+ struct SYNC_BlockListEntry *entries;
+
+};
+
+
+/**
+ * Function called with the results of a #SYNC_block_list().
+ *
+ * @param cls closure
+ * @param ld block list details
+ */
+typedef void
+(*SYNC_BlockListCallback)(void *cls,
+ const struct SYNC_BlockListDetails *ld);
+
+
+/**
+ * Handle for a block list operation.
+ */
+struct SYNC_BlockListOperation;
+
+
+/**
+ * List blocks for an account, starting from @a start_nonce.
+ *
+ * @param ctx for HTTP client request processing
+ * @param base_url base URL of the Sync server
+ * @param pub account public key
+ * @param limit maximum number of blocks to return
+ * @param start_nonce optional nonce to start listing from,
+ * NULL to start from the beginning
+ * @param cb function to call with the result
+ * @param cb_cls closure for @a cb
+ * @return handle for the operation
+ */
+struct SYNC_BlockListOperation *
+SYNC_block_list (struct GNUNET_CURL_Context *ctx,
+ const char *base_url,
+ const struct SYNC_AccountPublicKeyP *pub,
+ int16_t limit,
+ const struct SYNC_BlockNonce *start_nonce,
+ SYNC_BlockListCallback cb,
+ void *cb_cls);
+
+
+/**
+ * Cancel a block list operation.
+ *
+ * @param[in] op operation to cancel
+ */
+void
+SYNC_block_list_cancel (struct SYNC_BlockListOperation *op);
+
+
+/* ********************* Block operation API *********************** */
+
+
+/**
+ * High-level status of a block operation.
+ */
+enum SYNC_BlockOperationStatus
+{
/**
- * High level status of the upload operation.
+ * Block was successfully uploaded (201 Created).
*/
- enum SYNC_UploadStatus us;
+ SYNC_BOS_SUCCESS = 0,
/**
- * Details depending on @e us.
+ * Block already exists (304 Not Modified).
*/
- union
- {
+ SYNC_BOS_NOT_MODIFIED = 1,
- /**
- * Data returned if @e us is #SYNC_US_SUCCESS.
- */
- struct
- {
+ /**
+ * Payment required (402 Payment Required).
+ */
+ SYNC_BOS_PAYMENT_REQUIRED = 2,
+
+ /**
+ * HTTP 4xx error (but not 402).
+ */
+ SYNC_BOS_CLIENT_ERROR = 3,
- /**
- * Hash of the synchronized backup.
- */
- const struct GNUNET_HashCode *curr_backup_hash;
+ /**
+ * HTTP 5xx error.
+ */
+ SYNC_BOS_SERVER_ERROR = 4,
- } success;
+ /**
+ * Other HTTP error.
+ */
+ SYNC_BOS_HTTP_ERROR = 5
- /**
- * Previous backup. Returned if @e us is
- * #SYNC_US_CONFLICTING_BACKUP
- */
- struct
- {
- /**
- * Hash over @e existing_backup.
- */
- struct GNUNET_HashCode existing_backup_hash;
+};
- /**
- * Number of bytes in @e existing_backup.
- */
- size_t existing_backup_size;
- /**
- * The backup on the server, which does not match the
- * "previous" backup expected by the client and thus
- * needs to be decrypted, reconciled and re-uploaded.
- */
- const void *existing_backup;
+/**
+ * Result of a block operation.
+ */
+struct SYNC_BlockOperationDetails
+{
- } recovered_backup;
+ /**
+ * HTTP status code.
+ */
+ unsigned int http_status;
- struct
- {
- /**
- * A taler://pay/-URI with a request to pay the annual fee for
- * the service. Returned if @e us is #SYNC_US_PAYMENT_REQUIRED.
- */
- const char *payment_request;
+ /**
+ * Taler error code.
+ */
+ enum TALER_ErrorCode ec;
- } payment_required;
+ /**
+ * High level status.
+ */
+ enum SYNC_BlockOperationStatus bos;
- } details;
+ /**
+ * Payment URI, set if @e bos is #SYNC_BOS_PAYMENT_REQUIRED.
+ */
+ const char *pay_uri;
};
/**
- * Function called with the results of a #SYNC_upload().
+ * Function called with the results of a block operation.
*
* @param cls closure
- * @param ud details about the upload operation
+ * @param ud details about the operation
*/
typedef void
-(*SYNC_UploadCallback)(void *cls,
- const struct SYNC_UploadDetails *ud);
+(*SYNC_BlockOperationCallback)(void *cls,
+ const struct SYNC_BlockOperationDetails *ud);
/**
- * Handle for an upload operatoin.
+ * Handle for a block operation.
*/
-struct SYNC_UploadOperation;
+struct SYNC_BlockOperation;
+
/**
- * Options for payment.
+ * Parameters of a block append/update/delete operation, including the
+ * relink signatures of the relinked neighbours.
*/
-enum SYNC_PaymentOptions
+struct SYNC_BlockOperationArgs
{
+
/**
- * No special options.
+ * Nonce of the block being written or deleted.
*/
- SYNC_PO_NONE = 0,
+ const struct SYNC_BlockNonce *nonce;
/**
- * Trigger payment even if sync does not require it
- * yet (forced payment).
+ * Nonce of the preceding block, NULL for none.
*/
- SYNC_PO_FORCE_PAYMENT = 1,
+ const struct SYNC_BlockNonce *prev_nonce;
/**
- * Request a fresh order to be created, say because the
- * existing one was claimed (but not paid) by another wallet.
+ * Nonce of the succeeding block, NULL for none.
*/
- SYNC_PO_FRESH_ORDER = 2
+ const struct SYNC_BlockNonce *next_nonce;
+
+ /**
+ * Hash of the existing block data for an update, NULL for an append.
+ */
+ const struct GNUNET_HashCode *old_data_hash;
+
+ /**
+ * Hash of the block to delete (delete only; also the If-Match value).
+ */
+ const struct GNUNET_HashCode *block_hash;
+
+ /**
+ * Number of bytes in @e block_data.
+ */
+ size_t block_data_size;
+
+ /**
+ * The block data.
+ */
+ const void *block_data;
+
+ /**
+ * Number of entries in @e object_uids and @e object_incs.
+ */
+ size_t refs_len;
+
+ /**
+ * Array of object UIDs referenced by this block.
+ */
+ const struct SYNC_ObjectUID *object_uids;
+
+ /**
+ * Array of reference count adjustments.
+ */
+ const int16_t *object_incs;
+
+ /**
+ * Entry of the block at @e prev_nonce that is relinked by this
+ * operation (the tail on an append, the preceding neighbour on a
+ * delete); NULL if there is no such block. Its relink signature is
+ * computed from this entry.
+ */
+ const struct SYNC_BlockListEntry *prev_entry;
+
+ /**
+ * Entry of the block at @e next_nonce that is relinked by a delete;
+ * NULL if there is no such block (or for appends/updates).
+ */
+ const struct SYNC_BlockListEntry *next_entry;
+
};
+
/**
- * Upload a @a backup to a Sync server. Note that @a backup must
- * have already been compressed, padded and encrypted by the
- * client.
- *
- * While @a pub is theoretically protected by the HTTPS protocol and
- * required to access the backup, it should be assumed that an
- * adversary might be able to download the backups from the Sync
- * server -- or even run the Sync server. Thus, strong encryption
- * is essential and NOT implemented by this function.
- *
- * The use of Anastasis to safely store the Sync encryption keys and
- * @a pub is recommended. Storing @a priv in Anastasis depends on
- * your priorities: without @a priv, further updates to the backup are
- * not possible, and the user would have to pay for another
- * account. OTOH, without @a priv an adversary that compromised
- * Anastasis can only read the backups, but not alter or destroy them.
+ * Append a new block to a Sync server (POST).
*
* @param ctx for HTTP client request processing
* @param base_url base URL of the Sync server
- * @param priv private key of an account with the server
- * @param prev_backup_hash hash of the previous backup, NULL for the first upload ever
- * @param backup_size number of bytes in @a backup
- * @param backup the encrypted backup, must remain in
- * memory until we are done with the operation!
- * @param po payment options
- * @param paid_order_id ID of the paid order, NULL if no payment was made so far
+ * @param priv private key of the account
+ * @param args parameters of the operation; @a args->prev_entry is the
+ * tail's entry, from which its relink signature is computed
* @param cb function to call with the result
* @param cb_cls closure for @a cb
* @return handle for the operation
*/
-struct SYNC_UploadOperation *
-SYNC_upload (struct GNUNET_CURL_Context *ctx,
- const char *base_url,
- struct SYNC_AccountPrivateKeyP *priv,
- const struct GNUNET_HashCode *prev_backup_hash,
- size_t backup_size,
- const void *backup,
- enum SYNC_PaymentOptions po,
- const char *paid_order_id,
- SYNC_UploadCallback cb,
- void *cb_cls);
+struct SYNC_BlockOperation *
+SYNC_block_append (struct GNUNET_CURL_Context *ctx,
+ const char *base_url,
+ const struct SYNC_AccountPrivateKeyP *priv,
+ const struct SYNC_BlockOperationArgs *args,
+ SYNC_BlockOperationCallback cb,
+ void *cb_cls);
/**
- * Cancel the upload. Note that aborting an upload does NOT guarantee
- * that it did not complete, it is possible that the server did
- * receive the full request before the upload is aborted.
+ * Update an existing block on a Sync server (PUT).
*
- * @param[in] uo operation to cancel.
+ * @param ctx for HTTP client request processing
+ * @param base_url base URL of the Sync server
+ * @param priv private key of the account
+ * @param args parameters of the operation; @a args->old_data_hash is
+ * the hash of the existing block data (mandatory)
+ * @param cb function to call with the result
+ * @param cb_cls closure for @a cb
+ * @return handle for the operation
*/
-void
-SYNC_upload_cancel (struct SYNC_UploadOperation *uo);
+struct SYNC_BlockOperation *
+SYNC_block_update (struct GNUNET_CURL_Context *ctx,
+ const char *base_url,
+ const struct SYNC_AccountPrivateKeyP *priv,
+ const struct SYNC_BlockOperationArgs *args,
+ SYNC_BlockOperationCallback cb,
+ void *cb_cls);
/**
- * Detailed results from the successful download.
+ * Delete a block from a Sync server (DELETE).
+ *
+ * @param ctx for HTTP client request processing
+ * @param base_url base URL of the Sync server
+ * @param priv private key of the account
+ * @param args parameters of the operation; @a args->block_hash is the
+ * hash of the block to delete (for the If-Match header), and
+ * @a args->prev_entry/@a args->next_entry the entries of the
+ * neighbours being relinked
+ * @param cb function to call with the result
+ * @param cb_cls closure for @a cb
+ * @return handle for the operation
*/
-struct SYNC_DownloadDetails
-{
+struct SYNC_BlockOperation *
+SYNC_block_delete (struct GNUNET_CURL_Context *ctx,
+ const char *base_url,
+ const struct SYNC_AccountPrivateKeyP *priv,
+ const struct SYNC_BlockOperationArgs *args,
+ SYNC_BlockOperationCallback cb,
+ void *cb_cls);
- /**
- * HTTP status code.
- */
- unsigned int http_status;
- /**
- * Details depending on @e http_status.
- */
- union
- {
+/**
+ * Cancel a block operation.
+ *
+ * @param[in] op operation to cancel
+ */
+void
+SYNC_block_operation_cancel (struct SYNC_BlockOperation *op);
- /**
- * Details if status is #MHD_HTTP_OK.
- */
- struct
- {
- /**
- * Signature (already verified).
- */
- struct SYNC_AccountSignatureP sig;
+/* ********************* Object API *********************** */
- /**
- * Hash of the previous version.
- */
- struct GNUNET_HashCode prev_backup_hash;
- /**
- * Hash over @e backup and @e backup_size.
- */
- struct GNUNET_HashCode curr_backup_hash;
+/**
+ * Upload an encrypted object to the Sync server (POST).
+ *
+ * @param ctx for HTTP client request processing
+ * @param base_url base URL of the Sync server
+ * @param priv private key of the account
+ * @param uid UID for the object
+ * @param data_size number of bytes in @a data
+ * @param data the object data
+ * @param cb function to call with the result
+ * @param cb_cls closure for @a cb
+ * @return handle for the operation
+ */
+struct SYNC_BlockOperation *
+SYNC_object_upload (struct GNUNET_CURL_Context *ctx,
+ const char *base_url,
+ const struct SYNC_AccountPrivateKeyP *priv,
+ const struct SYNC_ObjectUID *uid,
+ size_t data_size,
+ const void *data,
+ SYNC_BlockOperationCallback cb,
+ void *cb_cls);
- /**
- * The backup we downloaded.
- */
- const void *backup;
- /**
- * Number of bytes in @e backup.
- */
- size_t backup_size;
- } ok;
+/* ********************* Object fetch API *********************** */
- } details;
-};
+/**
+ * Handle for an object fetch operation.
+ */
+struct SYNC_ObjectFetchOperation;
/**
- * Function called with the results of a #SYNC_download().
+ * Function called with the results of an object fetch.
*
* @param cls closure
- * @param dd download details
+ * @param http_status HTTP status code
+ * @param ec error code (0 on success)
+ * @param uid UID of the fetched object
+ * @param data_size number of bytes in @a data
+ * @param data object data (valid only during the callback)
*/
typedef void
-(*SYNC_DownloadCallback)(void *cls,
- const struct SYNC_DownloadDetails *dd);
+(*SYNC_ObjectFetchCallback)(void *cls,
+ unsigned int http_status,
+ enum TALER_ErrorCode ec,
+ const struct SYNC_ObjectUID *uid,
+ size_t data_size,
+ const void *data);
/**
- * Handle for a download operation.
- */
-struct SYNC_DownloadOperation;
-
-
-/**
- * Download the latest version of a backup for account @a pub.
+ * Fetch an object from the Sync server (GET).
*
* @param ctx for HTTP client request processing
* @param base_url base URL of the Sync server
- * @param pub account public key
- * @param cb function to call with the backup
+ * @param pub public key of the account
+ * @param uid UID of the object to fetch
+ * @param cb function to call with the result
* @param cb_cls closure for @a cb
* @return handle for the operation
*/
-struct SYNC_DownloadOperation *
-SYNC_download (struct GNUNET_CURL_Context *ctx,
- const char *base_url,
- const struct SYNC_AccountPublicKeyP *pub,
- SYNC_DownloadCallback cb,
- void *cb_cls);
+struct SYNC_ObjectFetchOperation *
+SYNC_object_fetch (struct GNUNET_CURL_Context *ctx,
+ const char *base_url,
+ const struct SYNC_AccountPublicKeyP *pub,
+ const struct SYNC_ObjectUID *uid,
+ SYNC_ObjectFetchCallback cb,
+ void *cb_cls);
/**
- * Cancel the download.
+ * Cancel an object fetch operation.
*
- * @param[in] download operation to cancel.
+ * @param[in] op operation to cancel
*/
void
-SYNC_download_cancel (struct SYNC_DownloadOperation *download);
+SYNC_object_fetch_cancel (struct SYNC_ObjectFetchOperation *op);
#endif /* SYNC_SERVICE_H */
diff --git a/src/include/sync/sync_signatures.h b/src/include/sync/sync_signatures.h
@@ -0,0 +1,157 @@
+/*
+ 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 Affero 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file include/sync/sync_signatures.h
+ * @brief Utility functions for sync signatures
+ * @author Iván Ávalos
+ */
+#ifndef SYNC_SIGNATURES_H
+#define SYNC_SIGNATURES_H
+
+#include <gnunet/gnunet_util_lib.h>
+#include "sync_service.h"
+
+
+/**
+ * Sign a block upload (append/update).
+ *
+ * @param account_priv private key to sign with
+ * @param nonce nonce identifying the block
+ * @param prev_nonce nonce of the preceding block, NULL for none
+ * @param next_nonce nonce of the succeeding block, NULL for none
+ * @param old_hash hash of existing block data for an update,
+ * NULL for an append
+ * @param new_hash hash of the new block data
+ * @param refs_hash from #SYNC_object_refs_hash()
+ * @param[out] upload_sig signature to create
+ */
+void
+SYNC_block_upload_sign (
+ const struct SYNC_AccountPrivateKeyP *account_priv,
+ const struct SYNC_BlockNonce *nonce,
+ const struct SYNC_BlockNonce *prev_nonce,
+ const struct SYNC_BlockNonce *next_nonce,
+ const struct GNUNET_HashCode *old_hash,
+ const struct GNUNET_HashCode *new_hash,
+ const struct GNUNET_HashCode *refs_hash,
+ struct SYNC_AccountSignatureP *upload_sig);
+
+
+/**
+ * Verify signature on a block upload (append/update).
+ *
+ * @param account_pub public key of the account
+ * @param nonce nonce identifying the block
+ * @param prev_nonce nonce of the preceding block, NULL for none
+ * @param next_nonce nonce of the succeeding block, NULL for none
+ * @param old_hash hash of existing block data for an update,
+ * NULL for an append
+ * @param new_hash hash of the new block data
+ * @param refs_hash from #SYNC_object_refs_hash()
+ * @param upload_sig signature to verify
+ * @return #GNUNET_OK if the signature is valid, #GNUNET_SYSERR otherwise
+ */
+enum GNUNET_GenericReturnValue
+SYNC_block_upload_verify (
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const struct SYNC_BlockNonce *nonce,
+ const struct SYNC_BlockNonce *prev_nonce,
+ const struct SYNC_BlockNonce *next_nonce,
+ const struct GNUNET_HashCode *old_hash,
+ const struct GNUNET_HashCode *new_hash,
+ const struct GNUNET_HashCode *refs_hash,
+ const struct SYNC_AccountSignatureP *upload_sig);
+
+
+/**
+ * Sign a block deletion.
+ *
+ * @param account_priv private key to sign with
+ * @param nonce nonce of the block to delete
+ * @param prev_nonce nonce of the preceding block, NULL for none
+ * @param next_nonce nonce of the succeeding block, NULL for none
+ * @param hash hash of the block to delete
+ * @param refs_hash from #SYNC_object_refs_hash()
+ * @param[out] delete_sig signature to create
+ */
+void
+SYNC_block_delete_sign (
+ const struct SYNC_AccountPrivateKeyP *account_priv,
+ const struct SYNC_BlockNonce *nonce,
+ const struct SYNC_BlockNonce *prev_nonce,
+ const struct SYNC_BlockNonce *next_nonce,
+ const struct GNUNET_HashCode *hash,
+ const struct GNUNET_HashCode *refs_hash,
+ struct SYNC_AccountSignatureP *delete_sig);
+
+
+/**
+ * Verify signature on a block deletion.
+ *
+ * @param account_pub public key of the account
+ * @param nonce nonce of the block to delete
+ * @param prev_nonce nonce of the preceding block, NULL for none
+ * @param next_nonce nonce of the succeeding block, NULL for none
+ * @param hash hash of the block to delete
+ * @param refs_hash from #SYNC_object_refs_hash()
+ * @param delete_sig signature to verify
+ * @return #GNUNET_OK if the signature is valid, #GNUNET_SYSERR otherwise
+ */
+enum GNUNET_GenericReturnValue
+SYNC_block_delete_verify (
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const struct SYNC_BlockNonce *nonce,
+ const struct SYNC_BlockNonce *prev_nonce,
+ const struct SYNC_BlockNonce *next_nonce,
+ const struct GNUNET_HashCode *hash,
+ const struct GNUNET_HashCode *refs_hash,
+ const struct SYNC_AccountSignatureP *delete_sig);
+
+
+/**
+ * Sign an object upload.
+ *
+ * @param account_priv private key to sign with
+ * @param uid UID of the object to upload
+ * @param hash hash of the object to upload
+ * @param[out] upload_sig signature to create
+ */
+void
+SYNC_object_upload_sign (
+ const struct SYNC_AccountPrivateKeyP *account_priv,
+ const struct SYNC_ObjectUID *uid,
+ const struct GNUNET_HashCode *hash,
+ struct SYNC_AccountSignatureP *upload_sig);
+
+
+/**
+ * Verify signature on an object upload.
+ *
+ * @param account_pub public key of the account
+ * @param uid UID of the object
+ * @param hash hash of the object
+ * @param upload_sig signature to verify
+ * @return #GNUNET_OK if the signature is valid, #GNUNET_SYSERR otherwise
+ */
+enum GNUNET_GenericReturnValue
+SYNC_object_upload_verify (
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const struct SYNC_ObjectUID *uid,
+ const struct GNUNET_HashCode *hash,
+ const struct SYNC_AccountSignatureP *upload_sig);
+
+
+#endif
diff --git a/src/include/sync/sync_testing_lib.h b/src/include/sync/sync_testing_lib.h
@@ -17,7 +17,7 @@
<http://www.gnu.org/licenses/>
*/
/**
- * @file include/sync_testing_lib.h
+ * @file include/sync/sync_testing_lib.h
* @brief API for writing an interpreter to test SYNC components
* @author Christian Grothoff <christian@grothoff.org>
*/
@@ -26,110 +26,190 @@
#include "sync/sync_service.h"
#include <gnunet/gnunet_json_lib.h>
+#include <stdbool.h>
#include <taler/taler_testing_lib.h>
-#include <microhttpd.h>
/**
- * Make the "backup download" command for a non-existent upload.
+ * Call @a op on all simple traits.
+ *
+ * @param op macro to call
+ */
+#define SYNC_TESTING_SIMPLE_TRAITS(op) \
+ op (sync_account_pub, const struct SYNC_AccountPublicKeyP) \
+ op (sync_account_priv, const struct SYNC_AccountPrivateKeyP) \
+ op (prev_hash, const struct GNUNET_HashCode) \
+ op (curr_hash, const struct GNUNET_HashCode) \
+ op (block_nonce, const struct SYNC_BlockNonce) \
+ op (prev_nonce, const struct SYNC_BlockNonce) \
+ op (next_nonce, const struct SYNC_BlockNonce) \
+ op (block_list_result, const struct SYNC_BlockListDetails) \
+ op (block_refs_hash, const struct GNUNET_HashCode)
+
+
+/* FIXME: eventually switch to libgnunettesting with the SYNC_ prefix for
+ the symbols instead of TALER_TESTING_! */
+SYNC_TESTING_SIMPLE_TRAITS (TALER_TESTING_MAKE_DECL_SIMPLE_TRAIT)
+
+
+/**
+ * Make the "block append" command.
*
* @param label command label
* @param sync_url base URL of the sync serving
- * the policy store request.
+ * @param upload_reference reference to a previous command that
+ * provides account keys, NULL to generate fresh keys
+ * @param nonce block nonce to use, NULL to generate a random one
+ * @param http_status expected HTTP status.
+ * @param block_data data to upload
+ * @param block_data_size number of bytes in @a block_data
+ * @param retry_upload #true to retry the upload from @a upload_reference,
+ * #false to chain a new block after it (prev_nonce ← ref's nonce)
+ * @param refs_len number of entries in @a object_uids and @a object_incs
+ * @param object_uids array of object UIDs referenced by this block
+ * @param object_incs array of reference count adjustments
* @return the command
*/
struct TALER_TESTING_Command
-SYNC_TESTING_cmd_backup_nx (const char *label,
- const char *sync_url);
+SYNC_TESTING_cmd_block_append (const char *label,
+ const char *sync_url,
+ const char *upload_reference,
+ const struct SYNC_BlockNonce *nonce,
+ unsigned int http_status,
+ const void *block_data,
+ size_t block_data_size,
+ bool retry_upload,
+ unsigned int refs_len,
+ const struct SYNC_ObjectUID *object_uids,
+ const int16_t *object_incs);
/**
- * Make the "backup download" command.
+ * Make the "block update" command.
*
* @param label command label
* @param sync_url base URL of the sync serving
- * the policy store request.
+ * @param upload_reference reference to a previous command that
+ * provides account keys, NULL to generate fresh keys
+ * @param nonce block nonce to use, NULL to generate a random one
+ * @param old_data_hash hash of the existing block data
* @param http_status expected HTTP status.
- * @param upload_ref reference to upload command
+ * @param block_data data to upload
+ * @param block_data_size number of bytes in @a block_data
+ * @param refs_len number of entries in @a object_uids and @a object_incs
+ * @param object_uids array of object UIDs referenced by this block
+ * @param object_incs array of reference count adjustments
* @return the command
*/
struct TALER_TESTING_Command
-SYNC_TESTING_cmd_backup_download (const char *label,
- const char *sync_url,
- unsigned int http_status,
- const char *upload_ref);
+SYNC_TESTING_cmd_block_update (const char *label,
+ const char *sync_url,
+ const char *upload_reference,
+ const struct SYNC_BlockNonce *nonce,
+ const struct GNUNET_HashCode *old_data_hash,
+ unsigned int http_status,
+ const void *block_data,
+ size_t block_data_size,
+ unsigned int refs_len,
+ const struct SYNC_ObjectUID *object_uids,
+ const int16_t *object_incs);
/**
- * Types of options for performing the upload. Used as a bitmask.
+ * Make the "block list" command.
+ *
+ * @param label command label
+ * @param sync_url base URL of the sync serving
+ * @param account_reference reference to a previous command that
+ * provides the account public key
+ * @param limit maximum number of blocks to return
+ * @param start_nonce optional starting nonce, NULL for beginning
+ * @param http_status expected HTTP status.
+ * @param num_expected number of expected entries (0 to skip checking)
+ * @param expected_nonces array of expected nonces (length @a num_expected)
+ * @param expected_hashes array of expected block hashes
+ * (length @a num_expected)
+ * @return the command
*/
-enum SYNC_TESTING_UploadOption
-{
- /**
- * Do everything by the book.
- */
- SYNC_TESTING_UO_NONE = 0,
-
- /**
- * Use random hash for previous upload instead of correct
- * previous hash.
- */
- SYNC_TESTING_UO_PREV_HASH_WRONG = 1,
-
- /**
- * Request payment.
- */
- SYNC_TESTING_UO_REQUEST_PAYMENT = 2,
-
- /**
- * Reference payment order ID from linked previous upload.
- */
- SYNC_TESTING_UO_REFERENCE_ORDER_ID = 4
+struct TALER_TESTING_Command
+SYNC_TESTING_cmd_block_list (const char *label,
+ const char *sync_url,
+ const char *account_reference,
+ int16_t limit,
+ const struct SYNC_BlockNonce *start_nonce,
+ unsigned int http_status,
+ unsigned int num_expected,
+ const struct SYNC_BlockNonce *expected_nonces,
+ const struct GNUNET_HashCode *expected_hashes);
-};
+/**
+ * Make the "block delete" command.
+ *
+ * @param label command label
+ * @param sync_url base URL of the sync serving
+ * @param account_reference reference to a previous command that
+ * provides account keys and block nonce/hash
+ * @param http_status expected HTTP status
+ * @param refs_len number of entries in @a object_uids and @a object_incs
+ * @param object_uids array of object UIDs referenced by this block
+ * @param object_incs array of reference count adjustments
+ * @return the command
+ */
+struct TALER_TESTING_Command
+SYNC_TESTING_cmd_block_delete (const char *label,
+ const char *sync_url,
+ const char *account_reference,
+ unsigned int http_status,
+ unsigned int refs_len,
+ const struct SYNC_ObjectUID *object_uids,
+ const int16_t *object_incs);
/**
- * Make the "backup upload" command.
+ * Make the "object upload" command.
*
* @param label command label
* @param sync_url base URL of the sync serving
- * the policy store request.
- * @param prev_upload reference to a previous upload we are
- * supposed to update, NULL for none
- * @param last_upload reference to the last upload for the
- * same account, used to check result on MHD_HTTP_CONFLICT
- * @param uo upload options
- * @param http_status expected HTTP status.
- * @param backup_data data to upload
- * @param backup_data_size number of bytes in @a backup_data
+ * @param account_reference reference to a previous command that
+ * provides account keys
+ * @param uid object UID to use, NULL to generate a random one
+ * @param http_status expected HTTP status
+ * @param data object data
+ * @param data_size number of bytes in @a data
* @return the command
*/
struct TALER_TESTING_Command
-SYNC_TESTING_cmd_backup_upload (const char *label,
+SYNC_TESTING_cmd_object_upload (const char *label,
const char *sync_url,
- const char *prev_upload,
- const char *last_upload,
- enum SYNC_TESTING_UploadOption uo,
+ const char *account_reference,
+ const struct SYNC_ObjectUID *uid,
unsigned int http_status,
- const void *backup_data,
- size_t backup_data_size);
+ const void *data,
+ size_t data_size);
+
/**
- * Call @a op on all simple traits.
+ * Make the "object fetch" command.
*
- * @param op macro to call
+ * @param label command label
+ * @param sync_url base URL of the sync serving
+ * @param account_reference reference to a previous command that
+ * provides account keys
+ * @param uid object UID to fetch
+ * @param http_status expected HTTP status
+ * @param expected_data expected object data (for verification)
+ * @param expected_data_size number of bytes in @a expected_data
+ * @return the command
*/
-#define SYNC_TESTING_SIMPLE_TRAITS(op) \
- op (sync_account_pub, const struct SYNC_AccountPublicKeyP) \
- op (sync_account_priv, const struct SYNC_AccountPrivateKeyP) \
- op (prev_hash, const struct GNUNET_HashCode) \
- op (curr_hash, const struct GNUNET_HashCode)
-
+struct TALER_TESTING_Command
+SYNC_TESTING_cmd_object_fetch (const char *label,
+ const char *sync_url,
+ const char *account_reference,
+ const struct SYNC_ObjectUID *uid,
+ unsigned int http_status,
+ const void *expected_data,
+ size_t expected_data_size);
-/* FIXME: eventually switch to libgnunettesting with the SYNC_ prefix for
- the symbols instead of TALER_TESTING_! */
-SYNC_TESTING_SIMPLE_TRAITS (TALER_TESTING_MAKE_DECL_SIMPLE_TRAIT)
#endif
diff --git a/src/include/sync/sync_util.h b/src/include/sync/sync_util.h
@@ -14,14 +14,23 @@
SYNC; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
- * @file include/sync_util.h
- * @brief Interface for common utility functions
+ * @file include/sync/sync_util.h
+ * @brief Interface for common utility functions and JSON
+ * serialization/parsing of blocks and objects
* @author Christian Grothoff
+ * @author Iván Ávalos
*/
#ifndef SYNC_UTIL_H
#define SYNC_UTIL_H
#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_json_lib.h>
+#include <jansson.h>
+#include "sync_service.h"
+
+
+/* ******************** Utility ************************ */
+
/**
* Return default project data used by Sync.
@@ -30,4 +39,201 @@ const struct GNUNET_OS_ProjectData *
SYNC_project_data (void);
-#endif
+/* ******************** Block entry ************************ */
+
+
+/**
+ * Generate a random block nonce.
+ *
+ * @param[out] nonce set to a fresh random nonce
+ */
+void
+SYNC_block_nonce_generate (
+ struct SYNC_BlockNonce *nonce);
+
+
+/**
+ * Parse a single block entry from a JSON object
+ * (as returned in GET /backups/$KEY/blocks responses).
+ *
+ * @param input JSON object to parse
+ * @param[out] entry where to write the data (pointers point into @a input)
+ * @return #GNUNET_OK upon successful parsing, #GNUNET_SYSERR upon error
+ */
+enum GNUNET_GenericReturnValue
+SYNC_block_entry_parse (
+ json_t *input,
+ struct SYNC_BlockListEntry *entry);
+
+
+/**
+ * Free data allocated by #SYNC_block_entry_parse().
+ *
+ * @param[in] entry entry to clear
+ */
+void
+SYNC_block_entry_clear (
+ struct SYNC_BlockListEntry *entry);
+
+
+/**
+ * Serialize a block entry into a JSON object
+ * (for building GET /backups/$KEY/blocks responses).
+ *
+ * @param nonce nonce identifyng the block
+ * @param block_hash hash of the block data
+ * @param prev_nonce nonce of the preceding block, or NULL
+ * @param next_nonce nonce of the succeeding block, or NULL
+ * @param data_size number of bytes in @a data
+ * @param data block data
+ * @param upload_sig signature stored with the block
+ * @param old_hash hash of the replaced block data (zeros for appends)
+ * @param refs_hash hash over the object references
+ * @return JSON object representing the entry
+ */
+json_t *
+SYNC_block_entry_serialize (
+ const struct SYNC_BlockNonce *nonce,
+ const struct GNUNET_HashCode *block_hash,
+ const struct SYNC_BlockNonce *prev_nonce,
+ const struct SYNC_BlockNonce *next_nonce,
+ size_t data_size,
+ const void *data,
+ const struct SYNC_AccountSignatureP *upload_sig,
+ const struct GNUNET_HashCode *old_hash,
+ const struct GNUNET_HashCode *refs_hash);
+
+
+/* ************************ Object entry ************************ */
+
+
+/**
+ * One object entry as returned in object responses.
+ */
+struct SYNC_ObjectEntry
+{
+ /**
+ * UID identifying the object.
+ */
+ struct SYNC_ObjectUID uid;
+
+ /**
+ * Number of bytes in @e data.
+ */
+ size_t data_size;
+
+ /**
+ * Object data, allocated by the parser.
+ */
+ void *data;
+};
+
+
+/**
+ * Parse a single object entry from a JSON object
+ * (as returned in GET /backups/$KEY/objects/$UID responses).
+ *
+ * @param input JSON object to parse
+ * @param[out] entry where to write the data (data allocated by parser,
+ * caller must free via #SYNC_object_entry_clear())
+ * @return #GNUNET_OK upon successful parsing, #GNUNET_SYSERR upon error
+ */
+enum GNUNET_GenericReturnValue
+SYNC_object_entry_parse (
+ json_t *input,
+ struct SYNC_ObjectEntry *entry);
+
+
+/**
+ * Free data allocated by #SYNC_object_entry_parse().
+ *
+ * @param[in] entry entry to clear
+ */
+void
+SYNC_object_entry_clear (
+ struct SYNC_ObjectEntry *entry);
+
+
+/**
+ * Serialize an object entry into a JSON object
+ * (for building GET /backups/$KEY/objects/$UID responses).
+ *
+ * @param uid UID of the object
+ * @param data_size number of bytes in @a data
+ * @param data object data
+ * @return JSON object representing the entry
+ */
+json_t *
+SYNC_object_entry_serialize (
+ const struct SYNC_ObjectUID *uid,
+ size_t data_size,
+ const void *data);
+
+
+/* ******************** Object references ******************** */
+
+
+/**
+ * Serialize the object references carried by a block operation
+ * into the JSON object used for the "object_refs" field: a map from
+ * the base32-encoded object UID to the reference count adjustment.
+ *
+ * @param refs_len number of entries in @a object_uids and @a object_incs
+ * @param object_uids array of object UIDs referenced by the block
+ * @param object_incs array of reference count adjustments
+ * @return JSON object representing the references, NULL if @a refs_len is 0
+ */
+json_t *
+SYNC_object_refs_serialize (
+ size_t refs_len,
+ const struct SYNC_ObjectUID *object_uids,
+ const int16_t *object_incs);
+
+
+/**
+ * Parse the "object_refs" field of a block operation into parallel
+ * arrays of object UIDs and reference count adjustments.
+ *
+ * On success the caller must #GNUNET_free() both output arrays; on
+ * failure nothing is allocated and the outputs are left untouched.
+ *
+ * @param refs JSON object to parse, NULL for "no references"
+ * @param[out] refs_len set to the number of entries parsed
+ * @param[out] object_uids set to a fresh array of object UIDs
+ * @param[out] object_incs set to a fresh array of adjustments
+ * @return #GNUNET_OK upon successful parsing, #GNUNET_SYSERR upon error
+ */
+enum GNUNET_GenericReturnValue
+SYNC_object_refs_parse (
+ const json_t *refs,
+ size_t *refs_len,
+ struct SYNC_ObjectUID **object_uids,
+ int16_t **object_incs);
+
+
+/**
+ * Compute the hash over the object references, as covered by the block
+ * upload and delete signatures.
+ *
+ * Binary encoding, sorted by UID: 64 raw UID bytes followed by the
+ * adjustment as an int16 in network byte order. Sorting keeps the hash
+ * independent of the order the references arrive in, as the JSON object
+ * they travel in does not preserve member order. UIDs must be unique,
+ * as the wire format keys them by UID.
+ *
+ * @param refs_len number of entries in @a object_uids and @a object_incs
+ * @param object_uids array of object UIDs referenced by the block
+ * @param object_incs array of reference count adjustments
+ * @param[out] refs_hash set to the hash over the references
+ * @return #GNUNET_OK on success,
+ * #GNUNET_SYSERR if a UID appears more than once
+ */
+enum GNUNET_GenericReturnValue
+SYNC_object_refs_hash (
+ size_t refs_len,
+ const struct SYNC_ObjectUID *object_uids,
+ const int16_t *object_incs,
+ struct GNUNET_HashCode *refs_hash);
+
+
+#endif /* SYNC_UTIL_H */
diff --git a/src/lib/meson.build b/src/lib/meson.build
@@ -1,26 +1,30 @@
# This build file is in the public domain
-libsync_la_SOURCES = [
+libsync_SOURCES = [
'sync_api_curl_defaults.c',
- 'sync_api_download.c',
- 'sync_api_upload.c',
+ 'sync_api_block_operation_common.c',
+ 'sync_api_block_append.c',
+ 'sync_api_block_update.c',
+ 'sync_api_block_delete.c',
+ 'sync_api_block_list.c',
+ 'sync_api_object_upload.c',
+ 'sync_api_object_fetch.c',
]
libsync = library(
'sync',
- libsync_la_SOURCES,
+ libsync_SOURCES,
soversion: solibversions['libsync']['soversion'],
version: solibversions['libsync']['soversion'],
install_rpath: rpath_option,
dependencies: [
+ libsyncutil_dep,
talerutil_dep,
talercurl_dep,
talerjson_dep,
gnunetutil_dep,
gnunetjson_dep,
gnunetcurl_dep,
- talerutil_dep,
- talerjson_dep,
json_dep,
curl_dep,
],
@@ -35,5 +39,3 @@ pkg.generate(
url: 'https://taler.net',
description: 'GNU Taler sync library',
)
-
-
diff --git a/src/lib/sync_api_block_append.c b/src/lib/sync_api_block_append.c
@@ -0,0 +1,45 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2014-2026 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as
+ published by the Free Software Foundation; either version 2.1,
+ 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 Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with TALER; see the file COPYING.LGPL. If not,
+ see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/sync_api_block_append.c
+ * @brief Implementation of the block append (POST) operation
+ * @author Iván Ávalos
+ */
+#include "sync_api_block_operation_common.h"
+
+
+struct SYNC_BlockOperation *
+SYNC_block_append (struct GNUNET_CURL_Context *ctx,
+ const char *base_url,
+ const struct SYNC_AccountPrivateKeyP *priv,
+ const struct SYNC_BlockOperationArgs *args,
+ SYNC_BlockOperationCallback cb,
+ void *cb_cls)
+{
+ return SYNC_block_operation_internal_ (ctx,
+ base_url,
+ priv,
+ args,
+ cb,
+ cb_cls,
+ false);
+}
+
+
+/* end of sync_api_block_append.c */
diff --git a/src/lib/sync_api_block_delete.c b/src/lib/sync_api_block_delete.c
@@ -0,0 +1,45 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2014-2026 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as
+ published by the Free Software Foundation; either version 2.1,
+ 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 Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with TALER; see the file COPYING.LGPL. If not,
+ see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/sync_api_block_delete.c
+ * @brief Implementation of the block delete (DELETE) operation
+ * @author Iván Ávalos
+ */
+#include "sync_api_block_operation_common.h"
+
+
+struct SYNC_BlockOperation *
+SYNC_block_delete (struct GNUNET_CURL_Context *ctx,
+ const char *base_url,
+ const struct SYNC_AccountPrivateKeyP *priv,
+ const struct SYNC_BlockOperationArgs *args,
+ SYNC_BlockOperationCallback cb,
+ void *cb_cls)
+{
+ return SYNC_block_operation_internal_ (ctx,
+ base_url,
+ priv,
+ args,
+ cb,
+ cb_cls,
+ true);
+}
+
+
+/* end of sync_api_block_delete.c */
diff --git a/src/lib/sync_api_block_list.c b/src/lib/sync_api_block_list.c
@@ -0,0 +1,238 @@
+/*
+ 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/>
+*/
+/**
+ * @file lib/sync_api_block_list.c
+ * @brief Implementation of the block list GET
+ * @author Iván Ávalos
+ */
+#include "platform.h"
+#include <curl/curl.h>
+#include <jansson.h>
+#include <microhttpd.h>
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_curl_lib.h>
+#include <gnunet/gnunet_json_lib.h>
+#include <taler/taler_json_lib.h>
+#include <taler/taler_curl_lib.h>
+#include <sync/sync_service.h>
+#include <sync/sync_util.h>
+#include "sync_api_curl_defaults.h"
+
+
+/**
+ * Handle for a block list operation.
+ */
+struct SYNC_BlockListOperation
+{
+
+ /**
+ * The url for this request.
+ */
+ char *url;
+
+ /**
+ * Handle for the request.
+ */
+ struct GNUNET_CURL_Job *job;
+
+ /**
+ * Reference to the execution context.
+ */
+ struct GNUNET_CURL_Context *ctx;
+
+ /**
+ * Function to call with the result.
+ */
+ SYNC_BlockListCallback cb;
+
+ /**
+ * Closure for @e cb.
+ */
+ void *cb_cls;
+
+};
+
+
+/**
+ * Function called when we're done processing the
+ * GET /backups/$KEY/blocks request.
+ *
+ * @param cls the `struct SYNC_BlockListOperation *`
+ * @param response_code HTTP response code, 0 on error
+ * @param response response body as parsed JSON, or NULL
+ */
+static void
+handle_block_list_finished (void *cls,
+ long response_code,
+ const void *response)
+{
+ struct SYNC_BlockListOperation *op = cls;
+ const json_t *json = response;
+ struct SYNC_BlockListDetails ld = {
+ .http_status = (unsigned int) response_code,
+ .ec = TALER_EC_INVALID
+ };
+
+ op->job = NULL;
+ switch (response_code)
+ {
+ case 0:
+ break;
+ case MHD_HTTP_OK:
+ if (NULL != json)
+ {
+ unsigned int idx;
+ json_t *entry;
+
+ ld.ec = TALER_EC_NONE;
+ ld.num_entries = json_array_size (json);
+ ld.entries = GNUNET_new_array (ld.num_entries,
+ struct SYNC_BlockListEntry);
+ json_array_foreach (json, idx, entry)
+ {
+ struct SYNC_BlockListEntry *e = &ld.entries[idx];
+
+ if (GNUNET_OK !=
+ SYNC_block_entry_parse (entry,
+ e))
+ {
+ GNUNET_break_op (0);
+ ld.ec = TALER_EC_GENERIC_PARAMETER_MALFORMED;
+ break;
+ }
+ }
+ }
+ else
+ {
+ ld.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+ }
+ break;
+ case MHD_HTTP_BAD_REQUEST:
+ case MHD_HTTP_NOT_FOUND:
+ case MHD_HTTP_PAYMENT_REQUIRED:
+ case MHD_HTTP_INTERNAL_SERVER_ERROR:
+ default:
+ if ( (response_code >= 400) &&
+ (response_code < 500) )
+ ld.ec = (NULL != json)
+ ? TALER_JSON_get_error_code (json)
+ : TALER_EC_GENERIC_PARAMETER_MALFORMED;
+ else if (response_code >= 500)
+ ld.ec = (NULL != json)
+ ? TALER_JSON_get_error_code (json)
+ : TALER_EC_GENERIC_INVALID_RESPONSE;
+ break;
+ }
+ if (NULL != op->cb)
+ {
+ op->cb (op->cb_cls,
+ &ld);
+ op->cb = NULL;
+ }
+ if (NULL != ld.entries)
+ {
+ for (unsigned int i = 0; i < ld.num_entries; i++)
+ SYNC_block_entry_clear (&ld.entries[i]);
+ GNUNET_free (ld.entries);
+ }
+ SYNC_block_list_cancel (op);
+}
+
+
+struct SYNC_BlockListOperation *
+SYNC_block_list (struct GNUNET_CURL_Context *ctx,
+ const char *base_url,
+ const struct SYNC_AccountPublicKeyP *pub,
+ int16_t limit,
+ const struct SYNC_BlockNonce *start_nonce,
+ SYNC_BlockListCallback cb,
+ void *cb_cls)
+{
+ struct SYNC_BlockListOperation *op;
+ CURL *eh;
+ char *pub_s;
+ char *nonce_s = NULL;
+ char *path;
+ char *limit_s;
+ char *url;
+
+ pub_s = GNUNET_STRINGS_data_to_string_alloc (pub,
+ sizeof (*pub));
+ GNUNET_asprintf (&path,
+ "backups/%s/blocks",
+ pub_s);
+ GNUNET_free (pub_s);
+ GNUNET_asprintf (&limit_s,
+ "%d",
+ (int) limit);
+ if (NULL != start_nonce)
+ nonce_s = GNUNET_STRINGS_data_to_string_alloc (
+ start_nonce,
+ sizeof (*start_nonce));
+ url = TALER_url_join (base_url,
+ path,
+ "limit",
+ limit_s,
+ "start_nonce",
+ nonce_s,
+ NULL);
+ GNUNET_free (nonce_s);
+ GNUNET_free (limit_s);
+ GNUNET_free (path);
+ if (NULL == url)
+ {
+ GNUNET_break (0);
+ return NULL;
+ }
+
+ op = GNUNET_new (struct SYNC_BlockListOperation);
+ op->ctx = ctx;
+ op->cb = cb;
+ op->cb_cls = cb_cls;
+ op->url = url;
+
+ eh = SYNC_curl_easy_get_ (op->url);
+ if (NULL == eh)
+ {
+ GNUNET_break (0);
+ GNUNET_free (op->url);
+ GNUNET_free (op);
+ return NULL;
+ }
+
+ op->job = GNUNET_CURL_job_add2 (ctx,
+ eh,
+ NULL,
+ &handle_block_list_finished,
+ op);
+ return op;
+}
+
+
+void
+SYNC_block_list_cancel (struct SYNC_BlockListOperation *op)
+{
+ if (NULL != op->job)
+ {
+ GNUNET_CURL_job_cancel (op->job);
+ op->job = NULL;
+ }
+ GNUNET_free (op->url);
+ GNUNET_free (op);
+}
diff --git a/src/lib/sync_api_block_operation_common.c b/src/lib/sync_api_block_operation_common.c
@@ -0,0 +1,518 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2014-2026 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as
+ published by the Free Software Foundation; either version 2.1,
+ 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 Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with TALER; see the file COPYING.LGPL. If not,
+ see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/sync_api_block_operation_common.c
+ * @brief Shared implementation for block append, update, and delete
+ * @author Iván Ávalos
+ */
+#include "sync_api_block_operation_common.h"
+
+
+char *
+SYNC_block_operation_make_url_ (
+ const char *base_url,
+ const struct SYNC_AccountPrivateKeyP *priv,
+ const char *collection,
+ size_t id_size,
+ const void *id)
+{
+ struct SYNC_AccountPublicKeyP pub;
+ char *pub_s;
+ char *id_s;
+ char *path;
+ char *url;
+
+ GNUNET_CRYPTO_eddsa_key_get_public (&priv->eddsa_priv,
+ &pub.eddsa_pub);
+ pub_s = GNUNET_STRINGS_data_to_string_alloc (&pub,
+ sizeof (pub));
+ id_s = GNUNET_STRINGS_data_to_string_alloc (id,
+ id_size);
+ GNUNET_asprintf (&path,
+ "backups/%s/%s/%s",
+ pub_s,
+ collection,
+ id_s);
+ GNUNET_free (pub_s);
+ GNUNET_free (id_s);
+ url = TALER_url_join (base_url,
+ path,
+ NULL);
+ GNUNET_free (path);
+ return url;
+}
+
+
+void
+SYNC_block_operation_add_etag_ (
+ struct SYNC_BlockOperation *op,
+ const char *header,
+ const struct GNUNET_HashCode *hash)
+{
+ char *val;
+ char *hdr;
+
+ val = GNUNET_STRINGS_data_to_string_alloc (hash,
+ sizeof (*hash));
+ GNUNET_asprintf (&hdr,
+ "%s: \"%s\"",
+ header,
+ val);
+ GNUNET_free (val);
+ op->post_ctx.headers = curl_slist_append (op->post_ctx.headers,
+ hdr);
+ GNUNET_free (hdr);
+}
+
+
+/**
+ * Handle HTTP header received by curl.
+ *
+ * @param buffer one line of HTTP header data
+ * @param size size of an item
+ * @param nitems number of items passed
+ * @param userdata our `struct SYNC_BlockOperation *`
+ * @return `size * nitems`
+ */
+size_t
+SYNC_block_operation_handle_header_ (char *buffer,
+ size_t size,
+ size_t nitems,
+ void *userdata)
+{
+ struct SYNC_BlockOperation *op = userdata;
+ size_t total = size * nitems;
+ char *ndup;
+ const char *hdr_type;
+ char *hdr_val;
+ char *sp;
+
+ ndup = GNUNET_strndup (buffer,
+ total);
+ hdr_type = strtok_r (ndup,
+ ":",
+ &sp);
+ if (NULL == hdr_type)
+ {
+ GNUNET_free (ndup);
+ return total;
+ }
+ hdr_val = strtok_r (NULL,
+ "",
+ &sp);
+ if (NULL == hdr_val)
+ {
+ GNUNET_free (ndup);
+ return total;
+ }
+ if (' ' == *hdr_val)
+ hdr_val++;
+ if (0 == strcasecmp (hdr_type,
+ "Taler"))
+ {
+ size_t len;
+
+ /* A second "Taler:" header would leak the first one */
+ GNUNET_free (op->pay_uri);
+ op->pay_uri = GNUNET_strdup (hdr_val);
+ len = strlen (op->pay_uri);
+ while ( (len > 0) &&
+ ( ('\n' == op->pay_uri[len - 1]) ||
+ ('\r' == op->pay_uri[len - 1]) ) )
+ {
+ len--;
+ op->pay_uri[len] = '\0';
+ }
+ }
+ GNUNET_free (ndup);
+ return total;
+}
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP /backups/$KEY/blocks/$NONCE request.
+ *
+ * @param cls the `struct SYNC_BlockOperation`
+ * @param response_code HTTP response code, 0 on error
+ * @param response response body parsed as JSON, or NULL
+ */
+void
+SYNC_block_operation_handle_finished_ (void *cls,
+ long response_code,
+ const void *response)
+{
+ struct SYNC_BlockOperation *op = cls;
+ const json_t *json = response;
+ struct SYNC_BlockOperationDetails ud = {
+ .http_status = (unsigned int) response_code,
+ .ec = TALER_EC_INVALID
+ };
+
+ op->job = NULL;
+ switch (response_code)
+ {
+ case 0:
+ break;
+ case MHD_HTTP_NO_CONTENT:
+ ud.bos = SYNC_BOS_SUCCESS;
+ ud.ec = TALER_EC_NONE;
+ break;
+ case MHD_HTTP_NOT_MODIFIED:
+ ud.bos = SYNC_BOS_NOT_MODIFIED;
+ ud.ec = TALER_EC_NONE;
+ break;
+ case MHD_HTTP_PAYMENT_REQUIRED:
+ ud.bos = SYNC_BOS_PAYMENT_REQUIRED;
+ ud.pay_uri = op->pay_uri;
+ ud.ec = TALER_EC_NONE;
+ break;
+ case MHD_HTTP_BAD_REQUEST:
+ ud.ec = (NULL != json)
+ ? TALER_JSON_get_error_code (json)
+ : TALER_EC_GENERIC_PARAMETER_MALFORMED;
+ break;
+ case MHD_HTTP_FORBIDDEN:
+ ud.ec = (NULL != json)
+ ? TALER_JSON_get_error_code (json)
+ : TALER_EC_SYNC_INVALID_SIGNATURE;
+ break;
+ case MHD_HTTP_CONTENT_TOO_LARGE:
+ case MHD_HTTP_INTERNAL_SERVER_ERROR:
+ default:
+ if ( (response_code >= 400) &&
+ (response_code < 500) )
+ ud.bos = SYNC_BOS_CLIENT_ERROR;
+ else if (response_code >= 500)
+ ud.bos = SYNC_BOS_SERVER_ERROR;
+ else
+ ud.bos = SYNC_BOS_HTTP_ERROR;
+ ud.ec = (NULL != json)
+ ? TALER_JSON_get_error_code (json)
+ : TALER_EC_GENERIC_INVALID_RESPONSE;
+ break;
+ }
+ if (NULL != op->cb)
+ {
+ op->cb (op->cb_cls,
+ &ud);
+ op->cb = NULL;
+ }
+ SYNC_block_operation_cancel (op);
+}
+
+
+/**
+ * Build the upload signature covering the relink of a block.
+ *
+ * A relink never changes the data or the references, so the signature
+ * covers the block's stored hash and refs_hash on both sides and its
+ * new links (the nonces of the previous and next blocks).
+ *
+ * @param priv private key of the account
+ * @param entry the block being relinked (from a block list)
+ * @param new_prev the block's new predecessor, NULL for none
+ * @param new_next the block's new successor, NULL for none
+ * @param[out] sig signature to create
+ */
+static void
+relink_sign (const struct SYNC_AccountPrivateKeyP *priv,
+ const struct SYNC_BlockListEntry *entry,
+ const struct SYNC_BlockNonce *new_prev,
+ const struct SYNC_BlockNonce *new_next,
+ struct SYNC_AccountSignatureP *sig)
+{
+ SYNC_block_upload_sign (priv,
+ &entry->nonce,
+ new_prev,
+ new_next,
+ &entry->hash,
+ &entry->hash,
+ &entry->refs_hash,
+ sig);
+}
+
+
+/**
+ * Serialize a relink object (relink_prev/relink_next): the new
+ * signature of the relinked neighbour.
+ *
+ * @param sig upload signature covering the relink
+ * @return JSON object to embed in the request body
+ */
+static json_t *
+relink_serialize (const struct SYNC_AccountSignatureP *sig)
+{
+ return GNUNET_JSON_PACK (
+ GNUNET_JSON_pack_data_auto ("upload_sig",
+ sig));
+}
+
+
+struct SYNC_BlockOperation *
+SYNC_block_operation_internal_ (
+ struct GNUNET_CURL_Context *ctx,
+ const char *base_url,
+ const struct SYNC_AccountPrivateKeyP *priv,
+ const struct SYNC_BlockOperationArgs *args,
+ SYNC_BlockOperationCallback cb,
+ void *cb_cls,
+ bool is_delete)
+{
+ struct SYNC_BlockOperation *op;
+ struct SYNC_AccountSignatureP account_sig;
+ struct SYNC_AccountSignatureP relink_sig;
+ struct GNUNET_HashCode new_data_hash;
+ struct GNUNET_HashCode refs_hash;
+ CURL *eh;
+ json_t *req;
+
+ if (NULL != args->block_data)
+ {
+ /* Compute hash of block data */
+ GNUNET_CRYPTO_hash (args->block_data,
+ args->block_data_size,
+ &new_data_hash);
+ }
+
+ /* Also rejects a repeated UID, which the wire format cannot carry */
+ if (GNUNET_OK !=
+ SYNC_object_refs_hash (args->refs_len,
+ args->object_uids,
+ args->object_incs,
+ &refs_hash))
+ {
+ GNUNET_break (0);
+ return NULL;
+ }
+
+ /* Build signature */
+ if (is_delete)
+ {
+ SYNC_block_delete_sign (priv,
+ args->nonce,
+ args->prev_nonce,
+ args->next_nonce,
+ args->block_hash,
+ &refs_hash,
+ &account_sig);
+ }
+ else
+ {
+ SYNC_block_upload_sign (priv,
+ args->nonce,
+ args->prev_nonce,
+ args->next_nonce,
+ args->old_data_hash,
+ &new_data_hash,
+ &refs_hash,
+ &account_sig);
+ }
+
+ /* Allocate operation handle */
+ op = GNUNET_new (struct SYNC_BlockOperation);
+ op->ctx = ctx;
+ op->cb = cb;
+ op->cb_cls = cb_cls;
+ op->post_ctx.disable_compression = true;
+ op->url = SYNC_block_operation_make_url_ (base_url,
+ priv,
+ "blocks",
+ sizeof (*args->nonce),
+ args->nonce);
+
+ /* Build JSON body */
+ if (is_delete)
+ {
+ json_t *relink_prev = NULL;
+ json_t *relink_next = NULL;
+
+ if (NULL != args->prev_entry)
+ {
+ relink_sign (priv,
+ args->prev_entry,
+ args->prev_entry->have_prev_nonce
+ ? &args->prev_entry->prev_nonce
+ : NULL,
+ args->next_nonce,
+ &relink_sig);
+ relink_prev = relink_serialize (&relink_sig);
+ }
+
+ if (NULL != args->next_entry)
+ {
+ relink_sign (priv,
+ args->next_entry,
+ args->prev_nonce,
+ args->next_entry->have_next_nonce
+ ? &args->next_entry->next_nonce
+ : NULL,
+ &relink_sig);
+ relink_next = relink_serialize (&relink_sig);
+ }
+
+ req = GNUNET_JSON_PACK (
+ GNUNET_JSON_pack_data_auto ("delete_sig",
+ &account_sig),
+ GNUNET_JSON_pack_allow_null (
+ GNUNET_JSON_pack_data_auto ("prev_nonce",
+ args->prev_nonce)),
+ GNUNET_JSON_pack_allow_null (
+ GNUNET_JSON_pack_data_auto ("next_nonce",
+ args->next_nonce)),
+ GNUNET_JSON_pack_allow_null (
+ GNUNET_JSON_pack_object_steal (
+ "object_refs",
+ SYNC_object_refs_serialize (args->refs_len,
+ args->object_uids,
+ args->object_incs))),
+ GNUNET_JSON_pack_allow_null (
+ GNUNET_JSON_pack_object_steal ("relink_prev",
+ relink_prev)),
+ GNUNET_JSON_pack_allow_null (
+ GNUNET_JSON_pack_object_steal ("relink_next",
+ relink_next)));
+ }
+ else
+ {
+ json_t *relink_prev = NULL;
+
+ if (NULL != args->prev_entry)
+ {
+ relink_sign (priv,
+ args->prev_entry,
+ args->prev_entry->have_prev_nonce
+ ? &args->prev_entry->prev_nonce
+ : NULL,
+ args->nonce,
+ &relink_sig);
+ relink_prev = relink_serialize (&relink_sig);
+ }
+
+ req = GNUNET_JSON_PACK (
+ GNUNET_JSON_pack_data_auto ("upload_sig",
+ &account_sig),
+ GNUNET_JSON_pack_allow_null (
+ GNUNET_JSON_pack_data_auto ("prev_nonce",
+ args->prev_nonce)),
+ GNUNET_JSON_pack_allow_null (
+ GNUNET_JSON_pack_data_auto ("next_nonce",
+ args->next_nonce)),
+ GNUNET_JSON_pack_data_varsize ("data",
+ args->block_data,
+ args->block_data_size),
+ GNUNET_JSON_pack_allow_null (
+ GNUNET_JSON_pack_object_steal (
+ "object_refs",
+ SYNC_object_refs_serialize (args->refs_len,
+ args->object_uids,
+ args->object_incs))),
+ GNUNET_JSON_pack_allow_null (
+ GNUNET_JSON_pack_object_steal ("relink_prev",
+ relink_prev)));
+ }
+
+ /* Set up CURL easy handle */
+ eh = SYNC_curl_easy_get_ (op->url);
+ if (NULL == eh)
+ {
+ GNUNET_break (0);
+ json_decref (req);
+ goto cleanup;
+ }
+ GNUNET_assert (CURLE_OK ==
+ curl_easy_setopt (eh,
+ CURLOPT_HEADERFUNCTION,
+ &SYNC_block_operation_handle_header_));
+ GNUNET_assert (CURLE_OK ==
+ curl_easy_setopt (eh,
+ CURLOPT_HEADERDATA,
+ op));
+
+ if (GNUNET_OK !=
+ TALER_curl_easy_post (&op->post_ctx,
+ eh,
+ req))
+ {
+ GNUNET_break (0);
+ json_decref (req);
+ curl_easy_cleanup (eh);
+ goto cleanup;
+ }
+ json_decref (req);
+
+ /* Override method to PUT/DELETE as needed */
+ if (! is_delete && NULL != args->old_data_hash)
+ {
+ GNUNET_assert (CURLE_OK ==
+ curl_easy_setopt (eh,
+ CURLOPT_CUSTOMREQUEST,
+ "PUT"));
+ }
+ if (is_delete)
+ {
+ GNUNET_assert (CURLE_OK ==
+ curl_easy_setopt (eh,
+ CURLOPT_CUSTOMREQUEST,
+ "DELETE"));
+ }
+
+ SYNC_block_operation_add_etag_ (op,
+ MHD_HTTP_HEADER_IF_NONE_MATCH,
+ &new_data_hash);
+ /* Add If-Match header for updates and deletes */
+ if (NULL != args->old_data_hash)
+ SYNC_block_operation_add_etag_ (op,
+ MHD_HTTP_HEADER_IF_MATCH,
+ args->old_data_hash);
+ if (is_delete)
+ SYNC_block_operation_add_etag_ (op,
+ MHD_HTTP_HEADER_IF_MATCH,
+ args->block_hash);
+
+ op->job = GNUNET_CURL_job_add2 (ctx,
+ eh,
+ op->post_ctx.headers,
+ &SYNC_block_operation_handle_finished_,
+ op);
+ return op;
+
+cleanup:
+ GNUNET_free (op->url);
+ GNUNET_free (op);
+ return NULL;
+}
+
+
+void
+SYNC_block_operation_cancel (struct SYNC_BlockOperation *op)
+{
+ if (NULL != op->job)
+ {
+ GNUNET_CURL_job_cancel (op->job);
+ op->job = NULL;
+ }
+ TALER_curl_easy_post_finished (&op->post_ctx);
+ GNUNET_free (op->pay_uri);
+ GNUNET_free (op->url);
+ GNUNET_free (op);
+}
+
+
+/* end of sync_api_block_operation_common.c */
diff --git a/src/lib/sync_api_block_operation_common.h b/src/lib/sync_api_block_operation_common.h
@@ -0,0 +1,178 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2014-2026 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as
+ published by the Free Software Foundation; either version 2.1,
+ 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 Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with TALER; see the file COPYING.LGPL. If not,
+ see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/sync_api_block_operation_common.h
+ * @brief Internal declarations shared between block append,
+ * block update, and block delete implementations.
+ * @author Iván Ávalos
+ */
+#ifndef SYNC_BLOCK_OPERATION_COMMON_H
+#define SYNC_BLOCK_OPERATION_COMMON_H
+
+#include "platform.h"
+#include <curl/curl.h>
+#include <jansson.h>
+#include <microhttpd.h>
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_curl_lib.h>
+#include <gnunet/gnunet_json_lib.h>
+#include <taler/taler_signatures.h>
+#include <taler/taler_json_lib.h>
+#include <taler/taler_curl_lib.h>
+#include <sync/sync_service.h>
+#include <sync/sync_signatures.h>
+#include <sync/sync_util.h>
+#include "sync_api_curl_defaults.h"
+
+
+/**
+ * Internal: full definition of the block operation handle.
+ */
+struct SYNC_BlockOperation
+{
+
+ /**
+ * The url for this request.
+ */
+ char *url;
+
+ /**
+ * Handle for the request.
+ */
+ struct GNUNET_CURL_Job *job;
+
+ /**
+ * Reference to the execution context.
+ */
+ struct GNUNET_CURL_Context *ctx;
+
+ /**
+ * Function to call with the result.
+ */
+ SYNC_BlockOperationCallback cb;
+
+ /**
+ * Closure for @e cb.
+ */
+ void *cb_cls;
+
+ /**
+ * Context for the POST body.
+ */
+ struct TALER_CURL_PostContext post_ctx;
+
+ /**
+ * Payment URI from the "Taler" header, or NULL.
+ */
+ char *pay_uri;
+
+};
+
+
+/**
+ * Build the URL of a resource under the account of @a priv.
+ *
+ * @param base_url base URL of the Sync server
+ * @param priv private key of the account
+ * @param collection collection under the account, e.g. "blocks"
+ * @param id_size number of bytes in @a id
+ * @param id identifier of the resource within @a collection
+ * @return the full URL, to be freed by the caller
+ */
+char *
+SYNC_block_operation_make_url_ (
+ const char *base_url,
+ const struct SYNC_AccountPrivateKeyP *priv,
+ const char *collection,
+ size_t id_size,
+ const void *id);
+
+
+/**
+ * Append an ETag-style header with the base32-encoded @a hash
+ * as its value to the headers of @a op.
+ *
+ * @param[in,out] op operation to add the header to
+ * @param header name of the header to add
+ * @param hash hash to encode as the header value
+ */
+void
+SYNC_block_operation_add_etag_ (
+ struct SYNC_BlockOperation *op,
+ const char *header,
+ const struct GNUNET_HashCode *hash);
+
+
+/**
+ * Internal shared helper that implements block append, update and
+ * delete.
+ *
+ * @param ctx for HTTP client request processing
+ * @param base_url base URL of the Sync server
+ * @param priv private key of the account
+ * @param args operation parameters (including relinks)
+ * @param cb function to call with the result
+ * @param cb_cls closure for @a cb
+ * @param is_delete true to send DELETE instead of POST/PUT
+ * @return handle for the operation
+ */
+struct SYNC_BlockOperation *
+SYNC_block_operation_internal_ (
+ struct GNUNET_CURL_Context *ctx,
+ const char *base_url,
+ const struct SYNC_AccountPrivateKeyP *priv,
+ const struct SYNC_BlockOperationArgs *args,
+ SYNC_BlockOperationCallback cb,
+ void *cb_cls,
+ bool is_delete);
+
+
+/**
+ * Handle HTTP header received by curl. Shared between block
+ * append, update, and delete implementations.
+ *
+ * @param buffer one line of HTTP header data
+ * @param size size of an item
+ * @param nitems number of items passed
+ * @param userdata our `struct SYNC_BlockOperation *`
+ * @return `size * nitems`
+ */
+size_t
+SYNC_block_operation_handle_header_ (char *buffer,
+ size_t size,
+ size_t nitems,
+ void *userdata);
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP /backups/$KEY/blocks request. Shared between
+ * block append, update, and delete.
+ *
+ * @param cls the `struct SYNC_BlockOperation`
+ * @param response_code HTTP response code, 0 on error
+ * @param response response body parsed as JSON, or NULL
+ */
+void
+SYNC_block_operation_handle_finished_ (void *cls,
+ long response_code,
+ const void *response);
+
+
+#endif
diff --git a/src/lib/sync_api_block_update.c b/src/lib/sync_api_block_update.c
@@ -0,0 +1,45 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2014-2026 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as
+ published by the Free Software Foundation; either version 2.1,
+ 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 Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with TALER; see the file COPYING.LGPL. If not,
+ see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/sync_api_block_update.c
+ * @brief Implementation of the block update (PUT) operation
+ * @author Iván Ávalos
+ */
+#include "sync_api_block_operation_common.h"
+
+
+struct SYNC_BlockOperation *
+SYNC_block_update (struct GNUNET_CURL_Context *ctx,
+ const char *base_url,
+ const struct SYNC_AccountPrivateKeyP *priv,
+ const struct SYNC_BlockOperationArgs *args,
+ SYNC_BlockOperationCallback cb,
+ void *cb_cls)
+{
+ return SYNC_block_operation_internal_ (ctx,
+ base_url,
+ priv,
+ args,
+ cb,
+ cb_cls,
+ false);
+}
+
+
+/* end of sync_api_block_update.c */
diff --git a/src/lib/sync_api_download.c b/src/lib/sync_api_download.c
@@ -1,307 +0,0 @@
-/*
- This file is part of TALER
- Copyright (C) 2014-2019 Taler Systems SA
-
- TALER is free software; you can redistribute it and/or modify
- it under the terms of the GNU Lesser General Public License as
- published by the Free Software Foundation; either version 2.1,
- 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 Lesser General Public License for more details.
-
- You should have received a copy of the GNU Lesser General Public
- License along with TALER; see the file COPYING.LGPL. If not,
- see <http://www.gnu.org/licenses/>
-*/
-
-/**
- * @file lib/sync_api_download.c
- * @brief Implementation of the download
- * @author Christian Grothoff
- */
-#include "platform.h"
-#include <curl/curl.h>
-#include <jansson.h>
-#include <microhttpd.h> /* just for HTTP status codes */
-#include <gnunet/gnunet_util_lib.h>
-#include <gnunet/gnunet_curl_lib.h>
-#include <taler/taler_signatures.h>
-#include "sync/sync_service.h"
-#include "sync_api_curl_defaults.h"
-
-
-/**
- * @brief Handle for a download operation.
- */
-struct SYNC_DownloadOperation
-{
-
- /**
- * The url for this request.
- */
- char *url;
-
- /**
- * Handle for the request.
- */
- struct GNUNET_CURL_Job *job;
-
- /**
- * Reference to the execution context.
- */
- struct GNUNET_CURL_Context *ctx;
-
- /**
- * Function to call with the result.
- */
- SYNC_DownloadCallback cb;
-
- /**
- * Closure for @e cb.
- */
- void *cb_cls;
-
- /**
- * Public key of the account we are downloading from.
- */
- struct SYNC_AccountPublicKeyP account_pub;
-
- /**
- * Signature returned in the "Sync-Signature"
- * header, or all zeros for none.
- */
- struct SYNC_AccountSignatureP account_sig;
-
- /**
- * Hash code returned by the server in the
- * "Sync-Previous" header, or all zeros for
- * none.
- */
- struct GNUNET_HashCode sync_previous;
-
-};
-
-
-/**
- * Function called when we're done processing the
- * HTTP /backup request.
- *
- * @param cls the `struct SYNC_DownloadOperation`
- * @param response_code HTTP response code, 0 on error
- * @param data data we downloaded
- * @param data_size number of bytes in @a data
- */
-static void
-handle_download_finished (void *cls,
- long response_code,
- const void *data,
- size_t data_size)
-{
- struct SYNC_DownloadOperation *download = cls;
- struct SYNC_DownloadDetails dd = {
- .http_status = (unsigned int) response_code
- };
-
- download->job = NULL;
- switch (response_code)
- {
- case 0:
- break;
- case MHD_HTTP_OK:
- {
- struct SYNC_UploadSignaturePS usp = {
- .purpose.purpose = htonl (TALER_SIGNATURE_SYNC_BACKUP_UPLOAD),
- .purpose.size = htonl (sizeof (usp)),
- .old_backup_hash = download->sync_previous
- };
-
- GNUNET_CRYPTO_hash (data,
- data_size,
- &usp.new_backup_hash);
- if (GNUNET_OK !=
- GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_SYNC_BACKUP_UPLOAD,
- &usp,
- &download->account_sig.eddsa_sig,
- &download->account_pub.eddsa_pub))
- {
- GNUNET_break_op (0);
- dd.http_status = 0;
- break;
- }
- /* Success, call callback with all details! */
- dd.details.ok.sig = download->account_sig;
- dd.details.ok.prev_backup_hash = download->sync_previous;
- dd.details.ok.curr_backup_hash = usp.new_backup_hash;
- dd.details.ok.backup = data;
- dd.details.ok.backup_size = data_size;
- download->cb (download->cb_cls,
- &dd);
- download->cb = NULL;
- SYNC_download_cancel (download);
- return;
- }
- case MHD_HTTP_BAD_REQUEST:
- /* This should never happen, either us or the sync server is buggy
- (or API version conflict); just pass JSON reply to the application */
- break;
- case MHD_HTTP_NOT_FOUND:
- /* Nothing really to verify */
- break;
- case MHD_HTTP_INTERNAL_SERVER_ERROR:
- /* Server had an internal issue; we should retry, but this API
- leaves this to the application */
- break;
- default:
- /* unexpected response code */
- GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
- "Unexpected response code %u\n",
- (unsigned int) response_code);
- GNUNET_break (0);
- dd.http_status = 0;
- break;
- }
- if (NULL != download->cb)
- {
- download->cb (download->cb_cls,
- &dd);
- download->cb = NULL;
- }
- SYNC_download_cancel (download);
-}
-
-
-/**
- * Handle HTTP header received by curl.
- *
- * @param buffer one line of HTTP header data
- * @param size size of an item
- * @param nitems number of items passed
- * @param userdata our `struct SYNC_DownloadOperation *`
- * @return `size * nitems`
- */
-static size_t
-handle_header (char *buffer,
- size_t size,
- size_t nitems,
- void *userdata)
-{
- struct SYNC_DownloadOperation *download = userdata;
- size_t total = size * nitems;
- char *ndup;
- const char *hdr_type;
- char *hdr_val;
- char *sp;
-
- ndup = GNUNET_strndup (buffer,
- total);
- hdr_type = strtok_r (ndup,
- ":",
- &sp);
- if (NULL == hdr_type)
- {
- GNUNET_free (ndup);
- return total;
- }
- hdr_val = strtok_r (NULL,
- "\n\r",
- &sp);
- if (NULL == hdr_val)
- {
- GNUNET_free (ndup);
- return total;
- }
- if (' ' == *hdr_val)
- hdr_val++;
- if (0 == strcasecmp (hdr_type,
- "Sync-Signature"))
- {
- if (GNUNET_OK !=
- GNUNET_STRINGS_string_to_data (hdr_val,
- strlen (hdr_val),
- &download->account_sig,
- sizeof (struct SYNC_AccountSignatureP)))
- {
- GNUNET_break_op (0);
- GNUNET_free (ndup);
- return 0;
- }
- }
- if (0 == strcasecmp (hdr_type,
- "Sync-Previous"))
- {
- if (GNUNET_OK !=
- GNUNET_STRINGS_string_to_data (hdr_val,
- strlen (hdr_val),
- &download->sync_previous,
- sizeof (struct GNUNET_HashCode)))
- {
- GNUNET_break_op (0);
- GNUNET_free (ndup);
- return 0;
- }
- }
- GNUNET_free (ndup);
- return total;
-}
-
-
-struct SYNC_DownloadOperation *
-SYNC_download (struct GNUNET_CURL_Context *ctx,
- const char *base_url,
- const struct SYNC_AccountPublicKeyP *pub,
- SYNC_DownloadCallback cb,
- void *cb_cls)
-{
- struct SYNC_DownloadOperation *download;
- char *pub_str;
- CURL *eh;
-
- download = GNUNET_new (struct SYNC_DownloadOperation);
- download->account_pub = *pub;
- pub_str = GNUNET_STRINGS_data_to_string_alloc (pub,
- sizeof (*pub));
- GNUNET_asprintf (&download->url,
- "%s%sbackups/%s",
- base_url,
- '/' == base_url[strlen (base_url) - 1]
- ? ""
- : "/",
- pub_str);
- GNUNET_free (pub_str);
- eh = SYNC_curl_easy_get_ (download->url);
- GNUNET_assert (CURLE_OK ==
- curl_easy_setopt (eh,
- CURLOPT_HEADERFUNCTION,
- &handle_header));
- GNUNET_assert (CURLE_OK ==
- curl_easy_setopt (eh,
- CURLOPT_HEADERDATA,
- download));
- download->cb = cb;
- download->cb_cls = cb_cls;
- download->job = GNUNET_CURL_job_add_raw (ctx,
- eh,
- NULL,
- &handle_download_finished,
- download);
- return download;
-}
-
-
-void
-SYNC_download_cancel (struct SYNC_DownloadOperation *download)
-{
- if (NULL != download->job)
- {
- GNUNET_CURL_job_cancel (download->job);
- download->job = NULL;
- }
- GNUNET_free (download->url);
- GNUNET_free (download);
-}
-
-
-/* end of sync_api_download.c */
diff --git a/src/lib/sync_api_object_fetch.c b/src/lib/sync_api_object_fetch.c
@@ -0,0 +1,217 @@
+/*
+ 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/>
+*/
+/**
+ * @file lib/sync_api_object_fetch.c
+ * @brief Implementation of the object fetch (GET) operation
+ * @author Iván Ávalos
+ */
+#include "platform.h"
+#include <curl/curl.h>
+#include <jansson.h>
+#include <microhttpd.h>
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_curl_lib.h>
+#include <gnunet/gnunet_json_lib.h>
+#include <taler/taler_json_lib.h>
+#include <taler/taler_curl_lib.h>
+#include <sync/sync_service.h>
+#include <sync/sync_util.h>
+#include "sync_api_curl_defaults.h"
+
+
+/**
+ * Handle for an object fetch operation.
+ */
+struct SYNC_ObjectFetchOperation
+{
+
+ /**
+ * The url for this request.
+ */
+ char *url;
+
+ /**
+ * Handle for the request.
+ */
+ struct GNUNET_CURL_Job *job;
+
+ /**
+ * Reference to the execution context.
+ */
+ struct GNUNET_CURL_Context *ctx;
+
+ /**
+ * Function to call with the result.
+ */
+ SYNC_ObjectFetchCallback cb;
+
+ /**
+ * Closure for @e cb.
+ */
+ void *cb_cls;
+
+};
+
+
+/**
+ * Function called when we're done processing the
+ * GET /backups/$KEY/objects/$UID request.
+ *
+ * @param cls the `struct SYNC_ObjectFetchOperation *`
+ * @param response_code HTTP response code, 0 on error
+ * @param response response body as parsed JSON, or NULL
+ */
+static void
+handle_object_fetch_finished (void *cls,
+ long response_code,
+ const void *response)
+{
+ struct SYNC_ObjectFetchOperation *op = cls;
+ const json_t *json = response;
+ unsigned int http_status = (unsigned int) response_code;
+ enum TALER_ErrorCode ec = TALER_EC_INVALID;
+ struct SYNC_ObjectEntry entry = { 0 };
+
+ op->job = NULL;
+ switch (response_code)
+ {
+ case 0:
+ break;
+ case MHD_HTTP_OK:
+ if (NULL != json)
+ {
+ if (GNUNET_OK !=
+ SYNC_object_entry_parse ((json_t *) json,
+ &entry))
+ {
+ GNUNET_break_op (0);
+ ec = TALER_EC_GENERIC_PARAMETER_MALFORMED;
+ break;
+ }
+ ec = TALER_EC_NONE;
+ }
+ else
+ {
+ ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+ }
+ break;
+ case MHD_HTTP_BAD_REQUEST:
+ case MHD_HTTP_NOT_FOUND:
+ case MHD_HTTP_INTERNAL_SERVER_ERROR:
+ default:
+ if ( (response_code >= 400) &&
+ (response_code < 500) )
+ ec = (NULL != json)
+ ? TALER_JSON_get_error_code (json)
+ : TALER_EC_GENERIC_PARAMETER_MALFORMED;
+ else if (response_code >= 500)
+ ec = (NULL != json)
+ ? TALER_JSON_get_error_code (json)
+ : TALER_EC_GENERIC_INVALID_RESPONSE;
+ break;
+ }
+ if (NULL != op->cb)
+ {
+ op->cb (op->cb_cls,
+ http_status,
+ ec,
+ (TALER_EC_NONE == ec) ? &entry.uid : NULL,
+ (TALER_EC_NONE == ec) ? entry.data_size : 0,
+ (TALER_EC_NONE == ec) ? entry.data : NULL);
+ op->cb = NULL;
+ }
+ if (TALER_EC_NONE == ec)
+ SYNC_object_entry_clear (&entry);
+ SYNC_object_fetch_cancel (op);
+}
+
+
+struct SYNC_ObjectFetchOperation *
+SYNC_object_fetch (struct GNUNET_CURL_Context *ctx,
+ const char *base_url,
+ const struct SYNC_AccountPublicKeyP *pub,
+ const struct SYNC_ObjectUID *uid,
+ SYNC_ObjectFetchCallback cb,
+ void *cb_cls)
+{
+ struct SYNC_ObjectFetchOperation *op;
+ CURL *eh;
+ char *pub_s;
+ char *uid_s;
+ char *path;
+ char *url;
+
+ pub_s = GNUNET_STRINGS_data_to_string_alloc (pub,
+ sizeof (*pub));
+ uid_s = GNUNET_STRINGS_data_to_string_alloc (uid,
+ sizeof (*uid));
+ GNUNET_asprintf (&path,
+ "backups/%s/objects/%s",
+ pub_s,
+ uid_s);
+ GNUNET_free (pub_s);
+ GNUNET_free (uid_s);
+ url = TALER_url_join (base_url,
+ path,
+ NULL);
+ GNUNET_free (path);
+ if (NULL == url)
+ {
+ GNUNET_break (0);
+ return NULL;
+ }
+
+ op = GNUNET_new (struct SYNC_ObjectFetchOperation);
+ op->ctx = ctx;
+ op->cb = cb;
+ op->cb_cls = cb_cls;
+ op->url = url;
+
+ eh = SYNC_curl_easy_get_ (op->url);
+ if (NULL == eh)
+ {
+ GNUNET_break (0);
+ GNUNET_free (op->url);
+ GNUNET_free (op);
+ return NULL;
+ }
+
+ op->job = GNUNET_CURL_job_add2 (ctx,
+ eh,
+ NULL,
+ &handle_object_fetch_finished,
+ op);
+ return op;
+}
+
+
+void
+SYNC_object_fetch_cancel (struct SYNC_ObjectFetchOperation *op)
+{
+ if (NULL != op->job)
+ {
+ GNUNET_CURL_job_cancel (op->job);
+ op->job = NULL;
+ }
+ GNUNET_free (op->url);
+ GNUNET_free (op);
+}
+
+
+/* end of sync_api_object_fetch.c */
diff --git a/src/lib/sync_api_object_upload.c b/src/lib/sync_api_object_upload.c
@@ -0,0 +1,121 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2014-2026 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as
+ published by the Free Software Foundation; either version 2.1,
+ 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 Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with TALER; see the file COPYING.LGPL. If not,
+ see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/sync_api_object_upload.c
+ * @brief Implementation of the object upload (POST) operation
+ * @author Iván Ávalos
+ */
+#include "sync_api_block_operation_common.h"
+
+
+struct SYNC_BlockOperation *
+SYNC_object_upload (struct GNUNET_CURL_Context *ctx,
+ const char *base_url,
+ const struct SYNC_AccountPrivateKeyP *priv,
+ const struct SYNC_ObjectUID *uid,
+ size_t data_size,
+ const void *data,
+ SYNC_BlockOperationCallback cb,
+ void *cb_cls)
+{
+ struct SYNC_BlockOperation *op;
+ struct SYNC_AccountSignatureP account_sig;
+ struct GNUNET_HashCode data_hash;
+ CURL *eh;
+
+ /* Compute hash of data */
+ GNUNET_CRYPTO_hash (data,
+ data_size,
+ &data_hash);
+
+ /* Build signature */
+ SYNC_object_upload_sign (priv,
+ uid,
+ &data_hash,
+ &account_sig);
+
+ /* Allocate operation handle */
+ op = GNUNET_new (struct SYNC_BlockOperation);
+ op->ctx = ctx;
+ op->cb = cb;
+ op->cb_cls = cb_cls;
+ op->post_ctx.disable_compression = true;
+ op->url = SYNC_block_operation_make_url_ (base_url,
+ priv,
+ "objects",
+ sizeof (*uid),
+ uid);
+
+ /* Build JSON body */
+ {
+ json_t *req;
+
+ req = GNUNET_JSON_PACK (
+ GNUNET_JSON_pack_data_auto ("object_sig",
+ &account_sig),
+ GNUNET_JSON_pack_data_varsize ("data",
+ data,
+ data_size));
+
+ /* Set up CURL easy handle */
+ eh = SYNC_curl_easy_get_ (op->url);
+ if (NULL == eh)
+ {
+ GNUNET_break (0);
+ json_decref (req);
+ goto cleanup;
+ }
+
+ GNUNET_assert (CURLE_OK ==
+ curl_easy_setopt (eh,
+ CURLOPT_HEADERFUNCTION,
+ &SYNC_block_operation_handle_header_));
+ GNUNET_assert (CURLE_OK ==
+ curl_easy_setopt (eh,
+ CURLOPT_HEADERDATA,
+ op));
+
+ if (GNUNET_OK !=
+ TALER_curl_easy_post (&op->post_ctx,
+ eh,
+ req))
+ {
+ GNUNET_break (0);
+ json_decref (req);
+ curl_easy_cleanup (eh);
+ goto cleanup;
+ }
+ json_decref (req);
+ }
+
+ op->job = GNUNET_CURL_job_add2 (ctx,
+ eh,
+ op->post_ctx.headers,
+ &SYNC_block_operation_handle_finished_,
+ op);
+ return op;
+
+cleanup:
+ GNUNET_free (op->url);
+ GNUNET_free (op);
+ return NULL;
+}
+
+
+/* end of sync_api_object_upload.c */
diff --git a/src/lib/sync_api_upload.c b/src/lib/sync_api_upload.c
@@ -1,441 +0,0 @@
-/*
- This file is part of TALER
- Copyright (C) 2014-2019 Taler Systems SA
-
- TALER is free software; you can redistribute it and/or modify
- it under the terms of the GNU Lesser General Public License as
- published by the Free Software Foundation; either version 2.1,
- 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 Lesser General Public License for more details.
-
- You should have received a copy of the GNU Lesser General Public
- License along with TALER; see the file COPYING.LGPL. If not,
- see <http://www.gnu.org/licenses/>
-*/
-
-/**
- * @file lib/sync_api_upload.c
- * @brief Implementation of the upload POST
- * @author Christian Grothoff
- */
-#include "platform.h"
-#include <curl/curl.h>
-#include <jansson.h>
-#include <microhttpd.h> /* just for HTTP status codes */
-#include <gnunet/gnunet_util_lib.h>
-#include <gnunet/gnunet_curl_lib.h>
-#include <taler/taler_signatures.h>
-#include <taler/taler_json_lib.h>
-#include "sync/sync_service.h"
-#include "sync_api_curl_defaults.h"
-
-
-/**
- * @brief Handle for an upload operation.
- */
-struct SYNC_UploadOperation
-{
-
- /**
- * The url for this request.
- */
- char *url;
-
- /**
- * Handle for the request.
- */
- struct GNUNET_CURL_Job *job;
-
- /**
- * Reference to the execution context.
- */
- struct GNUNET_CURL_Context *ctx;
-
- /**
- * Function to call with the result.
- */
- SYNC_UploadCallback cb;
-
- /**
- * Closure for @e cb.
- */
- void *cb_cls;
-
- /**
- * Payment URI we received from the service, or NULL.
- */
- char *pay_uri;
-
- /**
- * Hash of the data we are uploading.
- */
- struct GNUNET_HashCode new_upload_hash;
-};
-
-
-/**
- * Function called when we're done processing the
- * HTTP /backup request.
- *
- * @param cls the `struct SYNC_UploadOperation`
- * @param response_code HTTP response code, 0 on error
- * @param data data we downloaded
- * @param data_size number of bytes in @a data
- */
-static void
-handle_upload_finished (void *cls,
- long response_code,
- const void *data,
- size_t data_size)
-{
- struct SYNC_UploadOperation *uo = cls;
- struct SYNC_UploadDetails ud = {
- .http_status = (unsigned int) response_code,
- .ec = TALER_EC_INVALID
- };
-
- uo->job = NULL;
- switch (response_code)
- {
- case 0:
- break;
- case MHD_HTTP_NO_CONTENT:
- ud.us = SYNC_US_SUCCESS;
- ud.details.success.curr_backup_hash = &uo->new_upload_hash;
- ud.ec = TALER_EC_NONE;
- break;
- case MHD_HTTP_NOT_MODIFIED:
- ud.us = SYNC_US_SUCCESS;
- ud.details.success.curr_backup_hash = &uo->new_upload_hash;
- ud.ec = TALER_EC_NONE;
- break;
- case MHD_HTTP_BAD_REQUEST:
- GNUNET_break (0);
- ud.ec = TALER_JSON_get_error_code2 (data,
- data_size);
- break;
- case MHD_HTTP_PAYMENT_REQUIRED:
- ud.us = SYNC_US_PAYMENT_REQUIRED;
- ud.details.payment_required.payment_request = uo->pay_uri;
- ud.ec = TALER_EC_NONE;
- break;
- case MHD_HTTP_FORBIDDEN:
- GNUNET_break (0);
- ud.ec = TALER_JSON_get_error_code2 (data,
- data_size);
- break;
- case MHD_HTTP_CONFLICT:
- ud.us = SYNC_US_CONFLICTING_BACKUP;
- GNUNET_CRYPTO_hash (data,
- data_size,
- &ud.details.recovered_backup.existing_backup_hash);
- ud.details.recovered_backup.existing_backup_size
- = data_size;
- ud.details.recovered_backup.existing_backup
- = data;
- ud.ec = TALER_EC_NONE;
- break;
- case MHD_HTTP_GONE:
- ud.ec = TALER_JSON_get_error_code2 (data,
- data_size);
- break;
- case MHD_HTTP_LENGTH_REQUIRED:
- GNUNET_break (0);
- break;
- case MHD_HTTP_REQUEST_ENTITY_TOO_LARGE:
- ud.ec = TALER_JSON_get_error_code2 (data,
- data_size);
- break;
- case MHD_HTTP_TOO_MANY_REQUESTS:
- ud.ec = TALER_JSON_get_error_code2 (data,
- data_size);
- break;
- case MHD_HTTP_INTERNAL_SERVER_ERROR:
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Internal server error: `%.*s\n",
- (int) data_size,
- (const char *) data);
- break;
- }
- if (NULL != uo->cb)
- {
- uo->cb (uo->cb_cls,
- &ud);
- uo->cb = NULL;
- }
- SYNC_upload_cancel (uo);
-}
-
-
-/**
- * Handle HTTP header received by curl.
- *
- * @param buffer one line of HTTP header data
- * @param size size of an item
- * @param nitems number of items passed
- * @param userdata our `struct SYNC_DownloadOperation *`
- * @return `size * nitems`
- */
-static size_t
-handle_header (char *buffer,
- size_t size,
- size_t nitems,
- void *userdata)
-{
- struct SYNC_UploadOperation *uo = userdata;
- size_t total = size * nitems;
- char *ndup;
- const char *hdr_type;
- char *hdr_val;
- char *sp;
-
- ndup = GNUNET_strndup (buffer,
- total);
- hdr_type = strtok_r (ndup,
- ":",
- &sp);
- if (NULL == hdr_type)
- {
- GNUNET_free (ndup);
- return total;
- }
- hdr_val = strtok_r (NULL,
- "",
- &sp);
- if (NULL == hdr_val)
- {
- GNUNET_free (ndup);
- return total;
- }
- if (' ' == *hdr_val)
- hdr_val++;
- if (0 == strcasecmp (hdr_type,
- "Taler"))
- {
- size_t len;
-
- /* found payment URI we care about! */
- uo->pay_uri = GNUNET_strdup (hdr_val);
- len = strlen (uo->pay_uri);
- while ( (len > 0) &&
- ( ('\n' == uo->pay_uri[len - 1]) ||
- ('\r' == uo->pay_uri[len - 1]) ) )
- {
- len--;
- uo->pay_uri[len] = '\0';
- }
- }
- GNUNET_free (ndup);
- return total;
-}
-
-
-struct SYNC_UploadOperation *
-SYNC_upload (struct GNUNET_CURL_Context *ctx,
- const char *base_url,
- struct SYNC_AccountPrivateKeyP *priv,
- const struct GNUNET_HashCode *prev_backup_hash,
- size_t backup_size,
- const void *backup,
- enum SYNC_PaymentOptions po,
- const char *paid_order_id,
- SYNC_UploadCallback cb,
- void *cb_cls)
-{
- struct SYNC_AccountSignatureP account_sig;
- struct SYNC_UploadOperation *uo;
- CURL *eh;
- struct curl_slist *job_headers;
- struct SYNC_UploadSignaturePS usp = {
- .purpose.purpose = htonl (TALER_SIGNATURE_SYNC_BACKUP_UPLOAD),
- .purpose.size = htonl (sizeof (usp))
- };
-
- if (NULL != prev_backup_hash)
- usp.old_backup_hash = *prev_backup_hash;
- GNUNET_CRYPTO_hash (backup,
- backup_size,
- &usp.new_backup_hash);
- GNUNET_CRYPTO_eddsa_sign (&priv->eddsa_priv,
- &usp,
- &account_sig.eddsa_sig);
-
- /* setup our HTTP headers */
- job_headers = NULL;
- {
- struct curl_slist *ext;
- char *val;
- char *hdr;
-
- /* Set Sync-Signature header */
- val = GNUNET_STRINGS_data_to_string_alloc (&account_sig,
- sizeof (account_sig));
- GNUNET_asprintf (&hdr,
- "Sync-Signature: %s",
- val);
- GNUNET_free (val);
- ext = curl_slist_append (job_headers,
- hdr);
- GNUNET_free (hdr);
- if (NULL == ext)
- {
- GNUNET_break (0);
- curl_slist_free_all (job_headers);
- return NULL;
- }
- job_headers = ext;
-
- /* set If-None-Match header */
- val = GNUNET_STRINGS_data_to_string_alloc (&usp.new_backup_hash,
- sizeof (struct GNUNET_HashCode));
- GNUNET_asprintf (&hdr,
- "%s: \"%s\"",
- MHD_HTTP_HEADER_IF_NONE_MATCH,
- val);
- GNUNET_free (val);
- ext = curl_slist_append (job_headers,
- hdr);
- GNUNET_free (hdr);
- if (NULL == ext)
- {
- GNUNET_break (0);
- curl_slist_free_all (job_headers);
- return NULL;
- }
- job_headers = ext;
-
- /* Setup If-Match header */
- if (NULL != prev_backup_hash)
- {
- val = GNUNET_STRINGS_data_to_string_alloc (&usp.old_backup_hash,
- sizeof (struct
- GNUNET_HashCode));
- GNUNET_asprintf (&hdr,
- "If-Match: \"%s\"",
- val);
- GNUNET_free (val);
- ext = curl_slist_append (job_headers,
- hdr);
- GNUNET_free (hdr);
- if (NULL == ext)
- {
- GNUNET_break (0);
- curl_slist_free_all (job_headers);
- return NULL;
- }
- job_headers = ext;
- }
- }
- /* Finished setting up headers */
-
- uo = GNUNET_new (struct SYNC_UploadOperation);
- uo->new_upload_hash = usp.new_backup_hash;
- {
- char *path;
- char *account_s;
- struct SYNC_AccountPublicKeyP pub;
-
- GNUNET_CRYPTO_eddsa_key_get_public (&priv->eddsa_priv,
- &pub.eddsa_pub);
- account_s = GNUNET_STRINGS_data_to_string_alloc (&pub,
- sizeof (pub));
- GNUNET_asprintf (&path,
- "backups/%s",
- account_s);
- GNUNET_free (account_s);
- if (0 != (po & SYNC_PO_FRESH_ORDER))
- {
- uo->url = (0 != (po & SYNC_PO_FORCE_PAYMENT))
- ? TALER_url_join (base_url,
- path,
- "fresh",
- "y",
- "pay",
- "y",
- (NULL != paid_order_id)
- ? "paying"
- : NULL,
- paid_order_id,
- NULL)
- : TALER_url_join (base_url,
- path,
- "fresh",
- "y",
- (NULL != paid_order_id)
- ? "paying"
- : NULL,
- paid_order_id,
- NULL);
- }
- else
- {
- uo->url = (0 != (po & SYNC_PO_FORCE_PAYMENT))
- ? TALER_url_join (base_url,
- path,
- "pay",
- "y",
- (NULL != paid_order_id)
- ? "paying"
- : NULL,
- paid_order_id,
- NULL)
- : TALER_url_join (base_url,
- path,
- (NULL != paid_order_id)
- ? "paying"
- : NULL,
- paid_order_id,
- NULL);
- }
-
- GNUNET_free (path);
- }
- uo->ctx = ctx;
- uo->cb = cb;
- uo->cb_cls = cb_cls;
- eh = SYNC_curl_easy_get_ (uo->url);
- GNUNET_assert (CURLE_OK ==
- curl_easy_setopt (eh,
- CURLOPT_POSTFIELDS,
- backup));
- GNUNET_assert (CURLE_OK ==
- curl_easy_setopt (eh,
- CURLOPT_POSTFIELDSIZE,
- (long) backup_size));
- GNUNET_assert (CURLE_OK ==
- curl_easy_setopt (eh,
- CURLOPT_HEADERFUNCTION,
- &handle_header));
- GNUNET_assert (CURLE_OK ==
- curl_easy_setopt (eh,
- CURLOPT_HEADERDATA,
- uo));
- uo->job = GNUNET_CURL_job_add_raw (ctx,
- eh,
- job_headers,
- &handle_upload_finished,
- uo);
- curl_slist_free_all (job_headers);
- return uo;
-}
-
-
-void
-SYNC_upload_cancel (struct SYNC_UploadOperation *uo)
-{
- if (NULL != uo->job)
- {
- GNUNET_CURL_job_cancel (uo->job);
- uo->job = NULL;
- }
- GNUNET_free (uo->pay_uri);
- GNUNET_free (uo->url);
- GNUNET_free (uo);
-}
-
-
-/* end of sync_api_upload.c */
diff --git a/src/sync/meson.build b/src/sync/meson.build
@@ -2,13 +2,17 @@
install_data('sync.conf', install_dir: pkgcfgdir)
executable(
- 'sync-httpd',
+ 'sync-httpd2',
[
- 'sync-httpd.c',
- 'sync-httpd_backup.c',
- 'sync-httpd_backup_post.c',
- 'sync-httpd_config.c',
- 'sync-httpd_mhd.c',
+ 'sync-httpd2.c',
+ 'sync-httpd2_config.c',
+ 'sync-httpd2_post-backups-KEY-blocks-NONCE.c',
+ 'sync-httpd2_delete-backups-KEY-blocks-NONCE.c',
+ 'sync-httpd2_get-backups-KEY-objects-UID.c',
+ 'sync-httpd2_post-backups-KEY-objects-UID.c',
+ 'sync-httpd2_get-backups-KEY.c',
+ 'sync-httpd2_get-backups-KEY-blocks.c',
+ 'sync-httpd2_payments.c',
],
dependencies: [
libsyncutil_dep,
@@ -17,40 +21,13 @@ executable(
talerjson_dep,
talermerchant_dep,
talermhd_dep,
+ talermhd2_dep,
gnunetutil_dep,
gnunetjson_dep,
gnunetcurl_dep,
- mhd_dep,
+ mhd2_dep,
json_dep,
],
include_directories: [incdir, configuration_inc],
install: true,
)
-
-if mhd2_dep.found() and talermhd2_dep.found()
- executable(
- 'sync-httpd2',
- [
- 'sync-httpd2.c',
- 'sync-httpd2_backup.c',
- 'sync-httpd2_backup_post.c',
- 'sync-httpd2_config.c',
- ],
- dependencies: [
- libsyncutil_dep,
- libsyncdb_dep,
- talerutil_dep,
- talerjson_dep,
- talermerchant_dep,
- talermhd_dep,
- talermhd2_dep,
- gnunetutil_dep,
- gnunetjson_dep,
- gnunetcurl_dep,
- mhd2_dep,
- json_dep,
- ],
- include_directories: [incdir, configuration_inc],
- install: true,
- )
-endif
diff --git a/src/sync/sync-httpd.c b/src/sync/sync-httpd.c
@@ -1,655 +0,0 @@
-/*
- This file is part of TALER
- (C) 2019 Taler Systems SA
-
- TALER 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.
-
- 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/>
-*/
-/**
- * @file sync/sync-httpd.c
- * @brief HTTP serving layer intended to provide basic backup operations
- * @author Christian Grothoff
- */
-#include "platform.h"
-#include <microhttpd.h>
-#include <gnunet/gnunet_util_lib.h>
-#include "sync/sync_util.h"
-#include "sync-httpd.h"
-#include "sync-httpd_mhd.h"
-#include "sync/sync_database_lib.h"
-#include "sync-httpd_backup.h"
-#include "sync-httpd_config.h"
-
-
-/**
- * Should a "Connection: close" header be added to each HTTP response?
- */
-static int SH_sync_connection_close;
-
-/**
- * Upload limit to the service, in megabytes.
- */
-unsigned long long int SH_upload_limit_mb;
-
-/**
- * Annual fee for the backup account.
- */
-struct TALER_Amount SH_annual_fee;
-
-/**
- * Our Taler backend to process payments.
- */
-char *SH_backend_url;
-
-/**
- * Our fulfillment URL.
- */
-char *SH_fulfillment_url;
-
-/**
- * Our context for making HTTP requests.
- */
-struct GNUNET_CURL_Context *SH_ctx;
-
-/**
- * Reschedule context for #SH_ctx.
- */
-static struct GNUNET_CURL_RescheduleContext *rc;
-
-/**
- * Global return code
- */
-static int global_ret;
-
-/**
- * Set to true if we have started an MHD daemons.
- */
-static bool have_daemons;
-
-/**
- * Username and password to use for client authentication
- * (optional).
- */
-static char *userpass;
-
-/**
- * Type of the client's TLS certificate (optional).
- */
-static char *certtype;
-
-/**
- * File with the client's TLS certificate (optional).
- */
-static char *certfile;
-
-/**
- * File with the client's TLS private key (optional).
- */
-static char *keyfile;
-
-/**
- * This value goes in the Authorization:-header.
- */
-static char *apikey;
-
-/**
- * Passphrase to decrypt client's TLS private key file (optional).
- */
-static char *keypass;
-
-/**
- * Amount of insurance.
- */
-struct TALER_Amount SH_insurance;
-
-
-/**
- * A client has requested the given url using the given method
- * (#MHD_HTTP_METHOD_GET, #MHD_HTTP_METHOD_PUT,
- * #MHD_HTTP_METHOD_DELETE, #MHD_HTTP_METHOD_POST, etc). The callback
- * must call MHD callbacks to provide content to give back to the
- * client and return an HTTP status code (i.e. #MHD_HTTP_OK,
- * #MHD_HTTP_NOT_FOUND, etc.).
- *
- * @param cls argument given together with the function
- * pointer when the handler was registered with MHD
- * @param connection connection handle
- * @param url the requested url
- * @param method the HTTP method used (#MHD_HTTP_METHOD_GET,
- * #MHD_HTTP_METHOD_PUT, etc.)
- * @param version the HTTP version string (i.e.
- * #MHD_HTTP_VERSION_1_1)
- * @param upload_data the data being uploaded (excluding HEADERS,
- * for a POST that fits into memory and that is encoded
- * with a supported encoding, the POST data will NOT be
- * given in upload_data and is instead available as
- * part of #MHD_get_connection_values; very large POST
- * data *will* be made available incrementally in
- * @a upload_data)
- * @param upload_data_size set initially to the size of the
- * @a upload_data provided; the method must update this
- * value to the number of bytes NOT processed;
- * @param con_cls pointer that the callback can set to some
- * address and that will be preserved by MHD for future
- * calls for this request; since the access handler may
- * be called many times (i.e., for a PUT/POST operation
- * with plenty of upload data) this allows the application
- * to easily associate some request-specific state.
- * If necessary, this state can be cleaned up in the
- * global #MHD_RequestCompletedCallback (which
- * can be set with the #MHD_OPTION_NOTIFY_COMPLETED).
- * Initially, `*con_cls` will be NULL.
- * @return #MHD_YES if the connection was handled successfully,
- * #MHD_NO if the socket must be closed due to a serious
- * error while handling the request
- */
-static enum MHD_Result
-url_handler (void *cls,
- struct MHD_Connection *connection,
- const char *url,
- const char *method,
- const char *version,
- const char *upload_data,
- size_t *upload_data_size,
- void **con_cls)
-{
- static struct SH_RequestHandler handlers[] = {
- /* Landing page, tell humans to go away. */
- { "/", MHD_HTTP_METHOD_GET, "text/plain",
- "Hello, I'm sync. This HTTP server is not for humans.\n", 0,
- &SH_MHD_handler_static_response, MHD_HTTP_OK },
- { "/agpl", MHD_HTTP_METHOD_GET, "text/plain",
- NULL, 0,
- &SH_handler_config, MHD_HTTP_FOUND },
- { "/config", MHD_HTTP_METHOD_GET, "text/json",
- NULL, 0,
- &SH_handler_config, MHD_HTTP_OK },
- {NULL, NULL, NULL, NULL, 0, 0 }
- };
- static struct SH_RequestHandler h404 = {
- "", NULL, "text/html",
- "<html><title>404: not found</title></html>", 0,
- &SH_MHD_handler_static_response, MHD_HTTP_NOT_FOUND
- };
-
- struct TM_HandlerContext *hc = *con_cls;
- struct GNUNET_AsyncScopeId aid;
- const char *correlation_id = NULL;
- struct SYNC_AccountPublicKeyP account_pub;
-
- (void) cls;
- (void) version;
- if (NULL == hc)
- {
- GNUNET_async_scope_fresh (&aid);
- /* We only read the correlation ID on the first callback for every client */
- correlation_id = MHD_lookup_connection_value (connection,
- MHD_HEADER_KIND,
- "Sync-Correlation-Id");
- if ((NULL != correlation_id) &&
- (GNUNET_YES != GNUNET_CURL_is_valid_scope_id (correlation_id)))
- {
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "illegal incoming correlation ID\n");
- correlation_id = NULL;
- }
- }
- else
- {
- aid = hc->async_scope_id;
- }
- GNUNET_SCHEDULER_begin_async_scope (&aid);
-
- if (NULL != correlation_id)
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Handling request for (%s) URL '%s', correlation_id=%s\n",
- method,
- url,
- correlation_id);
- else
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Handling request (%s) for URL '%s'\n",
- method,
- url);
-
- if (0 == strncmp (url,
- "/backups/",
- strlen ("/backups/")))
- {
- const char *ac = &url[strlen ("/backups/")];
-
- if (GNUNET_OK !=
- GNUNET_CRYPTO_eddsa_public_key_from_string (ac,
- strlen (ac),
- &account_pub.eddsa_pub))
- {
- GNUNET_break_op (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_GENERIC_PARAMETER_MALFORMED,
- ac);
- }
- if (0 == strcasecmp (method,
- MHD_HTTP_METHOD_OPTIONS))
- {
- return TALER_MHD_reply_cors_preflight (connection);
- }
- if (0 == strcasecmp (method,
- MHD_HTTP_METHOD_GET))
- {
- return SH_backup_get (connection,
- &account_pub);
- }
- if (0 == strcasecmp (method,
- MHD_HTTP_METHOD_POST))
- {
- int ret;
-
- ret = SH_backup_post (connection,
- con_cls,
- &account_pub,
- upload_data,
- upload_data_size);
- hc = *con_cls;
- if (NULL != hc)
- {
- /* Store the async context ID, so we can restore it if
- * we get another callback for this request. */
- hc->async_scope_id = aid;
- }
- return ret;
- }
- }
- for (unsigned int i = 0; NULL != handlers[i].url; i++)
- {
- struct SH_RequestHandler *rh = &handlers[i];
-
- if (0 == strcmp (url,
- rh->url))
- {
- if (0 == strcasecmp (method,
- MHD_HTTP_METHOD_OPTIONS))
- {
- return TALER_MHD_reply_cors_preflight (connection);
- }
- if ( (NULL == rh->method) ||
- (0 == strcasecmp (method,
- rh->method)) )
- {
- enum MHD_Result ret;
-
- ret = rh->handler (rh,
- connection,
- con_cls,
- upload_data,
- upload_data_size);
- hc = *con_cls;
- if (NULL != hc)
- {
- /* Store the async context ID, so we can restore it if
- * we get another callback for this request. */
- hc->async_scope_id = aid;
- }
- return ret;
- }
- }
- }
- return SH_MHD_handler_static_response (&h404,
- connection,
- con_cls,
- upload_data,
- upload_data_size);
-}
-
-
-/**
- * Shutdown task. Invoked when the application is being terminated.
- *
- * @param cls NULL
- */
-static void
-do_shutdown (void *cls)
-{
- (void) cls;
- SH_resume_all_bc ();
- TALER_MHD_daemons_halt ();
- if (NULL != SH_ctx)
- {
- GNUNET_CURL_fini (SH_ctx);
- SH_ctx = NULL;
- }
- if (NULL != rc)
- {
- GNUNET_CURL_gnunet_rc_destroy (rc);
- rc = NULL;
- }
- TALER_MHD_daemons_destroy ();
- SYNCDB_fini ();
-}
-
-
-/**
- * Function called whenever MHD is done with a request. If the
- * request was a POST, we may have stored a `struct Buffer *` in the
- * @a con_cls that might still need to be cleaned up. Call the
- * respective function to free the memory.
- *
- * @param cls client-defined closure
- * @param connection connection handle
- * @param con_cls value as set by the last call to
- * the #MHD_AccessHandlerCallback
- * @param toe reason for request termination
- * @see #MHD_OPTION_NOTIFY_COMPLETED
- * @ingroup request
- */
-static void
-handle_mhd_completion_callback (void *cls,
- struct MHD_Connection *connection,
- void **con_cls,
- enum MHD_RequestTerminationCode toe)
-{
- struct TM_HandlerContext *hc = *con_cls;
-
- (void) cls;
- (void) connection;
- if (NULL == hc)
- return;
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Finished handling request with status %d\n",
- (int) toe);
- hc->cc (hc);
- *con_cls = NULL;
-}
-
-
-/**
- * Kick MHD to run now, to be called after MHD_resume_connection().
- * Basically, we need to explicitly resume MHD's event loop whenever
- * we made progress serving a request. This function re-schedules
- * the task processing MHD's activities to run immediately.
- */
-void
-SH_trigger_daemon ()
-{
- TALER_MHD_daemon_trigger ();
-}
-
-
-/**
- * Kick GNUnet Curl scheduler to begin curl interactions.
- */
-void
-SH_trigger_curl ()
-{
- GNUNET_CURL_gnunet_scheduler_reschedule (&rc);
-}
-
-
-/**
- * Callback invoked on every listen socket to start the
- * respective MHD HTTP daemon.
- *
- * @param cls unused
- * @param lsock the listen socket
- */
-static void
-start_daemon (void *cls,
- int lsock)
-{
- struct MHD_Daemon *mhd;
-
- (void) cls;
- GNUNET_assert (-1 != lsock);
- mhd = MHD_start_daemon (MHD_USE_SUSPEND_RESUME | MHD_USE_DUAL_STACK,
- 0 /* port */,
- NULL, NULL,
- &url_handler, NULL,
- MHD_OPTION_LISTEN_SOCKET, lsock,
- MHD_OPTION_NOTIFY_COMPLETED,
- &handle_mhd_completion_callback, NULL,
- MHD_OPTION_CONNECTION_TIMEOUT,
- (unsigned int) 10 /* 10s */,
- MHD_OPTION_END);
- if (NULL == mhd)
- {
- GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
- "Failed to launch HTTP service, exiting.\n");
- global_ret = EXIT_NO_RESTART;
- GNUNET_SCHEDULER_shutdown ();
- return;
- }
- have_daemons = true;
- TALER_MHD_daemon_start (mhd);
-}
-
-
-/**
- * Main function that will be run by the scheduler.
- *
- * @param cls closure
- * @param args remaining command-line arguments
- * @param cfgfile name of the configuration file used (for saving, can be
- * NULL!)
- * @param config configuration
- */
-static void
-run (void *cls,
- char *const *args,
- const char *cfgfile,
- const struct GNUNET_CONFIGURATION_Handle *config)
-{
- enum TALER_MHD_GlobalOptions go;
-
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Starting sync-httpd\n");
- go = TALER_MHD_GO_NONE;
- if (SH_sync_connection_close)
- go |= TALER_MHD_GO_FORCE_CONNECTION_CLOSE;
- TALER_MHD_setup (go);
- global_ret = EXIT_NOTCONFIGURED;
- GNUNET_SCHEDULER_add_shutdown (&do_shutdown,
- NULL);
- if (GNUNET_OK !=
- GNUNET_CONFIGURATION_get_value_number (config,
- "sync",
- "UPLOAD_LIMIT_MB",
- &SH_upload_limit_mb))
- {
- GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
- "sync",
- "UPLOAD_LIMIT_MB");
- GNUNET_SCHEDULER_shutdown ();
- return;
- }
- if (GNUNET_OK !=
- TALER_config_get_amount (config,
- "sync",
- "INSURANCE",
- &SH_insurance))
- {
- GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
- "sync",
- "INSURANCE");
- GNUNET_SCHEDULER_shutdown ();
- return;
- }
- if (GNUNET_OK !=
- TALER_config_get_amount (config,
- "sync",
- "ANNUAL_FEE",
- &SH_annual_fee))
- {
- GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
- "sync",
- "ANNUAL_FEE");
- GNUNET_SCHEDULER_shutdown ();
- return;
- }
- if (GNUNET_OK !=
- GNUNET_CONFIGURATION_get_value_string (config,
- "sync",
- "PAYMENT_BACKEND_URL",
- &SH_backend_url))
- {
- GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
- "sync",
- "PAYMENT_BACKEND_URL");
- GNUNET_SCHEDULER_shutdown ();
- return;
- }
- if (GNUNET_OK !=
- GNUNET_CONFIGURATION_get_value_string (config,
- "sync",
- "FULFILLMENT_URL",
- &SH_fulfillment_url))
- {
- GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
- "sync",
- "BASE_URL");
- GNUNET_SCHEDULER_shutdown ();
- return;
- }
-
- /* setup HTTP client event loop */
- SH_ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule,
- &rc);
- rc = GNUNET_CURL_gnunet_rc_create (SH_ctx);
- if (NULL != userpass)
- GNUNET_CURL_set_userpass (SH_ctx,
- userpass);
- if (NULL != keyfile)
- GNUNET_CURL_set_tlscert (SH_ctx,
- certtype,
- certfile,
- keyfile,
- keypass);
- if (GNUNET_OK ==
- GNUNET_CONFIGURATION_get_value_string (config,
- "sync",
- "API_KEY",
- &apikey))
- {
- char *auth_header;
-
- GNUNET_asprintf (&auth_header,
- "%s: %s",
- MHD_HTTP_HEADER_AUTHORIZATION,
- apikey);
- if (GNUNET_OK !=
- GNUNET_CURL_append_header (SH_ctx,
- auth_header))
- {
- GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
- "Failed to set %s header, trying without\n",
- MHD_HTTP_HEADER_AUTHORIZATION);
- }
- GNUNET_free (auth_header);
- }
-
- if (GNUNET_OK !=
- SYNCDB_init (config,
- false))
- {
- global_ret = EXIT_NOTCONFIGURED;
- GNUNET_SCHEDULER_shutdown ();
- return;
- }
- if (GNUNET_OK !=
- SYNCDB_preflight ())
- {
- GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
- "Database not setup. Did you run sync-dbinit?\n");
- GNUNET_SCHEDULER_shutdown ();
- return;
- }
- {
- enum GNUNET_GenericReturnValue ret;
-
- ret = TALER_MHD_listen_bind (config,
- "sync",
- &start_daemon,
- NULL);
- switch (ret)
- {
- case GNUNET_SYSERR:
- global_ret = EXIT_NOTCONFIGURED;
- GNUNET_SCHEDULER_shutdown ();
- return;
- case GNUNET_NO:
- if (! have_daemons)
- {
- global_ret = EXIT_NOTCONFIGURED;
- GNUNET_SCHEDULER_shutdown ();
- return;
- }
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Could not open all configured listen sockets\n");
- break;
- case GNUNET_OK:
- break;
- }
- }
- global_ret = EXIT_SUCCESS;
-}
-
-
-/**
- * The main function of the serve tool
- *
- * @param argc number of arguments from the command line
- * @param argv command line arguments
- * @return 0 ok, 1 on error
- */
-int
-main (int argc,
- char *const *argv)
-{
- struct GNUNET_GETOPT_CommandLineOption options[] = {
- GNUNET_GETOPT_option_string ('A',
- "auth",
- "USERNAME:PASSWORD",
- "use the given USERNAME and PASSWORD for client authentication",
- &userpass),
- GNUNET_GETOPT_option_flag ('C',
- "connection-close",
- "force HTTP connections to be closed after each request",
- &SH_sync_connection_close),
- GNUNET_GETOPT_option_string ('k',
- "key",
- "KEYFILE",
- "file with the private TLS key for TLS client authentication",
- &keyfile),
- GNUNET_GETOPT_option_string ('p',
- "pass",
- "KEYFILEPASSPHRASE",
- "passphrase needed to decrypt the TLS client private key file",
- &keypass),
- GNUNET_GETOPT_option_string ('t',
- "type",
- "CERTTYPE",
- "type of the TLS client certificate, defaults to PEM if not specified",
- &certtype),
- GNUNET_GETOPT_OPTION_END
- };
- enum GNUNET_GenericReturnValue ret;
-
- ret = GNUNET_PROGRAM_run (SYNC_project_data (),
- argc, argv,
- "sync-httpd",
- "sync HTTP interface",
- options,
- &run, NULL);
- if (GNUNET_NO == ret)
- return EXIT_SUCCESS;
- if (GNUNET_SYSERR == ret)
- return EXIT_INVALIDARGUMENT;
- return global_ret;
-}
diff --git a/src/sync/sync-httpd.h b/src/sync/sync-httpd.h
@@ -1,173 +0,0 @@
-/*
- This file is part of TALER
- Copyright (C) 2019-2021 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/>
-*/
-/**
- * @file sync/sync-httpd.h
- * @brief HTTP serving layer
- * @author Christian Grothoff
- */
-#ifndef sync_HTTPD_H
-#define sync_HTTPD_H
-
-#include "platform.h"
-#include <microhttpd.h>
-#include <taler/taler_mhd_lib.h>
-#include "sync/sync_database_lib.h"
-#include <gnunet/gnunet_mhd_compat.h>
-
-/**
- * @brief Struct describing an URL and the handler for it.
- */
-struct SH_RequestHandler
-{
-
- /**
- * URL the handler is for.
- */
- const char *url;
-
- /**
- * Method the handler is for, NULL for "all".
- */
- const char *method;
-
- /**
- * Mime type to use in reply (hint, can be NULL).
- */
- const char *mime_type;
-
- /**
- * Raw data for the @e handler
- */
- const void *data;
-
- /**
- * Number of bytes in @e data, 0 for 0-terminated.
- */
- size_t data_size;
-
- /**
- * Function to call to handle the request.
- *
- * @param rh this struct
- * @param mime_type the @e mime_type for the reply (hint, can be NULL)
- * @param connection the MHD connection to handle
- * @param[in,out] connection_cls the connection's closure (can be updated)
- * @param upload_data upload data
- * @param[in,out] upload_data_size number of bytes (left) in @a upload_data
- * @return MHD result code
- */
- enum MHD_Result (*handler)(struct SH_RequestHandler *rh,
- struct MHD_Connection *connection,
- void **connection_cls,
- const char *upload_data,
- size_t *upload_data_size);
-
- /**
- * Default response code.
- */
- unsigned int response_code;
-};
-
-
-/**
- * Each MHD response handler that sets the "connection_cls" to a
- * non-NULL value must use a struct that has this struct as its first
- * member. This struct contains a single callback, which will be
- * invoked to clean up the memory when the contection is completed.
- */
-struct TM_HandlerContext;
-
-/**
- * Signature of a function used to clean up the context
- * we keep in the "connection_cls" of MHD when handling
- * a request.
- *
- * @param hc header of the context to clean up.
- */
-typedef void
-(*TM_ContextCleanup)(struct TM_HandlerContext *hc);
-
-
-/**
- * Each MHD response handler that sets the "connection_cls" to a
- * non-NULL value must use a struct that has this struct as its first
- * member. This struct contains a single callback, which will be
- * invoked to clean up the memory when the connection is completed.
- */
-struct TM_HandlerContext
-{
-
- /**
- * Function to execute the handler-specific cleanup of the
- * (typically larger) context.
- */
- TM_ContextCleanup cc;
-
- /**
- * Asynchronous request context id.
- */
- struct GNUNET_AsyncScopeId async_scope_id;
-};
-
-
-/**
- * Upload limit to the service, in megabytes.
- */
-extern unsigned long long SH_upload_limit_mb;
-
-/**
- * Annual fee for the backup account.
- */
-extern struct TALER_Amount SH_annual_fee;
-
-/**
- * Our Taler backend to process payments.
- */
-extern char *SH_backend_url;
-
-/**
- * Our fulfillment URL
- */
-extern char *SH_fulfillment_url;
-
-/**
- * Our context for making HTTP requests.
- */
-extern struct GNUNET_CURL_Context *SH_ctx;
-
-/**
- * Amount of insurance.
- */
-extern struct TALER_Amount SH_insurance;
-
-/**
- * Kick MHD to run now, to be called after MHD_resume_connection().
- * Basically, we need to explicitly resume MHD's event loop whenever
- * we made progress serving a request. This function re-schedules
- * the task processing MHD's activities to run immediately.
- */
-void
-SH_trigger_daemon (void);
-
-
-/**
- * Kick GNUnet Curl scheduler to begin curl interactions.
- */
-void
-SH_trigger_curl (void);
-
-
-#endif
diff --git a/src/sync/sync-httpd2.c b/src/sync/sync-httpd2.c
@@ -14,7 +14,7 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
- * @file sync/sync-httpd.c
+ * @file sync/sync-httpd2.c
* @brief HTTP serving layer intended to provide basic backup operations
* @author Christian Grothoff
*/
@@ -23,8 +23,62 @@
#include "sync/sync_util.h"
#include "sync-httpd2.h"
#include "sync/sync_database_lib.h"
-#include "sync-httpd2_backup.h"
#include "sync-httpd2_config.h"
+#include "sync-httpd2_post-backups-KEY-blocks-NONCE.h"
+#include "sync-httpd2_delete-backups-KEY-blocks-NONCE.h"
+#include "sync-httpd2_get-backups-KEY-objects-UID.h"
+#include "sync-httpd2_post-backups-KEY-objects-UID.h"
+#include "sync-httpd2_get-backups-KEY.h"
+#include "sync-httpd2_get-backups-KEY-blocks.h"
+
+
+/**
+ * @brief A handler for a backup sub-operation under /backups/$KEY/.
+ *
+ * Each entry maps an operation name ("block" or "object") and an
+ * HTTP method to a handler function.
+ */
+struct SH_BackupOperationHandler
+{
+
+ /**
+ * Operation name, e.g. "block" or "object".
+ */
+ const char *operation;
+
+ /**
+ * HTTP method this handler is for.
+ */
+ enum MHD_HTTP_Method method;
+
+ /**
+ * Function to call.
+ *
+ * @param request the MHD request to handle
+ * @param account_pub public key of the account
+ * @param args path segments after the operation name
+ * @param upload_size number of bytes being uploaded
+ * @param is_update whether the operation is an update
+ * @return MHD action
+ */
+ const struct MHD_Action *(*handler)(
+ struct MHD_Request *request,
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const char *const args[],
+ uint_fast64_t upload_size,
+ bool is_update);
+
+ /**
+ * Number of path segments this handler expects
+ * after the operation name.
+ */
+ unsigned int nargs;
+
+ /**
+ * Is @e nargs only an upper bound?
+ */
+ bool nargs_is_upper_bound;
+};
/**
@@ -38,6 +92,43 @@ static int SH_sync_connection_close;
unsigned long long int SH_upload_limit_mb;
/**
+ * How many uploads of the maximum permitted size the shared upload
+ * buffer pool must have room for.
+ *
+ * Block uploads are handed to the handler in one piece
+ * (#MHD_action_process_upload_full()), so MHD buffers the whole body
+ * out of a daemon-wide pool. The pool is a budget, not a
+ * pre-allocation -- MHD mallocs per request and gives the space back
+ * when the request ends -- so this bounds the worst-case resident size
+ * of concurrent uploads rather than costing anything up front.
+ */
+#define UPLOAD_POOL_CONCURRENCY 8
+
+
+bool
+SH_upload_too_large (uint_fast64_t upload_size,
+ size_t *buff_size)
+{
+ uint_fast64_t limit;
+
+ limit = ((uint_fast64_t) SH_upload_limit_mb) * 1024 * 1024;
+ if (MHD_SIZE_UNKNOWN == upload_size)
+ {
+ /* Chunked upload: the length is not known yet, so bound the buffer
+ by the limit and let MHD fail the request if it does not fit. */
+ *buff_size = (size_t) GNUNET_MIN (limit,
+ (uint_fast64_t) SIZE_MAX);
+ return false;
+ }
+ if ( (upload_size >= limit) ||
+ (upload_size > SIZE_MAX) )
+ return true;
+ *buff_size = (size_t) upload_size;
+ return false;
+}
+
+
+/**
* Annual fee for the backup account.
*/
struct TALER_Amount SH_annual_fee;
@@ -183,6 +274,176 @@ respond_agpl (struct MHD_Request *request,
/**
+ * Maximum length of the encoded account public key in the URL.
+ * An EdDSA public key encodes to 52 characters.
+ */
+#define ACCOUNT_KEY_MAX 64
+
+/**
+ * Maximum reasonable subpath length after /backups/$KEY/.
+ */
+#define SUBPATH_MAX 256
+
+/**
+ * Maximum number of path segments we split a subpath into,
+ * including the operation name.
+ */
+#define SUBPATH_MAX_SEGMENTS 3
+
+
+/**
+ * Handle a request on a subpath under /backups/$KEY/.
+ *
+ * Parses the subpath (e.g. "block/NONCE" or "object/UID"),
+ * splits it into segments, and dispatches to the appropriate
+ * handler using the data-driven #SH_BackupOperationHandler
+ * table.
+ *
+ * @param request the MHD request to handle
+ * @param account_pub public key of the account
+ * @param method HTTP method of the request
+ * @param subpath remainder of the URL after /backups/$KEY/
+ * @param upload_size number of bytes being uploaded
+ * @return MHD action
+ */
+static const struct MHD_Action *
+handle_backup_subpath (struct MHD_Request *request,
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ enum MHD_HTTP_Method method,
+ const char *subpath,
+ uint_fast64_t upload_size)
+{
+ static struct SH_BackupOperationHandler bhandlers[] = {
+ {
+ .operation = "blocks",
+ .method = MHD_HTTP_METHOD_POST,
+ .handler = &SH_backup_block_post,
+ .nargs = 1,
+ },
+ {
+ .operation = "blocks",
+ /* FIXME: replace with PATCH when mhd2 supports it */
+ .method = MHD_HTTP_METHOD_PUT,
+ .handler = &SH_backup_block_post,
+ .nargs = 1,
+ },
+ {
+ .operation = "blocks",
+ .method = MHD_HTTP_METHOD_DELETE,
+ .handler = &SH_backup_block_delete,
+ .nargs = 1,
+ },
+ {
+ .operation = "blocks",
+ .method = MHD_HTTP_METHOD_GET,
+ .handler = &SH_backup_blocks_get,
+ .nargs = 0,
+ },
+ {
+ .operation = "objects",
+ .method = MHD_HTTP_METHOD_GET,
+ .handler = &SH_backup_object_get,
+ .nargs = 1,
+ },
+ {
+ .operation = "objects",
+ .method = MHD_HTTP_METHOD_POST,
+ .handler = &SH_backup_object_post,
+ .nargs = 1,
+ },
+ { .operation = NULL }
+ };
+ size_t slen;
+ char d[SUBPATH_MAX];
+ char *sp;
+ /* NULL-terminated, hence one more entry than we ever fill */
+ const char *args[SUBPATH_MAX_SEGMENTS + 1] = { NULL };
+ unsigned int i;
+
+ GNUNET_assert (NULL != request);
+ GNUNET_assert (NULL != subpath);
+ slen = strlen (subpath);
+ if (slen >= SUBPATH_MAX)
+ {
+ GNUNET_break_op (0);
+ return TALER_MHD2_reply_with_error (
+ request,
+ MHD_HTTP_STATUS_URI_TOO_LONG,
+ TALER_EC_GENERIC_URI_TOO_LONG,
+ subpath);
+ }
+ memcpy (d,
+ subpath,
+ slen + 1);
+
+ i = 0;
+ args[i++] = strtok_r (d,
+ "/",
+ &sp);
+ while ( (NULL != args[i - 1]) &&
+ (i < SUBPATH_MAX_SEGMENTS) )
+ args[i++] = strtok_r (NULL,
+ "/",
+ &sp);
+
+ if (NULL == args[0])
+ {
+ GNUNET_break_op (0);
+ return TALER_MHD2_reply_with_error (
+ request,
+ MHD_HTTP_STATUS_NOT_FOUND,
+ TALER_EC_GENERIC_ENDPOINT_UNKNOWN,
+ subpath);
+ }
+
+ if (MHD_HTTP_METHOD_OPTIONS == method)
+ return TALER_MHD2_reply_cors_preflight (request);
+
+ for (unsigned int j = 0; NULL != bhandlers[j].operation; j++)
+ {
+ const struct SH_BackupOperationHandler *bh = &bhandlers[j];
+ unsigned int nargs;
+
+ if (0 != strcmp (args[0],
+ bh->operation))
+ continue;
+ if (bh->method != method)
+ continue;
+
+ /* Count path segments after the operation name */
+ nargs = 0;
+ while (NULL != args[nargs + 1])
+ nargs++;
+
+ if (bh->nargs_is_upper_bound
+ ? (nargs > bh->nargs)
+ : (nargs != bh->nargs))
+ {
+ GNUNET_break_op (0);
+ return TALER_MHD2_reply_with_error (
+ request,
+ MHD_HTTP_STATUS_NOT_FOUND,
+ TALER_EC_GENERIC_ENDPOINT_UNKNOWN,
+ subpath);
+ }
+ return bh->handler (request,
+ account_pub,
+ &args[1],
+ upload_size,
+ /* FIXME: replace with PATCH when mhd2 supports it */
+ MHD_HTTP_METHOD_PUT == bh->method);
+ }
+
+ GNUNET_break_op (0);
+ return TALER_MHD2_reply_with_error (
+ request,
+ MHD_HTTP_STATUS_NOT_FOUND,
+ TALER_EC_GENERIC_ENDPOINT_UNKNOWN,
+ args[0]);
+}
+
+
+/**
* A client has requested the given url using the given method
* (#MHD_HTTP_METHOD_GET, #MHD_HTTP_METHOD_PUT,
* #MHD_HTTP_METHOD_DELETE, #MHD_HTTP_METHOD_POST, etc). The callback
@@ -191,14 +452,14 @@ respond_agpl (struct MHD_Request *request,
*
* @param cls argument given together with the function
* pointer when the handler was registered with MHD
- * @param request request the request to handle
+ * @param request the request to handle
* @param path the requested uri (without arguments after "?")
* @param method the HTTP method used (#MHD_HTTP_METHOD_GET,
* #MHD_HTTP_METHOD_PUT, etc.)
* @param upload_size the size of the message upload content payload,
- * #MHD_SIZE_UNKNOWN for chunked uploads (if the
- * final chunk has not been processed yet)
- * @return next action
+ * #MHD_SIZE_UNKNOWN for chunked uploads (if the final chunk
+ * has not been processed yet)
+ * @return next action
*/
static const struct MHD_Action *
url_handler (void *cls,
@@ -231,46 +492,82 @@ url_handler (void *cls,
struct SYNC_AccountPublicKeyP account_pub;
(void) cls;
-#if BUG
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Handling %s request for URL '%s'\n",
- MHD_http_method_to_string (method)->cstr,
- path->cstr);
-#endif
+ /* Backup API */
if (0 == strncmp (path->cstr,
"/backups/",
strlen ("/backups/")))
{
- const char *ac = &path->cstr[strlen ("/backups/")];
+ const char *ac = path->cstr + strlen ("/backups/");
+ const char *next_slash;
+ size_t ac_len;
+
+ /* Find end of account key (next '/' or end of string) */
+ next_slash = strchr (ac,
+ '/');
+ if (NULL == next_slash)
+ ac_len = strlen (ac);
+ else
+ ac_len = next_slash - ac;
- if (GNUNET_OK !=
- GNUNET_CRYPTO_eddsa_public_key_from_string (ac,
- strlen (ac),
- &account_pub.eddsa_pub))
{
- GNUNET_break_op (0);
- return TALER_MHD2_reply_with_error (request,
- MHD_HTTP_STATUS_BAD_REQUEST,
- TALER_EC_GENERIC_PARAMETER_MALFORMED,
- ac);
- }
- if (MHD_HTTP_METHOD_OPTIONS == method)
- {
- return TALER_MHD2_reply_cors_preflight (request);
- }
- if (MHD_HTTP_METHOD_GET == method)
- {
- return SH_backup_get (request,
- &account_pub);
+ /* Bound before we put it on the stack; the segment is
+ client-controlled */
+ char ac_str[ACCOUNT_KEY_MAX + 1];
+
+ if (ac_len > ACCOUNT_KEY_MAX)
+ {
+ GNUNET_break_op (0);
+ return TALER_MHD2_reply_with_error (
+ request,
+ MHD_HTTP_STATUS_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "account public key");
+ }
+ memcpy (ac_str,
+ ac,
+ ac_len);
+ ac_str[ac_len] = '\0';
+ if (GNUNET_OK !=
+ GNUNET_CRYPTO_eddsa_public_key_from_string (
+ ac_str,
+ ac_len,
+ &account_pub.eddsa_pub))
+ {
+ GNUNET_break_op (0);
+ return TALER_MHD2_reply_with_error (
+ request,
+ MHD_HTTP_STATUS_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ ac_str);
+ }
}
- if (MHD_HTTP_METHOD_POST == method)
+
+ /* Without a subpath this is the account itself; only GET makes
+ sense on it, everything else needs an operation */
+ if ( (NULL == next_slash) ||
+ ('\0' == next_slash[1]) )
{
- return SH_backup_post (request,
- &account_pub,
- upload_size);
+ if (MHD_HTTP_METHOD_OPTIONS == method)
+ return TALER_MHD2_reply_cors_preflight (request);
+ if (MHD_HTTP_METHOD_GET == method)
+ return SH_backup_account_get (request,
+ &account_pub);
+ GNUNET_break_op (0);
+ return TALER_MHD2_reply_with_error (
+ request,
+ MHD_HTTP_STATUS_NOT_FOUND,
+ TALER_EC_GENERIC_ENDPOINT_UNKNOWN,
+ path->cstr);
}
- // FIXME: return bad method!
+
+ /* Dispatch to sub-handler */
+ return handle_backup_subpath (request,
+ &account_pub,
+ method,
+ next_slash + 1,
+ upload_size);
}
+
for (unsigned int i = 0; NULL != handlers[i].url; i++)
{
struct SH_RequestHandler *rh = &handlers[i];
@@ -304,7 +601,6 @@ do_shutdown (void *cls)
{
(void) cls;
TALER_MHD2_daemons_halt ();
- SH_resume_all_bc ();
if (NULL != SH_ctx)
{
GNUNET_CURL_fini (SH_ctx);
@@ -326,9 +622,9 @@ do_shutdown (void *cls)
* we made progress serving a request. This function re-schedules
* the task processing MHD's activities to run immediately.
*/
-// FIXME: replace with direct call...
+/* FIXME: replace with direct call... */
void
-SH_trigger_daemon ()
+SH_trigger_daemon (void)
{
TALER_MHD2_daemons_trigger ();
}
@@ -338,7 +634,7 @@ SH_trigger_daemon ()
* Kick GNUnet Curl scheduler to begin curl interactions.
*/
void
-SH_trigger_curl ()
+SH_trigger_curl (void)
{
GNUNET_CURL_gnunet_scheduler_reschedule (&rc);
}
@@ -369,10 +665,23 @@ start_daemon (void *cls,
GNUNET_SCHEDULER_shutdown ();
return;
}
+ /* Without this, the pool defaults to a fraction of the memory the
+ daemon would use for connections -- about 1 MB, far below
+ UPLOAD_LIMIT_MB -- and MHD answers anything bigger with its own
+ 413 before the handler ever runs, whatever the configured limit
+ says. See the FIXME in sync-httpd2_post-backups-KEY-blocks-NONCE.c
+ for the incremental-upload alternative that would drop the pool
+ dependency entirely. */
GNUNET_assert (MHD_SC_OK ==
MHD_DAEMON_SET_OPTIONS (
mhd,
MHD_D_OPTION_DEFAULT_TIMEOUT_MILSEC (10000),
+ MHD_D_OPTION_LARGE_POOL_SIZE (
+ (size_t) GNUNET_MIN (
+ ((uint_fast64_t) SH_upload_limit_mb)
+ * 1024 * 1024
+ * UPLOAD_POOL_CONCURRENCY,
+ (uint_fast64_t) SIZE_MAX)),
MHD_D_OPTION_LISTEN_SOCKET (lsock)));
have_daemons = true;
TALER_MHD2_daemon_start (mhd);
@@ -453,6 +762,43 @@ run (void *cls,
GNUNET_SCHEDULER_shutdown ();
return;
}
+ /* The merchant backend API expects the base URL to end with '/'
+ (see TALER_url_join), so ensure it does. */
+ if ('/' != SH_backend_url[strlen (SH_backend_url) - 1])
+ {
+ char *tmp;
+
+ GNUNET_asprintf (&tmp,
+ "%s/",
+ SH_backend_url);
+ GNUNET_free (SH_backend_url);
+ SH_backend_url = tmp;
+ }
+ /* The payment flow turns this into a "taler://" URI, so it needs a
+ scheme and a host */
+ {
+ const char *host = NULL;
+
+ if (0 == strncasecmp ("https://",
+ SH_backend_url,
+ strlen ("https://")))
+ host = &SH_backend_url[strlen ("https://")];
+ else if (0 == strncasecmp ("http://",
+ SH_backend_url,
+ strlen ("http://")))
+ host = &SH_backend_url[strlen ("http://")];
+ if ( (NULL == host) ||
+ ('\0' == *host) )
+ {
+ GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
+ "sync",
+ "PAYMENT_BACKEND_URL",
+ "must be an http:// or https:// URL"
+ " with a host name");
+ GNUNET_SCHEDULER_shutdown ();
+ return;
+ }
+ }
if (GNUNET_OK !=
GNUNET_CONFIGURATION_get_value_string (config,
"sync",
@@ -461,7 +807,7 @@ run (void *cls,
{
GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
"sync",
- "BASE_URL");
+ "FULFILLMENT_URL");
GNUNET_SCHEDULER_shutdown ();
return;
}
@@ -469,6 +815,14 @@ run (void *cls,
/* setup HTTP client event loop */
SH_ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule,
&rc);
+ if (NULL == SH_ctx)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Failed to initialize HTTP client, exiting.\n");
+ global_ret = EXIT_NO_RESTART;
+ GNUNET_SCHEDULER_shutdown ();
+ return;
+ }
rc = GNUNET_CURL_gnunet_rc_create (SH_ctx);
if (NULL != userpass)
GNUNET_CURL_set_userpass (SH_ctx,
@@ -503,8 +857,7 @@ run (void *cls,
}
if (GNUNET_OK !=
- SYNCDB_init (config,
- false))
+ SYNCDB_init (config))
{
global_ret = EXIT_NOTCONFIGURED;
GNUNET_SCHEDULER_shutdown ();
@@ -566,6 +919,13 @@ main (int argc,
"USERNAME:PASSWORD",
"use the given USERNAME and PASSWORD for client authentication",
&userpass),
+ /* Note: -c, -h, -L, -l and -v are reserved by GNUNET_PROGRAM_run().
+ We use -E for the client certificate, as curl does. */
+ GNUNET_GETOPT_option_string ('E',
+ "cert",
+ "CERTFILE",
+ "file with the TLS client certificate for TLS client authentication",
+ &certfile),
GNUNET_GETOPT_option_flag ('C',
"connection-close",
"force HTTP connections to be closed after each request",
diff --git a/src/sync/sync-httpd2.h b/src/sync/sync-httpd2.h
@@ -24,7 +24,32 @@
#include "platform.h"
#include <microhttpd2.h>
#include <taler/taler_mhd2_lib.h>
-#include "sync/sync_database_lib.h"
+#include <sync/sync_database_lib.h>
+
+/* FIXME: implement in exchange */
+#ifndef TALER_EC_SYNC_CONFLICTING_WRITE
+#define TALER_EC_SYNC_CONFLICTING_WRITE 6115
+#endif
+#ifndef TALER_EC_SYNC_INCONSISTENT_BACKUP
+#define TALER_EC_SYNC_INCONSISTENT_BACKUP 6116
+#endif
+
+
+/**
+ * Like #TALER_MHD2_make_error() but with an explicit HTTP status,
+ * for error codes that do not have a registered HTTP mapping yet.
+ *
+ * @param sc HTTP status code to use
+ * @param ec Taler error code to report
+ * @param detail human-readable detail, or NULL
+ */
+#define SYNC_MHD_MAKE_ERROR(sc,ec,detail) \
+ TALER_MHD2_MAKE_JSON_PACK ((sc), \
+ TALER_MHD2_PACK_EC (ec), \
+ GNUNET_JSON_pack_allow_null ( \
+ GNUNET_JSON_pack_string ("detail", \
+ (detail))))
+
/**
* @brief Struct describing an URL and the handler for it.
@@ -51,7 +76,7 @@ struct SH_RequestHandler
*/
const struct MHD_Action *
(*handler)(struct MHD_Request *request,
- size_t upload_size);
+ uint_fast64_t upload_size);
};
@@ -102,6 +127,25 @@ struct TM_HandlerContext
extern unsigned long long SH_upload_limit_mb;
/**
+ * Check a request body against the configured upload limit and
+ * determine the upload buffer size to hand to MHD.
+ *
+ * MHD reports #MHD_SIZE_UNKNOWN while a chunked upload is still being
+ * received, so the length cannot be checked up-front in that case; the
+ * buffer is then bounded by the configured limit instead, which caps
+ * how much a client can send before MHD gives up.
+ *
+ * @param upload_size size reported by MHD, #MHD_SIZE_UNKNOWN if unknown
+ * @param[out] buff_size buffer size to pass to MHD, set unless the
+ * upload is rejected
+ * @return true if the upload is already known to exceed the limit
+ */
+bool
+SH_upload_too_large (uint_fast64_t upload_size,
+ size_t *buff_size);
+
+
+/**
* Annual fee for the backup account.
*/
extern struct TALER_Amount SH_annual_fee;
diff --git a/src/sync/sync-httpd2_backup.c b/src/sync/sync-httpd2_backup.c
@@ -1,237 +0,0 @@
-/*
- This file is part of TALER
- Copyright (C) 2019--2025 Taler Systems SA
-
- TALER 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.
-
- 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 Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-*/
-/**
- * @file sync-httpd2_backup.c
- * @brief functions to handle incoming requests for backups
- * @author Christian Grothoff
- */
-#include "platform.h"
-#include "sync-httpd2.h"
-#include <gnunet/gnunet_util_lib.h>
-#include "sync-httpd2_backup.h"
-
-
-/**
- * Handle request on @a request for retrieval of the latest
- * backup of @a account.
- *
- * @param request the MHD request to handle
- * @param account public key of the account the request is for
- * @return MHD action
- */
-const struct MHD_Action *
-SH_backup_get (struct MHD_Request *request,
- const struct SYNC_AccountPublicKeyP *account)
-{
- struct GNUNET_HashCode backup_hash;
- enum SYNC_DB_QueryStatus qs;
-
- qs = SYNCDB_lookup_account_TR (
- account,
- &backup_hash);
- switch (qs)
- {
- case SYNC_DB_OLD_BACKUP_MISSING:
- GNUNET_break (0);
- return TALER_MHD2_reply_with_error (request,
- MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
- NULL);
- case SYNC_DB_OLD_BACKUP_MISMATCH:
- GNUNET_break (0);
- return TALER_MHD2_reply_with_error (request,
- MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
- NULL);
- case SYNC_DB_PAYMENT_REQUIRED:
- return TALER_MHD2_reply_with_error (request,
- MHD_HTTP_STATUS_NOT_FOUND,
- TALER_EC_SYNC_ACCOUNT_UNKNOWN,
- NULL);
- case SYNC_DB_HARD_ERROR:
- return TALER_MHD2_reply_with_error (request,
- MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_FETCH_FAILED,
- NULL);
- case SYNC_DB_SOFT_ERROR:
- GNUNET_break (0);
- return TALER_MHD2_reply_with_error (request,
- MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_SOFT_FAILURE,
- NULL);
- case SYNC_DB_NO_RESULTS:
- {
- struct MHD_Response *resp;
-
- resp = MHD_response_from_empty (MHD_HTTP_STATUS_NO_CONTENT);
- TALER_MHD2_add_global_headers (resp,
- false);
- return MHD_action_from_response (request,
- resp);
- }
- case SYNC_DB_ONE_RESULT:
- {
- struct MHD_StringNullable inm;
-
- if ( (MHD_YES ==
- MHD_request_get_value (request,
- MHD_VK_HEADER,
- MHD_HTTP_HEADER_IF_NONE_MATCH,
- &inm)) &&
- (2 < inm.len) &&
- ('"' == inm.cstr[0]) &&
- ('=' == inm.cstr[inm.len - 1]) )
- {
- struct GNUNET_HashCode inm_h;
-
- if (GNUNET_OK !=
- GNUNET_STRINGS_string_to_data (inm.cstr + 1,
- inm.len - 2,
- &inm_h,
- sizeof (inm_h)))
- {
- GNUNET_break_op (0);
- return TALER_MHD2_reply_with_error (request,
- MHD_HTTP_STATUS_BAD_REQUEST,
- TALER_EC_SYNC_BAD_IF_NONE_MATCH,
- "Etag does not include a base32-encoded SHA-512 hash");
- }
- if (0 == GNUNET_memcmp (&inm_h,
- &backup_hash))
- {
- struct MHD_Response *resp;
-
- resp = MHD_response_from_empty (MHD_HTTP_STATUS_NOT_MODIFIED);
- TALER_MHD2_add_global_headers (resp,
- false);
- return MHD_action_from_response (request,
- resp);
- }
- }
- }
- /* We have a result, should fetch and return it! */
- break;
- }
- return MHD_action_from_response (request,
- SH_make_backup (account,
- MHD_HTTP_STATUS_OK));
-}
-
-
-/**
- * Return the current backup of @a account on @a request
- * using @a default_http_status on success.
- *
- * @param account account to query
- * @param default_http_status HTTP status to queue response
- * with on success (#MHD_HTTP_STATUS_OK or #MHD_HTTP_STATUS_CONFLICT)
- * @return MHD response
- */
-struct MHD_Response *
-SH_make_backup (const struct SYNC_AccountPublicKeyP *account,
- enum MHD_HTTP_StatusCode default_http_status)
-{
- enum SYNC_DB_QueryStatus qs;
- struct MHD_Response *resp;
- struct SYNC_AccountSignatureP account_sig;
- struct GNUNET_HashCode backup_hash;
- struct GNUNET_HashCode prev_hash;
- size_t backup_size;
- void *backup;
-
- qs = SYNCDB_lookup_backup_TR (
- account,
- &account_sig,
- &prev_hash,
- &backup_hash,
- &backup_size,
- &backup);
- switch (qs)
- {
- case SYNC_DB_OLD_BACKUP_MISSING:
- GNUNET_break (0);
- return TALER_MHD2_make_error (TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
- "unexpected return status (backup missing)");
- case SYNC_DB_OLD_BACKUP_MISMATCH:
- GNUNET_break (0);
- return TALER_MHD2_make_error (TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
- "unexpected return status (backup mismatch)");
- case SYNC_DB_PAYMENT_REQUIRED:
- GNUNET_break (0);
- return TALER_MHD2_make_error (TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
- "unexpected return status (payment required)")
- ;
- case SYNC_DB_HARD_ERROR:
- GNUNET_break (0);
- return TALER_MHD2_make_error (TALER_EC_GENERIC_DB_FETCH_FAILED,
- NULL);
- case SYNC_DB_SOFT_ERROR:
- GNUNET_break (0);
- return TALER_MHD2_make_error (TALER_EC_GENERIC_DB_SOFT_FAILURE,
- NULL);
- case SYNC_DB_NO_RESULTS:
- GNUNET_break (0);
- /* Note: can theoretically happen due to non-transactional nature if
- the backup expired / was gc'ed JUST between the two SQL calls.
- But too rare to handle properly, as doing a transaction would be
- expensive. Just admit to failure ;-) */
- return TALER_MHD2_make_error (TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
- NULL);
- case SYNC_DB_ONE_RESULT:
- /* interesting case below */
- break;
- }
- resp = MHD_response_from_buffer (default_http_status,
- backup_size,
- backup,
- &free,
- backup);
- TALER_MHD2_add_global_headers (resp,
- false);
- {
- char *sig_s;
- char *prev_s;
- char *etag;
- char *etagq;
-
- sig_s = GNUNET_STRINGS_data_to_string_alloc (&account_sig,
- sizeof (account_sig));
- prev_s = GNUNET_STRINGS_data_to_string_alloc (&prev_hash,
- sizeof (prev_hash));
- etag = GNUNET_STRINGS_data_to_string_alloc (&backup_hash,
- sizeof (backup_hash));
- GNUNET_break (MHD_SC_OK ==
- MHD_response_add_header (resp,
- "Sync-Signature",
- sig_s));
- GNUNET_break (MHD_SC_OK ==
- MHD_response_add_header (resp,
- "Sync-Previous",
- prev_s));
- GNUNET_asprintf (&etagq,
- "\"%s\"",
- etag);
- GNUNET_break (MHD_SC_OK ==
- MHD_response_add_header (resp,
- MHD_HTTP_HEADER_ETAG,
- etagq));
- GNUNET_free (etagq);
- GNUNET_free (etag);
- GNUNET_free (prev_s);
- GNUNET_free (sig_s);
- }
- return resp;
-}
diff --git a/src/sync/sync-httpd2_backup.h b/src/sync/sync-httpd2_backup.h
@@ -1,74 +0,0 @@
-/*
- This file is part of TALER
- Copyright (C) 2014--2025 Taler Systems SA
-
- TALER 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.
-
- 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 Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-*/
-/**
- * @file sync-httpd2_backup.h
- * @brief functions to handle incoming requests on /backup/
- * @author Christian Grothoff
- */
-#ifndef SYNC_HTTPD2_BACKUP_H
-#define SYNC_HTTPD2_BACKUP_H
-
-#include <microhttpd2.h>
-
-/**
- * Service is shutting down, resume all MHD requests NOW.
- */
-void
-SH_resume_all_bc (void);
-
-
-/**
- * Return the current backup of @a account on @a request
- * using @a default_http_status on success.
- *
- * @param account account to query
- * @param default_http_status HTTP status to queue response
- * with on success (#MHD_HTTP_STATUS_OK or #MHD_HTTP_STATUS_CONFLICT)
- * @return MHD response
- */
-struct MHD_Response *
-SH_make_backup (const struct SYNC_AccountPublicKeyP *account,
- enum MHD_HTTP_StatusCode default_http_status);
-
-
-/**
- * Handle request on @a request for retrieval of the latest
- * backup of @a account.
- *
- * @param request the MHD request to handle
- * @param account public key of the account the request is for
- * @return MHD action
- */
-const struct MHD_Action *
-SH_backup_get (struct MHD_Request *request,
- const struct SYNC_AccountPublicKeyP *account);
-
-
-/**
- * Handle POST /backup requests.
- *
- * @param request the MHD request to handle
- * @param account public key of the account the request is for
- * @param upload_size number of bytes being uploaded
- * @return MHD result code
- */
-const struct MHD_Action *
-SH_backup_post (struct MHD_Request *request,
- const struct SYNC_AccountPublicKeyP *account,
- uint_fast64_t upload_size);
-
-
-#endif
diff --git a/src/sync/sync-httpd2_backup_post.c b/src/sync/sync-httpd2_backup_post.c
@@ -1,1013 +0,0 @@
-/*
- This file is part of TALER
- Copyright (C) 2019--2025 Taler Systems SA
-
- TALER 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.
-
- 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 Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-*/
-/**
- * @file sync-httpd_backup_post.c
- * @brief functions to handle incoming requests for backups
- * @author Christian Grothoff
- */
-#include "platform.h"
-struct BackupContext;
-#define TALER_MERCHANT_POST_PRIVATE_ORDERS_RESULT_CLOSURE struct BackupContext
-#define TALER_MERCHANT_GET_PRIVATE_ORDER_RESULT_CLOSURE struct BackupContext
-#include "sync-httpd2.h"
-#include <gnunet/gnunet_util_lib.h>
-#include "sync-httpd2_backup.h"
-#include <microhttpd2.h>
-#include <taler/taler_json_lib.h>
-#include <taler/taler_mhd2_lib.h>
-#include <taler/taler_merchant_service.h>
-#include <taler/taler_signatures.h>
-
-
-/**
- * How long do we hold an HTTP client connection if
- * we are awaiting payment before giving up?
- */
-#define CHECK_PAYMENT_GENERIC_TIMEOUT GNUNET_TIME_relative_multiply ( \
- GNUNET_TIME_UNIT_MINUTES, 30)
-
-
-/**
- * Context for an upload operation.
- */
-struct BackupContext
-{
-
- /**
- * Context for cleanup logic.
- */
- struct TM_HandlerContext hc;
-
- /**
- * Signature of the account holder.
- */
- struct SYNC_AccountSignatureP account_sig;
-
- /**
- * Public key of the account holder.
- */
- struct SYNC_AccountPublicKeyP account;
-
- /**
- * Hash of the previous upload, or zeros if first upload.
- */
- struct GNUNET_HashCode old_backup_hash;
-
- /**
- * Hash of the upload we are receiving right now (as promised
- * by the client, to be verified!).
- */
- struct GNUNET_HashCode new_backup_hash;
-
- /**
- * Claim token, all zeros if not known. Only set if @e existing_order_id is non-NULL.
- */
- struct TALER_ClaimTokenP token;
-
- /**
- * Kept in DLL for shutdown handling while suspended.
- */
- struct BackupContext *next;
-
- /**
- * Kept in DLL for shutdown handling while suspended.
- */
- struct BackupContext *prev;
-
- /**
- * Used while suspended for resumption.
- */
- struct MHD_Request *request;
-
- /**
- * To be returned upon resuming.
- */
- struct MHD_Response *resp;
-
- /**
- * Used while we are awaiting proposal creation.
- */
- struct TALER_MERCHANT_PostPrivateOrdersHandle *po;
-
- /**
- * Used while we are waiting payment.
- */
- struct TALER_MERCHANT_GetPrivateOrderHandle *omgh;
-
- /**
- * Order under which the client promised payment, or NULL.
- */
- const char *order_id;
-
- /**
- * Order ID for the client that we found in our database.
- */
- char *existing_order_id;
-
- /**
- * Timestamp of the order in @e existing_order_id. Used to
- * select the most recent unpaid offer.
- */
- struct GNUNET_TIME_Timestamp existing_order_timestamp;
-
- /**
- * Do not look for an existing order, force a fresh order to be created.
- */
- bool force_fresh_order;
-};
-
-
-/**
- * Kept in DLL for shutdown handling while suspended.
- */
-static struct BackupContext *bc_head;
-
-/**
- * Kept in DLL for shutdown handling while suspended.
- */
-static struct BackupContext *bc_tail;
-
-
-/**
- * Service is shutting down, resume all MHD requests NOW.
- */
-void
-SH_resume_all_bc ()
-{
- struct BackupContext *bc;
-
- while (NULL != (bc = bc_head))
- {
- GNUNET_CONTAINER_DLL_remove (bc_head,
- bc_tail,
- bc);
- MHD_request_resume (bc->request);
- if (NULL != bc->po)
- {
- TALER_MERCHANT_post_private_orders_cancel (bc->po);
- bc->po = NULL;
- }
- if (NULL != bc->omgh)
- {
- TALER_MERCHANT_get_private_order_cancel (bc->omgh);
- bc->omgh = NULL;
- }
- }
-}
-
-
-/**
- * Function called to clean up a backup context.
- *
- * @param cls the `struct BackupContext`
- * @param data the details about the event
- * @param request_app_context the application request context
- */
-static void
-cleanup_ctx (void *cls,
- const struct MHD_RequestEndedData *data,
- void *request_app_context)
-{
- struct BackupContext *bc = cls;
-
- GNUNET_assert (cls == request_app_context);
- if (NULL != bc->po)
- {
- TALER_MERCHANT_post_private_orders_cancel (bc->po);
- bc->po = NULL;
- }
- if (NULL != bc->omgh)
- {
- TALER_MERCHANT_get_private_order_cancel (bc->omgh);
- bc->omgh = NULL;
- }
- GNUNET_free (bc->existing_order_id);
- GNUNET_free (bc);
-}
-
-
-/**
- * Transmit a payment request for @a order_id on @a request
- *
- * @param order_id our backend's order ID
- * @param token the claim token generated by the merchant (NULL if
- * it wasn't generated).
- * @return MHD response to use
- */
-static struct MHD_Response *
-make_payment_request (const char *order_id,
- const struct TALER_ClaimTokenP *token)
-{
- struct MHD_Response *resp;
-
- /* request payment via Taler */
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Creating payment request for order `%s'\n",
- order_id);
- resp = MHD_response_from_empty (MHD_HTTP_STATUS_PAYMENT_REQUIRED);
- TALER_MHD2_add_global_headers (resp,
- false);
- {
- char *hdr;
- const char *pfx;
- char *hn;
- struct GNUNET_Buffer hdr_buf = { 0 };
-
- if (0 == strncasecmp ("https://",
- SH_backend_url,
- strlen ("https://")))
- {
- pfx = "taler://";
- hn = &SH_backend_url[strlen ("https://")];
- }
- else if (0 == strncasecmp ("http://",
- SH_backend_url,
- strlen ("http://")))
- {
- pfx = "taler+http://";
- hn = &SH_backend_url[strlen ("http://")];
- }
- else
- {
- GNUNET_break (0);
- MHD_response_destroy (resp);
- return NULL;
- }
- if (0 == strlen (hn))
- {
- GNUNET_break (0);
- MHD_response_destroy (resp);
- return NULL;
- }
-
- GNUNET_buffer_write_str (&hdr_buf, pfx);
- GNUNET_buffer_write_str (&hdr_buf, "pay/");
- GNUNET_buffer_write_str (&hdr_buf, hn);
- GNUNET_buffer_write_path (&hdr_buf, order_id);
- /* No session ID */
- GNUNET_buffer_write_path (&hdr_buf, "");
- if (NULL != token)
- {
- GNUNET_buffer_write_str (&hdr_buf, "?c=");
- GNUNET_buffer_write_data_encoded (&hdr_buf,
- token,
- sizeof (*token));
- }
- hdr = GNUNET_buffer_reap_str (&hdr_buf);
- GNUNET_break (MHD_SC_OK ==
- MHD_response_add_header (resp,
- "Taler",
- hdr));
- GNUNET_free (hdr);
- }
- return resp;
-}
-
-
-/**
- * Callbacks of this type are used to serve the result of submitting a
- * /contract request to a merchant.
- *
- * @param cls our `struct BackupContext`
- * @param por response details
- */
-static void
-proposal_cb (
- struct BackupContext *bc,
- const struct TALER_MERCHANT_PostPrivateOrdersResponse *por)
-{
- enum SYNC_DB_QueryStatus qs;
-
- bc->po = NULL;
- GNUNET_CONTAINER_DLL_remove (bc_head,
- bc_tail,
- bc);
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Resuming request with order `%s'\n",
- bc->order_id);
- MHD_request_resume (bc->request);
- SH_trigger_daemon ();
- if (MHD_HTTP_STATUS_OK != por->hr.http_status)
- {
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Backend returned status %u/%u\n",
- por->hr.http_status,
- (unsigned int) por->hr.ec);
- GNUNET_break_op (0);
- bc->resp = TALER_MHD2_MAKE_JSON_PACK (
- MHD_HTTP_STATUS_BAD_GATEWAY,
- TALER_JSON_pack_ec (TALER_EC_SYNC_PAYMENT_CREATE_BACKEND_ERROR),
- GNUNET_JSON_pack_uint64 ("backend-ec",
- por->hr.ec),
- GNUNET_JSON_pack_uint64 ("backend-http-status",
- por->hr.http_status),
- GNUNET_JSON_pack_allow_null (
- GNUNET_JSON_pack_object_incref ("backend-reply",
- (json_t *) por->hr.reply)));
- return;
- }
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Storing payment request for order `%s'\n",
- por->details.ok.order_id);
- qs = SYNCDB_store_payment_TR (
- &bc->account,
- por->details.ok.order_id,
- por->details.ok.token,
- &SH_annual_fee);
- if (0 >= qs)
- {
- GNUNET_break (0);
- bc->resp = TALER_MHD2_make_error (TALER_EC_GENERIC_DB_STORE_FAILED,
- "Failed to persist payment request in sync database");
- GNUNET_assert (NULL != bc->resp);
- return;
- }
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Obtained fresh order `%s'\n",
- por->details.ok.order_id);
- bc->resp = make_payment_request (por->details.ok.order_id,
- por->details.ok.token);
- GNUNET_assert (NULL != bc->resp);
-}
-
-
-/**
- * Function called on all pending payments for the right
- * account.
- *
- * @param cls closure, our `struct BackupContext`
- * @param timestamp for how long have we been waiting
- * @param order_id order id in the backend
- * @param token claim token to use (or NULL for none)
- * @param amount how much is the order for
- */
-static void
-ongoing_payment_cb (void *cls,
- struct GNUNET_TIME_Timestamp timestamp,
- const char *order_id,
- const struct TALER_ClaimTokenP *token,
- const struct TALER_Amount *amount)
-{
- struct BackupContext *bc = cls;
-
- (void) amount;
- if (0 != TALER_amount_cmp (amount,
- &SH_annual_fee))
- return; /* can't reuse, fees changed */
- if ( (NULL == bc->existing_order_id) ||
- (GNUNET_TIME_timestamp_cmp (bc->existing_order_timestamp,
- <,
- timestamp)) )
- {
- GNUNET_free (bc->existing_order_id);
- bc->existing_order_id = GNUNET_strdup (order_id);
- bc->existing_order_timestamp = timestamp;
- if (NULL != token)
- bc->token = *token;
- }
-}
-
-
-/**
- * Callback to process a GET /check-payment request
- *
- * @param cls our `struct BackupContext`
- * @param osr order status
- */
-static void
-check_payment_cb (
- struct BackupContext *bc,
- const struct TALER_MERCHANT_GetPrivateOrderResponse *osr)
-{
- const struct TALER_MERCHANT_HttpResponse *hr = &osr->hr;
-
- /* refunds are not supported, verify */
- bc->omgh = NULL;
- GNUNET_CONTAINER_DLL_remove (bc_head,
- bc_tail,
- bc);
- MHD_request_resume (bc->request);
- SH_trigger_daemon ();
- switch (hr->http_status)
- {
- case 0:
- /* Likely timeout, complain! */
- bc->resp = TALER_MHD2_make_error (
- TALER_EC_SYNC_GENERIC_BACKEND_TIMEOUT,
- NULL);
- return;
- case MHD_HTTP_STATUS_OK:
- break; /* handled below */
- default:
- /* Unexpected backend response */
- bc->resp = TALER_MHD2_MAKE_JSON_PACK (
- MHD_HTTP_STATUS_BAD_GATEWAY,
- GNUNET_JSON_pack_uint64 ("code",
- TALER_EC_SYNC_GENERIC_BACKEND_ERROR),
- GNUNET_JSON_pack_string ("hint",
- TALER_ErrorCode_get_hint (
- TALER_EC_SYNC_GENERIC_BACKEND_ERROR)),
- GNUNET_JSON_pack_uint64 ("backend-ec",
- (json_int_t) hr->ec),
- GNUNET_JSON_pack_uint64 ("backend-http-status",
- (json_int_t) hr->http_status),
- GNUNET_JSON_pack_allow_null (
- GNUNET_JSON_pack_object_incref ("backend-reply",
- (json_t *) hr->reply)));
- return;
- }
-
- GNUNET_assert (MHD_HTTP_STATUS_OK == hr->http_status);
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Payment status checked: %d\n",
- osr->details.ok.status);
- switch (osr->details.ok.status)
- {
- case TALER_MERCHANT_OSC_PAID:
- {
- enum SYNC_DB_QueryStatus qs;
-
- qs = SYNCDB_increment_lifetime_TR (
- &bc->account,
- bc->order_id,
- GNUNET_TIME_UNIT_YEARS); /* always annual */
- if (0 <= qs)
- return; /* continue as planned */
- GNUNET_break (0);
- bc->resp = TALER_MHD2_make_error (TALER_EC_GENERIC_DB_STORE_FAILED,
- "increment lifetime");
- GNUNET_assert (NULL != bc->resp);
- return; /* continue as planned */
- }
- case TALER_MERCHANT_OSC_UNPAID:
- case TALER_MERCHANT_OSC_CLAIMED:
- break;
- }
- if (NULL != bc->existing_order_id)
- {
- /* repeat payment request */
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Repeating payment request\n");
- bc->resp = make_payment_request (bc->existing_order_id,
- (GNUNET_YES == GNUNET_is_zero (&bc->token))
- ? NULL
- : &bc->token);
- GNUNET_assert (NULL != bc->resp);
- return;
- }
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Timeout waiting for payment\n");
- bc->resp = TALER_MHD2_make_error (TALER_EC_SYNC_PAYMENT_GENERIC_TIMEOUT,
- "Timeout awaiting promised payment");
- GNUNET_assert (NULL != bc->resp);
-}
-
-
-/**
- * Helper function used to ask our backend to await
- * a payment for the user's account.
- *
- * @param bc context to begin payment for.
- * @param timeout when to give up trying
- * @param order_id which order to check for the payment
- */
-static void
-await_payment (struct BackupContext *bc,
- struct GNUNET_TIME_Relative timeout,
- const char *order_id)
-{
- GNUNET_CONTAINER_DLL_insert (bc_head,
- bc_tail,
- bc);
- bc->order_id = order_id;
- bc->omgh = TALER_MERCHANT_get_private_order_create (
- SH_ctx,
- SH_backend_url,
- order_id);
- GNUNET_assert (GNUNET_OK ==
- TALER_MERCHANT_get_private_order_set_options (
- bc->omgh,
- TALER_MERCHANT_get_private_order_option_timeout (timeout)));
- GNUNET_assert (TALER_EC_NONE ==
- TALER_MERCHANT_get_private_order_start (
- bc->omgh,
- &check_payment_cb,
- bc));
- SH_trigger_curl ();
-}
-
-
-/**
- * Helper function used to ask our backend to begin
- * processing a payment for the user's account.
- * May perform asynchronous operations by suspending the request
- * if required.
- *
- * @param bc context to begin payment for.
- * @param pay_req #GNUNET_YES if payment was explicitly requested,
- * #GNUNET_NO if payment is needed
- * @param[out] suspend set to true if the request should be suspended
- * @return MHD response
- */
-static struct MHD_Response *
-begin_payment (struct BackupContext *bc,
- int pay_req,
- bool *suspend)
-{
- *suspend = false;
- if (! bc->force_fresh_order)
- {
- enum GNUNET_DB_QueryStatus qs;
-
- qs = SYNCDB_lookup_pending_payments_by_account_TR (
- &bc->account,
- &ongoing_payment_cb,
- bc);
- if (qs < 0)
- {
- GNUNET_break (0);
- return TALER_MHD2_make_error (TALER_EC_GENERIC_DB_FETCH_FAILED,
- "pending payments");
- }
- if (NULL != bc->existing_order_id)
- {
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Have existing order, waiting for `%s' to complete\n",
- bc->existing_order_id);
- await_payment (bc,
- GNUNET_TIME_UNIT_ZERO /* no long polling */,
- bc->existing_order_id);
- *suspend = true;
- return NULL;
- }
- }
- GNUNET_CONTAINER_DLL_insert (bc_head,
- bc_tail,
- bc);
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Suspending request while creating order at `%s'\n",
- SH_backend_url);
- *suspend = true;
- {
- json_t *order;
-
- order = GNUNET_JSON_PACK (
- TALER_JSON_pack_amount ("amount",
- &SH_annual_fee),
- GNUNET_JSON_pack_string ("summary",
- "annual fee for sync service"),
- GNUNET_JSON_pack_string ("fulfillment_url",
- SH_fulfillment_url));
- bc->po = TALER_MERCHANT_post_private_orders_create (SH_ctx,
- SH_backend_url,
- order);
- json_decref (order);
- }
- GNUNET_assert (TALER_EC_NONE ==
- TALER_MERCHANT_post_private_orders_start (
- bc->po,
- &proposal_cb,
- bc));
- SH_trigger_curl ();
- return NULL;
-}
-
-
-/**
- * We got some query status from the DB. Handle the error cases.
- * May perform asynchronous operations by suspending the request
- * if required.
- *
- * @param bc request to handle status for
- * @param qs query status to handle
- * @param[out] suspend set to true if the request should be suspended
- * @return MHD response to return
- */
-static struct MHD_Response *
-handle_database_error (struct BackupContext *bc,
- enum SYNC_DB_QueryStatus qs,
- bool *suspend)
-{
- *suspend = false;
- switch (qs)
- {
- case SYNC_DB_OLD_BACKUP_MISSING:
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Update failed: no existing backup\n");
- return TALER_MHD2_make_error (TALER_EC_SYNC_PREVIOUS_BACKUP_UNKNOWN,
- NULL);
- case SYNC_DB_OLD_BACKUP_MISMATCH:
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Conflict detected, returning existing backup\n");
- return SH_make_backup (&bc->account,
- MHD_HTTP_STATUS_CONFLICT);
- case SYNC_DB_PAYMENT_REQUIRED:
- {
- struct MHD_StringNullable order_id;
-
- if ( (MHD_YES !=
- MHD_request_get_value (bc->request,
- MHD_VK_URI_QUERY_PARAM,
- "paying",
- &order_id)) ||
- (NULL == order_id.cstr) )
- {
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Payment required, starting payment process\n");
- return begin_payment (bc,
- GNUNET_NO,
- suspend);
- }
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Payment required, awaiting completion of `%s'\n",
- order_id.cstr);
- await_payment (bc,
- CHECK_PAYMENT_GENERIC_TIMEOUT,
- order_id.cstr);
- *suspend = true;
- return NULL;
- }
- case SYNC_DB_HARD_ERROR:
- GNUNET_break (0);
- return TALER_MHD2_make_error (TALER_EC_GENERIC_DB_COMMIT_FAILED,
- NULL);
- case SYNC_DB_SOFT_ERROR:
- GNUNET_break (0);
- return TALER_MHD2_make_error (TALER_EC_GENERIC_DB_SOFT_FAILURE,
- NULL);
- case SYNC_DB_NO_RESULTS:
- GNUNET_assert (0);
- return NULL;
- /* intentional fall-through! */
- case SYNC_DB_ONE_RESULT:
- GNUNET_assert (0);
- return NULL;
- }
- GNUNET_break (0);
- return NULL;
-}
-
-
-/**
- * Function to process data uploaded by a client.
- *
- * @param upload_cls our `struct BackupContext *`
- * @param request the request is being processed
- * @param content_data_size the size of the @a content_data,
- * zero when all data have been processed
- * @param[in] content_data the uploaded content data,
- * may be modified in the callback,
- * valid only until return from the callback,
- * NULL when all data have been processed
- * @return action specifying how to proceed:
- * #MHD_upload_action_continue() to continue upload (for incremental
- * upload processing only),
- * #MHD_upload_action_suspend() to stop reading the upload until
- * the request is resumed,
- * #MHD_upload_action_abort_request() to close the socket,
- * or a response to discard the rest of the upload and transmit
- * the response
- */
-static const struct MHD_UploadAction *
-handle_upload (void *upload_cls,
- struct MHD_Request *request,
- size_t content_data_size,
- void *content_data)
-{
- struct BackupContext *bc = upload_cls;
-
- if (NULL != bc->resp)
- {
- /* This can happen on resume */
- return MHD_upload_action_from_response (request,
- bc->resp);
- }
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Processing %llu bytes of upload data\n",
- (unsigned long long) content_data_size);
- /* Check hash matches */
- {
- struct GNUNET_HashCode our_hash;
-
- GNUNET_CRYPTO_hash (content_data,
- content_data_size,
- &our_hash);
- if (0 != GNUNET_memcmp (&our_hash,
- &bc->new_backup_hash))
- {
- GNUNET_break_op (0);
- return MHD_upload_action_from_response (
- request,
- TALER_MHD2_make_error (
- TALER_EC_SYNC_INVALID_UPLOAD,
- NULL));
- }
- }
-
- /* store backup to database */
- {
- enum SYNC_DB_QueryStatus qs;
-
- if (GNUNET_is_zero (&bc->old_backup_hash))
- {
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Uploading first backup to account\n");
- qs = SYNCDB_store_backup_TR (
- &bc->account,
- &bc->account_sig,
- &bc->new_backup_hash,
- content_data_size,
- content_data);
- }
- else
- {
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Uploading existing backup of account\n");
- qs = SYNCDB_update_backup_TR (
- &bc->account,
- &bc->old_backup_hash,
- &bc->account_sig,
- &bc->new_backup_hash,
- content_data_size,
- content_data);
- }
- if (qs < 0)
- {
- bool suspend;
- struct MHD_Response *resp;
-
- resp = handle_database_error (bc,
- qs,
- &suspend);
- if (suspend)
- return MHD_upload_action_suspend (request);
- return MHD_upload_action_from_response (request,
- resp);
- }
- if (0 == qs)
- {
- /* database says nothing actually changed, 304 (could
- theoretically happen if another equivalent upload succeeded
- since we last checked!) */
- struct MHD_Response *resp;
-
- resp = MHD_response_from_empty (MHD_HTTP_STATUS_NOT_MODIFIED);
- TALER_MHD2_add_global_headers (resp,
- false);
- return MHD_upload_action_from_response (request,
- resp);
- }
- }
-
- /* generate main (204) standard success reply */
- {
- struct MHD_Response *resp;
-
- resp = MHD_response_from_empty (MHD_HTTP_STATUS_NO_CONTENT);
- TALER_MHD2_add_global_headers (resp,
- false);
- return MHD_upload_action_from_response (request,
- resp);
- }
-}
-
-
-const struct MHD_Action *
-SH_backup_post (struct MHD_Request *request,
- const struct SYNC_AccountPublicKeyP *account,
- uint_fast64_t upload_size)
-{
- struct BackupContext *bc;
- union MHD_RequestInfoFixedData fi;
-
- GNUNET_assert (MHD_SC_OK ==
- MHD_request_get_info_fixed (
- request,
- MHD_REQUEST_INFO_FIXED_APP_CONTEXT,
- &fi));
- bc = *fi.v_app_context_ppvoid;
- if (NULL == bc)
- {
- /* first call, setup internals */
- bc = GNUNET_new (struct BackupContext);
- bc->request = request;
- bc->account = *account;
-#if BUG
- GNUNET_assert (MHD_SC_OK ==
- MHD_REQUEST_SET_OPTIONS (
- MHD_R_OPTION_TERMINATION_CALLBACK (&cleanup_ctx,
- bc)));
-#endif
- *fi.v_app_context_ppvoid = bc;
- }
- if (NULL != bc->resp)
- return MHD_action_from_response (request,
- bc->resp);
- if ( (upload_size / 1024 / 1024 >= SH_upload_limit_mb) ||
- (upload_size > SIZE_MAX) )
- {
- GNUNET_break_op (0);
- return TALER_MHD2_reply_with_error (
- request,
- MHD_HTTP_STATUS_CONTENT_TOO_LARGE,
- TALER_EC_SYNC_EXCESSIVE_CONTENT_LENGTH,
- NULL);
- }
- {
- struct MHD_StringNullable fresh;
-
- if (MHD_YES ==
- MHD_request_get_value (request,
- MHD_VK_URI_QUERY_PARAM,
- "fresh",
- &fresh))
- bc->force_fresh_order = true;
- }
- {
- struct MHD_StringNullable im;
-
- if ( (MHD_YES ==
- MHD_request_get_value (request,
- MHD_VK_HEADER,
- MHD_HTTP_HEADER_IF_MATCH,
- &im)) &&
- ( (2 >= im.len) ||
- ('"' != im.cstr[0]) ||
- ('"' != im.cstr[im.len - 1]) ||
- (GNUNET_OK !=
- GNUNET_STRINGS_string_to_data (im.cstr + 1,
- im.len - 2,
- &bc->old_backup_hash,
- sizeof (bc->old_backup_hash))) ) )
- {
- GNUNET_break_op (0);
- return TALER_MHD2_reply_with_error (
- request,
- MHD_HTTP_STATUS_BAD_REQUEST,
- TALER_EC_SYNC_BAD_IF_MATCH,
- NULL);
- }
- }
- {
- struct MHD_StringNullable sig_s;
-
- if ( (MHD_YES !=
- MHD_request_get_value (request,
- MHD_VK_HEADER,
- "Sync-Signature",
- &sig_s)) ||
- (GNUNET_OK !=
- GNUNET_STRINGS_string_to_data (sig_s.cstr,
- sig_s.len,
- &bc->account_sig,
- sizeof (bc->account_sig))) )
- {
- GNUNET_break_op (0);
- return TALER_MHD2_reply_with_error (
- request,
- MHD_HTTP_STATUS_BAD_REQUEST,
- TALER_EC_SYNC_BAD_SYNC_SIGNATURE,
- NULL);
- }
- }
- {
- struct MHD_StringNullable etag;
-
- if ( (MHD_YES !=
- MHD_request_get_value (request,
- MHD_VK_HEADER,
- MHD_HTTP_HEADER_IF_NONE_MATCH,
- &etag)) ||
- (2 >= etag.len) ||
- ('"' != etag.cstr[0]) ||
- ('"' != etag.cstr[etag.len - 1]) ||
- (GNUNET_OK !=
- GNUNET_STRINGS_string_to_data (etag.cstr + 1,
- etag.len - 2,
- &bc->new_backup_hash,
- sizeof (bc->new_backup_hash))) )
- {
- GNUNET_break_op (0);
- return TALER_MHD2_reply_with_error (
- request,
- MHD_HTTP_STATUS_BAD_REQUEST,
- TALER_EC_SYNC_BAD_IF_NONE_MATCH,
- NULL);
- }
- }
- /* validate signature */
- {
- struct SYNC_UploadSignaturePS usp = {
- .purpose.size = htonl (sizeof (usp)),
- .purpose.purpose = htonl (TALER_SIGNATURE_SYNC_BACKUP_UPLOAD),
- .old_backup_hash = bc->old_backup_hash,
- .new_backup_hash = bc->new_backup_hash
- };
-
- if (GNUNET_OK !=
- GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_SYNC_BACKUP_UPLOAD,
- &usp,
- &bc->account_sig.eddsa_sig,
- &account->eddsa_pub))
- {
- GNUNET_break_op (0);
- return TALER_MHD2_reply_with_error (
- request,
- MHD_HTTP_STATUS_FORBIDDEN,
- TALER_EC_SYNC_INVALID_SIGNATURE,
- NULL);
- }
- }
-
- /* Check database to see if the transaction is permissible */
- {
- struct GNUNET_HashCode hc;
- enum SYNC_DB_QueryStatus qs;
-
- qs = SYNCDB_lookup_account_TR (
- account,
- &hc);
- if (qs < 0)
- {
- bool suspend;
- struct MHD_Response *resp;
-
- resp = handle_database_error (bc,
- qs,
- &suspend);
- if (suspend)
- return MHD_action_suspend (request);
- return MHD_action_from_response (request,
- resp);
- }
- if (SYNC_DB_NO_RESULTS == qs)
- memset (&hc,
- 0,
- sizeof (hc));
- if (0 == GNUNET_memcmp (&hc,
- &bc->new_backup_hash))
- {
- /* Refuse upload: we already have that backup! */
- struct MHD_Response *resp;
-
- resp = MHD_response_from_empty (MHD_HTTP_STATUS_NOT_MODIFIED);
- TALER_MHD2_add_global_headers (resp,
- false);
- return MHD_action_from_response (request,
- resp);
- }
- if (0 != GNUNET_memcmp (&hc,
- &bc->old_backup_hash))
- {
- /* Refuse upload: if-none-match failed! */
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Conflict detected, returning existing backup\n");
- return MHD_action_from_response (
- request,
- SH_make_backup (account,
- MHD_HTTP_STATUS_CONFLICT));
- }
- }
- /* check if the client insists on paying */
- {
- struct MHD_StringNullable order_req;
-
- if (MHD_YES ==
- MHD_request_get_value (request,
- MHD_VK_URI_QUERY_PARAM,
- "pay",
- &order_req))
- {
- struct MHD_Response *resp;
- bool suspend;
-
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Payment requested, starting payment process\n");
- resp = begin_payment (bc,
- GNUNET_YES,
- &suspend);
- if (suspend)
- return MHD_action_suspend (request);
- return MHD_action_from_response (request,
- resp);
- }
- }
- /* ready to begin upload! */
- return MHD_action_process_upload_full (request,
- upload_size,
- &handle_upload,
- bc);
-}
diff --git a/src/sync/sync-httpd2_config.c b/src/sync/sync-httpd2_config.c
@@ -30,6 +30,11 @@
* 0: original design
* 1: adds ?fresh=y to POST backup operation to force fresh contract
* to be created
+ * 2: block upload signatures are stored server-side and returned with
+ * the block list, so wallets can verify the backup before applying
+ * (see PROMPTS.md P1)
+ * 3: adds GET /backups/$ACCOUNT_KEY, reporting when the account expires
+ * and how much of the storage allowance it uses
*/
const struct MHD_Action *
@@ -39,7 +44,7 @@ SH_handler_config (struct MHD_Request *request,
static struct MHD_Response *response;
static struct GNUNET_TIME_Absolute a;
- GNUNET_break (0 == upload_size);
+ GNUNET_break_op (0 == upload_size);
if ( (GNUNET_TIME_absolute_is_past (a)) &&
(NULL != response) )
{
@@ -75,7 +80,7 @@ SH_handler_config (struct MHD_Request *request,
TALER_JSON_pack_amount ("annual_fee",
&SH_annual_fee),
GNUNET_JSON_pack_string ("version",
- "2:2:0"));
+ "4:0:1"));
GNUNET_break (MHD_SC_OK ==
MHD_response_add_header (response,
MHD_HTTP_HEADER_EXPIRES,
diff --git a/src/sync/sync-httpd2_config.h b/src/sync/sync-httpd2_config.h
@@ -14,7 +14,7 @@
Sync; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/>
*/
/**
- * @file sync/sync-httpd_config.h
+ * @file sync/sync-httpd2_config.h
* @brief headers for /config handler
* @author Christian Grothoff
*/
@@ -36,4 +36,4 @@ SH_handler_config (struct MHD_Request *request,
#endif
-/* end of sync-httpd_config.h */
+/* end of sync-httpd2_config.h */
diff --git a/src/sync/sync-httpd2_delete-backups-KEY-blocks-NONCE.c b/src/sync/sync-httpd2_delete-backups-KEY-blocks-NONCE.c
@@ -0,0 +1,701 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 Taler Systems SA
+
+ TALER 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.
+
+ 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file sync-httpd2_delete-backups-KEY-blocks-NONCE.c
+ * @brief Handle DELETE /backups/$KEY/blocks/$NONCE requests
+ * @author Iván Ávalos
+ */
+#include "sync-httpd2.h"
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_json_lib.h>
+#include <taler/taler_mhd2_lib.h>
+#include <taler/taler_signatures.h>
+#include <sync/sync_signatures.h>
+#include <sync/sync_util.h>
+#include "sync-httpd2_delete-backups-KEY-blocks-NONCE.h"
+
+
+/**
+ * Context for a block delete operation.
+ */
+struct BlockDeleteContext
+{
+
+ /**
+ * Context for cleanup logic.
+ */
+ struct TM_HandlerContext hc;
+
+ /**
+ * Public key of the account holder.
+ */
+ struct SYNC_AccountPublicKeyP account_pub;
+
+ /**
+ * Nonce of the block to delete.
+ */
+ struct SYNC_BlockNonce nonce;
+
+ /**
+ * Nonce of the preceding block (NULL if first).
+ */
+ struct SYNC_BlockNonce prev_nonce;
+
+ /**
+ * True if @e prev_nonce was missing in the JSON body.
+ */
+ bool null_prev_nonce;
+
+ /**
+ * Nonce of the succeeding block (NULL if last).
+ */
+ struct SYNC_BlockNonce next_nonce;
+
+ /**
+ * True if @e next_nonce was missing in the JSON body.
+ */
+ bool null_next_nonce;
+
+ /**
+ * Hash of the block to delete (from If-Match).
+ */
+ struct GNUNET_HashCode block_hash;
+
+ /**
+ * Signature of the delete request.
+ */
+ struct SYNC_AccountSignatureP delete_sig;
+
+ /**
+ * Relink signature for the block at @e prev_nonce.
+ */
+ struct SYNC_AccountSignatureP prev_upload_sig;
+
+ /**
+ * True if the relink_prev object was present in the JSON body.
+ */
+ bool have_relink_prev;
+
+ /**
+ * Hash of the block at @e prev_nonce as fetched by this handler;
+ * the delete transaction re-checks the stored neighbour still
+ * hashes to it before storing the relink signature.
+ */
+ struct GNUNET_HashCode prev_hash;
+
+ /**
+ * Nonce preceding @e prev_nonce as fetched by this handler, as
+ * covered by @e prev_upload_sig; the delete transaction re-checks it
+ * for the same reason as @e prev_hash.
+ */
+ struct SYNC_BlockNonce prev_prev_nonce;
+
+ /**
+ * True if the block at @e prev_nonce has no predecessor, i.e.
+ * @e prev_prev_nonce is unset.
+ */
+ bool null_prev_prev_nonce;
+
+ /**
+ * Relink signature for the block at @e next_nonce.
+ */
+ struct SYNC_AccountSignatureP next_upload_sig;
+
+ /**
+ * True if the relink_next object was present in the JSON body.
+ */
+ bool have_relink_next;
+
+ /**
+ * Hash of the block at @e next_nonce as fetched by this handler;
+ * the delete transaction re-checks the stored neighbour still
+ * hashes to it before storing the relink signature.
+ */
+ struct GNUNET_HashCode next_hash;
+
+ /**
+ * Nonce succeeding @e next_nonce as fetched by this handler, as
+ * covered by @e next_upload_sig; the delete transaction re-checks it
+ * for the same reason as @e next_hash.
+ */
+ struct SYNC_BlockNonce next_next_nonce;
+
+ /**
+ * True if the block at @e next_nonce has no successor, i.e.
+ * @e next_next_nonce is unset.
+ */
+ bool null_next_next_nonce;
+
+ /**
+ * Array of object UIDs referenced by this block.
+ */
+ struct SYNC_ObjectUID *object_uids;
+
+ /**
+ * Array of object reference count adjustments.
+ */
+ int16_t *object_incs;
+
+ /**
+ * Length of @e object_uids and @e object_incs.
+ */
+ size_t refs_len;
+
+};
+
+
+/**
+ * Clean up and free a block delete context.
+ *
+ * @param hc the @e hc member of the `struct BlockDeleteContext`
+ * to clean up and free
+ */
+static void
+cleanup_block_delete_free (struct TM_HandlerContext *hc)
+{
+ struct BlockDeleteContext *bdc;
+
+ bdc = (struct BlockDeleteContext *) hc;
+ GNUNET_free (bdc->object_uids);
+ GNUNET_free (bdc->object_incs);
+ GNUNET_free (bdc);
+}
+
+
+/**
+ * Function to process data uploaded by a client.
+ *
+ * @param upload_cls our `struct BlockDeleteContext *`
+ * @param request the request being processed
+ * @param content_data_size the size of the @a content_data,
+ * zero when all data have been processed
+ * @param[in] content_data the uploaded content data,
+ * may be modified in the callback,
+ * valid only until return from the callback,
+ * NULL when all data have been processed
+ * @return action specifying how to proceed
+ */
+static const struct MHD_UploadAction *
+handle_delete_upload (void *upload_cls,
+ struct MHD_Request *request,
+ size_t content_data_size,
+ void *content_data)
+{
+ struct BlockDeleteContext *bdc = upload_cls;
+
+ if ( (0 == content_data_size) &&
+ (NULL == content_data) )
+ {
+ GNUNET_break_op (0);
+ return MHD_upload_action_from_response (
+ request,
+ TALER_MHD2_make_error (TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ NULL));
+ }
+
+ /* parse JSON request */
+ {
+ json_error_t jerr;
+ json_t *root;
+ const json_t *relink_prev = NULL;
+ const json_t *relink_next = NULL;
+
+ root = json_loadb (content_data,
+ content_data_size,
+ 0,
+ &jerr);
+ if (NULL == root)
+ {
+ GNUNET_break_op (0);
+ return MHD_upload_action_from_response (
+ request,
+ TALER_MHD2_make_error (TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ NULL));
+ }
+
+ const json_t *refs = NULL;
+ struct GNUNET_JSON_Specification spec[] = {
+ GNUNET_JSON_spec_fixed_auto ("delete_sig",
+ &bdc->delete_sig),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_fixed_auto ("prev_nonce",
+ &bdc->prev_nonce),
+ &bdc->null_prev_nonce),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_fixed_auto ("next_nonce",
+ &bdc->next_nonce),
+ &bdc->null_next_nonce),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_object_const ("object_refs",
+ &refs),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_object_const ("relink_prev",
+ &relink_prev),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_object_const ("relink_next",
+ &relink_next),
+ NULL),
+ GNUNET_JSON_spec_end (),
+ };
+ const char *ename;
+ unsigned int eline;
+
+ if (GNUNET_OK !=
+ GNUNET_JSON_parse (root,
+ spec,
+ &ename,
+ &eline))
+ {
+ GNUNET_break_op (0);
+ json_decref (root);
+ return MHD_upload_action_from_response (
+ request,
+ TALER_MHD2_make_error (TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ NULL));
+ }
+
+ if (GNUNET_OK !=
+ SYNC_object_refs_parse (refs,
+ &bdc->refs_len,
+ &bdc->object_uids,
+ &bdc->object_incs))
+ {
+ GNUNET_break_op (0);
+ json_decref (root);
+ return MHD_upload_action_from_response (
+ request,
+ TALER_MHD2_make_error (TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ NULL));
+ }
+
+ if (NULL != relink_prev)
+ {
+ struct GNUNET_JSON_Specification rspec[] = {
+ GNUNET_JSON_spec_fixed_auto ("upload_sig",
+ &bdc->prev_upload_sig),
+ GNUNET_JSON_spec_end (),
+ };
+
+ bdc->have_relink_prev = true;
+ if (GNUNET_OK != GNUNET_JSON_parse ((json_t *) relink_prev,
+ rspec,
+ &ename,
+ &eline))
+ {
+ GNUNET_break_op (0);
+ json_decref (root);
+ return MHD_upload_action_from_response (
+ request,
+ TALER_MHD2_make_error (TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ NULL));
+ }
+ }
+
+ if (NULL != relink_next)
+ {
+ struct GNUNET_JSON_Specification rspec[] = {
+ GNUNET_JSON_spec_fixed_auto ("upload_sig",
+ &bdc->next_upload_sig),
+ GNUNET_JSON_spec_end (),
+ };
+
+ bdc->have_relink_next = true;
+ if (GNUNET_OK != GNUNET_JSON_parse ((json_t *) relink_next,
+ rspec,
+ &ename,
+ &eline))
+ {
+ GNUNET_break_op (0);
+ json_decref (root);
+ return MHD_upload_action_from_response (
+ request,
+ TALER_MHD2_make_error (TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ NULL));
+ }
+ }
+
+ json_decref (root);
+ }
+
+ /* The relink signatures are required exactly when the respective
+ neighbour exists. */
+ {
+ if (bdc->null_prev_nonce != ! bdc->have_relink_prev)
+ {
+ GNUNET_break_op (0);
+ return MHD_upload_action_from_response (
+ request,
+ TALER_MHD2_make_error (TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ NULL));
+ }
+ if (bdc->null_next_nonce != ! bdc->have_relink_next)
+ {
+ GNUNET_break_op (0);
+ return MHD_upload_action_from_response (
+ request,
+ TALER_MHD2_make_error (TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ NULL));
+ }
+ }
+
+ /* Verify signature; it covers the object references too */
+ {
+ struct GNUNET_HashCode refs_hash;
+
+ if (GNUNET_OK !=
+ SYNC_object_refs_hash (bdc->refs_len,
+ bdc->object_uids,
+ bdc->object_incs,
+ &refs_hash))
+ {
+ GNUNET_break_op (0);
+ return MHD_upload_action_from_response (
+ request,
+ TALER_MHD2_make_error (TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "object_refs"));
+ }
+ if (GNUNET_OK !=
+ SYNC_block_delete_verify (&bdc->account_pub,
+ &bdc->nonce,
+ ! bdc->null_prev_nonce
+ ? &bdc->prev_nonce
+ : NULL,
+ ! bdc->null_next_nonce
+ ? &bdc->next_nonce
+ : NULL,
+ &bdc->block_hash,
+ &refs_hash,
+ &bdc->delete_sig))
+ {
+ GNUNET_break_op (0);
+ return MHD_upload_action_from_response (
+ request,
+ TALER_MHD2_make_error (TALER_EC_SYNC_INVALID_SIGNATURE,
+ NULL));
+ }
+ }
+
+ /* The deletion relinks both neighbours; verify their uploaded relink
+ signatures against the neighbours as they are stored now. The
+ delete transaction re-checks that the neighbours still hash to
+ what these signatures were verified against. */
+ {
+ struct GNUNET_HashCode n_refs;
+ struct SYNC_BlockNonce n_prev;
+ struct SYNC_BlockNonce n_next;
+ size_t n_data_size;
+ bool n_is_first;
+ bool n_is_last;
+ enum SYNC_DB_QueryStatus qs;
+
+ if (bdc->have_relink_prev)
+ {
+ qs = SYNCDB_lookup_block_TR (&bdc->account_pub,
+ &bdc->prev_nonce,
+ &bdc->prev_hash,
+ &n_prev,
+ &n_next,
+ &n_data_size,
+ &n_refs,
+ &n_is_first,
+ &n_is_last);
+ if (SYNC_DB_ONE_RESULT != qs)
+ {
+ GNUNET_break_op (0);
+ return MHD_upload_action_from_response (
+ request,
+ SYNC_MHD_MAKE_ERROR (MHD_HTTP_STATUS_CONFLICT,
+ TALER_EC_SYNC_CONFLICTING_WRITE,
+ NULL));
+ }
+ /* Remember the neighbour as it was verified against, so the
+ transaction can reject the delete if it changed meanwhile */
+ bdc->null_prev_prev_nonce = n_is_first;
+ if (! n_is_first)
+ bdc->prev_prev_nonce = n_prev;
+
+ if (GNUNET_OK !=
+ SYNC_block_upload_verify (&bdc->account_pub,
+ &bdc->prev_nonce,
+ n_is_first
+ ? NULL
+ : &n_prev,
+ ! bdc->null_next_nonce
+ ? &bdc->next_nonce
+ : NULL,
+ &bdc->prev_hash,
+ &bdc->prev_hash,
+ &n_refs,
+ &bdc->prev_upload_sig))
+ {
+ GNUNET_break_op (0);
+ return MHD_upload_action_from_response (
+ request,
+ TALER_MHD2_make_error (TALER_EC_SYNC_INVALID_SIGNATURE,
+ NULL));
+ }
+ }
+
+ if (bdc->have_relink_next)
+ {
+ qs = SYNCDB_lookup_block_TR (&bdc->account_pub,
+ &bdc->next_nonce,
+ &bdc->next_hash,
+ &n_prev,
+ &n_next,
+ &n_data_size,
+ &n_refs,
+ &n_is_first,
+ &n_is_last);
+ if (SYNC_DB_ONE_RESULT != qs)
+ {
+ GNUNET_break_op (0);
+ return MHD_upload_action_from_response (
+ request,
+ SYNC_MHD_MAKE_ERROR (MHD_HTTP_STATUS_CONFLICT,
+ TALER_EC_SYNC_CONFLICTING_WRITE,
+ NULL));
+ }
+ bdc->null_next_next_nonce = n_is_last;
+ if (! n_is_last)
+ bdc->next_next_nonce = n_next;
+
+ if (GNUNET_OK !=
+ SYNC_block_upload_verify (&bdc->account_pub,
+ &bdc->next_nonce,
+ ! bdc->null_prev_nonce
+ ? &bdc->prev_nonce
+ : NULL,
+ n_is_last
+ ? NULL
+ : &n_next,
+ &bdc->next_hash,
+ &bdc->next_hash,
+ &n_refs,
+ &bdc->next_upload_sig))
+ {
+ GNUNET_break_op (0);
+ return MHD_upload_action_from_response (
+ request,
+ TALER_MHD2_make_error (TALER_EC_SYNC_INVALID_SIGNATURE,
+ NULL));
+ }
+ }
+ }
+
+ /* Perform the delete */
+ {
+ enum SYNC_DB_QueryStatus qs;
+
+ qs = SYNCDB_delete_block_TR (&bdc->account_pub,
+ &bdc->nonce,
+ ! bdc->null_prev_nonce
+ ? &bdc->prev_nonce
+ : NULL,
+ ! bdc->null_next_nonce
+ ? &bdc->next_nonce
+ : NULL,
+ &bdc->block_hash,
+ bdc->refs_len,
+ bdc->object_uids,
+ bdc->object_incs,
+ bdc->have_relink_prev
+ ? &bdc->prev_upload_sig
+ : NULL,
+ bdc->have_relink_prev
+ ? &bdc->prev_hash
+ : NULL,
+ (bdc->have_relink_prev &&
+ ! bdc->null_prev_prev_nonce)
+ ? &bdc->prev_prev_nonce
+ : NULL,
+ bdc->have_relink_next
+ ? &bdc->next_upload_sig
+ : NULL,
+ bdc->have_relink_next
+ ? &bdc->next_hash
+ : NULL,
+ (bdc->have_relink_next &&
+ ! bdc->null_next_next_nonce)
+ ? &bdc->next_next_nonce
+ : NULL);
+ switch (qs)
+ {
+ case SYNC_DB_ONE_RESULT:
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Block deleted successfully\n");
+ {
+ struct MHD_Response *resp;
+
+ resp = MHD_response_from_empty (MHD_HTTP_STATUS_NO_CONTENT);
+ TALER_MHD2_add_global_headers (resp,
+ false);
+ return MHD_upload_action_from_response (request,
+ resp);
+ }
+ case SYNC_DB_PAYMENT_REQUIRED:
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Delete failed: payment required\n");
+ return MHD_upload_action_from_response (
+ request,
+ SYNC_MHD_MAKE_ERROR (MHD_HTTP_STATUS_PAYMENT_REQUIRED,
+ TALER_EC_SYNC_ACCOUNT_UNKNOWN,
+ NULL));
+ case SYNC_DB_TARGET_MISSING:
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Delete failed: block gone\n");
+ return MHD_upload_action_from_response (
+ request,
+ TALER_MHD2_make_error (TALER_EC_SYNC_PREVIOUS_BACKUP_UNKNOWN,
+ NULL));
+ case SYNC_DB_OUTDATED_WRITE:
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Delete failed: concurrent modification\n");
+ return MHD_upload_action_from_response (
+ request,
+ SYNC_MHD_MAKE_ERROR (MHD_HTTP_STATUS_CONFLICT,
+ TALER_EC_SYNC_CONFLICTING_WRITE,
+ NULL));
+ case SYNC_DB_INCONSISTENT_BACKUP:
+ /* Unknown object or refcount underflow; nothing was modified */
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Delete failed: object references do not match"
+ " the stored backup\n");
+ return MHD_upload_action_from_response (
+ request,
+ SYNC_MHD_MAKE_ERROR (MHD_HTTP_STATUS_CONFLICT,
+ TALER_EC_SYNC_INCONSISTENT_BACKUP,
+ NULL));
+ case SYNC_DB_HARD_ERROR:
+ GNUNET_break (0);
+ return MHD_upload_action_from_response (
+ request,
+ TALER_MHD2_make_error (TALER_EC_GENERIC_DB_COMMIT_FAILED,
+ NULL));
+ case SYNC_DB_SOFT_ERROR:
+ GNUNET_break (0);
+ return MHD_upload_action_from_response (
+ request,
+ TALER_MHD2_make_error (TALER_EC_GENERIC_DB_SOFT_FAILURE,
+ NULL));
+ default:
+ GNUNET_break (0);
+ return MHD_upload_action_from_response (
+ request,
+ TALER_MHD2_make_error (TALER_EC_SYNC_GENERIC_BACKEND_ERROR,
+ NULL));
+ }
+ }
+}
+
+
+const struct MHD_Action *
+SH_backup_block_delete (struct MHD_Request *request,
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const char *const args[],
+ uint_fast64_t upload_size,
+ bool is_update)
+{
+ struct BlockDeleteContext *bdc;
+ struct SYNC_BlockNonce nonce;
+ struct GNUNET_HashCode block_hash;
+ union MHD_RequestInfoFixedData fi;
+
+ (void) is_update;
+
+ /* Parse nonce from URL first to avoid allocating on error */
+ if (GNUNET_OK !=
+ GNUNET_STRINGS_string_to_data (args[0],
+ strlen (args[0]),
+ &nonce,
+ sizeof (nonce)))
+ {
+ GNUNET_break_op (0);
+ return MHD_action_from_response (
+ request,
+ SYNC_MHD_MAKE_ERROR (MHD_HTTP_STATUS_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "nonce"));
+ }
+
+ /* Parse If-Match header for block hash */
+ {
+ struct MHD_StringNullable etag;
+
+ if ( (MHD_YES !=
+ MHD_request_get_value (request,
+ MHD_VK_HEADER,
+ MHD_HTTP_HEADER_IF_MATCH,
+ &etag)) ||
+ (2 >= etag.len) ||
+ ('"' != etag.cstr[0]) ||
+ ('"' != etag.cstr[etag.len - 1]) ||
+ (GNUNET_OK !=
+ GNUNET_STRINGS_string_to_data (etag.cstr + 1,
+ etag.len - 2,
+ &block_hash,
+ sizeof (block_hash))) )
+ {
+ return MHD_action_from_response (
+ request,
+ TALER_MHD2_make_error (TALER_EC_SYNC_BAD_IF_MATCH,
+ NULL));
+ }
+ }
+
+ GNUNET_assert (MHD_SC_OK ==
+ MHD_request_get_info_fixed (
+ request,
+ MHD_REQUEST_INFO_FIXED_APP_CONTEXT,
+ &fi));
+ bdc = *fi.v_app_context_ppvoid;
+ if (NULL == bdc)
+ {
+ /* first call, setup internals */
+ bdc = GNUNET_new (struct BlockDeleteContext);
+ bdc->hc.cc = &cleanup_block_delete_free;
+ bdc->account_pub = *account_pub;
+ bdc->nonce = nonce;
+ bdc->block_hash = block_hash;
+ *fi.v_app_context_ppvoid = bdc;
+ }
+
+ /* The body only carries the delete signature, but it still has to be
+ bounded: a chunked request reports no length up-front. */
+ {
+ size_t buff_size;
+
+ if (SH_upload_too_large (upload_size,
+ &buff_size))
+ {
+ cleanup_block_delete_free (&bdc->hc);
+ *fi.v_app_context_ppvoid = NULL;
+ return MHD_action_from_response (
+ request,
+ SYNC_MHD_MAKE_ERROR (MHD_HTTP_STATUS_CONTENT_TOO_LARGE,
+ TALER_EC_SYNC_EXCESSIVE_CONTENT_LENGTH,
+ NULL));
+ }
+
+ /* Ready to begin reading the body for the delete signature! */
+ return MHD_action_process_upload_full (request,
+ buff_size,
+ &handle_delete_upload,
+ bdc);
+ }
+}
diff --git a/src/sync/sync-httpd2_delete-backups-KEY-blocks-NONCE.h b/src/sync/sync-httpd2_delete-backups-KEY-blocks-NONCE.h
@@ -0,0 +1,46 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 Taler Systems SA
+
+ TALER 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.
+
+ 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file sync-httpd2_delete-backups-KEY-blocks-NONCE.h
+ * @brief Handle DELETE /backups/$KEY/blocks/$NONCE requests
+ * @author Christian Grothoff
+ */
+#ifndef SYNC_HTTPD2_DELETE_BACKUPS_KEY_BLOCKS_NONCE_H
+#define SYNC_HTTPD2_DELETE_BACKUPS_KEY_BLOCKS_NONCE_H
+
+#include <microhttpd2.h>
+#include "sync/sync_service.h"
+
+
+/**
+ * Handle a DELETE "/backups/$KEY/blocks/$NONCE" request.
+ *
+ * @param request the MHD request to handle
+ * @param account_pub public key of the account
+ * @param args path segments: [0]="blocks", [1]=encoded NONCE
+ * @param upload_size number of bytes being uploaded (unused for DELETE)
+ * @param is_update whether the operation is an update (unused for DELETE)
+ * @return MHD action
+ */
+const struct MHD_Action *
+SH_backup_block_delete (struct MHD_Request *request,
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const char *const args[],
+ uint_fast64_t upload_size,
+ bool is_update);
+
+
+#endif
diff --git a/src/sync/sync-httpd2_get-backups-KEY-blocks.c b/src/sync/sync-httpd2_get-backups-KEY-blocks.c
@@ -0,0 +1,334 @@
+/*
+ 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 Affero 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file sync-httpd2_get-backups-KEY-blocks.c
+ * @brief GET /backups/$KEY/blocks — list blocks
+ * @author Iván Ávalos
+ */
+#include "sync-httpd2.h"
+#include "sync-httpd2_get-backups-KEY-blocks.h"
+#include <inttypes.h>
+#include <gnunet/gnunet_json_lib.h>
+#include <taler/taler_json_lib.h>
+#include <sync/sync_util.h>
+
+
+/**
+ * Context for collecting blocks from the DB iterator.
+ */
+struct BlocksGetContext
+{
+ /**
+ * Context for cleanup logic.
+ */
+ struct TM_HandlerContext hc;
+
+ /**
+ * Public key of the account holder.
+ */
+ struct SYNC_AccountPublicKeyP account_pub;
+
+ /**
+ * Maximum number of blocks to return (from ?limit=).
+ */
+ int16_t limit;
+
+ /**
+ * Nonce of the block to start listing at (from ?start_nonce=).
+ */
+ struct SYNC_BlockNonce start_nonce;
+
+ /**
+ * True if @e start_nonce was given.
+ */
+ bool have_start_nonce;
+
+ /**
+ * JSON array of the blocks collected so far.
+ */
+ json_t *array;
+};
+
+
+/**
+ * Clean up and free a blocks-get context.
+ *
+ * @param hc the @e hc member of the context to clean up and free
+ */
+static void
+cleanup_blocks_get_free (struct TM_HandlerContext *hc)
+{
+ struct BlocksGetContext *bgc;
+
+ bgc = (struct BlocksGetContext *) hc;
+ if (NULL != bgc->array)
+ json_decref (bgc->array);
+ GNUNET_free (bgc);
+}
+
+
+/**
+ * Clean up and clear a blocks-get context, then terminate
+ * the request with an error response.
+ *
+ * @param request MHD request (used for response)
+ * @param bgc context to clean up and free
+ * @param fi request info with app context pointer to clear
+ * @param sc HTTP status code
+ * @param ec Taler error code
+ * @param hint optional hint string for the error detail
+ */
+#define BAIL_WITH_ERROR(request, bgc, fi, sc, ec, hint) do { \
+ GNUNET_break_op (0); \
+ cleanup_blocks_get_free (&(bgc)->hc); \
+ *(fi).v_app_context_ppvoid = NULL; \
+ return TALER_MHD2_reply_with_error ( \
+ (request), \
+ (sc), \
+ (ec), \
+ (hint)); \
+} while (0)
+
+
+/**
+ * Clean up and clear a blocks-get context, then terminate
+ * the request with a response built elsewhere.
+ *
+ * @param request MHD request (used for response)
+ * @param bgc context to clean up and free
+ * @param fi request info with app context pointer to clear
+ * @param resp response to return (must be non-NULL)
+ */
+#define BAIL_WITH_RESPONSE(request, bgc, fi, resp) do { \
+ cleanup_blocks_get_free (&(bgc)->hc); \
+ *(fi).v_app_context_ppvoid = NULL; \
+ return MHD_action_from_response ((request), \
+ (resp)); \
+} while (0)
+
+
+/**
+ * Iterator callback for SYNCDB_lookup_block_range_TR.
+ * Appends each block as a JSON object to the context array.
+ *
+ * @param cls a `struct BlocksGetContext *`
+ * @param nonce block nonce
+ * @param block_hash hash of the block data
+ * @param prev_nonce previous block nonce (NULL if first)
+ * @param next_nonce next block nonce (NULL if last)
+ * @param data_size size of @a data
+ * @param data encrypted block contents
+ * @param upload_sig signature stored with the block
+ * @param old_hash hash of the replaced block data
+ * @param refs_hash hash over the object references
+ */
+static void
+block_iter (void *cls,
+ const struct SYNC_BlockNonce *nonce,
+ const struct GNUNET_HashCode *block_hash,
+ const struct SYNC_BlockNonce *prev_nonce,
+ const struct SYNC_BlockNonce *next_nonce,
+ size_t data_size,
+ const void *data,
+ const struct SYNC_AccountSignatureP *upload_sig,
+ const struct GNUNET_HashCode *old_hash,
+ const struct GNUNET_HashCode *refs_hash)
+{
+ struct BlocksGetContext *bgc = cls;
+ json_t *obj;
+
+ obj = SYNC_block_entry_serialize (nonce,
+ block_hash,
+ prev_nonce,
+ next_nonce,
+ data_size,
+ data,
+ upload_sig,
+ old_hash,
+ refs_hash);
+ json_array_append_new (bgc->array,
+ obj);
+}
+
+
+const struct MHD_Action *
+SH_backup_blocks_get (struct MHD_Request *request,
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const char *const args[],
+ uint_fast64_t upload_size,
+ bool is_update)
+{
+ union MHD_RequestInfoFixedData fi;
+ struct BlocksGetContext *bgc;
+ enum SYNC_DB_QueryStatus qs;
+
+ (void) args;
+ (void) upload_size;
+ (void) is_update;
+
+ GNUNET_assert (MHD_SC_OK ==
+ MHD_request_get_info_fixed (
+ request,
+ MHD_REQUEST_INFO_FIXED_APP_CONTEXT,
+ &fi));
+ bgc = *fi.v_app_context_ppvoid;
+
+ if (NULL == bgc)
+ {
+ struct MHD_StringNullable limit_str;
+
+ bgc = GNUNET_new (struct BlocksGetContext);
+ bgc->hc.cc = &cleanup_blocks_get_free;
+ bgc->account_pub = *account_pub;
+ *fi.v_app_context_ppvoid = bgc;
+
+ /* MHD returns MHD_YES with a NULL cstr for "?limit" without a
+ value, so check the pointer separately */
+ if ( (MHD_YES !=
+ MHD_request_get_value (request,
+ MHD_VK_URI_QUERY_PARAM,
+ "limit",
+ &limit_str)) ||
+ (NULL == limit_str.cstr) ||
+ (1 != sscanf (limit_str.cstr,
+ "%" SCNd16,
+ &bgc->limit)) ||
+ (0 >= bgc->limit) )
+ BAIL_WITH_ERROR (request,
+ bgc,
+ fi,
+ MHD_HTTP_STATUS_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "limit");
+
+ {
+ struct MHD_StringNullable sn_str;
+
+ if (MHD_YES ==
+ MHD_request_get_value (request,
+ MHD_VK_URI_QUERY_PARAM,
+ "start_nonce",
+ &sn_str))
+ {
+ if ( (NULL == sn_str.cstr) ||
+ (GNUNET_OK !=
+ GNUNET_STRINGS_string_to_data (sn_str.cstr,
+ sn_str.len,
+ &bgc->start_nonce,
+ sizeof (bgc->start_nonce))) )
+ BAIL_WITH_ERROR (request,
+ bgc,
+ fi,
+ MHD_HTTP_STATUS_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "start_nonce");
+ bgc->have_start_nonce = true;
+ }
+ }
+ }
+
+ bgc->array = json_array ();
+
+ qs = SYNCDB_lookup_block_range_TR (&bgc->account_pub,
+ bgc->have_start_nonce
+ ? &bgc->start_nonce
+ : NULL,
+ bgc->limit,
+ &block_iter,
+ bgc);
+
+ switch (qs)
+ {
+ case SYNC_DB_PAYMENT_REQUIRED:
+ {
+ struct MHD_Response *resp;
+
+ json_decref (bgc->array);
+ bgc->array = NULL;
+ resp = MHD_response_from_empty (
+ MHD_HTTP_STATUS_PAYMENT_REQUIRED);
+ TALER_MHD2_add_global_headers (resp,
+ false);
+ BAIL_WITH_RESPONSE (request,
+ bgc,
+ fi,
+ resp);
+ }
+
+ case SYNC_DB_TARGET_MISSING:
+ json_decref (bgc->array);
+ bgc->array = NULL;
+ BAIL_WITH_ERROR (request,
+ bgc,
+ fi,
+ MHD_HTTP_STATUS_NOT_FOUND,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "start_nonce block not found in chain");
+
+ case SYNC_DB_NO_RESULTS:
+ break;
+
+ case SYNC_DB_HARD_ERROR:
+ GNUNET_break (0);
+ json_decref (bgc->array);
+ bgc->array = NULL;
+ BAIL_WITH_ERROR (request,
+ bgc,
+ fi,
+ MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_COMMIT_FAILED,
+ NULL);
+
+ case SYNC_DB_SOFT_ERROR:
+ GNUNET_break (0);
+ json_decref (bgc->array);
+ bgc->array = NULL;
+ BAIL_WITH_ERROR (request,
+ bgc,
+ fi,
+ MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_SOFT_FAILURE,
+ NULL);
+
+ default:
+ if (qs < 0)
+ {
+ /* Do not serve a partial list as if it were complete */
+ GNUNET_break (0);
+ json_decref (bgc->array);
+ bgc->array = NULL;
+ BAIL_WITH_ERROR (request,
+ bgc,
+ fi,
+ MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ TALER_EC_SYNC_GENERIC_BACKEND_ERROR,
+ NULL);
+ }
+ break;
+ }
+
+ {
+ json_t *resp_json;
+
+ resp_json = bgc->array;
+ bgc->array = NULL;
+ cleanup_blocks_get_free (&bgc->hc);
+ *fi.v_app_context_ppvoid = NULL;
+ return TALER_MHD2_reply_json_steal (request,
+ resp_json,
+ MHD_HTTP_STATUS_OK);
+ }
+}
diff --git a/src/sync/sync-httpd2_get-backups-KEY-blocks.h b/src/sync/sync-httpd2_get-backups-KEY-blocks.h
@@ -0,0 +1,46 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 Taler Systems SA
+
+ TALER 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.
+
+ 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file sync-httpd2_get-backups-KEY-blocks.h
+ * @brief GET /backups/$KEY/blocks — list all blocks
+ * @author Iván Ávalos
+ */
+#ifndef SYNC_HTTPD2_GET_BACKUPS_KEY_BLOCKS_H
+#define SYNC_HTTPD2_GET_BACKUPS_KEY_BLOCKS_H
+
+#include <microhttpd2.h>
+#include "sync/sync_service.h"
+
+
+/**
+ * Handle GET /backups/$KEY/blocks.
+ *
+ * @param request the MHD request to handle
+ * @param account_pub public key of the account
+ * @param args path segments (args[0]="blocks")
+ * @param upload_size number of bytes being uploaded
+ * @param is_update whether the operation is an update
+ * @return MHD action
+ */
+const struct MHD_Action *
+SH_backup_blocks_get (struct MHD_Request *request,
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const char *const args[],
+ uint_fast64_t upload_size,
+ bool is_update);
+
+
+#endif
diff --git a/src/sync/sync-httpd2_get-backups-KEY-objects-UID.c b/src/sync/sync-httpd2_get-backups-KEY-objects-UID.c
@@ -0,0 +1,120 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 Taler Systems SA
+
+ TALER 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.
+
+ 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file sync-httpd2_get-backups-KEY-objects-UID.c
+ * @brief Handle GET /backups/$KEY/objects/$UID requests
+ * @author Iván Ávalos
+ */
+#include "platform.h"
+#include "sync-httpd2.h"
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_json_lib.h>
+#include <taler/taler_json_lib.h>
+#include <taler/taler_mhd2_lib.h>
+#include "sync-httpd2_get-backups-KEY-objects-UID.h"
+#include <sync/sync_util.h>
+
+
+const struct MHD_Action *
+SH_backup_object_get (struct MHD_Request *request,
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const char *const args[],
+ uint_fast64_t upload_size,
+ bool is_update)
+{
+ struct SYNC_ObjectUID uid;
+ struct GNUNET_HashCode object_hash;
+ int64_t refcount;
+ size_t data_size;
+ void *data = NULL;
+ enum SYNC_DB_QueryStatus qs;
+
+ (void) upload_size;
+ (void) is_update;
+
+ /* Parse UID from URL */
+ if (GNUNET_OK !=
+ GNUNET_STRINGS_string_to_data (args[0],
+ strlen (args[0]),
+ &uid,
+ sizeof (uid)))
+ {
+ return TALER_MHD2_reply_with_error (
+ request,
+ MHD_HTTP_STATUS_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "uid");
+ }
+
+ /* Look up object by UID */
+ qs = SYNCDB_lookup_object_uid (account_pub,
+ &uid,
+ &object_hash,
+ &refcount,
+ &data_size,
+ &data);
+ switch (qs)
+ {
+ case SYNC_DB_ONE_RESULT:
+ {
+ json_t *resp_json;
+
+ resp_json = SYNC_object_entry_serialize (&uid,
+ data_size,
+ data);
+ GNUNET_free (data);
+ return TALER_MHD2_reply_json_steal (request,
+ resp_json,
+ MHD_HTTP_STATUS_OK);
+ }
+ case SYNC_DB_NO_RESULTS:
+ return TALER_MHD2_reply_with_error (
+ request,
+ MHD_HTTP_STATUS_NOT_FOUND,
+ TALER_EC_SYNC_PREVIOUS_BACKUP_UNKNOWN,
+ "object not found");
+ case SYNC_DB_PAYMENT_REQUIRED:
+ return TALER_MHD2_reply_with_error (
+ request,
+ MHD_HTTP_STATUS_PAYMENT_REQUIRED,
+ TALER_EC_SYNC_ACCOUNT_UNKNOWN,
+ NULL);
+ case SYNC_DB_HARD_ERROR:
+ GNUNET_break (0);
+ return TALER_MHD2_reply_with_error (
+ request,
+ MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_COMMIT_FAILED,
+ NULL);
+ case SYNC_DB_SOFT_ERROR:
+ GNUNET_break (0);
+ return TALER_MHD2_reply_with_error (
+ request,
+ MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_SOFT_FAILURE,
+ NULL);
+ default:
+ GNUNET_break (0);
+ return TALER_MHD2_reply_with_error (
+ request,
+ MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
+ NULL);
+ }
+}
+
+
+/* end of sync-httpd2_get-backups-KEY-objects-UID.c */
diff --git a/src/sync/sync-httpd2_get-backups-KEY-objects-UID.h b/src/sync/sync-httpd2_get-backups-KEY-objects-UID.h
@@ -0,0 +1,46 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 Taler Systems SA
+
+ TALER 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.
+
+ 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file sync-httpd2_get-backups-KEY-objects-UID.h
+ * @brief Handle GET /backups/$KEY/objects/$UID requests
+ * @author Christian Grothoff
+ */
+#ifndef SYNC_HTTPD2_GET_BACKUPS_KEY_OBJECTS_UID_H
+#define SYNC_HTTPD2_GET_BACKUPS_KEY_OBJECTS_UID_H
+
+#include <microhttpd2.h>
+#include "sync/sync_service.h"
+
+
+/**
+ * Handle a GET "/backups/$KEY/objects/$UID" request.
+ *
+ * @param request the MHD request to handle
+ * @param account_pub public key of the account
+ * @param args path segments: [0]="objects", [1]=encoded UID
+ * @param upload_size number of bytes being uploaded (unused for GET)
+ * @param is_update whether the operation is an update (unused for GET)
+ * @return MHD action
+ */
+const struct MHD_Action *
+SH_backup_object_get (struct MHD_Request *request,
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const char *const args[],
+ uint_fast64_t upload_size,
+ bool is_update);
+
+
+#endif
diff --git a/src/sync/sync-httpd2_get-backups-KEY.c b/src/sync/sync-httpd2_get-backups-KEY.c
@@ -0,0 +1,88 @@
+/*
+ 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 Affero 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file sync-httpd2_get-backups-KEY.c
+ * @brief GET /backups/$KEY — account status
+ * @author Christian Grothoff
+ */
+#include "sync-httpd2.h"
+#include "sync-httpd2_get-backups-KEY.h"
+#include <gnunet/gnunet_json_lib.h>
+#include <taler/taler_json_lib.h>
+#include <sync/sync_util.h>
+
+
+const struct MHD_Action *
+SH_backup_account_get (struct MHD_Request *request,
+ const struct SYNC_AccountPublicKeyP *account_pub)
+{
+ struct GNUNET_TIME_Timestamp expiration_date;
+ uint64_t storage_used_bytes;
+ uint64_t block_count;
+ enum SYNC_DB_QueryStatus qs;
+
+ qs = SYNCDB_lookup_account_TR (account_pub,
+ &expiration_date,
+ &storage_used_bytes,
+ &block_count);
+ switch (qs)
+ {
+ case SYNC_DB_ONE_RESULT:
+ break;
+ case SYNC_DB_TARGET_MISSING:
+ /* Deliberately not 402: the client is asking *whether* it has to
+ pay, so an unknown account is simply not found. */
+ return TALER_MHD2_reply_with_error (
+ request,
+ MHD_HTTP_STATUS_NOT_FOUND,
+ TALER_EC_SYNC_ACCOUNT_UNKNOWN,
+ NULL);
+ case SYNC_DB_HARD_ERROR:
+ GNUNET_break (0);
+ return TALER_MHD2_reply_with_error (
+ request,
+ MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ NULL);
+ case SYNC_DB_SOFT_ERROR:
+ GNUNET_break (0);
+ return TALER_MHD2_reply_with_error (
+ request,
+ MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_SOFT_FAILURE,
+ NULL);
+ default:
+ GNUNET_break (0);
+ return TALER_MHD2_reply_with_error (
+ request,
+ MHD_HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ TALER_EC_SYNC_GENERIC_BACKEND_ERROR,
+ NULL);
+ }
+
+ /* An account past its expiration is reported as it stands, not
+ refused: the date is exactly what the client came for. */
+ return TALER_MHD2_reply_json_steal (
+ request,
+ GNUNET_JSON_PACK (
+ GNUNET_JSON_pack_timestamp ("expiration_date",
+ expiration_date),
+ GNUNET_JSON_pack_uint64 ("storage_used_bytes",
+ storage_used_bytes),
+ GNUNET_JSON_pack_uint64 ("block_count",
+ block_count)),
+ MHD_HTTP_STATUS_OK);
+}
diff --git a/src/sync/sync-httpd2_get-backups-KEY.h b/src/sync/sync-httpd2_get-backups-KEY.h
@@ -0,0 +1,38 @@
+/*
+ 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 Affero 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file sync-httpd2_get-backups-KEY.h
+ * @brief GET /backups/$KEY — account status
+ * @author Christian Grothoff
+ */
+#ifndef SYNC_HTTPD2_GET_BACKUPS_KEY_H
+#define SYNC_HTTPD2_GET_BACKUPS_KEY_H
+
+#include "sync-httpd2.h"
+
+/**
+ * Handle a GET /backups/$KEY request: report when the account expires
+ * and how much of the storage allowance it uses.
+ *
+ * @param request the MHD request
+ * @param account_pub public key of the account
+ * @return MHD action to take
+ */
+const struct MHD_Action *
+SH_backup_account_get (struct MHD_Request *request,
+ const struct SYNC_AccountPublicKeyP *account_pub);
+
+#endif
diff --git a/src/sync/sync-httpd2_payments.c b/src/sync/sync-httpd2_payments.c
@@ -0,0 +1,491 @@
+/*
+ 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 Affero 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file sync/sync-httpd2_payments.c
+ * @brief Helpers for merchant payment processing
+ * @author Christian Grothoff
+ */
+#include "sync-httpd2.h"
+#include "sync-httpd2_payments.h"
+
+#include <gnunet/gnunet_util_lib.h>
+#include <taler/merchant/post-private-orders.h>
+#include <taler/merchant/get-private-orders-ORDER_ID.h>
+#include <taler/taler_json_lib.h>
+
+
+/**
+ * Kept in DLL for shutdown handling while suspended.
+ */
+static struct BackupPaymentContext *bpc_head;
+
+/**
+ * Kept in DLL for shutdown handling while suspended.
+ */
+static struct BackupPaymentContext *bpc_tail;
+
+
+/**
+ * Transmit a payment request for @a order_id on @a request
+ *
+ * @param order_id our backend's order ID
+ * @param token the claim token generated by the merchant (NULL if
+ * it wasn't generated).
+ * @return MHD response to use
+ */
+static struct MHD_Response *
+make_payment_request (const char *order_id,
+ const struct TALER_ClaimTokenP *token)
+{
+ struct MHD_Response *resp;
+
+ /* request payment via Taler */
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Creating payment request for order `%s'\n",
+ order_id);
+ resp = MHD_response_from_empty (MHD_HTTP_STATUS_PAYMENT_REQUIRED);
+ TALER_MHD2_add_global_headers (resp,
+ false);
+ {
+ char *hdr;
+ const char *pfx;
+ char *hn;
+ struct GNUNET_Buffer hdr_buf = { 0 };
+
+ if (0 == strncasecmp ("https://",
+ SH_backend_url,
+ strlen ("https://")))
+ {
+ pfx = "taler://";
+ hn = &SH_backend_url[strlen ("https://")];
+ }
+ else if (0 == strncasecmp ("http://",
+ SH_backend_url,
+ strlen ("http://")))
+ {
+ pfx = "taler+http://";
+ hn = &SH_backend_url[strlen ("http://")];
+ }
+ else
+ {
+ GNUNET_break (0);
+ MHD_response_destroy (resp);
+ return NULL;
+ }
+ if (0 == strlen (hn))
+ {
+ GNUNET_break (0);
+ MHD_response_destroy (resp);
+ return NULL;
+ }
+
+ GNUNET_buffer_write_str (&hdr_buf, pfx);
+ GNUNET_buffer_write_str (&hdr_buf, "pay/");
+ GNUNET_buffer_write_str (&hdr_buf, hn);
+ GNUNET_buffer_write_path (&hdr_buf, order_id);
+ /* No session ID */
+ GNUNET_buffer_write_path (&hdr_buf, "");
+ if (NULL != token)
+ {
+ GNUNET_buffer_write_str (&hdr_buf, "?c=");
+ GNUNET_buffer_write_data_encoded (&hdr_buf,
+ token,
+ sizeof (*token));
+ }
+ hdr = GNUNET_buffer_reap_str (&hdr_buf);
+ GNUNET_break (MHD_SC_OK ==
+ MHD_response_add_header (resp,
+ "Taler",
+ hdr));
+ GNUNET_free (hdr);
+ }
+ return resp;
+}
+
+
+/**
+ * Resume the suspended request of @a bpc, so that the response we
+ * stored in @e resp gets transmitted.
+ *
+ * @param bpc context of the request to resume
+ */
+static void
+resume_request (struct BackupPaymentContext *bpc)
+{
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Resuming request with order `%s'\n",
+ bpc->order_id);
+ MHD_request_resume (bpc->request);
+ SH_trigger_daemon ();
+}
+
+
+/**
+ * Callbacks of this type are used to serve the result of submitting a
+ * /contract request to a merchant.
+ *
+ * @param bpc our payment context
+ * @param por response details
+ */
+static void
+proposal_cb (
+ struct BackupPaymentContext *bpc,
+ const struct TALER_MERCHANT_PostPrivateOrdersResponse *por)
+{
+ enum SYNC_DB_QueryStatus qs;
+
+ bpc->po = NULL;
+ GNUNET_CONTAINER_DLL_remove (bpc_head,
+ bpc_tail,
+ bpc);
+ if (MHD_HTTP_STATUS_OK != por->hr.http_status)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Backend returned status %u/%u\n",
+ por->hr.http_status,
+ (unsigned int) por->hr.ec);
+ GNUNET_break_op (0);
+ bpc->resp = TALER_MHD2_MAKE_JSON_PACK (
+ MHD_HTTP_STATUS_BAD_GATEWAY,
+ TALER_JSON_pack_ec (TALER_EC_SYNC_PAYMENT_CREATE_BACKEND_ERROR),
+ GNUNET_JSON_pack_uint64 ("backend-ec",
+ (json_int_t) por->hr.ec),
+ GNUNET_JSON_pack_uint64 ("backend-http-status",
+ (json_int_t) por->hr.http_status),
+ GNUNET_JSON_pack_allow_null (
+ GNUNET_JSON_pack_object_incref ("backend-reply",
+ (json_t *) por->hr.reply)));
+ resume_request (bpc);
+ return;
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Storing payment request for order `%s'\n",
+ por->details.ok.order_id);
+ qs = SYNCDB_store_payment_TR (
+ &bpc->account_pub,
+ por->details.ok.order_id,
+ por->details.ok.token,
+ &SH_annual_fee,
+ GNUNET_TIME_UNIT_YEARS);
+ if (0 >= qs)
+ {
+ GNUNET_break (0);
+ bpc->resp = TALER_MHD2_make_error (TALER_EC_GENERIC_DB_STORE_FAILED,
+ "Failed to persist payment request in sync database");
+ GNUNET_assert (NULL != bpc->resp);
+ resume_request (bpc);
+ return;
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Obtained fresh order `%s'\n",
+ por->details.ok.order_id);
+ bpc->resp = make_payment_request (por->details.ok.order_id,
+ por->details.ok.token);
+ GNUNET_assert (NULL != bpc->resp);
+ resume_request (bpc);
+}
+
+
+/**
+ * Function called on all pending payments for the right
+ * account.
+ *
+ * @param cls closure, our `struct BackupPaymentContext *`
+ * @param timestamp for how long have we been waiting
+ * @param order_id order id in the backend
+ * @param token claim token to use (or NULL for none)
+ * @param amount how much is the order for
+ */
+static void
+ongoing_payment_cb (void *cls,
+ struct GNUNET_TIME_Timestamp timestamp,
+ const char *order_id,
+ const struct TALER_ClaimTokenP *token,
+ const struct TALER_Amount *amount)
+{
+ struct BackupPaymentContext *bpc = cls;
+
+ if (0 != TALER_amount_cmp (amount,
+ &SH_annual_fee))
+ return; /* can't reuse, fees changed */
+ if ( (NULL == bpc->existing_order_id) ||
+ (GNUNET_TIME_timestamp_cmp (bpc->existing_order_timestamp,
+ <,
+ timestamp)) )
+ {
+ GNUNET_free (bpc->existing_order_id);
+ bpc->existing_order_id = GNUNET_strdup (order_id);
+ bpc->existing_order_timestamp = timestamp;
+ if (NULL != token)
+ bpc->token = *token;
+ }
+}
+
+
+/**
+ * Store the response to send for the order status @a osr in @a bpc.
+ * Leaving @e resp at NULL means the payment went through and the
+ * request should re-attempt its database operation.
+ *
+ * @param bpc our payment context
+ * @param osr order status
+ */
+static void
+set_payment_response (
+ struct BackupPaymentContext *bpc,
+ const struct TALER_MERCHANT_GetPrivateOrderResponse *osr)
+{
+ const struct TALER_MERCHANT_HttpResponse *hr = &osr->hr;
+
+ switch (hr->http_status)
+ {
+ case 0:
+ /* Likely timeout, complain! */
+ bpc->resp = TALER_MHD2_make_error (
+ TALER_EC_SYNC_GENERIC_BACKEND_TIMEOUT,
+ NULL);
+ return;
+ case MHD_HTTP_STATUS_OK:
+ break; /* handled below */
+ default:
+ /* Unexpected backend response */
+ bpc->resp = TALER_MHD2_MAKE_JSON_PACK (
+ MHD_HTTP_STATUS_BAD_GATEWAY,
+ GNUNET_JSON_pack_uint64 ("code",
+ TALER_EC_SYNC_GENERIC_BACKEND_ERROR),
+ GNUNET_JSON_pack_string ("hint",
+ TALER_ErrorCode_get_hint (
+ TALER_EC_SYNC_GENERIC_BACKEND_ERROR)),
+ GNUNET_JSON_pack_uint64 ("backend-ec",
+ (json_int_t) hr->ec),
+ GNUNET_JSON_pack_uint64 ("backend-http-status",
+ (json_int_t) hr->http_status),
+ GNUNET_JSON_pack_allow_null (
+ GNUNET_JSON_pack_object_incref ("backend-reply",
+ (json_t *) hr->reply)));
+ return;
+ }
+
+ GNUNET_assert (MHD_HTTP_STATUS_OK == hr->http_status);
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Payment status checked: %d\n",
+ osr->details.ok.status);
+ switch (osr->details.ok.status)
+ {
+ case TALER_MERCHANT_OSC_PAID:
+ {
+ enum SYNC_DB_QueryStatus qs;
+
+ qs = SYNCDB_increment_lifetime_TR (
+ &bpc->account_pub,
+ bpc->order_id,
+ GNUNET_TIME_UNIT_YEARS); /* always annual */
+ if (0 <= qs)
+ return; /* continue as planned */
+ GNUNET_break (0);
+ bpc->resp = TALER_MHD2_make_error (TALER_EC_GENERIC_DB_STORE_FAILED,
+ "increment lifetime");
+ GNUNET_assert (NULL != bpc->resp);
+ return; /* continue as planned */
+ }
+ case TALER_MERCHANT_OSC_UNPAID:
+ case TALER_MERCHANT_OSC_CLAIMED:
+ break;
+ }
+ if (NULL != bpc->existing_order_id)
+ {
+ /* repeat payment request */
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Repeating payment request\n");
+ bpc->resp = make_payment_request (
+ bpc->existing_order_id,
+ (GNUNET_YES == GNUNET_is_zero (&bpc->token))
+ ? NULL
+ : &bpc->token);
+ GNUNET_assert (NULL != bpc->resp);
+ return;
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Timeout waiting for payment\n");
+ bpc->resp = TALER_MHD2_make_error (TALER_EC_SYNC_PAYMENT_GENERIC_TIMEOUT,
+ "Timeout awaiting promised payment");
+ GNUNET_assert (NULL != bpc->resp);
+}
+
+
+/**
+ * Callback to process a GET /check-payment request
+ *
+ * @param bpc our payment context
+ * @param osr order status
+ */
+static void
+check_payment_cb (
+ struct BackupPaymentContext *bpc,
+ const struct TALER_MERCHANT_GetPrivateOrderResponse *osr)
+{
+ bpc->omgh = NULL;
+ GNUNET_CONTAINER_DLL_remove (bpc_head,
+ bpc_tail,
+ bpc);
+ /* Set the response before resuming, MHD needs it in place */
+ set_payment_response (bpc,
+ osr);
+ resume_request (bpc);
+}
+
+
+/**
+ * Helper function used to ask our backend to await
+ * a payment for the user's account.
+ *
+ * @param bpc context to await payment for
+ * @param timeout when to give up trying
+ * @param order_id which order to check for the payment
+ */
+static void
+await_payment (struct BackupPaymentContext *bpc,
+ struct GNUNET_TIME_Relative timeout,
+ const char *order_id)
+{
+ GNUNET_CONTAINER_DLL_insert (bpc_head,
+ bpc_tail,
+ bpc);
+ /* @a order_id may be MHD-owned and die when we return an action */
+ GNUNET_free (bpc->order_id);
+ bpc->order_id = GNUNET_strdup (order_id);
+ order_id = bpc->order_id;
+ bpc->omgh = TALER_MERCHANT_get_private_order_create (
+ SH_ctx,
+ SH_backend_url,
+ order_id);
+ GNUNET_assert (GNUNET_OK ==
+ TALER_MERCHANT_get_private_order_set_options (
+ bpc->omgh,
+ TALER_MERCHANT_get_private_order_option_timeout (timeout)));
+ GNUNET_assert (TALER_EC_NONE ==
+ TALER_MERCHANT_get_private_order_start (
+ bpc->omgh,
+ &check_payment_cb,
+ bpc));
+ SH_trigger_curl ();
+}
+
+
+struct MHD_Response *
+SH_begin_payment (struct BackupPaymentContext *bpc,
+ int pay_req,
+ bool *suspend)
+{
+ (void) pay_req;
+ *suspend = false;
+ if (! bpc->force_fresh_order)
+ {
+ enum SYNC_DB_QueryStatus qs;
+
+ qs = SYNCDB_lookup_pending_payments_by_account_TR (
+ &bpc->account_pub,
+ &ongoing_payment_cb,
+ bpc);
+ if (qs < 0)
+ {
+ GNUNET_break (0);
+ return TALER_MHD2_make_error (TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "pending payments");
+ }
+
+ if (NULL != bpc->existing_order_id)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Have existing order, waiting for `%s' to complete\n",
+ bpc->existing_order_id);
+ await_payment (bpc,
+ GNUNET_TIME_UNIT_ZERO /* no long polling */,
+ bpc->existing_order_id);
+ *suspend = true;
+ return NULL;
+ }
+ }
+ GNUNET_CONTAINER_DLL_insert (bpc_head,
+ bpc_tail,
+ bpc);
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Suspending request while creating order at `%s'\n",
+ SH_backend_url);
+ *suspend = true;
+ {
+ json_t *order;
+
+ order = GNUNET_JSON_PACK (
+ TALER_JSON_pack_amount ("amount",
+ &SH_annual_fee),
+ GNUNET_JSON_pack_string ("summary",
+ "annual fee for sync service"),
+ GNUNET_JSON_pack_string ("fulfillment_url",
+ SH_fulfillment_url));
+ bpc->po = TALER_MERCHANT_post_private_orders_create (SH_ctx,
+ SH_backend_url,
+ order);
+ json_decref (order);
+ }
+ GNUNET_assert (TALER_EC_NONE ==
+ TALER_MERCHANT_post_private_orders_start (
+ bpc->po,
+ &proposal_cb,
+ bpc));
+ SH_trigger_curl ();
+ return NULL;
+}
+
+
+struct MHD_Response *
+SH_await_payment (struct BackupPaymentContext *bpc,
+ const char *order_id,
+ bool *suspend)
+{
+ *suspend = true;
+ await_payment (bpc,
+ GNUNET_TIME_UNIT_ZERO,
+ order_id);
+ return NULL;
+}
+
+
+void
+SH_payment_cleanup (struct BackupPaymentContext *bpc)
+{
+ if ( (NULL != bpc->po) ||
+ (NULL != bpc->omgh) )
+ {
+ /* Request went away while we were talking to the merchant; drop
+ the operation before its closure is freed */
+ if (NULL != bpc->po)
+ {
+ TALER_MERCHANT_post_private_orders_cancel (bpc->po);
+ bpc->po = NULL;
+ }
+ if (NULL != bpc->omgh)
+ {
+ TALER_MERCHANT_get_private_order_cancel (bpc->omgh);
+ bpc->omgh = NULL;
+ }
+ GNUNET_CONTAINER_DLL_remove (bpc_head,
+ bpc_tail,
+ bpc);
+ }
+ GNUNET_free (bpc->existing_order_id);
+ GNUNET_free (bpc->order_id);
+}
diff --git a/src/sync/sync-httpd2_payments.h b/src/sync/sync-httpd2_payments.h
@@ -0,0 +1,151 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 Taler Systems SA
+
+ TALER 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.
+
+ 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file sync-httpd2_payments.h
+ * @brief Helpers for merchant payment processing
+ * @author Christian Grothoff
+ */
+#ifndef SYNC_HTTPD2_PAYMENTS_H
+#define SYNC_HTTPD2_PAYMENTS_H
+
+#include "sync/sync_service.h"
+#include <taler/taler_util.h>
+
+
+struct BackupPaymentContext;
+#define TALER_MERCHANT_POST_PRIVATE_ORDERS_RESULT_CLOSURE \
+ struct BackupPaymentContext
+#define TALER_MERCHANT_GET_PRIVATE_ORDER_RESULT_CLOSURE \
+ struct BackupPaymentContext
+
+
+/**
+ * Context for a merchant payment.
+ */
+struct BackupPaymentContext
+{
+ /**
+ * Public key of the account holder.
+ */
+ struct SYNC_AccountPublicKeyP account_pub;
+
+ /**
+ * Claim token, all zeros if not known.
+ *
+ * (only set if @e existing_order_id is non-NULL).
+ */
+ struct TALER_ClaimTokenP token;
+
+ /**
+ * Kept in DLL for shutdown handling while suspended.
+ */
+ struct BackupPaymentContext *next;
+
+ /**
+ * Kept in DLL for shutdown handling while suspended.
+ */
+ struct BackupPaymentContext *prev;
+
+ /**
+ * Used while suspended for resumption.
+ */
+ struct MHD_Request *request;
+
+ /**
+ * To be returned upon resuming.
+ */
+ struct MHD_Response *resp;
+
+ /**
+ * Used while we are awaiting proposal creation.
+ */
+ struct TALER_MERCHANT_PostPrivateOrdersHandle *po;
+
+ /**
+ * Used while we are waiting payment.
+ */
+ struct TALER_MERCHANT_GetPrivateOrderHandle *omgh;
+
+ /**
+ * Order under which the client promised payment, or NULL.
+ * Owned by this struct.
+ */
+ char *order_id;
+
+ /**
+ * Order ID for the client that we found in our database.
+ * Owned by this struct.
+ */
+ char *existing_order_id;
+
+ /**
+ * Timestamp of the order in @e existing_order_id. Used to
+ * select the most recent unpaid offer.
+ */
+ struct GNUNET_TIME_Timestamp existing_order_timestamp;
+
+ /**
+ * Do not look for an existing order, force a fresh order to be created.
+ */
+ bool force_fresh_order;
+};
+
+
+/**
+ * Helper function used to ask our backend to begin
+ * processing a payment for the user's account.
+ * May perform asynchronous operations by suspending the request
+ * if required.
+ *
+ * @param bpc context to begin payment for.
+ * @param pay_req #GNUNET_YES if payment was explicitly requested,
+ * #GNUNET_NO if payment is needed
+ * @param[out] suspend set to true if the request should be suspended
+ * @return MHD response
+ */
+struct MHD_Response *
+SH_begin_payment (struct BackupPaymentContext *bpc,
+ int pay_req,
+ bool *suspend);
+
+
+/**
+ * Helper function used to await completion of an
+ * already-initiated payment.
+ * May perform asynchronous operations by suspending the request
+ * if required.
+ *
+ * @param bpc context for the payment to await
+ * @param order_id order that the client promised to pay
+ * @param[out] suspend set to true if the request should be suspended
+ * @return MHD response
+ */
+struct MHD_Response *
+SH_await_payment (struct BackupPaymentContext *bpc,
+ const char *order_id,
+ bool *suspend);
+
+
+/**
+ * Release resources held by @a bpc, cancelling a merchant operation
+ * that is still in flight. Does not free @a bpc itself.
+ *
+ * @param[in,out] bpc payment context to clean up
+ */
+void
+SH_payment_cleanup (struct BackupPaymentContext *bpc);
+
+#endif
diff --git a/src/sync/sync-httpd2_post-backups-KEY-blocks-NONCE.c b/src/sync/sync-httpd2_post-backups-KEY-blocks-NONCE.c
@@ -0,0 +1,985 @@
+/*
+ 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 Affero 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file sync-httpd2_post-backups-KEY-blocks-NONCE.c
+ * @brief Handle POST /backups/$KEY/blocks/$NONCE requests
+ * @author Iván Ávalos
+ */
+#include "sync-httpd2.h"
+#include "sync-httpd2_payments.h"
+#include <gnunet/gnunet_time_lib.h>
+#include <gnunet/gnunet_util_lib.h>
+#include <taler/taler_signatures.h>
+#include <sync/sync_signatures.h>
+#include <sync/sync_util.h>
+
+
+/**
+ * How long do we hold an HTTP client connection if
+ * we are awaiting payment before giving up?
+ */
+#define CHECK_PAYMENT_GENERIC_TIMEOUT GNUNET_TIME_relative_multiply ( \
+ GNUNET_TIME_UNIT_MINUTES, 30)
+
+
+/**
+ * Context for a block append operation.
+ */
+struct BlockUploadContext
+{
+ /**
+ * Context for cleanup logic.
+ */
+ struct TM_HandlerContext hc;
+
+ /**
+ * Buffer size to use for reading the request body.
+ */
+ size_t buff_size;
+
+ /**
+ * Whether an existing block is being updated.
+ */
+ bool is_update;
+
+ /**
+ * Public key of the account holder.
+ */
+ struct SYNC_AccountPublicKeyP account_pub;
+
+ /**
+ * Signature of the upload request.
+ */
+ struct SYNC_AccountSignatureP upload_sig;
+
+ /**
+ * Nonce of the block to append/delete.
+ */
+ struct SYNC_BlockNonce nonce;
+
+ /**
+ * Nonce of the preceding block in the DLL.
+ * (set to NULL when uploading the first block)
+ */
+ struct SYNC_BlockNonce prev_nonce;
+
+ /**
+ * True if @e prev_nonce was missing in the JSON body.
+ */
+ bool null_prev_nonce;
+
+ /**
+ * Encrypted block contents.
+ */
+ void *block_data;
+
+ /**
+ * Length of @e block_data.
+ */
+ size_t block_data_len;
+
+ /**
+ * Nonce of the succeeding block in the DLL.
+ * (set to NULL when appending at the end)
+ */
+ struct SYNC_BlockNonce next_nonce;
+
+ /**
+ * True if @e next_nonce was missing in the JSON body.
+ */
+ bool null_next_nonce;
+
+ /**
+ * Previous hash of the block to update.
+ *
+ * (set to NULL if not updating a block)
+ */
+ struct GNUNET_HashCode old_data_hash;
+
+ /**
+ * Hash of the block to upload.
+ */
+ struct GNUNET_HashCode new_data_hash;
+
+ /**
+ * Relink signature for the block at @e prev_nonce (append only).
+ */
+ struct SYNC_AccountSignatureP prev_upload_sig;
+
+ /**
+ * True if the relink_prev object was present in the JSON body.
+ */
+ bool have_relink_prev;
+
+ /**
+ * Hash of the block at @e prev_nonce as fetched by this handler
+ * (append only); the append transaction re-checks the stored tail
+ * still hashes to it before storing the relink signature.
+ */
+ struct GNUNET_HashCode prev_hash;
+
+ /**
+ * Nonce preceding @e prev_nonce as fetched by this handler (append
+ * only), as covered by @e prev_upload_sig; the append transaction
+ * re-checks it for the same reason as @e prev_hash.
+ */
+ struct SYNC_BlockNonce prev_prev_nonce;
+
+ /**
+ * True if the block at @e prev_nonce has no predecessor, i.e.
+ * @e prev_prev_nonce is unset.
+ */
+ bool null_prev_prev_nonce;
+
+ /**
+ * Length of @e object_uids and @e object_incs
+ */
+ size_t refs_len;
+
+ /**
+ * Array of object UID references.
+ */
+ struct SYNC_ObjectUID *object_uids;
+
+ /**
+ * Array of object reference increments.
+ */
+ int16_t *object_incs;
+
+ /**
+ * Hash over @e object_uids and @e object_incs, as covered by
+ * @e upload_sig. Kept in the context because the request may be
+ * suspended for payment and resumed straight into the database
+ * operation, past the code that computes it.
+ */
+ struct GNUNET_HashCode refs_hash;
+
+ /**
+ * Context for a merchant payment.
+ */
+ struct BackupPaymentContext bpc;
+};
+
+
+/**
+ * Clean up and clear a block upload context, then terminate
+ * the request with an error response.
+ *
+ * @param request MHD request (used for response)
+ * @param buc context to clean up and free
+ * @param fi request info with app context pointer to clear
+ * @param sc HTTP status code
+ * @param ec Taler error code
+ */
+#define BAIL_WITH_ERROR(request, buc, fi, sc, ec) do { \
+ GNUNET_break_op (0); \
+ cleanup_block_upload_free (&(buc)->hc); \
+ *(fi).v_app_context_ppvoid = NULL; \
+ return TALER_MHD2_reply_with_error ( \
+ (request), \
+ (sc), \
+ (ec), \
+ NULL); \
+} while (0)
+
+
+/**
+ * Clean up and clear a block upload context, then terminate
+ * the request with a response built elsewhere.
+ *
+ * @param request MHD request (used for response)
+ * @param buc context to clean up and free
+ * @param fi request info with app context pointer to clear
+ * @param resp response to return (must be non-NULL)
+ */
+#define BAIL_WITH_RESPONSE(request, buc, fi, resp) do { \
+ cleanup_block_upload_free (&(buc)->hc); \
+ *(fi).v_app_context_ppvoid = NULL; \
+ return MHD_action_from_response ((request), \
+ (resp)); \
+} while (0)
+
+
+/**
+ * Log an operator error and return an upload action
+ * with a malformed-parameter error response.
+ *
+ * @param request MHD request
+ * @param buc block upload context (may have `block_data`, `object_uids`,
+ * `object_incs` allocated)
+ * @param root JSON root to decref
+ */
+#define BAIL_UPLOAD_MALFORMED(request, buc, root) do { \
+ GNUNET_break_op (0); \
+ json_decref (root); \
+ cleanup_block_upload (buc); \
+ return MHD_upload_action_from_response ( \
+ (request), \
+ TALER_MHD2_make_error ( \
+ TALER_EC_GENERIC_PARAMETER_MALFORMED, \
+ NULL)); \
+} while (0)
+
+
+/**
+ * Terminate an upload handler by cleaning up the block upload
+ * context and returning a response.
+ *
+ * @param request MHD request
+ * @param buc block upload context to clean up
+ * @param resp response to return (must be non-NULL)
+ */
+#define BAIL_UPLOAD_RESPONSE(request, buc, resp) do { \
+ cleanup_block_upload (buc); \
+ return MHD_upload_action_from_response ((request), \
+ (resp)); \
+} while (0)
+
+
+/**
+ * Clean up memory used by a block upload context.
+ * Frees everything except @a buc itself when
+ * #cleanup_block_upload_free() is used.
+ *
+ * @param buc the block upload context to clean up
+ */
+static void
+cleanup_block_upload (struct BlockUploadContext *buc)
+{
+ GNUNET_free (buc->block_data);
+ buc->block_data = NULL;
+ GNUNET_free (buc->object_uids);
+ buc->object_uids = NULL;
+ GNUNET_free (buc->object_incs);
+ buc->object_incs = NULL;
+ SH_payment_cleanup (&buc->bpc);
+}
+
+
+/**
+ * Clean up and free a block upload context.
+ *
+ * @param hc the @e hc member of the `struct BlockUploadContext`
+ * to clean up and free
+ */
+static void
+cleanup_block_upload_free (struct TM_HandlerContext *hc)
+{
+ struct BlockUploadContext *buc;
+
+ buc = (struct BlockUploadContext *) hc;
+ cleanup_block_upload (buc);
+ GNUNET_free (buc);
+}
+
+
+/**
+ * We got some query status from the DB. Handle the error cases.
+ * May perform asynchronous operations by suspending the request
+ * if required.
+ *
+ * @param buc request to handle status for
+ * @param qs query status to handle
+ * @param[out] suspend set to true if the request should be suspended
+ * @return MHD response to return
+ */
+static struct MHD_Response *
+handle_database_error (struct BlockUploadContext *buc,
+ enum SYNC_DB_QueryStatus qs,
+ bool *suspend)
+{
+ *suspend = false;
+ switch (qs)
+ {
+ case SYNC_DB_TARGET_MISSING:
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Update failed: no existing block\n");
+ return TALER_MHD2_make_error (TALER_EC_SYNC_PREVIOUS_BACKUP_UNKNOWN,
+ NULL);
+ case SYNC_DB_OUTDATED_WRITE:
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Conflict detected\n");
+ return SYNC_MHD_MAKE_ERROR (MHD_HTTP_STATUS_CONFLICT,
+ TALER_EC_SYNC_CONFLICTING_WRITE,
+ NULL);
+ case SYNC_DB_INCONSISTENT_BACKUP:
+ /* Unknown object or refcount underflow; nothing was modified */
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Object references do not match the stored backup\n");
+ return SYNC_MHD_MAKE_ERROR (MHD_HTTP_STATUS_CONFLICT,
+ TALER_EC_SYNC_INCONSISTENT_BACKUP,
+ NULL);
+ case SYNC_DB_PAYMENT_REQUIRED:
+ {
+ struct MHD_StringNullable order_id;
+
+ if ( (MHD_YES !=
+ MHD_request_get_value (buc->bpc.request,
+ MHD_VK_URI_QUERY_PARAM,
+ "paying",
+ &order_id)) ||
+ (NULL == order_id.cstr) )
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Payment required, starting payment process\n");
+ return SH_begin_payment (&buc->bpc,
+ GNUNET_NO,
+ suspend);
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Payment required, awaiting completion of `%s'\n",
+ order_id.cstr);
+ return SH_await_payment (&buc->bpc,
+ order_id.cstr,
+ suspend);
+ }
+ case SYNC_DB_HARD_ERROR:
+ GNUNET_break (0);
+ return TALER_MHD2_make_error (TALER_EC_GENERIC_DB_COMMIT_FAILED,
+ NULL);
+ case SYNC_DB_SOFT_ERROR:
+ GNUNET_break (0);
+ return TALER_MHD2_make_error (TALER_EC_GENERIC_DB_SOFT_FAILURE,
+ NULL);
+ case SYNC_DB_NO_RESULTS:
+ case SYNC_DB_ONE_RESULT:
+ /* not an error, so must not be passed here */
+ GNUNET_assert (0);
+ return NULL;
+ }
+ GNUNET_break (0);
+ return TALER_MHD2_make_error (TALER_EC_SYNC_GENERIC_BACKEND_ERROR,
+ NULL);
+}
+
+
+/**
+ * Function to process data uploaded by a client.
+ *
+ * @param upload_cls our `struct BlockUploadContext *`
+ * @param request the request is being processed
+ * @param content_data_size the size of the @a content_data,
+ * zero when all data have been processed
+ * @param[in] content_data the uploaded content data,
+ * may be modified in the callback,
+ * valid only until return from the callback,
+ * NULL when all data have been processed
+ * @return action specifying how to proceed:
+ * #MHD_upload_action_continue() to continue upload (for incremental
+ * upload processing only),
+ * #MHD_upload_action_suspend() to stop reading the upload until
+ * the request is resumed,
+ * #MHD_upload_action_abort_request() to close the socket,
+ * or a response to discard the rest of the upload and transmit
+ * the response
+ */
+static const struct MHD_UploadAction *
+handle_upload (void *upload_cls,
+ struct MHD_Request *request,
+ size_t content_data_size,
+ void *content_data)
+{
+ struct BlockUploadContext *buc = upload_cls;
+
+ if (NULL != buc->bpc.resp)
+ {
+ /* This can happen on resume */
+ return MHD_upload_action_from_response (request,
+ buc->bpc.resp);
+ }
+ if (NULL != buc->block_data)
+ {
+ /* Resume after payment; body was already parsed and verified */
+ goto retry_db_op;
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Processing %llu bytes of upload data\n",
+ (unsigned long long) content_data_size);
+
+ /* parse JSON request */
+ {
+ json_error_t jerr;
+ json_t *root;
+ const json_t *refs = NULL;
+ const json_t *relink_prev = NULL;
+
+ root = json_loadb (content_data,
+ content_data_size,
+ 0,
+ &jerr);
+ if (NULL == root)
+ {
+ GNUNET_break_op (0);
+ return MHD_upload_action_from_response (
+ request,
+ TALER_MHD2_make_error (TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ NULL));
+ }
+
+ struct GNUNET_JSON_Specification spec[] = {
+ GNUNET_JSON_spec_fixed_auto ("upload_sig",
+ &buc->upload_sig),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_fixed_auto ("prev_nonce",
+ &buc->prev_nonce),
+ &buc->null_prev_nonce),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_fixed_auto ("next_nonce",
+ &buc->next_nonce),
+ &buc->null_next_nonce),
+ GNUNET_JSON_spec_varsize ("data",
+ &buc->block_data,
+ &buc->block_data_len),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_object_const ("object_refs",
+ &refs),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_object_const ("relink_prev",
+ &relink_prev),
+ NULL),
+ GNUNET_JSON_spec_end (),
+ };
+ const char *ename;
+ unsigned int eline;
+
+ if (GNUNET_OK != GNUNET_JSON_parse (root,
+ spec,
+ &ename,
+ &eline))
+ BAIL_UPLOAD_MALFORMED (request,
+ buc,
+ root);
+
+ if (GNUNET_OK !=
+ SYNC_object_refs_parse (refs,
+ &buc->refs_len,
+ &buc->object_uids,
+ &buc->object_incs))
+ BAIL_UPLOAD_MALFORMED (request,
+ buc,
+ root);
+
+ if (NULL != relink_prev)
+ {
+ struct GNUNET_JSON_Specification rspec[] = {
+ GNUNET_JSON_spec_fixed_auto ("upload_sig",
+ &buc->prev_upload_sig),
+ GNUNET_JSON_spec_end (),
+ };
+
+ buc->have_relink_prev = true;
+ if (GNUNET_OK != GNUNET_JSON_parse ((json_t *) relink_prev,
+ rspec,
+ &ename,
+ &eline))
+ BAIL_UPLOAD_MALFORMED (request,
+ buc,
+ root);
+ }
+
+ json_decref (root);
+ }
+
+ /* Consistency of the request shape: only appends are supported (no
+ middle insert), and the tail is relinked by an append, so its
+ re-signature is mandatory when there is a tail. */
+ {
+ if (! buc->is_update)
+ {
+ if (! buc->null_next_nonce)
+ BAIL_UPLOAD_MALFORMED (request,
+ buc,
+ NULL);
+ if (buc->null_prev_nonce != ! buc->have_relink_prev)
+ BAIL_UPLOAD_MALFORMED (request,
+ buc,
+ NULL);
+ }
+ else if (buc->have_relink_prev)
+ {
+ /* An update does not change the links. */
+ BAIL_UPLOAD_MALFORMED (request,
+ buc,
+ NULL);
+ }
+ }
+
+ /* Validate signature; it covers the object references too */
+ {
+ if (GNUNET_OK !=
+ SYNC_object_refs_hash (buc->refs_len,
+ buc->object_uids,
+ buc->object_incs,
+ &buc->refs_hash))
+ {
+ GNUNET_break_op (0);
+ cleanup_block_upload (buc);
+ return MHD_upload_action_from_response (
+ request,
+ TALER_MHD2_make_error (TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "object_refs"));
+ }
+ if (GNUNET_OK !=
+ SYNC_block_upload_verify (&buc->account_pub,
+ &buc->nonce,
+ ! buc->null_prev_nonce
+ ? &buc->prev_nonce
+ : NULL,
+ ! buc->null_next_nonce
+ ? &buc->next_nonce
+ : NULL,
+ &buc->old_data_hash,
+ &buc->new_data_hash,
+ &buc->refs_hash,
+ &buc->upload_sig))
+ {
+ GNUNET_break_op (0);
+ cleanup_block_upload (buc);
+ return MHD_upload_action_from_response (
+ request,
+ TALER_MHD2_make_error (TALER_EC_SYNC_INVALID_SIGNATURE,
+ NULL));
+ }
+ }
+
+ /* An append relinks the current tail: verify the uploaded relink
+ signature against the tail as it is stored now. The append
+ transaction re-checks that the tail still hashes to what this
+ signature was verified against. */
+ if (! buc->is_update && ! buc->null_prev_nonce)
+ {
+ struct GNUNET_HashCode tail_refs;
+ struct SYNC_BlockNonce tail_prev;
+ struct SYNC_BlockNonce tail_next;
+ size_t tail_data_size;
+ bool tail_is_first;
+ bool tail_is_last;
+ enum SYNC_DB_QueryStatus qs;
+
+ qs = SYNCDB_lookup_block_TR (&buc->account_pub,
+ &buc->prev_nonce,
+ &buc->prev_hash,
+ &tail_prev,
+ &tail_next,
+ &tail_data_size,
+ &tail_refs,
+ &tail_is_first,
+ &tail_is_last);
+ switch (qs)
+ {
+ case SYNC_DB_ONE_RESULT:
+ break;
+ case SYNC_DB_TARGET_MISSING:
+ case SYNC_DB_PAYMENT_REQUIRED:
+ case SYNC_DB_HARD_ERROR:
+ case SYNC_DB_SOFT_ERROR:
+ {
+ bool suspend;
+ struct MHD_Response *resp;
+
+ resp = handle_database_error (buc,
+ qs,
+ &suspend);
+ if (suspend)
+ return MHD_upload_action_suspend (request);
+ BAIL_UPLOAD_RESPONSE (request,
+ buc,
+ resp);
+ }
+ default:
+ GNUNET_break (0);
+ BAIL_UPLOAD_MALFORMED (request,
+ buc,
+ NULL);
+ }
+
+ /* Remember the tail as it was verified against, so the transaction
+ can reject the append if it changed in the meantime */
+ buc->null_prev_prev_nonce = tail_is_first;
+ if (! tail_is_first)
+ buc->prev_prev_nonce = tail_prev;
+
+ if (GNUNET_OK !=
+ SYNC_block_upload_verify (&buc->account_pub,
+ &buc->prev_nonce,
+ tail_is_first
+ ? NULL
+ : &tail_prev,
+ &buc->nonce,
+ &buc->prev_hash,
+ &buc->prev_hash,
+ &tail_refs,
+ &buc->prev_upload_sig))
+ {
+ GNUNET_break_op (0);
+ cleanup_block_upload (buc);
+ return MHD_upload_action_from_response (
+ request,
+ TALER_MHD2_make_error (TALER_EC_SYNC_INVALID_SIGNATURE,
+ NULL));
+ }
+ }
+
+ /* Check hash matches */
+ {
+ struct GNUNET_HashCode our_hash;
+
+ GNUNET_CRYPTO_hash (buc->block_data,
+ buc->block_data_len,
+ &our_hash);
+ if (0 != GNUNET_memcmp (&our_hash,
+ &buc->new_data_hash))
+ {
+ GNUNET_break_op (0);
+ cleanup_block_upload (buc);
+ return MHD_upload_action_from_response (
+ request,
+ TALER_MHD2_make_error (
+ TALER_EC_SYNC_INVALID_UPLOAD,
+ NULL));
+ }
+ }
+
+retry_db_op:
+ /* store backup to database */
+ {
+ enum SYNC_DB_QueryStatus qs;
+
+ if (! buc->is_update)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Uploading a new block\n");
+ qs = SYNCDB_append_block_TR (
+ &buc->account_pub,
+ &buc->nonce,
+ (! buc->null_prev_nonce &&
+ ! GNUNET_is_zero (&buc->prev_nonce))
+ ? &buc->prev_nonce
+ : NULL,
+ &buc->new_data_hash,
+ buc->block_data_len,
+ buc->block_data,
+ buc->refs_len,
+ buc->object_uids,
+ buc->object_incs,
+ &buc->upload_sig,
+ &buc->refs_hash,
+ (! buc->null_prev_nonce &&
+ ! GNUNET_is_zero (&buc->prev_nonce))
+ ? &buc->prev_upload_sig
+ : NULL,
+ (! buc->null_prev_nonce &&
+ ! GNUNET_is_zero (&buc->prev_nonce))
+ ? &buc->prev_hash
+ : NULL,
+ (! buc->null_prev_nonce &&
+ ! GNUNET_is_zero (&buc->prev_nonce) &&
+ ! buc->null_prev_prev_nonce)
+ ? &buc->prev_prev_nonce
+ : NULL);
+ }
+ else
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Updating an existing block\n");
+ qs = SYNCDB_update_block_TR (
+ &buc->account_pub,
+ &buc->nonce,
+ (! buc->null_prev_nonce &&
+ ! GNUNET_is_zero (&buc->prev_nonce))
+ ? &buc->prev_nonce
+ : NULL,
+ (! buc->null_next_nonce &&
+ ! GNUNET_is_zero (&buc->next_nonce))
+ ? &buc->next_nonce
+ : NULL,
+ &buc->old_data_hash,
+ &buc->new_data_hash,
+ buc->block_data_len,
+ buc->block_data,
+ buc->refs_len,
+ buc->object_uids,
+ buc->object_incs,
+ &buc->upload_sig,
+ &buc->refs_hash);
+ }
+ if (qs < 0)
+ {
+ bool suspend;
+ struct MHD_Response *resp;
+
+ resp = handle_database_error (buc,
+ qs,
+ &suspend);
+ if (suspend)
+ return MHD_upload_action_suspend (request);
+ BAIL_UPLOAD_RESPONSE (request,
+ buc,
+ resp);
+ }
+ if (0 == qs)
+ {
+ /* database says nothing actually changed, 304 (could
+ theoretically happen if another equivalent upload succeeded
+ since we last checked!) */
+ struct MHD_Response *resp;
+
+ resp = MHD_response_from_empty (MHD_HTTP_STATUS_NOT_MODIFIED);
+ TALER_MHD2_add_global_headers (resp,
+ false);
+ BAIL_UPLOAD_RESPONSE (request,
+ buc,
+ resp);
+ }
+ }
+
+ /* generate main (204) standard success reply */
+ {
+ struct MHD_Response *resp;
+
+ resp = MHD_response_from_empty (MHD_HTTP_STATUS_NO_CONTENT);
+ TALER_MHD2_add_global_headers (resp,
+ false);
+ BAIL_UPLOAD_RESPONSE (request,
+ buc,
+ resp);
+ }
+}
+
+
+const struct MHD_Action *
+SH_backup_block_post (struct MHD_Request *request,
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const char *const args[],
+ uint_fast64_t upload_size,
+ bool is_update)
+{
+ struct BlockUploadContext *buc;
+ union MHD_RequestInfoFixedData fi;
+
+ GNUNET_assert (MHD_SC_OK ==
+ MHD_request_get_info_fixed (
+ request,
+ MHD_REQUEST_INFO_FIXED_APP_CONTEXT,
+ &fi));
+ buc = *fi.v_app_context_ppvoid;
+ if (NULL == buc)
+ {
+ /* first call, setup internals */
+ buc = GNUNET_new (struct BlockUploadContext);
+ buc->hc.cc = &cleanup_block_upload_free;
+ buc->bpc.request = request;
+ buc->is_update = is_update;
+ buc->account_pub = *account_pub;
+ buc->bpc.account_pub = *account_pub;
+ *fi.v_app_context_ppvoid = buc;
+ }
+
+ if (NULL != buc->bpc.resp)
+ BAIL_WITH_RESPONSE (request,
+ buc,
+ fi,
+ buc->bpc.resp);
+
+ if (SH_upload_too_large (upload_size,
+ &buc->buff_size))
+ BAIL_WITH_ERROR (request,
+ buc,
+ fi,
+ MHD_HTTP_STATUS_CONTENT_TOO_LARGE,
+ TALER_EC_SYNC_EXCESSIVE_CONTENT_LENGTH);
+
+ {
+ struct MHD_StringNullable fresh;
+
+ if (MHD_YES ==
+ MHD_request_get_value (request,
+ MHD_VK_URI_QUERY_PARAM,
+ "fresh",
+ &fresh))
+ buc->bpc.force_fresh_order = true;
+ }
+
+ {
+ struct MHD_StringNullable im;
+
+ /* old_data_hash only provided for block update */
+ if (is_update &&
+ (MHD_YES ==
+ MHD_request_get_value (request,
+ MHD_VK_HEADER,
+ MHD_HTTP_HEADER_IF_MATCH,
+ &im)) &&
+ ( (2 >= im.len) ||
+ ('"' != im.cstr[0]) ||
+ ('"' != im.cstr[im.len - 1]) ||
+ (GNUNET_OK !=
+ GNUNET_STRINGS_string_to_data (im.cstr + 1,
+ im.len - 2,
+ &buc->old_data_hash,
+ sizeof (buc->old_data_hash))) ))
+ BAIL_WITH_ERROR (request,
+ buc,
+ fi,
+ MHD_HTTP_STATUS_BAD_REQUEST,
+ TALER_EC_SYNC_BAD_IF_MATCH);
+ }
+
+ {
+ struct MHD_StringNullable etag;
+
+ if ( (MHD_YES !=
+ MHD_request_get_value (request,
+ MHD_VK_HEADER,
+ MHD_HTTP_HEADER_IF_NONE_MATCH,
+ &etag)) ||
+ (2 >= etag.len) ||
+ ('"' != etag.cstr[0]) ||
+ ('"' != etag.cstr[etag.len - 1]) ||
+ (GNUNET_OK !=
+ GNUNET_STRINGS_string_to_data (etag.cstr + 1,
+ etag.len - 2,
+ &buc->new_data_hash,
+ sizeof (buc->new_data_hash))) )
+ {
+ BAIL_WITH_ERROR (request,
+ buc,
+ fi,
+ MHD_HTTP_STATUS_BAD_REQUEST,
+ TALER_EC_SYNC_BAD_IF_NONE_MATCH);
+ }
+ }
+
+ {
+ if (GNUNET_OK !=
+ GNUNET_STRINGS_string_to_data (args[0],
+ strlen (args[0]),
+ &buc->nonce,
+ sizeof (buc->nonce)))
+ {
+ BAIL_WITH_ERROR (request,
+ buc,
+ fi,
+ MHD_HTTP_STATUS_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED);
+ }
+ }
+
+ /* Check database to see if the transaction is permissible */
+ {
+ struct GNUNET_HashCode hc;
+ size_t data_size;
+ struct GNUNET_HashCode refs_hash;
+ bool is_first;
+ bool is_last;
+ enum SYNC_DB_QueryStatus qs;
+
+ qs = SYNCDB_lookup_block_TR (&buc->account_pub,
+ &buc->nonce,
+ &hc,
+ &buc->prev_nonce,
+ &buc->next_nonce,
+ &data_size,
+ &refs_hash,
+ &is_first,
+ &is_last);
+
+ if (qs < 0)
+ {
+ bool suspend;
+ struct MHD_Response *resp;
+
+ if ((SYNC_DB_TARGET_MISSING == qs) &&
+ (! buc->is_update))
+ {
+ /* New upload: block doesn't exist yet, that's fine */
+ qs = SYNC_DB_NO_RESULTS;
+ memset (&hc,
+ 0,
+ sizeof (hc));
+ }
+ else
+ {
+ resp = handle_database_error (buc,
+ qs,
+ &suspend);
+ if (suspend)
+ return MHD_action_suspend (request);
+ BAIL_WITH_RESPONSE (request,
+ buc,
+ fi,
+ resp);
+ }
+ }
+
+ if (0 == GNUNET_memcmp (&hc,
+ &buc->new_data_hash))
+ {
+ /* Refuse upload, we already have that backup! */
+ struct MHD_Response *resp;
+
+ resp = MHD_response_from_empty (MHD_HTTP_STATUS_NOT_MODIFIED);
+ TALER_MHD2_add_global_headers (resp,
+ false);
+ BAIL_WITH_RESPONSE (request,
+ buc,
+ fi,
+ resp);
+ }
+ }
+
+ /* Check if the client insists on paying */
+ {
+ struct MHD_StringNullable order_req;
+
+ if (MHD_YES ==
+ MHD_request_get_value (request,
+ MHD_VK_URI_QUERY_PARAM,
+ "pay",
+ &order_req))
+ {
+ struct MHD_Response *resp;
+ bool suspend;
+
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Payment requested, starting payment process\n");
+ resp = SH_begin_payment (&buc->bpc,
+ GNUNET_YES,
+ &suspend);
+ if (suspend)
+ return MHD_action_suspend (request);
+ BAIL_WITH_RESPONSE (request,
+ buc,
+ fi,
+ resp);
+ }
+ }
+
+ /* Ready to begin upload!
+
+ FIXME: taking the body in one piece makes MHD buffer it whole out
+ of the daemon-wide "large" pool, which is why sync-httpd2.c has to
+ size that pool from UPLOAD_LIMIT_MB. Switching to
+ MHD_action_process_upload() with an incremental callback would let
+ MHD stream the body instead and drop the pool dependency, at the
+ cost of this handler having to accumulate (or parse) the JSON
+ across calls. */
+ return MHD_action_process_upload_full (request,
+ buc->buff_size,
+ &handle_upload,
+ buc);
+}
diff --git a/src/sync/sync-httpd2_post-backups-KEY-blocks-NONCE.h b/src/sync/sync-httpd2_post-backups-KEY-blocks-NONCE.h
@@ -0,0 +1,46 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 Taler Systems SA
+
+ TALER 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.
+
+ 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file sync-httpd2_post-backups-KEY-blocks-NONCE.h
+ * @brief Handle POST /backups/$KEY/blocks/$NONCE requests
+ * @author Christian Grothoff
+ */
+#ifndef SYNC_HTTPD2_POST_BACKUPS_KEY_BLOCKS_NONCE_H
+#define SYNC_HTTPD2_POST_BACKUPS_KEY_BLOCKS_NONCE_H
+
+#include <microhttpd2.h>
+#include "sync/sync_service.h"
+
+
+/**
+ * Handle a POST "/backups/$KEY/blocks/$NONCE" request.
+ *
+ * @param request the MHD request to handle
+ * @param account_pub public key of the account
+ * @param args path segments: [0]=encoded NONCE
+ * @param upload_size number of bytes being uploaded
+ * @param is_update whether the operation is an update
+ * @return MHD action
+ */
+const struct MHD_Action *
+SH_backup_block_post (struct MHD_Request *request,
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const char *const args[],
+ uint_fast64_t upload_size,
+ bool is_update);
+
+
+#endif
diff --git a/src/sync/sync-httpd2_post-backups-KEY-objects-UID.c b/src/sync/sync-httpd2_post-backups-KEY-objects-UID.c
@@ -0,0 +1,313 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 Taler Systems SA
+
+ TALER 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.
+
+ 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file sync-httpd2_post-backups-KEY-objects-UID.c
+ * @brief Handle POST /backups/$KEY/objects/$UID requests
+ * @author Iván Ávalos
+ */
+#include "platform.h"
+#include "sync-httpd2.h"
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_json_lib.h>
+#include <taler/taler_json_lib.h>
+#include <taler/taler_mhd2_lib.h>
+#include <taler/taler_signatures.h>
+#include <sync/sync_signatures.h>
+#include "sync-httpd2_post-backups-KEY-objects-UID.h"
+
+
+/**
+ * Default lifetime for uploaded objects.
+ */
+#define OBJECT_DEFAULT_LIFETIME GNUNET_TIME_relative_multiply ( \
+ GNUNET_TIME_UNIT_DAYS, 30)
+
+
+/**
+ * Context for an object upload operation.
+ */
+struct ObjectUploadContext
+{
+ /**
+ * Context for cleanup logic.
+ */
+ struct TM_HandlerContext hc;
+
+ /**
+ * Buffer size to use for reading the request body.
+ */
+ size_t buff_size;
+
+ /**
+ * Public key of the account holder.
+ */
+ struct SYNC_AccountPublicKeyP account_pub;
+
+ /**
+ * UID of the object to upload.
+ */
+ struct SYNC_ObjectUID uid;
+
+ /**
+ * Object data.
+ */
+ void *data;
+
+ /**
+ * Length of @e data.
+ */
+ size_t data_size;
+
+ /**
+ * Hash of @e data.
+ */
+ struct GNUNET_HashCode data_hash;
+
+ /**
+ * Signature from the request.
+ */
+ struct SYNC_AccountSignatureP object_sig;
+};
+
+
+/**
+ * Clean up and free an object upload context.
+ *
+ * @param hc the @e hc member of the context to clean up and free
+ */
+static void
+cleanup_object_upload_free (struct TM_HandlerContext *hc)
+{
+ struct ObjectUploadContext *ouc;
+
+ ouc = (struct ObjectUploadContext *) hc;
+ GNUNET_free (ouc->data);
+ GNUNET_free (ouc);
+}
+
+
+/**
+ * Function to process data uploaded by a client.
+ *
+ * @param upload_cls our `struct ObjectUploadContext *`
+ * @param request the request being processed
+ * @param content_data_size the size of the @a content_data,
+ * zero when all data have been processed
+ * @param[in] content_data the uploaded content data
+ * @return action specifying how to proceed
+ */
+static const struct MHD_UploadAction *
+handle_object_upload (void *upload_cls,
+ struct MHD_Request *request,
+ size_t content_data_size,
+ void *content_data)
+{
+ struct ObjectUploadContext *ouc = upload_cls;
+
+ if ( (0 == content_data_size) &&
+ (NULL == content_data) )
+ {
+ GNUNET_break_op (0);
+ return MHD_upload_action_from_response (
+ request,
+ TALER_MHD2_make_error (TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ NULL));
+ }
+
+ /* parse JSON request */
+ {
+ json_error_t jerr;
+ json_t *root;
+
+ root = json_loadb (content_data,
+ content_data_size,
+ 0,
+ &jerr);
+ if (NULL == root)
+ {
+ GNUNET_break_op (0);
+ return MHD_upload_action_from_response (
+ request,
+ TALER_MHD2_make_error (TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ NULL));
+ }
+
+ struct GNUNET_JSON_Specification spec[] = {
+ GNUNET_JSON_spec_fixed_auto ("object_sig",
+ &ouc->object_sig),
+ GNUNET_JSON_spec_varsize ("data",
+ &ouc->data,
+ &ouc->data_size),
+ GNUNET_JSON_spec_end (),
+ };
+ const char *ename;
+ unsigned int eline;
+
+ if (GNUNET_OK !=
+ GNUNET_JSON_parse (root,
+ spec,
+ &ename,
+ &eline))
+ {
+ GNUNET_break_op (0);
+ json_decref (root);
+ return MHD_upload_action_from_response (
+ request,
+ TALER_MHD2_make_error (TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ NULL));
+ }
+ json_decref (root);
+ }
+
+ /* Compute hash of data */
+ GNUNET_CRYPTO_hash (ouc->data,
+ ouc->data_size,
+ &ouc->data_hash);
+
+ /* Verify signature */
+ if (GNUNET_OK !=
+ SYNC_object_upload_verify (&ouc->account_pub,
+ &ouc->uid,
+ &ouc->data_hash,
+ &ouc->object_sig))
+ {
+ GNUNET_break_op (0);
+ return MHD_upload_action_from_response (
+ request,
+ TALER_MHD2_make_error (TALER_EC_SYNC_INVALID_SIGNATURE,
+ NULL));
+ }
+
+ /* Store object */
+ {
+ enum SYNC_DB_QueryStatus qs;
+
+ qs = SYNCDB_store_object_TR (&ouc->uid,
+ &ouc->account_pub,
+ &ouc->data_hash,
+ OBJECT_DEFAULT_LIFETIME,
+ ouc->data_size,
+ ouc->data);
+ switch (qs)
+ {
+ case SYNC_DB_ONE_RESULT:
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Object stored successfully\n");
+ {
+ struct MHD_Response *resp;
+
+ resp = MHD_response_from_empty (MHD_HTTP_STATUS_NO_CONTENT);
+ TALER_MHD2_add_global_headers (resp,
+ false);
+ return MHD_upload_action_from_response (request,
+ resp);
+ }
+ case SYNC_DB_PAYMENT_REQUIRED:
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Object upload failed: payment required\n");
+ return MHD_upload_action_from_response (
+ request,
+ SYNC_MHD_MAKE_ERROR (MHD_HTTP_STATUS_PAYMENT_REQUIRED,
+ TALER_EC_SYNC_ACCOUNT_UNKNOWN,
+ NULL));
+ case SYNC_DB_HARD_ERROR:
+ GNUNET_break (0);
+ return MHD_upload_action_from_response (
+ request,
+ TALER_MHD2_make_error (TALER_EC_GENERIC_DB_COMMIT_FAILED,
+ NULL));
+ case SYNC_DB_SOFT_ERROR:
+ GNUNET_break (0);
+ return MHD_upload_action_from_response (
+ request,
+ TALER_MHD2_make_error (TALER_EC_GENERIC_DB_SOFT_FAILURE,
+ NULL));
+ default:
+ GNUNET_break (0);
+ return MHD_upload_action_from_response (
+ request,
+ TALER_MHD2_make_error (TALER_EC_GENERIC_DB_COMMIT_FAILED,
+ NULL));
+ }
+ }
+}
+
+
+const struct MHD_Action *
+SH_backup_object_post (struct MHD_Request *request,
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const char *const args[],
+ uint_fast64_t upload_size,
+ bool is_update)
+{
+ struct ObjectUploadContext *ouc;
+ union MHD_RequestInfoFixedData fi;
+
+ (void) is_update;
+
+ GNUNET_assert (MHD_SC_OK ==
+ MHD_request_get_info_fixed (
+ request,
+ MHD_REQUEST_INFO_FIXED_APP_CONTEXT,
+ &fi));
+ ouc = *fi.v_app_context_ppvoid;
+ if (NULL == ouc)
+ {
+ /* first call, setup internals */
+ ouc = GNUNET_new (struct ObjectUploadContext);
+ ouc->hc.cc = &cleanup_object_upload_free;
+ ouc->account_pub = *account_pub;
+ *fi.v_app_context_ppvoid = ouc;
+
+ /* Parse UID from URL */
+ if (GNUNET_OK !=
+ GNUNET_STRINGS_string_to_data (args[0],
+ strlen (args[0]),
+ &ouc->uid,
+ sizeof (ouc->uid)))
+ {
+ cleanup_object_upload_free (&ouc->hc);
+ *fi.v_app_context_ppvoid = NULL;
+ return MHD_action_from_response (
+ request,
+ SYNC_MHD_MAKE_ERROR (MHD_HTTP_STATUS_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "uid"));
+ }
+ }
+
+ /* Same upload limit as for blocks */
+ if (SH_upload_too_large (upload_size,
+ &ouc->buff_size))
+ {
+ cleanup_object_upload_free (&ouc->hc);
+ *fi.v_app_context_ppvoid = NULL;
+ return MHD_action_from_response (
+ request,
+ SYNC_MHD_MAKE_ERROR (MHD_HTTP_STATUS_CONTENT_TOO_LARGE,
+ TALER_EC_SYNC_EXCESSIVE_CONTENT_LENGTH,
+ NULL));
+ }
+
+ /* Ready to begin reading the body */
+ return MHD_action_process_upload_full (request,
+ ouc->buff_size,
+ &handle_object_upload,
+ ouc);
+}
+
+
+/* end of sync-httpd2_post-backups-KEY-objects-UID.c */
diff --git a/src/sync/sync-httpd2_post-backups-KEY-objects-UID.h b/src/sync/sync-httpd2_post-backups-KEY-objects-UID.h
@@ -0,0 +1,46 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 Taler Systems SA
+
+ TALER 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.
+
+ 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file sync-httpd2_post-backups-KEY-objects-UID.h
+ * @brief Handle POST /backups/$KEY/objects/$UID requests
+ * @author Christian Grothoff
+ */
+#ifndef SYNC_HTTPD2_POST_BACKUPS_KEY_OBJECTS_UID_H
+#define SYNC_HTTPD2_POST_BACKUPS_KEY_OBJECTS_UID_H
+
+#include <microhttpd2.h>
+#include "sync/sync_service.h"
+
+
+/**
+ * Handle a POST "/backups/$KEY/objects/$UID" request.
+ *
+ * @param request the MHD request to handle
+ * @param account_pub public key of the account
+ * @param args path segments: [0]="objects", [1]=encoded UID
+ * @param upload_size number of bytes being uploaded
+ * @param is_update whether the operation is an update
+ * @return MHD action
+ */
+const struct MHD_Action *
+SH_backup_object_post (struct MHD_Request *request,
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const char *const args[],
+ uint_fast64_t upload_size,
+ bool is_update);
+
+
+#endif
diff --git a/src/sync/sync-httpd_backup.c b/src/sync/sync-httpd_backup.c
@@ -1,263 +0,0 @@
-/*
- This file is part of TALER
- Copyright (C) 2019 Taler Systems SA
-
- TALER 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.
-
- 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 Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-*/
-/**
- * @file sync-httpd_backup.c
- * @brief functions to handle incoming requests for backups
- * @author Christian Grothoff
- */
-#include "platform.h"
-#include "sync-httpd.h"
-#include <gnunet/gnunet_util_lib.h>
-#include "sync-httpd_backup.h"
-
-
-/**
- * Handle request on @a connection for retrieval of the latest
- * backup of @a account.
- *
- * @param connection the MHD connection to handle
- * @param account public key of the account the request is for
- * @return MHD result code
- */
-enum MHD_Result
-SH_backup_get (struct MHD_Connection *connection,
- const struct SYNC_AccountPublicKeyP *account)
-{
- struct GNUNET_HashCode backup_hash;
- enum SYNC_DB_QueryStatus qs;
- enum MHD_Result ret;
-
- qs = SYNCDB_lookup_account_TR (
- account,
- &backup_hash);
- switch (qs)
- {
- case SYNC_DB_OLD_BACKUP_MISSING:
- GNUNET_break (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
- NULL);
- case SYNC_DB_OLD_BACKUP_MISMATCH:
- GNUNET_break (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
- NULL);
- case SYNC_DB_PAYMENT_REQUIRED:
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_NOT_FOUND,
- TALER_EC_SYNC_ACCOUNT_UNKNOWN,
- NULL);
- case SYNC_DB_HARD_ERROR:
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_FETCH_FAILED,
- NULL);
- case SYNC_DB_SOFT_ERROR:
- GNUNET_break (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_SOFT_FAILURE,
- NULL);
- case SYNC_DB_NO_RESULTS:
- {
- struct MHD_Response *resp;
-
- resp = MHD_create_response_from_buffer (0,
- NULL,
- MHD_RESPMEM_PERSISTENT);
- TALER_MHD_add_global_headers (resp,
- false);
- ret = MHD_queue_response (connection,
- MHD_HTTP_NO_CONTENT,
- resp);
- MHD_destroy_response (resp);
- }
- return ret;
- case SYNC_DB_ONE_RESULT:
- {
- const char *inm;
-
- inm = MHD_lookup_connection_value (connection,
- MHD_HEADER_KIND,
- MHD_HTTP_HEADER_IF_NONE_MATCH);
- if ( (NULL != inm) &&
- (2 < strlen (inm)) &&
- ('"' == inm[0]) &&
- ('=' == inm[strlen (inm) - 1]) )
- {
- struct GNUNET_HashCode inm_h;
-
- if (GNUNET_OK !=
- GNUNET_STRINGS_string_to_data (inm + 1,
- strlen (inm) - 2,
- &inm_h,
- sizeof (inm_h)))
- {
- GNUNET_break_op (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_SYNC_BAD_IF_NONE_MATCH,
- "Etag does not include a base32-encoded SHA-512 hash");
- }
- if (0 == GNUNET_memcmp (&inm_h,
- &backup_hash))
- {
- struct MHD_Response *resp;
-
- resp = MHD_create_response_from_buffer (0,
- NULL,
- MHD_RESPMEM_PERSISTENT);
- TALER_MHD_add_global_headers (resp,
- false);
- ret = MHD_queue_response (connection,
- MHD_HTTP_NOT_MODIFIED,
- resp);
- MHD_destroy_response (resp);
- return ret;
- }
- }
- }
- /* We have a result, should fetch and return it! */
- break;
- }
- return SH_return_backup (connection,
- account,
- MHD_HTTP_OK);
-}
-
-
-/**
- * Return the current backup of @a account on @a connection
- * using @a default_http_status on success.
- *
- * @param connection MHD connection to use
- * @param account account to query
- * @param default_http_status HTTP status to queue response
- * with on success (#MHD_HTTP_OK or #MHD_HTTP_CONFLICT)
- * @return MHD result code
- */
-enum MHD_Result
-SH_return_backup (struct MHD_Connection *connection,
- const struct SYNC_AccountPublicKeyP *account,
- unsigned int default_http_status)
-{
- enum SYNC_DB_QueryStatus qs;
- struct MHD_Response *resp;
- enum MHD_Result ret;
- struct SYNC_AccountSignatureP account_sig;
- struct GNUNET_HashCode backup_hash;
- struct GNUNET_HashCode prev_hash;
- size_t backup_size;
- void *backup;
-
- qs = SYNCDB_lookup_backup_TR (
- account,
- &account_sig,
- &prev_hash,
- &backup_hash,
- &backup_size,
- &backup);
- switch (qs)
- {
- case SYNC_DB_OLD_BACKUP_MISSING:
- GNUNET_break (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
- "unexpected return status (backup missing)");
- case SYNC_DB_OLD_BACKUP_MISMATCH:
- GNUNET_break (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
- "unexpected return status (backup mismatch)");
- case SYNC_DB_PAYMENT_REQUIRED:
- GNUNET_break (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
- "unexpected return status (payment required)");
- case SYNC_DB_HARD_ERROR:
- GNUNET_break (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_FETCH_FAILED,
- NULL);
- case SYNC_DB_SOFT_ERROR:
- GNUNET_break (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_SOFT_FAILURE,
- NULL);
- case SYNC_DB_NO_RESULTS:
- GNUNET_break (0);
- /* Note: can theoretically happen due to non-transactional nature if
- the backup expired / was gc'ed JUST between the two SQL calls.
- But too rare to handle properly, as doing a transaction would be
- expensive. Just admit to failure ;-) */
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
- NULL);
- case SYNC_DB_ONE_RESULT:
- /* interesting case below */
- break;
- }
- resp = MHD_create_response_from_buffer (backup_size,
- backup,
- MHD_RESPMEM_MUST_FREE);
- TALER_MHD_add_global_headers (resp,
- false);
- {
- char *sig_s;
- char *prev_s;
- char *etag;
- char *etagq;
-
- sig_s = GNUNET_STRINGS_data_to_string_alloc (&account_sig,
- sizeof (account_sig));
- prev_s = GNUNET_STRINGS_data_to_string_alloc (&prev_hash,
- sizeof (prev_hash));
- etag = GNUNET_STRINGS_data_to_string_alloc (&backup_hash,
- sizeof (backup_hash));
- GNUNET_break (MHD_YES ==
- MHD_add_response_header (resp,
- "Sync-Signature",
- sig_s));
- GNUNET_break (MHD_YES ==
- MHD_add_response_header (resp,
- "Sync-Previous",
- prev_s));
- GNUNET_asprintf (&etagq,
- "\"%s\"",
- etag);
- GNUNET_break (MHD_YES ==
- MHD_add_response_header (resp,
- MHD_HTTP_HEADER_ETAG,
- etagq));
- GNUNET_free (etagq);
- GNUNET_free (etag);
- GNUNET_free (prev_s);
- GNUNET_free (sig_s);
- }
- ret = MHD_queue_response (connection,
- default_http_status,
- resp);
- MHD_destroy_response (resp);
- return ret;
-}
diff --git a/src/sync/sync-httpd_backup.h b/src/sync/sync-httpd_backup.h
@@ -1,79 +0,0 @@
-/*
- This file is part of TALER
- Copyright (C) 2014, 2015, 2016 GNUnet e.V.
-
- TALER 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.
-
- 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 Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-*/
-/**
- * @file sync-httpd_backup.h
- * @brief functions to handle incoming requests on /backup/
- * @author Christian Grothoff
- */
-#ifndef SYNC_HTTPD_BACKUP_H
-#define SYNC_HTTPD_BACKUP_H
-#include <microhttpd.h>
-
-/**
- * Service is shutting down, resume all MHD connections NOW.
- */
-void
-SH_resume_all_bc (void);
-
-
-/**
- * Return the current backup of @a account on @a connection
- * using @a default_http_status on success.
- *
- * @param connection MHD connection to use
- * @param account account to query
- * @param default_http_status HTTP status to queue response
- * with on success (#MHD_HTTP_OK or #MHD_HTTP_CONFLICT)
- * @return MHD result code
- */
-enum MHD_Result
-SH_return_backup (struct MHD_Connection *connection,
- const struct SYNC_AccountPublicKeyP *account,
- unsigned int default_http_status);
-
-
-/**
- * Handle request on @a connection for retrieval of the latest
- * backup of @a account.
- *
- * @param connection the MHD connection to handle
- * @param account public key of the account the request is for
- * @return MHD result code
- */
-enum MHD_Result
-SH_backup_get (struct MHD_Connection *connection,
- const struct SYNC_AccountPublicKeyP *account);
-
-
-/**
- * Handle POST /backup requests.
- *
- * @param connection the MHD connection to handle
- * @param[in,out] con_cls the connection's closure (can be updated)
- * @param account public key of the account the request is for
- * @param upload_data upload data
- * @param[in,out] upload_data_size number of bytes (left) in @a upload_data
- * @return MHD result code
- */
-enum MHD_Result
-SH_backup_post (struct MHD_Connection *connection,
- void **con_cls,
- const struct SYNC_AccountPublicKeyP *account,
- const char *upload_data,
- size_t *upload_data_size);
-
-
-#endif
diff --git a/src/sync/sync-httpd_backup_post.c b/src/sync/sync-httpd_backup_post.c
@@ -1,1055 +0,0 @@
-/*
- This file is part of TALER
- Copyright (C) 2019 Taler Systems SA
-
- TALER 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.
-
- 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 Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-*/
-/**
- * @file sync-httpd_backup_post.c
- * @brief functions to handle incoming requests for backups
- * @author Christian Grothoff
- */
-#include "platform.h"
-struct BackupContext;
-#define TALER_MERCHANT_POST_PRIVATE_ORDERS_RESULT_CLOSURE struct BackupContext
-#define TALER_MERCHANT_GET_PRIVATE_ORDER_RESULT_CLOSURE struct BackupContext
-#include "sync-httpd.h"
-#include <gnunet/gnunet_util_lib.h>
-#include "sync-httpd_backup.h"
-#include <taler/taler_json_lib.h>
-#include <taler/taler_merchant_service.h>
-#include <taler/taler_signatures.h>
-
-
-/**
- * How long do we hold an HTTP client connection if
- * we are awaiting payment before giving up?
- */
-#define CHECK_PAYMENT_GENERIC_TIMEOUT GNUNET_TIME_relative_multiply ( \
- GNUNET_TIME_UNIT_MINUTES, 30)
-
-
-/**
- * Context for an upload operation.
- */
-struct BackupContext
-{
-
- /**
- * Context for cleanup logic.
- */
- struct TM_HandlerContext hc;
-
- /**
- * Signature of the account holder.
- */
- struct SYNC_AccountSignatureP account_sig;
-
- /**
- * Public key of the account holder.
- */
- struct SYNC_AccountPublicKeyP account;
-
- /**
- * Hash of the previous upload, or zeros if first upload.
- */
- struct GNUNET_HashCode old_backup_hash;
-
- /**
- * Hash of the upload we are receiving right now (as promised
- * by the client, to be verified!).
- */
- struct GNUNET_HashCode new_backup_hash;
-
- /**
- * Claim token, all zeros if not known. Only set if @e existing_order_id is non-NULL.
- */
- struct TALER_ClaimTokenP token;
-
- /**
- * Hash context for the upload.
- */
- struct GNUNET_HashContext *hash_ctx;
-
- /**
- * Kept in DLL for shutdown handling while suspended.
- */
- struct BackupContext *next;
-
- /**
- * Kept in DLL for shutdown handling while suspended.
- */
- struct BackupContext *prev;
-
- /**
- * Used while suspended for resumption.
- */
- struct MHD_Connection *con;
-
- /**
- * Upload, with as many bytes as we have received so far.
- */
- char *upload;
-
- /**
- * Used while we are awaiting proposal creation.
- */
- struct TALER_MERCHANT_PostPrivateOrdersHandle *po;
-
- /**
- * Used while we are waiting payment.
- */
- struct TALER_MERCHANT_GetPrivateOrderHandle *omgh;
-
- /**
- * HTTP response code to use on resume, if non-NULL.
-
- */
- struct MHD_Response *resp;
-
- /**
- * Order under which the client promised payment, or NULL.
- */
- const char *order_id;
-
- /**
- * Order ID for the client that we found in our database.
- */
- char *existing_order_id;
-
- /**
- * Timestamp of the order in @e existing_order_id. Used to
- * select the most recent unpaid offer.
- */
- struct GNUNET_TIME_Timestamp existing_order_timestamp;
-
- /**
- * Expected total upload size.
- */
- size_t upload_size;
-
- /**
- * Current offset for the upload.
- */
- size_t upload_off;
-
- /**
- * HTTP response code to use on resume, if resp is set.
- */
- unsigned int response_code;
-
- /**
- * Do not look for an existing order, force a fresh order to be created.
- */
- bool force_fresh_order;
-};
-
-
-/**
- * Kept in DLL for shutdown handling while suspended.
- */
-static struct BackupContext *bc_head;
-
-/**
- * Kept in DLL for shutdown handling while suspended.
- */
-static struct BackupContext *bc_tail;
-
-
-/**
- * Service is shutting down, resume all MHD connections NOW.
- */
-void
-SH_resume_all_bc ()
-{
- struct BackupContext *bc;
-
- while (NULL != (bc = bc_head))
- {
- GNUNET_CONTAINER_DLL_remove (bc_head,
- bc_tail,
- bc);
- MHD_resume_connection (bc->con);
- if (NULL != bc->po)
- {
- TALER_MERCHANT_post_private_orders_cancel (bc->po);
- bc->po = NULL;
- }
- if (NULL != bc->omgh)
- {
- TALER_MERCHANT_get_private_order_cancel (bc->omgh);
- bc->omgh = NULL;
- }
- }
-}
-
-
-/**
- * Function called to clean up a backup context.
- *
- * @param hc a `struct BackupContext`
- */
-static void
-cleanup_ctx (struct TM_HandlerContext *hc)
-{
- struct BackupContext *bc = (struct BackupContext *) hc;
-
- if (NULL != bc->po)
- {
- TALER_MERCHANT_post_private_orders_cancel (bc->po);
- bc->po = NULL;
- }
- if (NULL != bc->omgh)
- {
- TALER_MERCHANT_get_private_order_cancel (bc->omgh);
- bc->omgh = NULL;
- }
- if (NULL != bc->hash_ctx)
- GNUNET_CRYPTO_hash_context_abort (bc->hash_ctx);
- if (NULL != bc->resp)
- {
- MHD_destroy_response (bc->resp);
- bc->resp = NULL;
- }
- GNUNET_free (bc->existing_order_id);
- GNUNET_free (bc->upload);
- GNUNET_free (bc);
-}
-
-
-/**
- * Transmit a payment request for @a order_id on @a connection
- *
- * @param order_id our backend's order ID
- * @param token the claim token generated by the merchant (NULL if
- * it wasn't generated).
- * @return MHD response to use
- */
-static struct MHD_Response *
-make_payment_request (const char *order_id,
- const struct TALER_ClaimTokenP *token)
-{
- struct MHD_Response *resp;
-
- /* request payment via Taler */
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Creating payment request for order `%s'\n",
- order_id);
- resp = MHD_create_response_from_buffer (0,
- NULL,
- MHD_RESPMEM_PERSISTENT);
- TALER_MHD_add_global_headers (resp,
- false);
- {
- char *hdr;
- const char *pfx;
- char *hn;
- struct GNUNET_Buffer hdr_buf = { 0 };
-
- if (0 == strncasecmp ("https://",
- SH_backend_url,
- strlen ("https://")))
- {
- pfx = "taler://";
- hn = &SH_backend_url[strlen ("https://")];
- }
- else if (0 == strncasecmp ("http://",
- SH_backend_url,
- strlen ("http://")))
- {
- pfx = "taler+http://";
- hn = &SH_backend_url[strlen ("http://")];
- }
- else
- {
- GNUNET_break (0);
- MHD_destroy_response (resp);
- return NULL;
- }
- if (0 == strlen (hn))
- {
- GNUNET_break (0);
- MHD_destroy_response (resp);
- return NULL;
- }
-
- GNUNET_buffer_write_str (&hdr_buf, pfx);
- GNUNET_buffer_write_str (&hdr_buf, "pay/");
- GNUNET_buffer_write_str (&hdr_buf, hn);
- GNUNET_buffer_write_path (&hdr_buf, order_id);
- /* No session ID */
- GNUNET_buffer_write_path (&hdr_buf, "");
- if (NULL != token)
- {
- GNUNET_buffer_write_str (&hdr_buf, "?c=");
- GNUNET_buffer_write_data_encoded (&hdr_buf, token, sizeof (*token));
- }
- hdr = GNUNET_buffer_reap_str (&hdr_buf);
- GNUNET_break (MHD_YES ==
- MHD_add_response_header (resp,
- "Taler",
- hdr));
- GNUNET_free (hdr);
- }
- return resp;
-}
-
-
-/**
- * Callbacks of this type are used to serve the result of submitting a
- * /contract request to a merchant.
- *
- * @param cls our `struct BackupContext`
- * @param por response details
- */
-static void
-proposal_cb (
- struct BackupContext *bc,
- const struct TALER_MERCHANT_PostPrivateOrdersResponse *por)
-{
- enum SYNC_DB_QueryStatus qs;
-
- bc->po = NULL;
- GNUNET_CONTAINER_DLL_remove (bc_head,
- bc_tail,
- bc);
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Resuming connection with order `%s'\n",
- bc->order_id);
- MHD_resume_connection (bc->con);
- SH_trigger_daemon ();
- if (MHD_HTTP_OK != por->hr.http_status)
- {
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Backend returned status %u/%u\n",
- por->hr.http_status,
- (unsigned int) por->hr.ec);
- GNUNET_break_op (0);
- bc->resp = TALER_MHD_MAKE_JSON_PACK (
- TALER_JSON_pack_ec (TALER_EC_SYNC_PAYMENT_CREATE_BACKEND_ERROR),
- GNUNET_JSON_pack_uint64 ("backend-ec",
- por->hr.ec),
- GNUNET_JSON_pack_uint64 ("backend-http-status",
- por->hr.http_status),
- GNUNET_JSON_pack_allow_null (
- GNUNET_JSON_pack_object_incref ("backend-reply",
- (json_t *) por->hr.reply)));
- bc->response_code = MHD_HTTP_BAD_GATEWAY;
- return;
- }
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Storing payment request for order `%s'\n",
- por->details.ok.order_id);
- qs = SYNCDB_store_payment_TR (
- &bc->account,
- por->details.ok.order_id,
- por->details.ok.token,
- &SH_annual_fee);
- if (0 >= qs)
- {
- GNUNET_break (0);
- bc->resp = TALER_MHD_make_error (TALER_EC_GENERIC_DB_STORE_FAILED,
- "Failed to persist payment request in sync database");
- GNUNET_assert (NULL != bc->resp);
- bc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR;
- return;
- }
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Obtained fresh order `%s'\n",
- por->details.ok.order_id);
- bc->resp = make_payment_request (por->details.ok.order_id,
- por->details.ok.token);
- GNUNET_assert (NULL != bc->resp);
- bc->response_code = MHD_HTTP_PAYMENT_REQUIRED;
-}
-
-
-/**
- * Function called on all pending payments for the right
- * account.
- *
- * @param cls closure, our `struct BackupContext`
- * @param timestamp for how long have we been waiting
- * @param order_id order id in the backend
- * @param token claim token to use (or NULL for none)
- * @param amount how much is the order for
- */
-static void
-ongoing_payment_cb (void *cls,
- struct GNUNET_TIME_Timestamp timestamp,
- const char *order_id,
- const struct TALER_ClaimTokenP *token,
- const struct TALER_Amount *amount)
-{
- struct BackupContext *bc = cls;
-
- (void) amount;
- if (0 != TALER_amount_cmp (amount,
- &SH_annual_fee))
- return; /* can't reuse, fees changed */
- if ( (NULL == bc->existing_order_id) ||
- (GNUNET_TIME_timestamp_cmp (bc->existing_order_timestamp,
- <,
- timestamp)) )
- {
- GNUNET_free (bc->existing_order_id);
- bc->existing_order_id = GNUNET_strdup (order_id);
- bc->existing_order_timestamp = timestamp;
- if (NULL != token)
- bc->token = *token;
- }
-}
-
-
-/**
- * Callback to process a GET /check-payment request
- *
- * @param cls our `struct BackupContext`
- * @param osr order status
- */
-static void
-check_payment_cb (struct BackupContext *bc,
- const struct TALER_MERCHANT_GetPrivateOrderResponse *osr)
-{
- const struct TALER_MERCHANT_HttpResponse *hr = &osr->hr;
-
- /* refunds are not supported, verify */
- bc->omgh = NULL;
- GNUNET_CONTAINER_DLL_remove (bc_head,
- bc_tail,
- bc);
- MHD_resume_connection (bc->con);
- SH_trigger_daemon ();
- switch (hr->http_status)
- {
- case 0:
- /* Likely timeout, complain! */
- bc->response_code = MHD_HTTP_GATEWAY_TIMEOUT;
- bc->resp = TALER_MHD_make_error (
- TALER_EC_SYNC_GENERIC_BACKEND_TIMEOUT,
- NULL);
- return;
- case MHD_HTTP_OK:
- break; /* handled below */
- default:
- /* Unexpected backend response */
- bc->response_code = MHD_HTTP_BAD_GATEWAY;
- bc->resp = TALER_MHD_MAKE_JSON_PACK (
- GNUNET_JSON_pack_uint64 ("code",
- TALER_EC_SYNC_GENERIC_BACKEND_ERROR),
- GNUNET_JSON_pack_string ("hint",
- TALER_ErrorCode_get_hint (
- TALER_EC_SYNC_GENERIC_BACKEND_ERROR)),
- GNUNET_JSON_pack_uint64 ("backend-ec",
- (json_int_t) hr->ec),
- GNUNET_JSON_pack_uint64 ("backend-http-status",
- (json_int_t) hr->http_status),
- GNUNET_JSON_pack_allow_null (
- GNUNET_JSON_pack_object_incref ("backend-reply",
- (json_t *) hr->reply)));
- return;
- }
-
- GNUNET_assert (MHD_HTTP_OK == hr->http_status);
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Payment status checked: %d\n",
- osr->details.ok.status);
- switch (osr->details.ok.status)
- {
- case TALER_MERCHANT_OSC_PAID:
- {
- enum SYNC_DB_QueryStatus qs;
-
- qs = SYNCDB_increment_lifetime_TR (
- &bc->account,
- bc->order_id,
- GNUNET_TIME_UNIT_YEARS); /* always annual */
- if (0 <= qs)
- return; /* continue as planned */
- GNUNET_break (0);
- bc->resp = TALER_MHD_make_error (TALER_EC_GENERIC_DB_STORE_FAILED,
- "increment lifetime");
- GNUNET_assert (NULL != bc->resp);
- bc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR;
- return; /* continue as planned */
- }
- case TALER_MERCHANT_OSC_UNPAID:
- case TALER_MERCHANT_OSC_CLAIMED:
- break;
- }
- if (NULL != bc->existing_order_id)
- {
- /* repeat payment request */
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Repeating payment request\n");
- bc->resp = make_payment_request (bc->existing_order_id,
- (GNUNET_YES == GNUNET_is_zero (&bc->token))
- ? NULL
- : &bc->token);
- GNUNET_assert (NULL != bc->resp);
- bc->response_code = MHD_HTTP_PAYMENT_REQUIRED;
- return;
- }
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Timeout waiting for payment\n");
- bc->resp = TALER_MHD_make_error (TALER_EC_SYNC_PAYMENT_GENERIC_TIMEOUT,
- "Timeout awaiting promised payment");
- GNUNET_assert (NULL != bc->resp);
- bc->response_code = MHD_HTTP_REQUEST_TIMEOUT;
-}
-
-
-/**
- * Helper function used to ask our backend to await
- * a payment for the user's account.
- *
- * @param bc context to begin payment for.
- * @param timeout when to give up trying
- * @param order_id which order to check for the payment
- */
-static void
-await_payment (struct BackupContext *bc,
- struct GNUNET_TIME_Relative timeout,
- const char *order_id)
-{
- GNUNET_CONTAINER_DLL_insert (bc_head,
- bc_tail,
- bc);
- MHD_suspend_connection (bc->con);
- bc->order_id = order_id;
- bc->omgh = TALER_MERCHANT_get_private_order_create (
- SH_ctx,
- SH_backend_url,
- order_id);
- GNUNET_assert (GNUNET_OK ==
- TALER_MERCHANT_get_private_order_set_options (
- bc->omgh,
- TALER_MERCHANT_get_private_order_option_timeout (timeout)));
- GNUNET_assert (TALER_EC_NONE ==
- TALER_MERCHANT_get_private_order_start (
- bc->omgh,
- &check_payment_cb,
- bc));
- SH_trigger_curl ();
-}
-
-
-/**
- * Helper function used to ask our backend to begin
- * processing a payment for the user's account.
- * May perform asynchronous operations by suspending the connection
- * if required.
- *
- * @param bc context to begin payment for.
- * @param pay_req #GNUNET_YES if payment was explicitly requested,
- * #GNUNET_NO if payment is needed
- * @return MHD status code
- */
-static enum MHD_Result
-begin_payment (struct BackupContext *bc,
- int pay_req)
-{
- if (! bc->force_fresh_order)
- {
- enum GNUNET_DB_QueryStatus qs;
-
- qs = SYNCDB_lookup_pending_payments_by_account_TR (
- &bc->account,
- &ongoing_payment_cb,
- bc);
- if (qs < 0)
- {
- struct MHD_Response *resp;
- enum MHD_Result ret;
-
- resp = TALER_MHD_make_error (TALER_EC_GENERIC_DB_FETCH_FAILED,
- "pending payments");
- ret = MHD_queue_response (bc->con,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- resp);
- GNUNET_break (MHD_YES == ret);
- MHD_destroy_response (resp);
- return ret;
- }
- if (NULL != bc->existing_order_id)
- {
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Have existing order, waiting for `%s' to complete\n",
- bc->existing_order_id);
- await_payment (bc,
- GNUNET_TIME_UNIT_ZERO /* no long polling */,
- bc->existing_order_id);
- return MHD_YES;
- }
- }
- GNUNET_CONTAINER_DLL_insert (bc_head,
- bc_tail,
- bc);
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Suspending connection while creating order at `%s'\n",
- SH_backend_url);
- MHD_suspend_connection (bc->con);
- {
- json_t *order;
-
- order = GNUNET_JSON_PACK (
- TALER_JSON_pack_amount ("amount",
- &SH_annual_fee),
- GNUNET_JSON_pack_string ("summary",
- "annual fee for sync service"),
- GNUNET_JSON_pack_string ("fulfillment_url",
- SH_fulfillment_url));
- bc->po = TALER_MERCHANT_post_private_orders_create (SH_ctx,
- SH_backend_url,
- order);
- json_decref (order);
- }
- GNUNET_assert (TALER_EC_NONE ==
- TALER_MERCHANT_post_private_orders_start (
- bc->po,
- &proposal_cb,
- bc));
- SH_trigger_curl ();
- return MHD_YES;
-}
-
-
-/**
- * We got some query status from the DB. Handle the error cases.
- * May perform asynchronous operations by suspending the connection
- * if required.
- *
- * @param bc connection to handle status for
- * @param qs query status to handle
- * @return #MHD_YES or #MHD_NO
- */
-static enum MHD_Result
-handle_database_error (struct BackupContext *bc,
- enum SYNC_DB_QueryStatus qs)
-{
- switch (qs)
- {
- case SYNC_DB_OLD_BACKUP_MISSING:
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Update failed: no existing backup\n");
- return TALER_MHD_reply_with_error (bc->con,
- MHD_HTTP_NOT_FOUND,
- TALER_EC_SYNC_PREVIOUS_BACKUP_UNKNOWN,
- NULL);
- case SYNC_DB_OLD_BACKUP_MISMATCH:
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Conflict detected, returning existing backup\n");
- return SH_return_backup (bc->con,
- &bc->account,
- MHD_HTTP_CONFLICT);
- case SYNC_DB_PAYMENT_REQUIRED:
- {
- const char *order_id;
-
- order_id = MHD_lookup_connection_value (bc->con,
- MHD_GET_ARGUMENT_KIND,
- "paying");
- if (NULL == order_id)
- {
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Payment required, starting payment process\n");
- return begin_payment (bc,
- GNUNET_NO);
- }
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Payment required, awaiting completion of `%s'\n",
- order_id);
- await_payment (bc,
- CHECK_PAYMENT_GENERIC_TIMEOUT,
- order_id);
- }
- return MHD_YES;
- case SYNC_DB_HARD_ERROR:
- GNUNET_break (0);
- return TALER_MHD_reply_with_error (bc->con,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_COMMIT_FAILED,
- NULL);
- case SYNC_DB_SOFT_ERROR:
- GNUNET_break (0);
- return TALER_MHD_reply_with_error (bc->con,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_SOFT_FAILURE,
- NULL);
- case SYNC_DB_NO_RESULTS:
- GNUNET_assert (0);
- return MHD_NO;
- /* intentional fall-through! */
- case SYNC_DB_ONE_RESULT:
- GNUNET_assert (0);
- return MHD_NO;
- }
- GNUNET_break (0);
- return MHD_NO;
-}
-
-
-enum MHD_Result
-SH_backup_post (struct MHD_Connection *connection,
- void **con_cls,
- const struct SYNC_AccountPublicKeyP *account,
- const char *upload_data,
- size_t *upload_data_size)
-{
- struct BackupContext *bc;
-
- bc = *con_cls;
- if (NULL == bc)
- {
- /* first call, setup internals */
- bc = GNUNET_new (struct BackupContext);
- bc->hc.cc = &cleanup_ctx;
- bc->con = connection;
- bc->account = *account;
- {
- const char *fresh;
-
- fresh = MHD_lookup_connection_value (connection,
- MHD_GET_ARGUMENT_KIND,
- "fresh");
- if (NULL != fresh)
- bc->force_fresh_order = true;
- }
- *con_cls = bc;
-
- /* now setup 'bc' */
- {
- const char *lens;
- unsigned long len;
-
- lens = MHD_lookup_connection_value (connection,
- MHD_HEADER_KIND,
- MHD_HTTP_HEADER_CONTENT_LENGTH);
- if ( (NULL == lens) ||
- (1 !=
- sscanf (lens,
- "%lu",
- &len)) )
- {
- GNUNET_break_op (0);
- return TALER_MHD_reply_with_error (
- connection,
- MHD_HTTP_BAD_REQUEST,
- (NULL == lens)
- ? TALER_EC_SYNC_MALFORMED_CONTENT_LENGTH
- : TALER_EC_SYNC_MISSING_CONTENT_LENGTH,
- lens);
- }
- if (len / 1024 / 1024 >= SH_upload_limit_mb)
- {
- GNUNET_break_op (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_PAYLOAD_TOO_LARGE,
- TALER_EC_SYNC_EXCESSIVE_CONTENT_LENGTH,
- NULL);
- }
- bc->upload = GNUNET_malloc_large (len);
- if (NULL == bc->upload)
- {
- GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
- "malloc");
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_PAYLOAD_TOO_LARGE,
- TALER_EC_SYNC_OUT_OF_MEMORY_ON_CONTENT_LENGTH,
- NULL);
- }
- bc->upload_size = (size_t) len;
- }
- {
- const char *im;
-
- im = MHD_lookup_connection_value (connection,
- MHD_HEADER_KIND,
- MHD_HTTP_HEADER_IF_MATCH);
- if (NULL != im)
- {
- if ( (2 >= strlen (im)) ||
- ('"' != im[0]) ||
- ('"' != im[strlen (im) - 1]) ||
- (GNUNET_OK !=
- GNUNET_STRINGS_string_to_data (im + 1,
- strlen (im) - 2,
- &bc->old_backup_hash,
- sizeof (bc->old_backup_hash))) )
- {
- GNUNET_break_op (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_SYNC_BAD_IF_MATCH,
- NULL);
- }
- }
- }
- {
- const char *sig_s;
-
- sig_s = MHD_lookup_connection_value (connection,
- MHD_HEADER_KIND,
- "Sync-Signature");
- if ( (NULL == sig_s) ||
- (GNUNET_OK !=
- GNUNET_STRINGS_string_to_data (sig_s,
- strlen (sig_s),
- &bc->account_sig,
- sizeof (bc->account_sig))) )
- {
- GNUNET_break_op (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_SYNC_BAD_SYNC_SIGNATURE,
- NULL);
- }
- }
- {
- const char *etag;
-
- etag = MHD_lookup_connection_value (connection,
- MHD_HEADER_KIND,
- MHD_HTTP_HEADER_IF_NONE_MATCH);
- if ( (NULL == etag) ||
- (2 >= strlen (etag)) ||
- ('"' != etag[0]) ||
- ('"' != etag[strlen (etag) - 1]) ||
- (GNUNET_OK !=
- GNUNET_STRINGS_string_to_data (etag + 1,
- strlen (etag) - 2,
- &bc->new_backup_hash,
- sizeof (bc->new_backup_hash))) )
- {
- GNUNET_break_op (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_SYNC_BAD_IF_NONE_MATCH,
- NULL);
- }
- }
- /* validate signature */
- {
- struct SYNC_UploadSignaturePS usp = {
- .purpose.size = htonl (sizeof (usp)),
- .purpose.purpose = htonl (TALER_SIGNATURE_SYNC_BACKUP_UPLOAD),
- .old_backup_hash = bc->old_backup_hash,
- .new_backup_hash = bc->new_backup_hash
- };
-
- if (GNUNET_OK !=
- GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_SYNC_BACKUP_UPLOAD,
- &usp,
- &bc->account_sig.eddsa_sig,
- &account->eddsa_pub))
- {
- GNUNET_break_op (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_FORBIDDEN,
- TALER_EC_SYNC_INVALID_SIGNATURE,
- NULL);
- }
- }
- /* get ready to hash (done here as we may go async for payments next) */
- bc->hash_ctx = GNUNET_CRYPTO_hash_context_start ();
-
- /* Check database to see if the transaction is permissible */
- {
- struct GNUNET_HashCode hc;
- enum SYNC_DB_QueryStatus qs;
-
- qs = SYNCDB_lookup_account_TR (
- account,
- &hc);
- if (qs < 0)
- return handle_database_error (bc,
- qs);
- if (SYNC_DB_NO_RESULTS == qs)
- memset (&hc, 0, sizeof (hc));
- if (0 == GNUNET_memcmp (&hc,
- &bc->new_backup_hash))
- {
- /* Refuse upload: we already have that backup! */
- struct MHD_Response *resp;
- enum MHD_Result ret;
-
- resp = MHD_create_response_from_buffer (0,
- NULL,
- MHD_RESPMEM_PERSISTENT);
- TALER_MHD_add_global_headers (resp,
- false);
- ret = MHD_queue_response (connection,
- MHD_HTTP_NOT_MODIFIED,
- resp);
- GNUNET_break (MHD_YES == ret);
- MHD_destroy_response (resp);
- return ret;
- }
- if (0 != GNUNET_memcmp (&hc,
- &bc->old_backup_hash))
- {
- /* Refuse upload: if-none-match failed! */
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Conflict detected, returning existing backup\n");
- return SH_return_backup (connection,
- account,
- MHD_HTTP_CONFLICT);
- }
- }
- /* check if the client insists on paying */
- {
- const char *order_req;
-
- order_req = MHD_lookup_connection_value (connection,
- MHD_GET_ARGUMENT_KIND,
- "pay");
- if (NULL != order_req)
- {
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Payment requested, starting payment process\n");
- return begin_payment (bc,
- GNUNET_YES);
- }
- }
- /* ready to begin! */
- return MHD_YES;
- }
- /* handle upload */
- if (0 != *upload_data_size)
- {
- /* check MHD invariant */
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Processing %u bytes of upload data\n",
- (unsigned int) *upload_data_size);
- GNUNET_assert (bc->upload_off + *upload_data_size <= bc->upload_size);
- memcpy (&bc->upload[bc->upload_off],
- upload_data,
- *upload_data_size);
- bc->upload_off += *upload_data_size;
- GNUNET_CRYPTO_hash_context_read (bc->hash_ctx,
- upload_data,
- *upload_data_size);
- *upload_data_size = 0;
- return MHD_YES;
- }
- else if ( (0 == bc->upload_off) &&
- (0 != bc->upload_size) &&
- (NULL == bc->resp) )
- {
- /* wait for upload */
- return MHD_YES;
- }
- if (NULL != bc->resp)
- {
- enum MHD_Result ret;
-
- /* We generated a response asynchronously, queue that */
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Returning asynchronously generated response with HTTP status %u\n",
- bc->response_code);
- ret = MHD_queue_response (connection,
- bc->response_code,
- bc->resp);
- GNUNET_break (MHD_YES == ret);
- MHD_destroy_response (bc->resp);
- bc->resp = NULL;
- return ret;
- }
-
- /* finished with upload, check hash */
- {
- struct GNUNET_HashCode our_hash;
-
- GNUNET_CRYPTO_hash_context_finish (bc->hash_ctx,
- &our_hash);
- bc->hash_ctx = NULL;
- if (0 != GNUNET_memcmp (&our_hash,
- &bc->new_backup_hash))
- {
- GNUNET_break_op (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_SYNC_INVALID_UPLOAD,
- NULL);
- }
- }
-
- /* store backup to database */
- {
- enum SYNC_DB_QueryStatus qs;
-
- if (GNUNET_YES == GNUNET_is_zero (&bc->old_backup_hash))
- {
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Uploading first backup to account\n");
- qs = SYNCDB_store_backup_TR (
- account,
- &bc->account_sig,
- &bc->new_backup_hash,
- bc->upload_size,
- bc->upload);
- }
- else
- {
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Uploading existing backup of account\n");
- qs = SYNCDB_update_backup_TR (
- account,
- &bc->old_backup_hash,
- &bc->account_sig,
- &bc->new_backup_hash,
- bc->upload_size,
- bc->upload);
- }
- if (qs < 0)
- return handle_database_error (bc,
- qs);
- if (0 == qs)
- {
- /* database says nothing actually changed, 304 (could
- theoretically happen if another equivalent upload succeeded
- since we last checked!) */
- struct MHD_Response *resp;
- enum MHD_Result ret;
-
- resp = MHD_create_response_from_buffer (0,
- NULL,
- MHD_RESPMEM_PERSISTENT);
- TALER_MHD_add_global_headers (resp,
- false);
- ret = MHD_queue_response (connection,
- MHD_HTTP_NOT_MODIFIED,
- resp);
- GNUNET_break (MHD_YES == ret);
- MHD_destroy_response (resp);
- return ret;
- }
- }
-
- /* generate main (204) standard success reply */
- {
- struct MHD_Response *resp;
- enum MHD_Result ret;
-
- resp = MHD_create_response_from_buffer (0,
- NULL,
- MHD_RESPMEM_PERSISTENT);
- TALER_MHD_add_global_headers (resp,
- false);
- ret = MHD_queue_response (connection,
- MHD_HTTP_NO_CONTENT,
- resp);
- GNUNET_break (MHD_YES == ret);
- MHD_destroy_response (resp);
- return ret;
- }
-}
diff --git a/src/sync/sync-httpd_config.c b/src/sync/sync-httpd_config.c
@@ -1,68 +0,0 @@
-/*
- This file is part of Sync
- Copyright (C) 2020,2024 Taler Systems SA
-
- Sync is free software; you can redistribute it and/or modify it under the
- terms of the GNU Lesser General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- Sync 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
- Sync; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/>
-*/
-/**
- * @file sync/sync-httpd_config.c
- * @brief headers for /config handler
- * @author Christian Grothoff
- */
-#include "platform.h"
-#include "sync-httpd_config.h"
-#include <taler/taler_json_lib.h>
-
-
-/*
- * Protocol version history:
- *
- * 0: original design
- * 1: adds ?fresh=y to POST backup operation to force fresh contract
- * to be created
- */
-
-enum MHD_Result
-SH_handler_config (struct SH_RequestHandler *rh,
- struct MHD_Connection *connection,
- void **connection_cls,
- const char *upload_data,
- size_t *upload_data_size)
-{
- static struct MHD_Response *response;
-
- (void) connection_cls;
- (void) upload_data;
- (void) upload_data_size;
- if (NULL == response)
- {
- response = TALER_MHD_MAKE_JSON_PACK (
- GNUNET_JSON_pack_string ("name",
- "sync"),
- GNUNET_JSON_pack_string ("implementation",
- "urn:net:taler:specs:sync:c-reference"),
- GNUNET_JSON_pack_uint64 ("storage_limit_in_megabytes",
- SH_upload_limit_mb),
- TALER_JSON_pack_amount ("liability_limit",
- &SH_insurance),
- TALER_JSON_pack_amount ("annual_fee",
- &SH_annual_fee),
- GNUNET_JSON_pack_string ("version",
- "2:3:0"));
- }
- return MHD_queue_response (connection,
- MHD_HTTP_OK,
- response);
-}
-
-
-/* end of sync-httpd_config.c */
diff --git a/src/sync/sync-httpd_config.h b/src/sync/sync-httpd_config.h
@@ -1,45 +0,0 @@
-/*
- This file is part of Sync
- Copyright (C) 2020 Taler Systems SA
-
- Sync is free software; you can redistribute it and/or modify it under the
- terms of the GNU Lesser General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- Sync 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
- Sync; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/>
-*/
-/**
- * @file sync/sync-httpd_config.h
- * @brief headers for /config handler
- * @author Christian Grothoff
- */
-#ifndef SYNC_HTTPD_CONFIG_H
-#define SYNC_HTTPD_CONFIG_H
-#include <microhttpd.h>
-#include "sync-httpd.h"
-
-/**
- * Manages a /config call.
- *
- * @param rh context of the handler
- * @param connection the MHD connection to handle
- * @param[in,out] connection_cls the connection's closure (can be updated)
- * @param upload_data upload data
- * @param[in,out] upload_data_size number of bytes (left) in @a upload_data
- * @return MHD result code
- */
-enum MHD_Result
-SH_handler_config (struct SH_RequestHandler *rh,
- struct MHD_Connection *connection,
- void **connection_cls,
- const char *upload_data,
- size_t *upload_data_size);
-
-#endif
-
-/* end of sync-httpd_config.h */
diff --git a/src/sync/sync-httpd_mhd.c b/src/sync/sync-httpd_mhd.c
@@ -1,66 +0,0 @@
-/*
- This file is part of TALER
- Copyright (C) 2014, 2015, 2016 Taler Systems SA
-
- TALER 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.
-
- 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 Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-*/
-/**
- * @file sync-httpd_mhd.c
- * @brief helpers for MHD interaction; these are TALER_EXCHANGE_handler_ functions
- * that generate simple MHD replies that do not require any real operations
- * to be performed (error handling, static pages, etc.)
- * @author Florian Dold
- * @author Benedikt Mueller
- * @author Christian Grothoff
- */
-#include "platform.h"
-#include <jansson.h>
-#include "sync-httpd_mhd.h"
-
-
-enum MHD_Result
-SH_MHD_handler_static_response (struct SH_RequestHandler *rh,
- struct MHD_Connection *connection,
- void **connection_cls,
- const char *upload_data,
- size_t *upload_data_size)
-{
- (void) connection_cls;
- (void) upload_data;
- (void) upload_data_size;
- if (0 == rh->data_size)
- rh->data_size = strlen ((const char *) rh->data);
- return TALER_MHD_reply_static (connection,
- rh->response_code,
- rh->mime_type,
- (void *) rh->data,
- rh->data_size);
-}
-
-
-enum MHD_Result
-SH_MHD_handler_agpl_redirect (struct SH_RequestHandler *rh,
- struct MHD_Connection *connection,
- void **connection_cls,
- const char *upload_data,
- size_t *upload_data_size)
-{
- (void) rh;
- (void) connection_cls;
- (void) upload_data;
- (void) upload_data_size;
- return TALER_MHD_reply_agpl (connection,
- "http://www.git.taler.net/sync.git");
-}
-
-
-/* end of sync-httpd_mhd.c */
diff --git a/src/sync/sync-httpd_mhd.h b/src/sync/sync-httpd_mhd.h
@@ -1,69 +0,0 @@
-/*
- This file is part of TALER
- Copyright (C) 2014, 2015 GNUnet e.V. and INRIA
-
- TALER 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.
-
- 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 Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-*/
-
-/**
- * @file sync-httpd_mhd.h
- * @brief helpers for MHD interaction, used to generate simple responses
- * @author Florian Dold
- * @author Benedikt Mueller
- * @author Christian Grothoff
- */
-#ifndef sync_HTTPD_MHD_H
-#define sync_HTTPD_MHD_H
-#include <gnunet/gnunet_util_lib.h>
-#include <microhttpd.h>
-#include "sync-httpd.h"
-
-
-/**
- * Function to call to handle the request by sending
- * back static data from the @a rh.
- *
- * @param rh context of the handler
- * @param connection the MHD connection to handle
- * @param[in,out] connection_cls the connection's closure (can be updated)
- * @param upload_data upload data
- * @param[in,out] upload_data_size number of bytes (left) in @a upload_data
- * @return MHD result code
- */
-enum MHD_Result
-SH_MHD_handler_static_response (struct SH_RequestHandler *rh,
- struct MHD_Connection *connection,
- void **connection_cls,
- const char *upload_data,
- size_t *upload_data_size);
-
-
-/**
- * Function to call to handle the request by sending
- * back a redirect to the AGPL source code.
- *
- * @param rh context of the handler
- * @param connection the MHD connection to handle
- * @param[in,out] connection_cls the connection's closure (can be updated)
- * @param upload_data upload data
- * @param[in,out] upload_data_size number of bytes (left) in @a upload_data
- * @return MHD result code
- */
-enum MHD_Result
-SH_MHD_handler_agpl_redirect (struct SH_RequestHandler *rh,
- struct MHD_Connection *connection,
- void **connection_cls,
- const char *upload_data,
- size_t *upload_data_size);
-
-
-#endif
diff --git a/src/syncdb/Makefile.sql b/src/syncdb/Makefile.sql
@@ -1,4 +1,6 @@
-procedures.sql: procedures.sql.in syncdb_*_TR.sql
+# Note: syncdb_*.sql covers the per-operation _TR files as well as the
+# shared helpers (syncdb_object_refcounts.sql, syncdb_account_check.sql).
+procedures.sql: procedures.sql.in syncdb_*.sql
gcc -E -P -undef - < procedures.sql.in 2>/dev/null | sed -e "s/--.*//" | awk 'NF' - >$@
CLEANFILES = \
diff --git a/src/syncdb/meson.build b/src/syncdb/meson.build
@@ -22,13 +22,18 @@ libsyncdb_SOURCES = [
'syncdb_create_tables.c',
'syncdb_preflight.c',
'syncdb_gc.c',
- 'syncdb_store_payment_TR.c',
- 'syncdb_lookup_pending_payments_by_account_TR.c',
- 'syncdb_store_backup_TR.c',
- 'syncdb_update_backup_TR.c',
+ 'syncdb_append_block_TR.c',
+ 'syncdb_delete_block_TR.c',
+ 'syncdb_update_block_TR.c',
'syncdb_lookup_account_TR.c',
- 'syncdb_lookup_backup_TR.c',
+ 'syncdb_lookup_block_range_TR.c',
+ 'syncdb_lookup_block_TR.c',
'syncdb_increment_lifetime_TR.c',
+ 'syncdb_lookup_pending_payments_by_account.c',
+ 'syncdb_store_payment_TR.c',
+ 'syncdb_store_object_TR.c',
+ 'syncdb_lookup_object.c',
+ 'syncdb_lookup_object_uid.c',
]
libsyncdb = library(
'syncdb',
diff --git a/src/syncdb/procedures.sql.in b/src/syncdb/procedures.sql.in
@@ -18,9 +18,16 @@ BEGIN;
SET search_path TO sync;
+#include "syncdb_object_refcounts.sql"
+#include "syncdb_account_check.sql"
#include "syncdb_increment_lifetime_TR.sql"
-#include "syncdb_lookup_account_TR.sql"
-#include "syncdb_store_backup_TR.sql"
-#include "syncdb_update_backup_TR.sql"
+#include "syncdb_store_payment_TR.sql"
+#include "syncdb_store_object_TR.sql"
+#include "syncdb_append_block_TR.sql"
+#include "syncdb_delete_block_TR.sql"
+#include "syncdb_update_block_TR.sql"
+#include "syncdb_lookup_block_range_TR.sql"
+#include "syncdb_lookup_block_TR.sql"
+#include "syncdb_gc.sql"
COMMIT;
diff --git a/src/syncdb/sync-0001.sql b/src/syncdb/sync-0001.sql
@@ -33,6 +33,35 @@ CREATE TYPE taler_amount
COMMENT ON TYPE taler_amount
IS 'Stores an amount, fraction is in units of 1/100000000 of the base value';
+CREATE TYPE sync_do_lookup_block_range_return_type
+ AS
+ (out_account_missing BOOLEAN
+ ,out_block_missing BOOLEAN
+ ,out_backup_empty BOOLEAN
+ ,nonce BYTEA
+ ,block_hash BYTEA
+ ,prev_nonce BYTEA
+ ,next_nonce BYTEA
+ ,data_blob BYTEA
+ ,upload_sig BYTEA
+ ,old_hash BYTEA
+ ,refs_hash BYTEA
+ );
+
+CREATE TYPE sync_do_lookup_block_return_type
+ AS
+ (out_account_missing BOOLEAN
+ ,out_block_missing BOOLEAN
+ ,out_is_first BOOLEAN
+ ,out_is_last BOOLEAN
+ ,nonce BYTEA
+ ,block_hash BYTEA
+ ,prev_nonce BYTEA
+ ,next_nonce BYTEA
+ ,data_size INT8
+ ,refs_hash BYTEA
+ );
+
CREATE TABLE IF NOT EXISTS accounts
(account_pub BYTEA PRIMARY KEY CHECK (length(account_pub)=32)
,expiration_date INT8 NOT NULL);
@@ -40,26 +69,43 @@ CREATE TABLE IF NOT EXISTS accounts
CREATE INDEX IF NOT EXISTS accounts_expire ON
accounts (expiration_date);
-
CREATE TABLE IF NOT EXISTS payments
- (account_pub BYTEA CHECK (length(account_pub)=32)
+ (account_pub BYTEA REFERENCES accounts (account_pub) ON DELETE CASCADE
,order_id TEXT PRIMARY KEY
- ,token BYTEA CHECK (length(token)=16)
- ,timestamp INT8 NOT NULL
+ ,claim_token BYTEA CHECK (length(claim_token)=16)
+ ,order_timestamp INT8 NOT NULL
,amount taler_amount NOT NULL
,paid BOOLEAN NOT NULL DEFAULT FALSE);
CREATE INDEX IF NOT EXISTS payments_timestamp ON
- payments (paid,timestamp);
+ payments (paid, order_timestamp);
+
+CREATE TABLE IF NOT EXISTS blocks
+ (nonce BYTEA PRIMARY KEY CHECK (length(nonce)=24)
+ ,account_pub BYTEA REFERENCES accounts (account_pub) ON DELETE CASCADE
+ ,block_hash BYTEA NOT NULL CHECK (length(block_hash)=64)
+ ,prev_nonce BYTEA REFERENCES blocks (nonce) ON DELETE RESTRICT
+ ,next_nonce BYTEA REFERENCES blocks (nonce) ON DELETE RESTRICT
+ ,data_blob BYTEA NOT NULL
+ ,upload_sig BYTEA NOT NULL DEFAULT ''
+ ,old_hash BYTEA NOT NULL DEFAULT ''
+ ,refs_hash BYTEA NOT NULL DEFAULT '');
+ALTER TABLE IF EXISTS accounts
+ ADD COLUMN IF NOT EXISTS first_block BYTEA REFERENCES blocks (nonce) ON DELETE CASCADE,
+ ADD COLUMN IF NOT EXISTS last_block BYTEA REFERENCES blocks (nonce) ON DELETE CASCADE;
-CREATE TABLE IF NOT EXISTS backups
- (account_pub BYTEA PRIMARY KEY REFERENCES accounts (account_pub) ON DELETE CASCADE
- ,account_sig BYTEA NOT NULL CHECK (length(account_sig)=64)
- ,prev_hash BYTEA NOT NULL CHECK (length(prev_hash)=64)
- ,backup_hash BYTEA NOT NULL CHECK (length(backup_hash)=64)
- ,data BYTEA NOT NULL);
+CREATE TABLE IF NOT EXISTS objects
+ (uid BYTEA PRIMARY KEY CHECK (length(uid)=64)
+ ,account_pub BYTEA REFERENCES accounts (account_pub) ON DELETE CASCADE
+ ,object_hash BYTEA NOT NULL CHECK (length(object_hash)=64)
+ ,refcount INT8 NOT NULL DEFAULT 0 CHECK (refcount >= 0)
+ ,expiration_date INT8
+ ,data_blob BYTEA NOT NULL);
+CREATE INDEX IF NOT EXISTS objects_expire ON
+ objects (expiration_date)
+ WHERE refcount = 0;
-- Complete transaction
COMMIT;
diff --git a/src/syncdb/sync-dbinit.c b/src/syncdb/sync-dbinit.c
@@ -54,9 +54,11 @@ run (void *cls,
const char *cfgfile,
const struct GNUNET_CONFIGURATION_Handle *cfg)
{
+ (void) cls;
+ (void) args;
+ (void) cfgfile;
if (GNUNET_OK !=
- SYNCDB_init (cfg,
- true))
+ SYNCDB_init_admin (cfg))
{
fprintf (stderr,
"Failed to initialize database.\n");
@@ -82,15 +84,21 @@ run (void *cls,
{
struct GNUNET_TIME_Absolute now;
struct GNUNET_TIME_Absolute ancient;
+ struct GNUNET_TIME_Absolute recent;
now = GNUNET_TIME_absolute_get ();
ancient = GNUNET_TIME_absolute_subtract (now,
GNUNET_TIME_relative_multiply (
GNUNET_TIME_UNIT_YEARS,
6));
- if (0 >
+ recent = GNUNET_TIME_absolute_subtract (now,
+ GNUNET_TIME_relative_multiply (
+ GNUNET_TIME_UNIT_DAYS,
+ 1));
+ if (GNUNET_OK !=
SYNCDB_gc (now,
- ancient))
+ ancient,
+ recent))
{
fprintf (stderr,
"Garbage collection failed!\n");
diff --git a/src/syncdb/sync_db_plugin.c b/src/syncdb/sync_db_plugin.c
@@ -1,87 +0,0 @@
-/*
- This file is part of TALER
- Copyright (C) 2019 Taler Systems SA
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU Lesser 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/>
-*/
-/**
- * @file syncdb/sync_db_plugin.c
- * @brief Logic to load database plugin
- * @author Christian Grothoff
- * @author Sree Harsha Totakura <sreeharsha@totakura.in>
- */
-#include "platform.h"
-#include "sync/sync_util.h"
-#include "sync/sync_database_lib.h"
-#include <ltdl.h>
-
-
-struct SYNC_DatabasePlugin *
-SYNC_DB_plugin_load (const struct GNUNET_CONFIGURATION_Handle *cfg,
- bool skip_preflight)
-{
- char *plugin_name;
- char *lib_name;
- struct SYNC_DatabasePlugin *plugin;
-
- if (GNUNET_SYSERR ==
- GNUNET_CONFIGURATION_get_value_string (cfg,
- "sync",
- "db",
- &plugin_name))
- {
- GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
- "sync",
- "db");
- return NULL;
- }
- (void) GNUNET_asprintf (&lib_name,
- "libsync_plugin_db_%s",
- plugin_name);
- GNUNET_free (plugin_name);
- plugin = GNUNET_PLUGIN_load (SYNC_project_data (),
- lib_name,
- (void *) cfg);
- if (NULL == plugin)
- {
- GNUNET_free (lib_name);
- return NULL;
- }
- plugin->library_name = lib_name;
- if ( (! skip_preflight) &&
- (GNUNET_OK !=
- plugin->preflight (plugin->cls)) )
- {
- GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
- "Database not ready. Try running sync-dbinit!\n");
- SYNC_DB_plugin_unload (plugin);
- return NULL;
- }
- return plugin;
-}
-
-
-void
-SYNC_DB_plugin_unload (struct SYNC_DatabasePlugin *plugin)
-{
- char *lib_name;
-
- if (NULL == plugin)
- return;
- lib_name = plugin->library_name;
- GNUNET_assert (NULL == GNUNET_PLUGIN_unload (lib_name,
- plugin));
- GNUNET_free (lib_name);
-}
-
-
-/* end of sync_db_plugin.c */
diff --git a/src/syncdb/syncdb_account_check.sql b/src/syncdb/syncdb_account_check.sql
@@ -0,0 +1,55 @@
+--
+-- 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/>
+--
+
+--
+-- Helper to check that an account exists and has a valid (non-expired) payment.
+-- Returns TRUE in out_account_missing if the account is missing or expired.
+--
+
+DROP FUNCTION IF EXISTS sync_check_account_payment;
+CREATE FUNCTION sync_check_account_payment(
+ IN in_account_pub BYTEA)
+RETURNS TABLE (
+ out_account_missing BOOLEAN)
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ my_expiration_date INT8;
+BEGIN
+ out_account_missing := FALSE;
+
+ SELECT expiration_date
+ INTO my_expiration_date
+ FROM accounts
+ WHERE account_pub = in_account_pub;
+
+ IF NOT FOUND
+ THEN
+ out_account_missing := TRUE;
+ RETURN NEXT;
+ RETURN;
+ END IF;
+
+ IF my_expiration_date <= (EXTRACT(EPOCH FROM NOW()) * 1000000)::INT8
+ THEN
+ out_account_missing := TRUE;
+ RETURN NEXT;
+ RETURN;
+ END IF;
+
+ RETURN NEXT;
+ RETURN;
+END $$;
diff --git a/src/syncdb/syncdb_append_block_TR.c b/src/syncdb/syncdb_append_block_TR.c
@@ -0,0 +1,133 @@
+/*
+ This file is part of TALER
+ (C) 2026 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Lesser 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/>
+*/
+/**
+ * @file syncdb/syncdb_append_block_TR.c
+ * @brief append block at the end of DLL
+ * @author Iván Ávalos
+ */
+#include "syncdb_pg.h"
+
+enum SYNC_DB_QueryStatus
+SYNCDB_append_block_TR (
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const struct SYNC_BlockNonce *block_nonce,
+ const struct SYNC_BlockNonce *prev_nonce,
+ const struct GNUNET_HashCode *data_hash,
+ size_t data_size,
+ const void *data,
+ unsigned int refs_length,
+ const struct SYNC_ObjectUID *object_uids,
+ const int16_t *object_incs,
+ const struct SYNC_AccountSignatureP *upload_sig,
+ const struct GNUNET_HashCode *refs_hash,
+ const struct SYNC_AccountSignatureP *prev_upload_sig,
+ const struct GNUNET_HashCode *prev_hash,
+ const struct SYNC_BlockNonce *prev_prev_nonce)
+{
+ bool account_missing;
+ bool outdated_append;
+ bool negative_refcount;
+ bool refs_missing;
+ bool inserted;
+ bool refs_updated;
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (account_pub),
+ GNUNET_PQ_query_param_auto_from_type (block_nonce),
+ (NULL != prev_nonce)
+ ? GNUNET_PQ_query_param_auto_from_type (prev_nonce)
+ : GNUNET_PQ_query_param_null (),
+ GNUNET_PQ_query_param_auto_from_type (data_hash),
+ GNUNET_PQ_query_param_fixed_size (data,
+ data_size),
+ GNUNET_PQ_query_param_array_auto_from_type (refs_length,
+ object_uids,
+ pg->conn),
+ /* FIXME: add int16 support to GNUnet */
+ GNUNET_PQ_query_param_array_uint16 (refs_length,
+ (const uint16_t *) object_incs,
+ pg->conn),
+ GNUNET_PQ_query_param_auto_from_type (upload_sig),
+ GNUNET_PQ_query_param_auto_from_type (refs_hash),
+ (NULL != prev_upload_sig)
+ ? GNUNET_PQ_query_param_auto_from_type (prev_upload_sig)
+ : GNUNET_PQ_query_param_null (),
+ (NULL != prev_hash)
+ ? GNUNET_PQ_query_param_auto_from_type (prev_hash)
+ : GNUNET_PQ_query_param_null (),
+ (NULL != prev_prev_nonce)
+ ? GNUNET_PQ_query_param_auto_from_type (prev_prev_nonce)
+ : GNUNET_PQ_query_param_null (),
+ GNUNET_PQ_query_param_end,
+ };
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_bool ("out_account_missing",
+ &account_missing),
+ GNUNET_PQ_result_spec_bool ("out_outdated_append",
+ &outdated_append),
+ GNUNET_PQ_result_spec_bool ("out_negative_refcount",
+ &negative_refcount),
+ GNUNET_PQ_result_spec_bool ("out_refs_missing",
+ &refs_missing),
+ GNUNET_PQ_result_spec_bool ("out_inserted",
+ &inserted),
+ GNUNET_PQ_result_spec_bool ("out_refs_updated",
+ &refs_updated),
+ GNUNET_PQ_result_spec_end,
+ };
+ enum GNUNET_DB_QueryStatus qs;
+
+ SYNCDB_preflight ();
+ PREPARE ("do_append_block",
+ "SELECT"
+ " out_account_missing"
+ ",out_outdated_append"
+ ",out_negative_refcount"
+ ",out_refs_missing"
+ ",out_inserted"
+ ",out_refs_updated"
+ " FROM sync_do_append_block"
+ " ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)");
+ qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "do_append_block",
+ 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:
+ GNUNET_break (0);
+ return SYNC_DB_HARD_ERROR;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ if (inserted && refs_updated)
+ return SYNC_DB_ONE_RESULT;
+ if (account_missing)
+ return SYNC_DB_PAYMENT_REQUIRED;
+ if (outdated_append)
+ return SYNC_DB_OUTDATED_WRITE;
+ if (negative_refcount || refs_missing)
+ return SYNC_DB_INCONSISTENT_BACKUP;
+ /* Nothing else failed, so the INSERT hit the primary key */
+ return SYNC_DB_OUTDATED_WRITE;
+ default:
+ GNUNET_break (0);
+ return SYNC_DB_HARD_ERROR;
+ }
+}
diff --git a/src/syncdb/syncdb_append_block_TR.sql b/src/syncdb/syncdb_append_block_TR.sql
@@ -0,0 +1,200 @@
+--
+-- 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/>
+--
+
+-- The append relinks the previous tail, so the client uploads the
+-- tail's new signature (in_prev_upload_sig). That signature covers the
+-- tail as the caller read it outside this transaction, so the whole
+-- signed context has to be re-checked here or a concurrent write could
+-- leave the tail with a signature over links it no longer has: the
+-- transaction re-checks the tail against in_prev_hash and
+-- in_prev_prev_nonce. The signed refs_hash needs no separate check,
+-- since sync_do_update_block() is the only thing that changes it and it
+-- always changes the block hash along with it.
+DROP FUNCTION IF EXISTS sync_do_append_block;
+CREATE FUNCTION sync_do_append_block (
+ IN in_account_pub BYTEA,
+ IN in_curr_nonce BYTEA,
+ IN in_prev_nonce BYTEA,
+ IN in_data_hash BYTEA,
+ IN in_data_blob BYTEA,
+ IN ina_object_uids BYTEA[],
+ IN ina_object_incs INT2[],
+ IN in_upload_sig BYTEA,
+ IN in_refs_hash BYTEA,
+ IN in_prev_upload_sig BYTEA,
+ IN in_prev_hash BYTEA,
+ IN in_prev_prev_nonce BYTEA,
+ OUT out_account_missing BOOLEAN,
+ OUT out_outdated_append BOOLEAN,
+ OUT out_negative_refcount BOOLEAN,
+ OUT out_refs_missing BOOLEAN,
+ OUT out_inserted BOOLEAN,
+ OUT out_refs_updated BOOLEAN)
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ my_last_block_nonce BYTEA;
+ my_prev_hash BYTEA;
+ my_prev_prev_nonce BYTEA;
+BEGIN
+ out_account_missing := FALSE;
+ out_negative_refcount := FALSE;
+ out_refs_missing := FALSE;
+ out_outdated_append := FALSE;
+ out_inserted := FALSE;
+ out_refs_updated := FALSE;
+
+ -- Check if account exists and has valid payment
+ SELECT a.out_account_missing
+ INTO out_account_missing
+ FROM sync_check_account_payment(in_account_pub) AS a;
+ IF out_account_missing
+ THEN
+ RETURN;
+ END IF;
+
+ -- Get current tail of the DLL
+ SELECT last_block
+ INTO my_last_block_nonce
+ FROM accounts
+ WHERE account_pub = in_account_pub;
+
+ -- Ensure that block is being appended to the end
+ -- (or beginning if backup is empty)
+ IF NOT (in_prev_nonce IS NOT DISTINCT FROM my_last_block_nonce)
+ THEN
+ out_outdated_append := TRUE;
+ RETURN;
+ END IF;
+
+ IF in_prev_nonce IS NOT NULL
+ THEN
+ -- The tail must still look the way the relink signature was
+ -- verified against (see header comment). Its next_nonce needs no
+ -- check: it is the account's last block, so it has none, and this
+ -- transaction is what sets it.
+ SELECT block_hash
+ ,prev_nonce
+ INTO my_prev_hash
+ ,my_prev_prev_nonce
+ FROM blocks
+ WHERE account_pub = in_account_pub
+ AND nonce = in_prev_nonce;
+
+ IF NOT FOUND
+ OR my_prev_hash IS DISTINCT FROM in_prev_hash
+ OR my_prev_prev_nonce IS DISTINCT FROM in_prev_prev_nonce
+ THEN
+ out_outdated_append := TRUE;
+ RETURN;
+ END IF;
+ END IF;
+
+ -- Check referenced objects exist and no increment underflows,
+ -- before we touch anything
+ SELECT c.out_negative_refcount
+ ,c.out_refs_missing
+ INTO out_negative_refcount
+ ,out_refs_missing
+ FROM sync_check_object_refcounts_pre(in_account_pub,
+ ina_object_uids,
+ ina_object_incs) AS c;
+ IF out_negative_refcount OR out_refs_missing
+ THEN
+ RETURN;
+ END IF;
+
+ -- Try to append the new block
+ INSERT INTO blocks
+ (nonce
+ ,account_pub
+ ,block_hash
+ ,prev_nonce
+ ,data_blob
+ ,upload_sig
+ ,old_hash
+ ,refs_hash)
+ VALUES
+ (in_curr_nonce
+ ,in_account_pub
+ ,in_data_hash
+ ,in_prev_nonce
+ ,in_data_blob
+ ,in_upload_sig
+ ,decode(repeat('00', 64), 'hex')
+ ,in_refs_hash)
+ ON CONFLICT DO NOTHING;
+
+ IF NOT FOUND
+ THEN
+ out_inserted := FALSE;
+ RETURN;
+ ELSE
+ out_inserted := TRUE;
+ END IF;
+
+ -- Update the reference in the previous block; it was checked to be
+ -- this account's tail above. Store the tail's relink signature with
+ -- it; a relink never changes the data, so old_hash is set to the
+ -- current block hash.
+ IF in_prev_nonce IS NOT NULL
+ THEN
+ UPDATE blocks
+ SET next_nonce = in_curr_nonce
+ ,upload_sig = in_prev_upload_sig
+ ,old_hash = block_hash
+ WHERE account_pub = in_account_pub
+ AND nonce = in_prev_nonce;
+
+ IF NOT FOUND
+ THEN
+ RAISE EXCEPTION 'previous block % vanished for account %',
+ in_prev_nonce, in_account_pub;
+ END IF;
+ END IF;
+
+ -- Update reference to first and last block in the backup
+ UPDATE accounts
+ SET last_block = in_curr_nonce
+ ,first_block = CASE
+ -- if first block in the backup, update reference
+ WHEN in_prev_nonce IS NULL
+ THEN in_curr_nonce
+ -- otherwise, keep reference intact
+ ELSE first_block
+ END
+ WHERE account_pub = in_account_pub;
+
+ IF NOT FOUND
+ THEN
+ RAISE EXCEPTION 'account % vanished', in_account_pub;
+ END IF;
+
+ -- Update object refcounts and delete unreferenced objects; the
+ -- pre-check above means this can only fail on a concurrent change
+ SELECT u.out_negative_refcount
+ ,u.out_refs_updated
+ INTO out_negative_refcount
+ ,out_refs_updated
+ FROM sync_update_object_refcounts(in_account_pub,
+ ina_object_uids,
+ ina_object_incs) AS u;
+ IF NOT out_refs_updated
+ THEN
+ RAISE EXCEPTION 'object refcounts changed concurrently for account %',
+ in_account_pub;
+ END IF;
+END $$;
diff --git a/src/syncdb/syncdb_create_tables.c b/src/syncdb/syncdb_create_tables.c
@@ -24,24 +24,27 @@
enum GNUNET_GenericReturnValue
SYNCDB_create_tables (void)
{
- struct GNUNET_PQ_Context *conn;
- struct GNUNET_PQ_ExecuteStatement es[] = {
- 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",
- "sync-",
- es,
- NULL);
- if (NULL == conn)
+ if (GNUNET_SYSERR ==
+ GNUNET_PQ_load_versioning (pg->conn))
+ {
+ GNUNET_break (0);
+ return GNUNET_SYSERR;
+ }
+ if (GNUNET_OK !=
+ GNUNET_PQ_run_sql (pg->conn,
+ "sync-"))
+ {
+ GNUNET_break (0);
+ return GNUNET_SYSERR;
+ }
+ if (GNUNET_OK !=
+ GNUNET_PQ_exec_sql (pg->conn,
+ "procedures"))
+ {
+ GNUNET_break (0);
return GNUNET_SYSERR;
- ret = GNUNET_PQ_exec_sql (conn,
- "procedures");
- GNUNET_PQ_disconnect (conn);
- return ret;
+ }
+ return GNUNET_OK;
}
diff --git a/src/syncdb/syncdb_delete_block_TR.c b/src/syncdb/syncdb_delete_block_TR.c
@@ -0,0 +1,150 @@
+/*
+ This file is part of TALER
+ (C) 2026 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Lesser 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/>
+*/
+/**
+ * @file syncdb/syncdb_delete_block_TR.c
+ * @brief delete a block in the DLL
+ * @author Iván Ávalos
+ */
+#include "syncdb_pg.h"
+
+
+enum SYNC_DB_QueryStatus
+SYNCDB_delete_block_TR (
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const struct SYNC_BlockNonce *block_nonce,
+ const struct SYNC_BlockNonce *prev_nonce,
+ const struct SYNC_BlockNonce *next_nonce,
+ const struct GNUNET_HashCode *block_hash,
+ unsigned int refs_length,
+ const struct SYNC_ObjectUID *object_uids,
+ const int16_t *object_incs,
+ const struct SYNC_AccountSignatureP *prev_upload_sig,
+ const struct GNUNET_HashCode *prev_hash,
+ const struct SYNC_BlockNonce *prev_prev_nonce,
+ const struct SYNC_AccountSignatureP *next_upload_sig,
+ const struct GNUNET_HashCode *next_hash,
+ const struct SYNC_BlockNonce *next_next_nonce)
+{
+ bool account_missing;
+ bool block_missing;
+ bool outdated_write;
+ bool refs_missing;
+ bool negative_refcount;
+ bool deleted;
+ bool refs_updated;
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (account_pub),
+ GNUNET_PQ_query_param_auto_from_type (block_nonce),
+ (NULL != prev_nonce)
+ ? GNUNET_PQ_query_param_auto_from_type (prev_nonce)
+ : GNUNET_PQ_query_param_null (),
+ (NULL != next_nonce)
+ ? GNUNET_PQ_query_param_auto_from_type (next_nonce)
+ : GNUNET_PQ_query_param_null (),
+ (NULL != block_hash)
+ ? GNUNET_PQ_query_param_auto_from_type (block_hash)
+ : GNUNET_PQ_query_param_null (),
+ GNUNET_PQ_query_param_array_auto_from_type (refs_length,
+ object_uids,
+ pg->conn),
+ GNUNET_PQ_query_param_array_uint16 (refs_length,
+ (const uint16_t *) object_incs,
+ pg->conn),
+ (NULL != prev_upload_sig)
+ ? GNUNET_PQ_query_param_auto_from_type (prev_upload_sig)
+ : GNUNET_PQ_query_param_null (),
+ (NULL != prev_hash)
+ ? GNUNET_PQ_query_param_auto_from_type (prev_hash)
+ : GNUNET_PQ_query_param_null (),
+ (NULL != prev_prev_nonce)
+ ? GNUNET_PQ_query_param_auto_from_type (prev_prev_nonce)
+ : GNUNET_PQ_query_param_null (),
+ (NULL != next_upload_sig)
+ ? GNUNET_PQ_query_param_auto_from_type (next_upload_sig)
+ : GNUNET_PQ_query_param_null (),
+ (NULL != next_hash)
+ ? GNUNET_PQ_query_param_auto_from_type (next_hash)
+ : GNUNET_PQ_query_param_null (),
+ (NULL != next_next_nonce)
+ ? GNUNET_PQ_query_param_auto_from_type (next_next_nonce)
+ : GNUNET_PQ_query_param_null (),
+ GNUNET_PQ_query_param_end,
+ };
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_bool ("out_account_missing",
+ &account_missing),
+ GNUNET_PQ_result_spec_bool ("out_block_missing",
+ &block_missing),
+ GNUNET_PQ_result_spec_bool ("out_outdated_write",
+ &outdated_write),
+ GNUNET_PQ_result_spec_bool ("out_refs_missing",
+ &refs_missing),
+ GNUNET_PQ_result_spec_bool ("out_negative_refcount",
+ &negative_refcount),
+ GNUNET_PQ_result_spec_bool ("out_deleted",
+ &deleted),
+ GNUNET_PQ_result_spec_bool ("out_refs_updated",
+ &refs_updated),
+ GNUNET_PQ_result_spec_end,
+ };
+
+ enum GNUNET_DB_QueryStatus qs;
+
+ SYNCDB_preflight ();
+ PREPARE ("do_delete_block",
+ "SELECT"
+ " out_account_missing"
+ ",out_block_missing"
+ ",out_outdated_write"
+ ",out_refs_missing"
+ ",out_negative_refcount"
+ ",out_deleted"
+ ",out_refs_updated"
+ " FROM sync_do_delete_block"
+ " ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)");
+ qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "do_delete_block",
+ 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:
+ GNUNET_break (0);
+ return SYNC_DB_HARD_ERROR;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ if (deleted)
+ return SYNC_DB_ONE_RESULT;
+ if (account_missing)
+ return SYNC_DB_PAYMENT_REQUIRED;
+ if (block_missing)
+ return SYNC_DB_TARGET_MISSING;
+ if (outdated_write)
+ return SYNC_DB_OUTDATED_WRITE;
+ if (refs_missing || negative_refcount)
+ return SYNC_DB_INCONSISTENT_BACKUP;
+ GNUNET_break (0);
+ return SYNC_DB_HARD_ERROR;
+ default:
+ GNUNET_break (0);
+ return SYNC_DB_HARD_ERROR;
+ }
+}
diff --git a/src/syncdb/syncdb_delete_block_TR.sql b/src/syncdb/syncdb_delete_block_TR.sql
@@ -0,0 +1,235 @@
+--
+-- 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/>
+--
+
+-- The deletion relinks both neighbours, so the client uploads their new
+-- signatures (in_prev_upload_sig, in_next_upload_sig). Those signatures
+-- cover the neighbours as the caller read them outside this
+-- transaction, so the whole signed context has to be re-checked here or
+-- a concurrent write could leave a neighbour with a signature over links
+-- it no longer has: the transaction re-checks them against in_prev_hash
+-- and in_prev_prev_nonce, resp. in_next_hash and in_next_next_nonce.
+-- The signed refs_hash needs no separate check, since
+-- sync_do_update_block() is the only thing that changes it and it always
+-- changes the block hash along with it.
+DROP FUNCTION IF EXISTS sync_do_delete_block;
+CREATE FUNCTION sync_do_delete_block (
+ IN in_account_pub BYTEA,
+ IN in_block_nonce BYTEA,
+ IN in_prev_nonce BYTEA,
+ IN in_next_nonce BYTEA,
+ IN in_block_hash BYTEA,
+ IN ina_object_uids BYTEA[],
+ IN ina_object_incs INT2[],
+ IN in_prev_upload_sig BYTEA,
+ IN in_prev_hash BYTEA,
+ IN in_prev_prev_nonce BYTEA,
+ IN in_next_upload_sig BYTEA,
+ IN in_next_hash BYTEA,
+ IN in_next_next_nonce BYTEA,
+ OUT out_account_missing BOOLEAN,
+ OUT out_block_missing BOOLEAN,
+ OUT out_outdated_write BOOLEAN,
+ OUT out_refs_missing BOOLEAN,
+ OUT out_negative_refcount BOOLEAN,
+ OUT out_deleted BOOLEAN,
+ OUT out_refs_updated BOOLEAN)
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ my_prev_nonce BYTEA;
+ my_next_nonce BYTEA;
+ my_block_hash BYTEA;
+ my_n_hash BYTEA;
+ my_n_link BYTEA;
+BEGIN
+ out_account_missing := FALSE;
+ out_block_missing := FALSE;
+ out_outdated_write := FALSE;
+ out_refs_missing := FALSE;
+ out_negative_refcount := FALSE;
+ out_deleted := FALSE;
+ out_refs_updated := FALSE;
+
+ -- Check if account exists and has valid payment
+ SELECT a.out_account_missing
+ INTO out_account_missing
+ FROM sync_check_account_payment(in_account_pub) AS a;
+ IF out_account_missing
+ THEN
+ RETURN;
+ END IF;
+
+ SELECT prev_nonce
+ ,next_nonce
+ ,block_hash
+ INTO my_prev_nonce
+ ,my_next_nonce
+ ,my_block_hash
+ FROM blocks
+ WHERE account_pub = in_account_pub
+ AND nonce = in_block_nonce;
+
+ IF NOT FOUND
+ THEN
+ out_block_missing := TRUE;
+ RETURN;
+ END IF;
+
+ -- Does existing hash matches expected old hash?
+ -- Does previous and next nonces match expectations?
+ IF my_block_hash IS DISTINCT FROM in_block_hash OR
+ my_prev_nonce IS DISTINCT FROM in_prev_nonce OR
+ my_next_nonce IS DISTINCT FROM in_next_nonce
+ THEN
+ out_outdated_write := TRUE;
+ RETURN;
+ END IF;
+
+ -- The neighbours must still look the way the relink signatures were
+ -- verified against (see header comment). Only the link this
+ -- transaction does not rewrite is checked; the one it does rewrite is
+ -- the block being deleted, which was checked above.
+ IF in_prev_nonce IS NOT NULL
+ THEN
+ SELECT block_hash
+ ,prev_nonce
+ INTO my_n_hash
+ ,my_n_link
+ FROM blocks
+ WHERE account_pub = in_account_pub
+ AND nonce = in_prev_nonce;
+
+ IF NOT FOUND
+ OR my_n_hash IS DISTINCT FROM in_prev_hash
+ OR my_n_link IS DISTINCT FROM in_prev_prev_nonce
+ THEN
+ out_outdated_write := TRUE;
+ RETURN;
+ END IF;
+ END IF;
+
+ IF in_next_nonce IS NOT NULL
+ THEN
+ SELECT block_hash
+ ,next_nonce
+ INTO my_n_hash
+ ,my_n_link
+ FROM blocks
+ WHERE account_pub = in_account_pub
+ AND nonce = in_next_nonce;
+
+ IF NOT FOUND
+ OR my_n_hash IS DISTINCT FROM in_next_hash
+ OR my_n_link IS DISTINCT FROM in_next_next_nonce
+ THEN
+ out_outdated_write := TRUE;
+ RETURN;
+ END IF;
+ END IF;
+
+ -- Check referenced objects exist and no increment underflows,
+ -- before we touch anything
+ SELECT c.out_negative_refcount
+ ,c.out_refs_missing
+ INTO out_negative_refcount
+ ,out_refs_missing
+ FROM sync_check_object_refcounts_pre(in_account_pub,
+ ina_object_uids,
+ ina_object_incs) AS c;
+ IF out_negative_refcount OR out_refs_missing
+ THEN
+ RETURN;
+ END IF;
+
+ -- Update previous block's next reference (if necessary); the
+ -- neighbours come from the block we are deleting, so they must exist.
+ -- Store the relink signature with the neighbour; a relink never
+ -- changes the data, so old_hash is set to the current block hash.
+ IF my_prev_nonce IS NOT NULL
+ THEN
+ UPDATE blocks
+ SET next_nonce = my_next_nonce
+ ,upload_sig = in_prev_upload_sig
+ ,old_hash = block_hash
+ WHERE account_pub = in_account_pub
+ AND nonce = my_prev_nonce;
+
+ IF NOT FOUND
+ THEN
+ RAISE EXCEPTION 'previous block % vanished for account %',
+ my_prev_nonce, in_account_pub;
+ END IF;
+ END IF;
+
+ -- Update next block's previous reference (if necessary)
+ IF my_next_nonce IS NOT NULL
+ THEN
+ UPDATE blocks
+ SET prev_nonce = my_prev_nonce
+ ,upload_sig = in_next_upload_sig
+ ,old_hash = block_hash
+ WHERE account_pub = in_account_pub
+ AND nonce = my_next_nonce;
+
+ IF NOT FOUND
+ THEN
+ RAISE EXCEPTION 'next block % vanished for account %',
+ my_next_nonce, in_account_pub;
+ END IF;
+ END IF;
+
+ -- Update first and last block references (if necessary)
+ UPDATE accounts
+ SET first_block = CASE
+ WHEN my_prev_nonce IS NULL
+ THEN my_next_nonce
+ ELSE first_block
+ END
+ ,last_block = CASE
+ WHEN my_next_nonce IS NULL
+ THEN my_prev_nonce
+ ELSE last_block
+ END
+ WHERE account_pub = in_account_pub;
+
+ -- Delete block
+ DELETE FROM blocks
+ WHERE account_pub = in_account_pub
+ AND nonce = in_block_nonce;
+
+ IF NOT FOUND
+ THEN
+ RAISE EXCEPTION 'block % vanished for account %',
+ in_block_nonce, in_account_pub;
+ END IF;
+
+ -- Update object refcounts and delete unreferenced objects; the
+ -- pre-check above means this can only fail on a concurrent change
+ SELECT u.out_negative_refcount
+ ,u.out_refs_updated
+ INTO out_negative_refcount
+ ,out_refs_updated
+ FROM sync_update_object_refcounts(in_account_pub,
+ ina_object_uids,
+ ina_object_incs) AS u;
+ IF NOT out_refs_updated
+ THEN
+ RAISE EXCEPTION 'object refcounts changed concurrently for account %',
+ in_account_pub;
+ END IF;
+
+ out_deleted := TRUE;
+END $$;
diff --git a/src/syncdb/syncdb_drop_tables.c b/src/syncdb/syncdb_drop_tables.c
@@ -24,25 +24,8 @@
enum GNUNET_GenericReturnValue
SYNCDB_drop_tables (void)
{
- struct GNUNET_PQ_Context *conn;
- enum GNUNET_GenericReturnValue ret;
-
- if (NULL != pg->conn)
- {
- GNUNET_PQ_disconnect (pg->conn);
- pg->conn = NULL;
- }
- conn = GNUNET_PQ_connect_with_cfg (pg->cfg,
- "syncdb-postgres",
- NULL,
- NULL,
- NULL);
- if (NULL == conn)
- return GNUNET_SYSERR;
- ret = GNUNET_PQ_exec_sql (conn,
- "drop");
- GNUNET_PQ_disconnect (conn);
- return ret;
+ return GNUNET_PQ_exec_sql (pg->conn,
+ "drop");
}
diff --git a/src/syncdb/syncdb_gc.c b/src/syncdb/syncdb_gc.c
@@ -21,39 +21,34 @@
#include "syncdb_pg.h"
-enum GNUNET_DB_QueryStatus
+enum GNUNET_GenericReturnValue
SYNCDB_gc (struct GNUNET_TIME_Absolute expire_backups,
- struct GNUNET_TIME_Absolute expire_pending_payments)
+ struct GNUNET_TIME_Absolute expire_pending_payments,
+ struct GNUNET_TIME_Absolute expire_objects)
{
struct GNUNET_PQ_QueryParam params[] = {
GNUNET_PQ_query_param_absolute_time (&expire_backups),
- GNUNET_PQ_query_param_end
- };
- struct GNUNET_PQ_QueryParam params2[] = {
GNUNET_PQ_query_param_absolute_time (&expire_pending_payments),
+ GNUNET_PQ_query_param_absolute_time (&expire_objects),
GNUNET_PQ_query_param_end
};
- enum GNUNET_DB_QueryStatus qs;
-
- SYNCDB_preflight ();
- PREPARE ("gc_accounts",
- "DELETE FROM accounts "
- "WHERE"
- " expiration_date < $1;");
- PREPARE ("gc_pending_payments",
- "DELETE FROM payments "
- "WHERE"
- " paid=FALSE"
- " AND"
- " timestamp < $1;");
- qs = GNUNET_PQ_eval_prepared_non_select (pg->conn,
- "gc_accounts",
- params);
- if (qs < 0)
- return qs;
- return GNUNET_PQ_eval_prepared_non_select (pg->conn,
- "gc_pending_payments",
- params2);
+
+ if (GNUNET_OK !=
+ GNUNET_PQ_prepare_anon (
+ pg->conn,
+ "CALL sync.sync_do_gc ($1, $2, $3);"))
+ {
+ GNUNET_break (0);
+ return GNUNET_SYSERR;
+ }
+ if (0 > GNUNET_PQ_eval_prepared_non_select (pg->conn,
+ "",
+ params))
+ {
+ GNUNET_break (0);
+ return GNUNET_SYSERR;
+ }
+ return GNUNET_OK;
}
diff --git a/src/syncdb/syncdb_gc.sql b/src/syncdb/syncdb_gc.sql
@@ -0,0 +1,40 @@
+--
+-- 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 PROCEDURE IF EXISTS sync_do_gc;
+CREATE PROCEDURE sync_do_gc(
+ in_expire_backups INT8
+ ,in_expire_pending_payments INT8
+ ,in_expire_objects INT8)
+LANGUAGE plpgsql
+AS $$
+BEGIN
+ DELETE
+ FROM accounts
+ WHERE expiration_date < in_expire_backups;
+ DELETE
+ FROM payments
+ WHERE paid=FALSE
+ AND order_timestamp < in_expire_pending_payments;
+ -- Only unreferenced objects may be collected; matches the
+ -- "objects_expire" partial index
+ DELETE
+ FROM objects
+ WHERE expiration_date < in_expire_objects
+ AND refcount = 0;
+END $$;
+COMMENT ON PROCEDURE sync_do_gc
+ IS 'Performs garbage collection on sync data';
diff --git a/src/syncdb/syncdb_increment_lifetime_TR.c b/src/syncdb/syncdb_increment_lifetime_TR.c
@@ -29,6 +29,7 @@ SYNCDB_increment_lifetime_TR (
{
struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get ();
bool no_payment;
+ bool no_account;
struct GNUNET_PQ_QueryParam params[] = {
GNUNET_PQ_query_param_auto_from_type (account_pub),
GNUNET_PQ_query_param_string (order_id),
@@ -39,6 +40,8 @@ SYNCDB_increment_lifetime_TR (
struct GNUNET_PQ_ResultSpec rs[] = {
GNUNET_PQ_result_spec_bool ("out_no_payment",
&no_payment),
+ GNUNET_PQ_result_spec_bool ("out_no_account",
+ &no_account),
GNUNET_PQ_result_spec_end
};
enum GNUNET_DB_QueryStatus qs;
@@ -47,6 +50,7 @@ SYNCDB_increment_lifetime_TR (
PREPARE ("do_increment_lifetime",
"SELECT"
" out_no_payment"
+ ",out_no_account"
" FROM sync_do_increment_lifetime"
" ($1,$2,$3,$4);");
qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
@@ -65,6 +69,8 @@ SYNCDB_increment_lifetime_TR (
case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
if (no_payment)
return SYNC_DB_NO_RESULTS;
+ if (no_account)
+ return SYNC_DB_TARGET_MISSING;
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
@@ -21,13 +21,13 @@ CREATE FUNCTION sync_do_increment_lifetime (
IN in_order_id TEXT,
IN in_lifetime_us INT8,
IN in_now_us INT8,
- OUT out_no_payment BOOLEAN)
+ OUT out_no_payment BOOLEAN,
+ OUT out_no_account BOOLEAN)
LANGUAGE plpgsql
AS $$
-DECLARE
- my_expiration INT8;
BEGIN
- out_no_payment = FALSE;
+ out_no_payment := FALSE;
+ out_no_account := FALSE;
-- Mark payment as done
UPDATE payments
@@ -38,29 +38,18 @@ BEGIN
IF NOT FOUND
THEN
- out_no_payment = TRUE;
+ out_no_payment := TRUE;
RETURN;
END IF;
- -- Get current expiration
- SELECT expiration_date
- INTO my_expiration
- FROM accounts
+ -- Extend account expiration (account already created by store_payment)
+ UPDATE accounts
+ SET expiration_date=expiration_date + in_lifetime_us
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;
+ out_no_account := TRUE;
+ RETURN;
END IF;
END $$;
diff --git a/src/syncdb/syncdb_lookup_account_TR.c b/src/syncdb/syncdb_lookup_account_TR.c
@@ -1,6 +1,6 @@
/*
This file is part of TALER
- (C) 2014--2022 Taler Systems SA
+ (C) 2026 Taler Systems SA
TALER is free software; you can redistribute it and/or modify it under the
terms of the GNU Lesser General Public License as published by the Free Software
@@ -15,7 +15,7 @@
*/
/**
* @file syncdb/syncdb_lookup_account_TR.c
- * @brief lookup account and backup metadata
+ * @brief look up the status of an account
* @author Christian Grothoff
*/
#include "syncdb_pg.h"
@@ -24,52 +24,55 @@
enum SYNC_DB_QueryStatus
SYNCDB_lookup_account_TR (
const struct SYNC_AccountPublicKeyP *account_pub,
- struct GNUNET_HashCode *backup_hash)
+ struct GNUNET_TIME_Timestamp *expiration_date,
+ uint64_t *storage_used_bytes,
+ uint64_t *block_count)
{
- 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_timestamp ("expiration_date",
+ expiration_date),
+ GNUNET_PQ_result_spec_uint64 ("storage_used_bytes",
+ storage_used_bytes),
+ GNUNET_PQ_result_spec_uint64 ("block_count",
+ block_count),
GNUNET_PQ_result_spec_end
};
enum GNUNET_DB_QueryStatus qs;
SYNCDB_preflight ();
- PREPARE ("do_lookup_account",
+ /* No stored procedure: this only reads, and an expired account is a
+ result here rather than an error, so none of the checks the
+ procedures share apply. */
+ PREPARE ("account_select",
"SELECT"
- " out_backup_hash"
- ",out_payment_required"
- ",out_no_backup"
- " FROM sync_do_lookup_account"
- " ($1);");
+ " a.expiration_date"
+ ",(SELECT COALESCE(SUM(octet_length(b.data_blob)),0)"
+ " FROM blocks b"
+ " WHERE b.account_pub = a.account_pub) AS storage_used_bytes"
+ ",(SELECT COUNT(*)"
+ " FROM blocks b"
+ " WHERE b.account_pub = a.account_pub) AS block_count"
+ " FROM accounts a"
+ " WHERE a.account_pub=$1");
qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
- "do_lookup_account",
+ "account_select",
params,
rs);
switch (qs)
{
case GNUNET_DB_STATUS_HARD_ERROR:
+ GNUNET_break (0);
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:
- GNUNET_break (0);
- return SYNC_DB_HARD_ERROR;
+ return SYNC_DB_TARGET_MISSING;
case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
- 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);
diff --git a/src/syncdb/syncdb_lookup_account_TR.sql b/src/syncdb/syncdb_lookup_account_TR.sql
@@ -1,54 +0,0 @@
---
--- 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_lookup_backup_TR.c b/src/syncdb/syncdb_lookup_backup_TR.c
@@ -1,84 +0,0 @@
-/*
- This file is part of TALER
- (C) 2014--2022 Taler Systems SA
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU Lesser 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/>
-*/
-/**
- * @file syncdb/syncdb_lookup_backup_TR.c
- * @brief obtain backup data
- * @author Christian Grothoff
- */
-#include "syncdb_pg.h"
-
-
-enum SYNC_DB_QueryStatus
-SYNCDB_lookup_backup_TR (
- const struct SYNC_AccountPublicKeyP *account_pub,
- struct SYNC_AccountSignatureP *account_sig,
- struct GNUNET_HashCode *prev_hash,
- struct GNUNET_HashCode *backup_hash,
- size_t *backup_size,
- void **backup)
-{
- enum GNUNET_DB_QueryStatus qs;
- 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 ("account_sig",
- account_sig),
- GNUNET_PQ_result_spec_auto_from_type ("prev_hash",
- prev_hash),
- GNUNET_PQ_result_spec_auto_from_type ("backup_hash",
- backup_hash),
- GNUNET_PQ_result_spec_variable_size ("data",
- backup,
- backup_size),
- GNUNET_PQ_result_spec_end
- };
-
- SYNCDB_preflight ();
- PREPARE ("backup_select",
- "SELECT "
- " account_sig"
- ",prev_hash"
- ",backup_hash"
- ",data "
- "FROM"
- " backups "
- "WHERE"
- " account_pub=$1;");
- qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
- "backup_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:
- return SYNC_DB_NO_RESULTS;
- case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
- return SYNC_DB_ONE_RESULT;
- default:
- GNUNET_break (0);
- return SYNC_DB_HARD_ERROR;
- }
-}
-
-
-/* end of syncdb_lookup_backup_TR.c */
diff --git a/src/syncdb/syncdb_lookup_block_TR.c b/src/syncdb/syncdb_lookup_block_TR.c
@@ -0,0 +1,112 @@
+/*
+ This file is part of TALER
+ (C) 2026 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Lesser 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/>
+*/
+/**
+ * @file syncdb/syncdb_lookup_block_TR.c
+ * @brief lookup a single block by nonce
+ * @author Iván Ávalos
+ */
+#include "syncdb_pg.h"
+
+
+enum SYNC_DB_QueryStatus
+SYNCDB_lookup_block_TR (
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const struct SYNC_BlockNonce *nonce,
+ struct GNUNET_HashCode *block_hash,
+ struct SYNC_BlockNonce *prev_nonce,
+ struct SYNC_BlockNonce *next_nonce,
+ size_t *data_size,
+ struct GNUNET_HashCode *refs_hash,
+ bool *is_first,
+ bool *is_last)
+{
+ bool account_missing;
+ bool block_missing;
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (account_pub),
+ GNUNET_PQ_query_param_auto_from_type (nonce),
+ GNUNET_PQ_query_param_end,
+ };
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_bool ("out_account_missing",
+ &account_missing),
+ GNUNET_PQ_result_spec_bool ("out_block_missing",
+ &block_missing),
+ GNUNET_PQ_result_spec_bool ("out_is_first",
+ is_first),
+ GNUNET_PQ_result_spec_bool ("out_is_last",
+ is_last),
+ GNUNET_PQ_result_spec_auto_from_type ("block_hash",
+ block_hash),
+ GNUNET_PQ_result_spec_allow_null (
+ GNUNET_PQ_result_spec_auto_from_type ("prev_nonce",
+ prev_nonce),
+ NULL),
+ GNUNET_PQ_result_spec_allow_null (
+ GNUNET_PQ_result_spec_auto_from_type ("next_nonce",
+ next_nonce),
+ NULL),
+ GNUNET_PQ_result_spec_uint64 ("data_size",
+ data_size),
+ GNUNET_PQ_result_spec_auto_from_type ("refs_hash",
+ refs_hash),
+ GNUNET_PQ_result_spec_end,
+ };
+ enum GNUNET_DB_QueryStatus qs;
+
+ SYNCDB_preflight ();
+ PREPARE ("block_select",
+ "SELECT"
+ " out_account_missing"
+ ",out_block_missing"
+ ",out_is_first"
+ ",out_is_last"
+ ",nonce"
+ ",block_hash"
+ ",prev_nonce"
+ ",next_nonce"
+ ",data_size"
+ ",refs_hash"
+ " FROM sync_do_lookup_block"
+ " ($1,$2)");
+ qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "block_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:
+ GNUNET_break (0);
+ return SYNC_DB_HARD_ERROR;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ if (account_missing)
+ return SYNC_DB_PAYMENT_REQUIRED;
+ if (block_missing)
+ return SYNC_DB_TARGET_MISSING;
+ return SYNC_DB_ONE_RESULT;
+ default:
+ GNUNET_break (0);
+ return SYNC_DB_HARD_ERROR;
+ }
+}
+
+
+/* end of syncdb_lookup_block_TR.c */
diff --git a/src/syncdb/syncdb_lookup_block_TR.sql b/src/syncdb/syncdb_lookup_block_TR.sql
@@ -0,0 +1,94 @@
+--
+-- 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 sync_do_lookup_block;
+CREATE FUNCTION sync_do_lookup_block (
+ IN in_account_pub BYTEA,
+ IN in_nonce BYTEA)
+RETURNS sync_do_lookup_block_return_type
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ my_first_block BYTEA;
+ my_last_block BYTEA;
+ my_hash BYTEA;
+ my_prev BYTEA;
+ my_next BYTEA;
+ my_data_size INT8;
+ my_refs_hash BYTEA;
+BEGIN
+ -- Check if account exists
+ SELECT first_block
+ ,last_block
+ INTO my_first_block
+ ,my_last_block
+ FROM accounts
+ WHERE account_pub = in_account_pub;
+
+ IF NOT FOUND
+ THEN
+ RETURN (TRUE -- out_account_missing
+ ,FALSE -- out_block_missing
+ ,FALSE -- out_is_first
+ ,FALSE -- out_is_last
+ ,in_nonce -- nonce
+ ,decode(repeat('00', 64), 'hex') -- block_hash
+ ,decode(repeat('00', 24), 'hex') -- prev_nonce
+ ,decode(repeat('00', 24), 'hex') -- next_nonce
+ ,0::INT8 -- data_size
+ ,decode(repeat('00', 64), 'hex')); -- refs_hash
+ END IF;
+
+ -- Look up the block
+ SELECT block_hash
+ ,prev_nonce
+ ,next_nonce
+ ,octet_length(data_blob)
+ ,refs_hash
+ INTO my_hash
+ ,my_prev
+ ,my_next
+ ,my_data_size
+ ,my_refs_hash
+ FROM blocks
+ WHERE account_pub = in_account_pub
+ AND nonce = in_nonce;
+
+ IF NOT FOUND
+ THEN
+ RETURN (FALSE -- out_account_missing
+ ,TRUE -- out_block_missing
+ ,FALSE -- out_is_first
+ ,FALSE -- out_is_last
+ ,in_nonce -- nonce
+ ,decode(repeat('00', 64), 'hex') -- block_hash
+ ,decode(repeat('00', 24), 'hex') -- prev_nonce
+ ,decode(repeat('00', 24), 'hex') -- next_nonce
+ ,0::INT8 -- data_size
+ ,decode(repeat('00', 64), 'hex')); -- refs_hash
+ END IF;
+
+ RETURN (FALSE -- out_account_missing
+ ,FALSE -- out_block_missing
+ ,in_nonce IS NOT DISTINCT FROM my_first_block -- out_is_first
+ ,in_nonce IS NOT DISTINCT FROM my_last_block -- out_is_last
+ ,in_nonce -- nonce
+ ,my_hash -- block_hash
+ ,my_prev -- prev_nonce
+ ,my_next -- next_nonce
+ ,my_data_size -- data_size
+ ,my_refs_hash); -- refs_hash
+END $$;
diff --git a/src/syncdb/syncdb_lookup_block_range_TR.c b/src/syncdb/syncdb_lookup_block_range_TR.c
@@ -0,0 +1,228 @@
+/*
+ This file is part of TALER
+ (C) 2026 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Lesser 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/>
+*/
+/**
+ * @file syncdb/syncdb_lookup_block_range_TR.c
+ * @brief lookup a (limited) range of blocks starting at a nonce
+ * @author Iván Ávalos
+ */
+#include "syncdb_pg.h"
+
+
+/**
+ * Closure for the block iterator callback.
+ */
+struct BlockIteratorContext
+{
+ /**
+ * Iterator function to call for each block result.
+ */
+ SYNC_DB_BlockIterator it;
+
+ /**
+ * Closure for @e it.
+ */
+ void *it_cls;
+
+ /**
+ * Query status to return.
+ */
+ enum SYNC_DB_QueryStatus qs;
+
+ /**
+ * True if the account does not exist or is unpaid.
+ */
+ bool out_account_missing;
+
+ /**
+ * True if the requested start block does not exist.
+ */
+ bool out_block_missing;
+
+ /**
+ * True if the account has no blocks at all.
+ */
+ bool out_backup_empty;
+};
+
+
+/**
+ * Helper function for #SYNCDB_lookup_block_range_TR().
+ * To be called with the results of a SELECT statement
+ * that has returned @a num_results results.
+ *
+ * @param cls closure of type `struct BlockIteratorContext *`
+ * @param result the postgres result
+ * @param num_results the number of results in @a result
+ */
+static void
+block_range_cb (void *cls,
+ PGresult *result,
+ unsigned int num_results)
+{
+ struct BlockIteratorContext *bic = cls;
+
+ for (unsigned int i = 0; i < num_results; i++)
+ {
+ bool out_account_missing;
+ bool out_block_missing;
+ bool out_backup_empty;
+ bool null_prev_nonce = false;
+ bool null_next_nonce = false;
+ struct SYNC_BlockNonce nonce;
+ struct GNUNET_HashCode block_hash;
+ struct SYNC_BlockNonce prev_nonce;
+ struct SYNC_BlockNonce next_nonce;
+ size_t data_size = 0;
+ void *data = NULL;
+ struct SYNC_AccountSignatureP upload_sig;
+ struct GNUNET_HashCode old_hash;
+ struct GNUNET_HashCode refs_hash;
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_bool ("out_account_missing",
+ &out_account_missing),
+ GNUNET_PQ_result_spec_bool ("out_block_missing",
+ &out_block_missing),
+ GNUNET_PQ_result_spec_bool ("out_backup_empty",
+ &out_backup_empty),
+ GNUNET_PQ_result_spec_auto_from_type ("nonce",
+ &nonce),
+ GNUNET_PQ_result_spec_auto_from_type ("block_hash",
+ &block_hash),
+ GNUNET_PQ_result_spec_allow_null (
+ GNUNET_PQ_result_spec_auto_from_type ("prev_nonce",
+ &prev_nonce),
+ &null_prev_nonce),
+ GNUNET_PQ_result_spec_allow_null (
+ GNUNET_PQ_result_spec_auto_from_type ("next_nonce",
+ &next_nonce),
+ &null_next_nonce),
+ GNUNET_PQ_result_spec_variable_size ("data_blob",
+ &data,
+ &data_size),
+ GNUNET_PQ_result_spec_auto_from_type ("upload_sig",
+ &upload_sig),
+ GNUNET_PQ_result_spec_auto_from_type ("old_hash",
+ &old_hash),
+ GNUNET_PQ_result_spec_auto_from_type ("refs_hash",
+ &refs_hash),
+ GNUNET_PQ_result_spec_end,
+ };
+
+ if (GNUNET_OK !=
+ GNUNET_PQ_extract_result (result,
+ rs,
+ i))
+ {
+ GNUNET_break (0);
+ bic->qs = SYNC_DB_SOFT_ERROR;
+ return;
+ }
+
+ bic->qs = i + 1;
+ bic->out_account_missing = out_account_missing;
+ bic->out_block_missing = out_block_missing;
+ bic->out_backup_empty = out_backup_empty;
+ if ( (! out_account_missing) &&
+ (! out_block_missing) &&
+ (! out_backup_empty) )
+ bic->it (bic->it_cls,
+ &nonce,
+ &block_hash,
+ null_prev_nonce
+ ? NULL
+ : &prev_nonce,
+ null_next_nonce
+ ? NULL
+ : &next_nonce,
+ data_size,
+ data,
+ &upload_sig,
+ &old_hash,
+ &refs_hash);
+ GNUNET_PQ_cleanup_result (rs);
+ }
+}
+
+
+enum SYNC_DB_QueryStatus
+SYNCDB_lookup_block_range_TR (
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const struct SYNC_BlockNonce *start_nonce,
+ const int16_t limit,
+ SYNC_DB_BlockIterator it,
+ void *it_cls)
+{
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (account_pub),
+ (NULL != start_nonce)
+ ? GNUNET_PQ_query_param_auto_from_type (start_nonce)
+ : GNUNET_PQ_query_param_null (),
+ GNUNET_PQ_query_param_int16 (&limit),
+ GNUNET_PQ_query_param_end,
+ };
+ struct BlockIteratorContext bic = {
+ .it = it,
+ .it_cls = it_cls,
+ };
+ enum GNUNET_DB_QueryStatus qs;
+
+ SYNCDB_preflight ();
+ PREPARE ("blocks_select_range",
+ "SELECT"
+ " out_account_missing"
+ ",out_block_missing"
+ ",out_backup_empty"
+ ",nonce"
+ ",block_hash"
+ ",prev_nonce"
+ ",next_nonce"
+ ",data_blob"
+ ",upload_sig"
+ ",old_hash"
+ ",refs_hash"
+ " FROM sync_do_lookup_block_range"
+ " ($1,$2,$3)");
+ qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn,
+ "blocks_select_range",
+ params,
+ &block_range_cb,
+ &bic);
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ GNUNET_break (0);
+ return SYNC_DB_HARD_ERROR;
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ return SYNC_DB_SOFT_ERROR;
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ /* the stored procedure always returns a status row */
+ GNUNET_break (0);
+ return SYNC_DB_NO_RESULTS;
+ default:
+ break;
+ }
+ /* map the flags of the (single) status row to SYNC_DB codes */
+ if (bic.out_account_missing)
+ return SYNC_DB_PAYMENT_REQUIRED;
+ if (bic.out_block_missing)
+ return SYNC_DB_TARGET_MISSING;
+ if (bic.out_backup_empty)
+ return SYNC_DB_NO_RESULTS;
+ return bic.qs;
+}
+
+
+/* end of syncdb_lookup_block_range_TR.c */
diff --git a/src/syncdb/syncdb_lookup_block_range_TR.sql b/src/syncdb/syncdb_lookup_block_range_TR.sql
@@ -0,0 +1,142 @@
+--
+-- 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 sync_do_lookup_block_range;
+CREATE FUNCTION sync_do_lookup_block_range (
+ IN in_account_pub BYTEA,
+ IN in_start_nonce BYTEA,
+ IN in_limit INT2)
+RETURNS SETOF sync_do_lookup_block_range_return_type
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ my_first_block BYTEA;
+ my_curr_block BYTEA;
+ my_prev_block BYTEA;
+ my_curr_hash BYTEA;
+ my_curr_prev BYTEA;
+ my_curr_next BYTEA;
+ my_curr_data BYTEA;
+ my_curr_upload_sig BYTEA;
+ my_curr_old_hash BYTEA;
+ my_curr_refs_hash BYTEA;
+ cnt INT := 0;
+BEGIN
+
+ -- Check if account exists
+ SELECT first_block
+ INTO my_first_block
+ FROM accounts
+ WHERE account_pub = in_account_pub;
+
+ IF NOT FOUND
+ THEN
+ RETURN NEXT (TRUE -- out_account_missing
+ ,FALSE
+ ,FALSE
+ ,COALESCE(in_start_nonce,
+ decode(repeat('00', 24), 'hex'))
+ ,decode(repeat('00', 64), 'hex')
+ ,decode(repeat('00', 24), 'hex')
+ ,decode(repeat('00', 24), 'hex')
+ ,decode(repeat('00', 0), 'hex')
+ ,decode(repeat('00', 64), 'hex')
+ ,decode(repeat('00', 64), 'hex')
+ ,decode(repeat('00', 64), 'hex'));
+ RETURN;
+ END IF;
+
+ -- Check if backup is empty
+ IF my_first_block IS NULL
+ THEN
+ RETURN NEXT (FALSE
+ ,FALSE
+ ,TRUE -- out_backup_empty
+ ,COALESCE(in_start_nonce,
+ decode(repeat('00', 24), 'hex'))
+ ,decode(repeat('00', 64), 'hex')
+ ,decode(repeat('00', 24), 'hex')
+ ,decode(repeat('00', 24), 'hex')
+ ,decode(repeat('00', 0), 'hex')
+ ,decode(repeat('00', 64), 'hex')
+ ,decode(repeat('00', 64), 'hex')
+ ,decode(repeat('00', 64), 'hex'));
+ RETURN;
+ END IF;
+
+ my_curr_block := COALESCE(in_start_nonce, my_first_block);
+
+ IF in_limit <= 0
+ THEN
+ RETURN;
+ END IF;
+
+ WHILE cnt < in_limit
+ AND my_curr_block IS NOT NULL
+ LOOP
+ my_prev_block := my_curr_block;
+
+ SELECT nonce
+ ,block_hash
+ ,prev_nonce
+ ,next_nonce
+ ,data_blob
+ ,upload_sig
+ ,old_hash
+ ,refs_hash
+ INTO my_curr_block
+ ,my_curr_hash
+ ,my_curr_prev
+ ,my_curr_next
+ ,my_curr_data
+ ,my_curr_upload_sig
+ ,my_curr_old_hash
+ ,my_curr_refs_hash
+ FROM blocks
+ WHERE account_pub = in_account_pub
+ AND nonce = my_curr_block;
+
+ IF NOT FOUND
+ THEN
+ RETURN NEXT (FALSE
+ ,TRUE -- out_block_missing
+ ,FALSE
+ ,my_prev_block
+ ,decode(repeat('00', 64), 'hex')
+ ,decode(repeat('00', 24), 'hex')
+ ,decode(repeat('00', 24), 'hex')
+ ,decode(repeat('00', 0), 'hex')
+ ,decode(repeat('00', 64), 'hex')
+ ,decode(repeat('00', 64), 'hex')
+ ,decode(repeat('00', 64), 'hex'));
+ RETURN;
+ END IF;
+
+ RETURN NEXT (FALSE, FALSE, FALSE
+ ,my_curr_block
+ ,my_curr_hash
+ ,my_curr_prev
+ ,my_curr_next
+ ,my_curr_data
+ ,my_curr_upload_sig
+ ,my_curr_old_hash
+ ,my_curr_refs_hash);
+ cnt := cnt + 1;
+ my_curr_block := my_curr_next;
+ END LOOP;
+
+ RETURN;
+END $$;
diff --git a/src/syncdb/syncdb_lookup_object.c b/src/syncdb/syncdb_lookup_object.c
@@ -0,0 +1,87 @@
+/*
+ This file is part of TALER
+ (C) 2026 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Lesser 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/>
+*/
+/**
+ * @file syncdb/syncdb_lookup_object.c
+ * @brief lookup object on sync account
+ * @author Iván Ávalos
+ */
+#include "syncdb_pg.h"
+
+
+enum SYNC_DB_QueryStatus
+SYNCDB_lookup_object (
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const struct GNUNET_HashCode *object_hash,
+ int64_t *refcount,
+ struct GNUNET_TIME_Timestamp *expiration_date,
+ size_t *data_size,
+ void *data)
+{
+ void *tmp_data;
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (account_pub),
+ GNUNET_PQ_query_param_auto_from_type (object_hash),
+ GNUNET_PQ_query_param_end,
+ };
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_int64 ("refcount",
+ refcount),
+ GNUNET_PQ_result_spec_auto_from_type ("expiration_date",
+ expiration_date),
+ GNUNET_PQ_result_spec_variable_size ("data_blob",
+ &tmp_data,
+ data_size),
+ GNUNET_PQ_result_spec_end,
+ };
+ enum GNUNET_DB_QueryStatus qs;
+
+ SYNCDB_preflight ();
+ PREPARE ("do_lookup_object",
+ "SELECT"
+ " refcount"
+ ",expiration_date"
+ ",data_blob"
+ " FROM objects"
+ " WHERE"
+ " account_pub=$1"
+ " AND"
+ " object_hash=$2");
+ qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "do_lookup_object",
+ 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:
+ /* no such object; a perfectly normal outcome */
+ return SYNC_DB_NO_RESULTS;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ if (NULL != data)
+ *(void **) data = tmp_data;
+ else
+ GNUNET_free (tmp_data);
+ return SYNC_DB_ONE_RESULT;
+ default:
+ GNUNET_break (0);
+ return SYNC_DB_HARD_ERROR;
+ }
+}
diff --git a/src/syncdb/syncdb_lookup_object_uid.c b/src/syncdb/syncdb_lookup_object_uid.c
@@ -0,0 +1,89 @@
+/*
+ This file is part of TALER
+ (C) 2026 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Lesser 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/>
+*/
+/**
+ * @file syncdb/syncdb_lookup_object_uid.c
+ * @brief lookup object on sync account by UID
+ * @author Iván Ávalos
+ */
+#include "syncdb_pg.h"
+
+
+enum SYNC_DB_QueryStatus
+SYNCDB_lookup_object_uid (
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const struct SYNC_ObjectUID *uid,
+ struct GNUNET_HashCode *object_hash,
+ int64_t *refcount,
+ size_t *data_size,
+ void *data)
+{
+ void *tmp_data;
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (account_pub),
+ GNUNET_PQ_query_param_auto_from_type (uid),
+ GNUNET_PQ_query_param_end,
+ };
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_auto_from_type ("object_hash",
+ object_hash),
+ GNUNET_PQ_result_spec_int64 ("refcount",
+ refcount),
+ GNUNET_PQ_result_spec_variable_size ("data_blob",
+ &tmp_data,
+ data_size),
+ GNUNET_PQ_result_spec_end,
+ };
+ enum GNUNET_DB_QueryStatus qs;
+
+ SYNCDB_preflight ();
+ PREPARE ("do_lookup_object_uid",
+ "SELECT"
+ " object_hash"
+ ",refcount"
+ ",data_blob"
+ " FROM objects"
+ " WHERE"
+ " account_pub=$1"
+ " AND"
+ " uid=$2");
+ qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "do_lookup_object_uid",
+ 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_NO_RESULTS;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ if (NULL != data)
+ *(void **) data = tmp_data;
+ else
+ GNUNET_free (tmp_data);
+ return SYNC_DB_ONE_RESULT;
+ default:
+ GNUNET_break (0);
+ return SYNC_DB_HARD_ERROR;
+ }
+}
+
+
+/* end of syncdb_lookup_object_uid.c */
diff --git a/src/syncdb/syncdb_lookup_pending_payments_by_account.c b/src/syncdb/syncdb_lookup_pending_payments_by_account.c
@@ -0,0 +1,150 @@
+/*
+ This file is part of TALER
+ (C) 2014--2022 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Lesser 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/>
+*/
+/**
+ * @file syncdb/syncdb_lookup_pending_payments_by_account.c
+ * @brief lookup pending payments by account
+ * @author Christian Grothoff
+ */
+#include "syncdb_pg.h"
+
+
+/**
+ * Closure for #payment_by_account_cb.
+ */
+struct PaymentIteratorContext
+{
+ /**
+ * Function to call on each result
+ */
+ SYNC_DB_PaymentPendingIterator it;
+
+ /**
+ * Closure for @e it.
+ */
+ void *it_cls;
+
+ /**
+ * Query status to return.
+ */
+ enum SYNC_DB_QueryStatus qs;
+
+};
+
+
+/**
+ * Helper function for #SYNCDB_lookup_pending_payments_by_account_TR().
+ * To be called with the results of a SELECT statement
+ * that has returned @a num_results results.
+ *
+ * @param cls closure of type `struct PaymentIteratorContext *`
+ * @param result the postgres result
+ * @param num_results the number of results in @a result
+ */
+static void
+payment_by_account_cb (void *cls,
+ PGresult *result,
+ unsigned int num_results)
+{
+ struct PaymentIteratorContext *pic = cls;
+
+ for (unsigned int i = 0; i < num_results; i++)
+ {
+ struct GNUNET_TIME_Timestamp timestamp;
+ char *order_id;
+ struct TALER_Amount amount;
+ struct TALER_ClaimTokenP token;
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_timestamp ("order_timestamp",
+ ×tamp),
+ GNUNET_PQ_result_spec_string ("order_id",
+ &order_id),
+ GNUNET_PQ_result_spec_auto_from_type ("claim_token",
+ &token),
+ TALER_PQ_result_spec_amount ("amount",
+ pg->currency,
+ &amount),
+ GNUNET_PQ_result_spec_end
+ };
+
+ if (GNUNET_OK !=
+ GNUNET_PQ_extract_result (result,
+ rs,
+ i))
+ {
+ GNUNET_break (0);
+ pic->qs = SYNC_DB_HARD_ERROR;
+ return;
+ }
+ pic->qs = i + 1;
+ pic->it (pic->it_cls,
+ timestamp,
+ order_id,
+ &token,
+ &amount);
+ GNUNET_PQ_cleanup_result (rs);
+ }
+}
+
+
+enum SYNC_DB_QueryStatus
+SYNCDB_lookup_pending_payments_by_account_TR (
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ SYNC_DB_PaymentPendingIterator it,
+ void *it_cls)
+{
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (account_pub),
+ GNUNET_PQ_query_param_end
+ };
+ struct PaymentIteratorContext pic = {
+ .it = it,
+ .it_cls = it_cls
+ };
+ enum GNUNET_DB_QueryStatus qs;
+
+ SYNCDB_preflight ();
+ PREPARE ("payments_select_by_account",
+ "SELECT"
+ " order_timestamp"
+ ",order_id"
+ ",claim_token"
+ ",amount"
+ " FROM payments"
+ " WHERE"
+ " paid=FALSE"
+ " AND"
+ " account_pub=$1;");
+ qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn,
+ "payments_select_by_account",
+ params,
+ &payment_by_account_cb,
+ &pic);
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ GNUNET_break (0);
+ return SYNC_DB_HARD_ERROR;
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ return SYNC_DB_SOFT_ERROR;
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ return SYNC_DB_NO_RESULTS;
+ default:
+ return pic.qs;
+ }
+}
+
+
+/* end of syncdb_lookup_pending_payments_by_account.c */
diff --git a/src/syncdb/syncdb_lookup_pending_payments_by_account_TR.c b/src/syncdb/syncdb_lookup_pending_payments_by_account_TR.c
@@ -1,142 +0,0 @@
-/*
- This file is part of TALER
- (C) 2014--2022 Taler Systems SA
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU Lesser 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/>
-*/
-/**
- * @file syncdb/syncdb_lookup_pending_payments_by_account_TR.c
- * @brief lookup pending payments by account
- * @author Christian Grothoff
- */
-#include "syncdb_pg.h"
-
-
-/**
- * Closure for #payment_by_account_cb.
- */
-struct PaymentIteratorContext
-{
- /**
- * Function to call on each result
- */
- SYNC_DB_PaymentPendingIterator it;
-
- /**
- * Closure for @e it.
- */
- void *it_cls;
-
- /**
- * Query status to return.
- */
- enum GNUNET_DB_QueryStatus qs;
-
-};
-
-
-/**
- * Helper function for #SYNCDB_lookup_pending_payments_by_account_TR().
- * To be called with the results of a SELECT statement
- * that has returned @a num_results results.
- *
- * @param cls closure of type `struct PaymentIteratorContext *`
- * @param result the postgres result
- * @param num_results the number of results in @a result
- */
-static void
-payment_by_account_cb (void *cls,
- PGresult *result,
- unsigned int num_results)
-{
- struct PaymentIteratorContext *pic = cls;
-
- for (unsigned int i = 0; i < num_results; i++)
- {
- struct GNUNET_TIME_Timestamp timestamp;
- char *order_id;
- struct TALER_Amount amount;
- struct TALER_ClaimTokenP token;
- struct GNUNET_PQ_ResultSpec rs[] = {
- GNUNET_PQ_result_spec_timestamp ("timestamp",
- ×tamp),
- GNUNET_PQ_result_spec_string ("order_id",
- &order_id),
- GNUNET_PQ_result_spec_auto_from_type ("token",
- &token),
- TALER_PQ_result_spec_amount ("amount",
- pg->currency,
- &amount),
- GNUNET_PQ_result_spec_end
- };
-
- if (GNUNET_OK !=
- GNUNET_PQ_extract_result (result,
- rs,
- i))
- {
- GNUNET_break (0);
- pic->qs = GNUNET_DB_STATUS_HARD_ERROR;
- return;
- }
- pic->qs = i + 1;
- pic->it (pic->it_cls,
- timestamp,
- order_id,
- &token,
- &amount);
- GNUNET_PQ_cleanup_result (rs);
- }
-}
-
-
-enum GNUNET_DB_QueryStatus
-SYNCDB_lookup_pending_payments_by_account_TR (
- const struct SYNC_AccountPublicKeyP *account_pub,
- SYNC_DB_PaymentPendingIterator it,
- void *it_cls)
-{
- struct GNUNET_PQ_QueryParam params[] = {
- GNUNET_PQ_query_param_auto_from_type (account_pub),
- GNUNET_PQ_query_param_end
- };
- struct PaymentIteratorContext pic = {
- .it = it,
- .it_cls = it_cls
- };
- enum GNUNET_DB_QueryStatus qs;
-
- SYNCDB_preflight ();
- PREPARE ("payments_select_by_account",
- "SELECT"
- " timestamp"
- ",order_id"
- ",token"
- ",amount"
- " FROM payments"
- " WHERE"
- " paid=FALSE"
- " AND"
- " account_pub=$1;");
- qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn,
- "payments_select_by_account",
- params,
- &payment_by_account_cb,
- &pic);
- if (qs > 0)
- return pic.qs;
- GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR != qs);
- return qs;
-}
-
-
-/* end of syncdb_lookup_pending_payments_by_account_TR.c */
diff --git a/src/syncdb/syncdb_object_refcounts.sql b/src/syncdb/syncdb_object_refcounts.sql
@@ -0,0 +1,145 @@
+--
+-- 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/>
+--
+
+--
+-- Helper to update object refcounts for a given account and parallel
+-- arrays of object uuids and increments.
+--
+-- Increments are aggregated per UID before they are applied, so the
+-- outcome does not depend on the order UIDs appear in the arrays, and
+-- agrees with sync_check_object_refcounts_pre() by construction.
+--
+DROP FUNCTION IF EXISTS sync_update_object_refcounts;
+CREATE FUNCTION sync_update_object_refcounts(
+ IN in_account_pub BYTEA,
+ IN ina_object_uids BYTEA[],
+ IN ina_object_incs INT2[])
+RETURNS TABLE (
+ out_negative_refcount BOOLEAN,
+ out_refs_updated BOOLEAN)
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ my_rec RECORD;
+ my_new_refcount INT8;
+BEGIN
+ out_negative_refcount := FALSE;
+ out_refs_updated := TRUE;
+
+ IF array_length(ina_object_uids,1) IS DISTINCT FROM array_length(ina_object_incs,1)
+ THEN
+ RAISE EXCEPTION 'uuid and inc arrays must have same length';
+ END IF;
+
+ IF COALESCE(array_length(ina_object_uids, 1), 0) = 0
+ THEN
+ RETURN NEXT;
+ RETURN;
+ END IF;
+
+ FOR my_rec IN
+ SELECT uid, SUM(inc)::INT8 AS total_inc
+ FROM unnest(ina_object_uids, ina_object_incs) AS t(uid, inc)
+ GROUP BY uid
+ LOOP
+ UPDATE objects o
+ SET refcount = o.refcount + my_rec.total_inc
+ WHERE o.uid = my_rec.uid
+ AND o.account_pub = in_account_pub
+ RETURNING o.refcount INTO my_new_refcount;
+
+ IF NOT FOUND
+ THEN
+ -- No such object for this account
+ out_refs_updated := FALSE;
+ RETURN NEXT;
+ RETURN;
+ END IF;
+
+ IF my_new_refcount < 0
+ THEN
+ out_negative_refcount := TRUE;
+ out_refs_updated := FALSE;
+ RETURN NEXT;
+ RETURN;
+ END IF;
+
+ -- Collect the object when its last reference goes away. A
+ -- freshly uploaded object also sits at refcount 0, hence the
+ -- decrement check.
+ IF (my_new_refcount = 0) AND
+ (my_rec.total_inc < 0)
+ THEN
+ DELETE FROM objects
+ WHERE uid = my_rec.uid
+ AND account_pub = in_account_pub;
+ END IF;
+ END LOOP;
+
+ RETURN NEXT;
+ RETURN;
+END $$;
+
+--
+-- Checks, without modifying anything, whether the given increments can
+-- be applied: every referenced UID must exist for the account, and no
+-- aggregated decrement may drive a refcount below zero. To be called
+-- before any state is mutated.
+--
+DROP FUNCTION IF EXISTS sync_check_object_refcounts_pre;
+CREATE FUNCTION sync_check_object_refcounts_pre(
+ IN in_account_pub BYTEA,
+ IN ina_object_uids BYTEA[],
+ IN ina_object_incs INT2[])
+RETURNS TABLE (
+ out_negative_refcount BOOLEAN,
+ out_refs_missing BOOLEAN)
+LANGUAGE plpgsql
+AS $$
+BEGIN
+ out_negative_refcount := FALSE;
+ out_refs_missing := FALSE;
+
+ IF array_length(ina_object_uids,1) IS DISTINCT FROM array_length(ina_object_incs,1)
+ THEN
+ RAISE EXCEPTION 'uuid and inc arrays must have same length';
+ END IF;
+
+ IF COALESCE(array_length(ina_object_uids, 1), 0) = 0
+ THEN
+ RETURN NEXT;
+ RETURN;
+ END IF;
+
+ SELECT
+ bool_or(o.uid IS NOT NULL AND a.total_inc < 0 AND o.refcount < (- a.total_inc))
+ ,bool_or(o.uid IS NULL)
+ INTO out_negative_refcount
+ ,out_refs_missing
+ FROM (
+ SELECT uid, SUM(inc)::INT8 AS total_inc
+ FROM unnest(ina_object_uids, ina_object_incs) AS t(uid, inc)
+ GROUP BY uid
+ ) a
+ LEFT JOIN objects o
+ ON o.uid = a.uid
+ AND o.account_pub = in_account_pub;
+
+ out_negative_refcount := COALESCE(out_negative_refcount, FALSE);
+ out_refs_missing := COALESCE(out_refs_missing, FALSE);
+ RETURN NEXT;
+ RETURN;
+END $$;
diff --git a/src/syncdb/syncdb_pg.c b/src/syncdb/syncdb_pg.c
@@ -18,6 +18,9 @@
* @brief shared database state and helpers for sync postgres
* @author Christian Grothoff
*/
+struct PostgresClosure;
+#define GNUNET_PQ_RECONNECT_CALLBACK_CLOSURE \
+ struct PostgresClosure
#include "syncdb_pg.h"
#include "sync/sync_util.h"
@@ -28,9 +31,44 @@
struct PostgresClosure *pg;
-enum GNUNET_GenericReturnValue
-SYNCDB_init (const struct GNUNET_CONFIGURATION_Handle *cfg,
- bool skip_preflight)
+/**
+ * Function called each time we connect or reconnect to the
+ * database.
+ *
+ * @param pg_ database context
+ * @param pq database connection handle
+ */
+static void
+reconnect_cb (struct PostgresClosure *pg_,
+ struct GNUNET_PQ_Context *pq)
+{
+ struct GNUNET_PQ_ExecuteStatement es[] = {
+ GNUNET_PQ_make_execute ("SET search_path TO sync;"),
+ GNUNET_PQ_EXECUTE_STATEMENT_END
+ };
+
+ if (GNUNET_OK !=
+ GNUNET_PQ_exec_statements (pq,
+ es))
+ {
+ GNUNET_break (0);
+ return;
+ }
+ pg_->prep_gen++;
+}
+
+
+/**
+ * Initialize the global database closure #pg from @a cfg.
+ *
+ * @param cfg configuration to use
+ * @param check_current true to refuse to start unless the
+ * database schema is up-to-date
+ * @return #GNUNET_OK on success
+ */
+static enum GNUNET_GenericReturnValue
+do_init (const struct GNUNET_CONFIGURATION_Handle *cfg,
+ bool check_current)
{
pg = GNUNET_new (struct PostgresClosure);
pg->cfg = cfg;
@@ -61,19 +99,46 @@ SYNCDB_init (const struct GNUNET_CONFIGURATION_Handle *cfg,
pg = NULL;
return GNUNET_SYSERR;
}
- if ( (! skip_preflight) &&
- (GNUNET_OK !=
- SYNCDB_preflight ()) )
+ pg->conn = GNUNET_PQ_init (cfg,
+ "syncdb-postgres",
+ &reconnect_cb,
+ pg);
+ if (NULL == pg->conn)
+ {
+ SYNCDB_fini ();
+ return GNUNET_NO;
+ }
+ if (check_current &&
+ (GNUNET_OK !=
+ GNUNET_PQ_check_current (pg->conn,
+ "sync-")))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
- "Database not ready. Try running sync-dbinit!\n");
+ "Database schema is not up-to-date."
+ " Try running sync-dbinit!\n");
SYNCDB_fini ();
- return GNUNET_SYSERR;
+ return GNUNET_NO;
}
return GNUNET_OK;
}
+enum GNUNET_GenericReturnValue
+SYNCDB_init (const struct GNUNET_CONFIGURATION_Handle *cfg)
+{
+ return do_init (cfg,
+ true);
+}
+
+
+enum GNUNET_GenericReturnValue
+SYNCDB_init_admin (const struct GNUNET_CONFIGURATION_Handle *cfg)
+{
+ return do_init (cfg,
+ false);
+}
+
+
void
SYNCDB_fini (void)
{
diff --git a/src/syncdb/syncdb_pg.h b/src/syncdb/syncdb_pg.h
@@ -76,7 +76,8 @@ extern struct PostgresClosure *pg;
/**
* Prepares SQL statement @a sql under @a name for
* connection @a pg once.
- * Returns with #GNUNET_DB_STATUS_HARD_ERROR on failure.
+ * Returns with #SYNC_DB_HARD_ERROR on failure, so this may only
+ * be used in functions returning `enum SYNC_DB_QueryStatus`.
*
* @param name name to prepare the statement under
* @param sql actual SQL text
@@ -97,7 +98,7 @@ extern struct PostgresClosure *pg;
ps)) \
{ \
GNUNET_break (0); \
- return GNUNET_DB_STATUS_HARD_ERROR; \
+ return SYNC_DB_HARD_ERROR; \
} \
gen = pg->prep_gen; \
} \
diff --git a/src/syncdb/syncdb_preflight.c b/src/syncdb/syncdb_preflight.c
@@ -21,53 +21,6 @@
#include "syncdb_pg.h"
-static enum GNUNET_GenericReturnValue
-internal_setup (void)
-{
- if (NULL == pg->conn)
- {
-#if AUTO_EXPLAIN
- /* Enable verbose logging to see where queries do not
- properly use indices */
- struct GNUNET_PQ_ExecuteStatement es[] = {
- GNUNET_PQ_make_try_execute ("LOAD 'auto_explain';"),
- GNUNET_PQ_make_try_execute ("SET auto_explain.log_min_duration=50;"),
- GNUNET_PQ_make_try_execute ("SET auto_explain.log_timing=TRUE;"),
- GNUNET_PQ_make_try_execute ("SET auto_explain.log_analyze=TRUE;"),
- /* https://wiki.postgresql.org/wiki/Serializable suggests to really
- force the default to 'serializable' if SSI is to be used. */
- GNUNET_PQ_make_try_execute (
- "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE;"),
- GNUNET_PQ_make_try_execute ("SET enable_sort=OFF;"),
- GNUNET_PQ_make_try_execute ("SET enable_seqscan=OFF;"),
- GNUNET_PQ_make_execute ("SET search_path TO sync;"),
- GNUNET_PQ_EXECUTE_STATEMENT_END
- };
-#else
- struct GNUNET_PQ_ExecuteStatement es[] = {
- GNUNET_PQ_make_execute ("SET search_path TO sync;"),
- GNUNET_PQ_EXECUTE_STATEMENT_END
- };
-#endif
- struct GNUNET_PQ_Context *db_conn;
-
- db_conn = GNUNET_PQ_connect_with_cfg2 (pg->cfg,
- "syncdb-postgres",
- "sync-",
- es,
- NULL,
- GNUNET_PQ_FLAG_CHECK_CURRENT);
- if (NULL == db_conn)
- return GNUNET_SYSERR;
- pg->conn = db_conn;
- pg->prep_gen++;
- }
- if (NULL == pg->transaction_name)
- GNUNET_PQ_reconnect_if_down (pg->conn);
- return GNUNET_OK;
-}
-
-
enum GNUNET_GenericReturnValue
SYNCDB_preflight (void)
{
@@ -76,22 +29,11 @@ SYNCDB_preflight (void)
GNUNET_PQ_EXECUTE_STATEMENT_END
};
- if (NULL == pg->conn)
- {
- if (GNUNET_OK !=
- internal_setup ())
- {
- GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
- "Failed to ensure DB is initialized\n");
- return GNUNET_SYSERR;
- }
- }
- else
+ if (NULL == pg->transaction_name)
{
GNUNET_PQ_reconnect_if_down (pg->conn);
+ return GNUNET_OK;
}
- if (NULL == pg->transaction_name)
- return GNUNET_OK; /* all good */
if (GNUNET_OK ==
GNUNET_PQ_exec_statements (pg->conn,
es))
diff --git a/src/syncdb/syncdb_store_backup_TR.c b/src/syncdb/syncdb_store_backup_TR.c
@@ -1,100 +0,0 @@
-/*
- This file is part of TALER
- (C) 2014--2022 Taler Systems SA
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU Lesser 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/>
-*/
-/**
- * @file syncdb/syncdb_store_backup_TR.c
- * @brief store first backup
- * @author Christian Grothoff
- */
-#include "syncdb_pg.h"
-
-
-enum SYNC_DB_QueryStatus
-SYNCDB_store_backup_TR (
- const struct SYNC_AccountPublicKeyP *account_pub,
- const struct SYNC_AccountSignatureP *account_sig,
- const struct GNUNET_HashCode *backup_hash,
- 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;
-
- SYNCDB_preflight ();
- PREPARE ("do_store_backup",
- "SELECT"
- " 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:
- 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:
- GNUNET_break (0);
- return SYNC_DB_HARD_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_HARD_ERROR;
- default:
- GNUNET_break (0);
- return SYNC_DB_HARD_ERROR;
- }
-}
-
-
-/* end of syncdb_store_backup_TR.c */
diff --git a/src/syncdb/syncdb_store_backup_TR.sql b/src/syncdb/syncdb_store_backup_TR.sql
@@ -1,93 +0,0 @@
---
--- 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_store_object_TR.c b/src/syncdb/syncdb_store_object_TR.c
@@ -0,0 +1,89 @@
+/*
+ This file is part of TALER
+ (C) 2026 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Lesser 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/>
+*/
+/**
+ * @file syncdb/syncdb_store_object_TR.c
+ * @brief store object into sync database
+ * @author Iván Ávalos
+ */
+#include "syncdb_pg.h"
+
+
+enum SYNC_DB_QueryStatus
+SYNCDB_store_object_TR (
+ const struct SYNC_ObjectUID *uid,
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const struct GNUNET_HashCode *object_hash,
+ struct GNUNET_TIME_Relative lifetime,
+ size_t data_size,
+ const void *data)
+{
+ enum GNUNET_DB_QueryStatus qs;
+ bool out_account_missing;
+ bool out_inserted;
+ struct GNUNET_TIME_Timestamp expiration
+ = GNUNET_TIME_relative_to_timestamp (lifetime);
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (account_pub),
+ GNUNET_PQ_query_param_auto_from_type (uid),
+ GNUNET_PQ_query_param_auto_from_type (object_hash),
+ GNUNET_PQ_query_param_timestamp (&expiration),
+ GNUNET_PQ_query_param_fixed_size (data,
+ data_size),
+ GNUNET_PQ_query_param_end,
+ };
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_bool ("out_account_missing",
+ &out_account_missing),
+ GNUNET_PQ_result_spec_bool ("out_inserted",
+ &out_inserted),
+ GNUNET_PQ_result_spec_end,
+ };
+ SYNCDB_preflight ();
+ PREPARE ("do_store_object",
+ "SELECT"
+ " out_account_missing"
+ ",out_inserted"
+ " FROM sync_do_store_object"
+ " ($1,$2,$3,$4,$5)");
+ qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "do_store_object",
+ 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:
+ GNUNET_break (0);
+ return SYNC_DB_HARD_ERROR;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ if (out_account_missing)
+ return SYNC_DB_PAYMENT_REQUIRED;
+ if (! out_inserted)
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "Object with given UID already exists\n");
+ return SYNC_DB_ONE_RESULT;
+ default:
+ GNUNET_break (0);
+ return SYNC_DB_HARD_ERROR;
+ }
+}
+
+
+/* end of syncdb_store_object_TR.c */
diff --git a/src/syncdb/syncdb_store_object_TR.sql b/src/syncdb/syncdb_store_object_TR.sql
@@ -0,0 +1,61 @@
+--
+-- 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 sync_do_store_object;
+CREATE FUNCTION sync_do_store_object (
+ IN in_account_pub BYTEA,
+ IN in_uid BYTEA,
+ IN in_object_hash BYTEA,
+ IN in_expiration_date INT8,
+ IN in_data_blob BYTEA,
+ OUT out_account_missing BOOLEAN,
+ OUT out_inserted BOOLEAN)
+LANGUAGE plpgsql
+AS $$
+BEGIN
+ out_account_missing := FALSE;
+ out_inserted := FALSE;
+
+ SELECT a.out_account_missing
+ INTO out_account_missing
+ FROM sync_check_account_payment(in_account_pub) AS a;
+ IF out_account_missing
+ THEN
+ RETURN;
+ END IF;
+
+ INSERT INTO objects
+ (uid
+ ,account_pub
+ ,object_hash
+ ,expiration_date
+ ,data_blob)
+ VALUES
+ (in_uid
+ ,in_account_pub
+ ,in_object_hash
+ ,in_expiration_date
+ ,in_data_blob)
+ ON CONFLICT DO NOTHING;
+
+ IF NOT FOUND
+ THEN
+ out_inserted := FALSE;
+ RETURN;
+ END IF;
+
+ out_inserted := TRUE;
+END $$;
diff --git a/src/syncdb/syncdb_store_payment_TR.c b/src/syncdb/syncdb_store_payment_TR.c
@@ -26,11 +26,13 @@ SYNCDB_store_payment_TR (
const struct SYNC_AccountPublicKeyP *account_pub,
const char *order_id,
const struct TALER_ClaimTokenP *token,
- const struct TALER_Amount *amount)
+ const struct TALER_Amount *amount,
+ struct GNUNET_TIME_Relative lifetime)
{
- enum GNUNET_DB_QueryStatus qs;
- struct TALER_ClaimTokenP tok;
struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get ();
+ /* all-zero claim token means "no claim token" */
+ struct TALER_ClaimTokenP tok = { 0 };
+ bool inserted;
struct GNUNET_PQ_QueryParam params[] = {
GNUNET_PQ_query_param_auto_from_type (account_pub),
GNUNET_PQ_query_param_string (order_id),
@@ -38,39 +40,46 @@ SYNCDB_store_payment_TR (
GNUNET_PQ_query_param_timestamp (&now),
TALER_PQ_query_param_amount (pg->conn,
amount),
+ 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_inserted",
+ &inserted),
+ GNUNET_PQ_result_spec_end
+ };
+ enum GNUNET_DB_QueryStatus qs;
- if (NULL == token)
- memset (&tok, 0, sizeof (tok));
- else
+ if (NULL != token)
tok = *token;
SYNCDB_preflight ();
- PREPARE ("payment_insert",
- "INSERT INTO payments "
- "(account_pub"
- ",order_id"
- ",token"
- ",timestamp"
- ",amount"
- ") VALUES "
- "($1,$2,$3,$4,$5);");
- qs = GNUNET_PQ_eval_prepared_non_select (pg->conn,
- "payment_insert",
- params);
+ PREPARE ("do_store_payment",
+ "SELECT"
+ " out_inserted"
+ " FROM sync_do_store_payment"
+ " ($1,$2,$3,$4,$5,$6,$7)");
+ qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "do_store_payment",
+ 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:
GNUNET_break (0);
- return SYNC_DB_NO_RESULTS;
+ return SYNC_DB_HARD_ERROR;
case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ if (! inserted)
+ {
+ GNUNET_break (0);
+ return SYNC_DB_NO_RESULTS;
+ }
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;
diff --git a/src/syncdb/syncdb_store_payment_TR.sql b/src/syncdb/syncdb_store_payment_TR.sql
@@ -0,0 +1,55 @@
+--
+-- 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 sync_do_store_payment;
+CREATE FUNCTION sync_do_store_payment (
+ IN in_account_pub BYTEA,
+ IN in_order_id TEXT,
+ IN in_claim_token BYTEA,
+ IN in_order_timestamp INT8,
+ IN in_amount taler_amount,
+ IN in_lifetime_us INT8,
+ IN in_now_us INT8,
+ OUT out_inserted BOOLEAN)
+LANGUAGE plpgsql
+AS $$
+BEGIN
+ -- Create account if it doesn't exist yet, so the FK
+ -- from payments.account_pub is satisfied.
+ INSERT INTO accounts
+ (account_pub
+ ,expiration_date)
+ VALUES
+ (in_account_pub
+ ,in_now_us)
+ ON CONFLICT DO NOTHING;
+
+ -- Store the payment record
+ INSERT INTO payments
+ (account_pub
+ ,order_id
+ ,claim_token
+ ,order_timestamp
+ ,amount)
+ VALUES
+ (in_account_pub
+ ,in_order_id
+ ,in_claim_token
+ ,in_order_timestamp
+ ,in_amount);
+ out_inserted := TRUE;
+END $$;
diff --git a/src/syncdb/syncdb_update_backup_TR.c b/src/syncdb/syncdb_update_backup_TR.c
@@ -1,108 +0,0 @@
-/*
- This file is part of TALER
- (C) 2014--2022 Taler Systems SA
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU Lesser 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/>
-*/
-/**
- * @file syncdb/syncdb_update_backup_TR.c
- * @brief update existing backup
- * @author Christian Grothoff
- */
-#include "syncdb_pg.h"
-
-
-enum SYNC_DB_QueryStatus
-SYNCDB_update_backup_TR (
- const struct SYNC_AccountPublicKeyP *account_pub,
- const struct GNUNET_HashCode *old_backup_hash,
- const struct SYNC_AccountSignatureP *account_sig,
- const struct GNUNET_HashCode *backup_hash,
- 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;
-
- SYNCDB_preflight ();
- PREPARE ("do_update_backup",
- "SELECT"
- " 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:
- 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:
- GNUNET_break (0);
- return SYNC_DB_HARD_ERROR;
- case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
- 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;
- }
-}
-
-
-/* end of syncdb_update_backup_TR.c */
diff --git a/src/syncdb/syncdb_update_backup_TR.sql b/src/syncdb/syncdb_update_backup_TR.sql
@@ -1,97 +0,0 @@
---
--- 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 $$;
diff --git a/src/syncdb/syncdb_update_block_TR.c b/src/syncdb/syncdb_update_block_TR.c
@@ -0,0 +1,139 @@
+/*
+ This file is part of TALER
+ (C) 2026 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Lesser 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/>
+*/
+/**
+ * @file syncdb/syncdb_update_block_TR.c
+ * @brief update contents of a block in the DLL
+ * @author Iván Ávalos
+ */
+#include "syncdb_pg.h"
+
+
+enum SYNC_DB_QueryStatus
+SYNCDB_update_block_TR (
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const struct SYNC_BlockNonce *block_nonce,
+ const struct SYNC_BlockNonce *prev_nonce,
+ const struct SYNC_BlockNonce *next_nonce,
+ const struct GNUNET_HashCode *old_data_hash,
+ const struct GNUNET_HashCode *new_data_hash,
+ size_t data_size,
+ const void *data,
+ unsigned int refs_length,
+ const struct SYNC_ObjectUID *object_uids,
+ const int16_t *object_incs,
+ const struct SYNC_AccountSignatureP *upload_sig,
+ const struct GNUNET_HashCode *refs_hash)
+{
+ bool account_missing;
+ bool block_missing;
+ bool outdated_write;
+ bool negative_refcount;
+ bool refs_missing;
+ bool updated;
+ bool no_change;
+ bool refs_updated;
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (account_pub),
+ GNUNET_PQ_query_param_auto_from_type (block_nonce),
+ (NULL != prev_nonce)
+ ? GNUNET_PQ_query_param_auto_from_type (prev_nonce)
+ : GNUNET_PQ_query_param_null (),
+ (NULL != next_nonce)
+ ? GNUNET_PQ_query_param_auto_from_type (next_nonce)
+ : GNUNET_PQ_query_param_null (),
+ GNUNET_PQ_query_param_auto_from_type (old_data_hash),
+ GNUNET_PQ_query_param_auto_from_type (new_data_hash),
+ GNUNET_PQ_query_param_fixed_size (data,
+ data_size),
+ GNUNET_PQ_query_param_array_auto_from_type (refs_length,
+ object_uids,
+ pg->conn),
+ GNUNET_PQ_query_param_array_uint16 (refs_length,
+ (const uint16_t *) object_incs,
+ pg->conn),
+ GNUNET_PQ_query_param_auto_from_type (upload_sig),
+ GNUNET_PQ_query_param_auto_from_type (refs_hash),
+ GNUNET_PQ_query_param_end,
+ };
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_bool ("out_account_missing",
+ &account_missing),
+ GNUNET_PQ_result_spec_bool ("out_block_missing",
+ &block_missing),
+ GNUNET_PQ_result_spec_bool ("out_outdated_write",
+ &outdated_write),
+ GNUNET_PQ_result_spec_bool ("out_negative_refcount",
+ &negative_refcount),
+ GNUNET_PQ_result_spec_bool ("out_refs_missing",
+ &refs_missing),
+ GNUNET_PQ_result_spec_bool ("out_updated",
+ &updated),
+ GNUNET_PQ_result_spec_bool ("out_no_change",
+ &no_change),
+ GNUNET_PQ_result_spec_bool ("out_refs_updated",
+ &refs_updated),
+ GNUNET_PQ_result_spec_end,
+ };
+ enum GNUNET_DB_QueryStatus qs;
+
+ SYNCDB_preflight ();
+ PREPARE ("do_update_block",
+ "SELECT"
+ " out_account_missing"
+ ",out_block_missing"
+ ",out_outdated_write"
+ ",out_negative_refcount"
+ ",out_refs_missing"
+ ",out_updated"
+ ",out_no_change"
+ ",out_refs_updated"
+ " FROM sync_do_update_block"
+ " ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)");
+ qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "do_update_block",
+ 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:
+ GNUNET_break (0);
+ return SYNC_DB_HARD_ERROR;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ if (updated)
+ return SYNC_DB_ONE_RESULT;
+ if (account_missing)
+ return SYNC_DB_PAYMENT_REQUIRED;
+ if (block_missing)
+ return SYNC_DB_TARGET_MISSING;
+ if (outdated_write)
+ return SYNC_DB_OUTDATED_WRITE;
+ if (no_change)
+ return SYNC_DB_NO_RESULTS;
+ if (negative_refcount || refs_missing)
+ return SYNC_DB_INCONSISTENT_BACKUP;
+ GNUNET_break (0);
+ return SYNC_DB_HARD_ERROR;
+ default:
+ GNUNET_break (0);
+ return SYNC_DB_HARD_ERROR;
+ }
+}
diff --git a/src/syncdb/syncdb_update_block_TR.sql b/src/syncdb/syncdb_update_block_TR.sql
@@ -0,0 +1,144 @@
+--
+-- 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 sync_do_update_block;
+CREATE FUNCTION sync_do_update_block (
+ IN in_account_pub BYTEA,
+ IN in_block_nonce BYTEA,
+ IN in_prev_nonce BYTEA,
+ IN in_next_nonce BYTEA,
+ IN in_old_data_hash BYTEA,
+ IN in_new_data_hash BYTEA,
+ IN in_data_blob BYTEA,
+ IN ina_object_uids BYTEA[],
+ IN ina_object_incs INT2[],
+ IN in_upload_sig BYTEA,
+ IN in_refs_hash BYTEA,
+ OUT out_account_missing BOOLEAN,
+ OUT out_block_missing BOOLEAN,
+ OUT out_outdated_write BOOLEAN,
+ OUT out_negative_refcount BOOLEAN,
+ OUT out_refs_missing BOOLEAN,
+ OUT out_updated BOOLEAN,
+ OUT out_no_change BOOLEAN,
+ OUT out_refs_updated BOOLEAN)
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ my_prev_nonce BYTEA;
+ my_next_nonce BYTEA;
+ my_existing_hash BYTEA;
+BEGIN
+ out_account_missing := FALSE;
+ out_block_missing := FALSE;
+ out_outdated_write := FALSE;
+ out_negative_refcount := FALSE;
+ out_refs_missing := FALSE;
+ out_updated := FALSE;
+ out_no_change := FALSE;
+ out_refs_updated := FALSE;
+
+ -- Check if account exists and has valid payment
+ SELECT a.out_account_missing
+ INTO out_account_missing
+ FROM sync_check_account_payment(in_account_pub) AS a;
+ IF out_account_missing
+ THEN
+ RETURN;
+ END IF;
+
+ -- Check if old block exists
+ SELECT prev_nonce
+ ,next_nonce
+ ,block_hash
+ INTO my_prev_nonce
+ ,my_next_nonce
+ ,my_existing_hash
+ FROM blocks
+ WHERE account_pub = in_account_pub
+ AND nonce = in_block_nonce;
+
+ IF NOT FOUND
+ THEN
+ out_block_missing := TRUE;
+ RETURN;
+ END IF;
+
+ -- Is the new hash identical?
+ IF my_existing_hash = in_new_data_hash
+ THEN
+ out_no_change := TRUE;
+ RETURN;
+ END IF;
+
+ -- Does existing hash matches expected old hash?
+ -- Does previous and next nonces match expectations?
+ IF my_existing_hash IS DISTINCT FROM in_old_data_hash OR
+ my_prev_nonce IS DISTINCT FROM in_prev_nonce OR
+ my_next_nonce IS DISTINCT FROM in_next_nonce
+ THEN
+ out_outdated_write := TRUE;
+ RETURN;
+ END IF;
+
+ -- Check referenced objects exist and no increment underflows,
+ -- before we touch anything
+ SELECT c.out_negative_refcount
+ ,c.out_refs_missing
+ INTO out_negative_refcount
+ ,out_refs_missing
+ FROM sync_check_object_refcounts_pre(in_account_pub,
+ ina_object_uids,
+ ina_object_incs) AS c;
+ IF out_negative_refcount OR out_refs_missing
+ THEN
+ RETURN;
+ END IF;
+
+ -- Try to update existing block in place; the upload signature and
+ -- its signed context are stored with the block
+ UPDATE blocks
+ SET block_hash = in_new_data_hash
+ ,data_blob = in_data_blob
+ ,upload_sig = in_upload_sig
+ ,old_hash = in_old_data_hash
+ ,refs_hash = in_refs_hash
+ WHERE account_pub = in_account_pub
+ AND nonce = in_block_nonce;
+
+ IF NOT FOUND
+ THEN
+ RAISE EXCEPTION 'block % vanished for account %',
+ in_block_nonce, in_account_pub;
+ END IF;
+
+ -- Update object refcounts and delete unreferenced objects; the
+ -- pre-check above means this can only fail on a concurrent change
+ SELECT u.out_negative_refcount
+ ,u.out_refs_updated
+ INTO out_negative_refcount
+ ,out_refs_updated
+ FROM sync_update_object_refcounts(in_account_pub,
+ ina_object_uids,
+ ina_object_incs) AS u;
+ IF NOT out_refs_updated
+ THEN
+ RAISE EXCEPTION 'object refcounts changed concurrently for account %',
+ in_account_pub;
+ END IF;
+
+ out_updated := TRUE;
+END $$;
diff --git a/src/syncdb/test_sync_db.c b/src/syncdb/test_sync_db.c
@@ -1,5 +1,5 @@
/*
- This file is part of
+ This file is part of TALER
(C) 2019 Taler Systems SA
TALER is free software; you can redistribute it and/or modify it under the
@@ -14,7 +14,7 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
- * @file sync/test_sync_db.c
+ * @file syncdb/test_sync_db.c
* @brief testcase for sync postgres db
* @author Christian Grothoff
*/
@@ -34,12 +34,11 @@
} while (0)
#define RND_BLK(ptr) \
- GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, ptr, sizeof (* \
- ptr))
+ GNUNET_CRYPTO_random_block (ptr, sizeof (*ptr))
/**
- * Global return value for the test. Initially -1, set to 0 upon
- * completion. Other values indicate some kind of error.
+ * Global return value for the test. Initially #EXIT_FAILURE, set to
+ * 0 upon completion. Other values indicate some kind of error.
*/
static int result;
@@ -60,6 +59,9 @@ payment_it (void *cls,
const struct TALER_ClaimTokenP *token,
const struct TALER_Amount *amount)
{
+ (void) timestamp;
+ (void) token;
+ (void) amount;
GNUNET_assert (NULL == cls);
GNUNET_assert (0 == strcmp (order_id,
"fake-order-2"));
@@ -67,6 +69,113 @@ payment_it (void *cls,
/**
+ * Iterator used by the #SYNCDB_lookup_block_range_TR() tests,
+ * simply counts the blocks it is called with.
+ *
+ * @param cls an `int *` counter to increment
+ * @param nonce block nonce
+ * @param block_hash hash of the block data
+ * @param prev_nonce previous block nonce (NULL if first)
+ * @param next_nonce next block nonce (NULL if last)
+ * @param data_size size of @a data
+ * @param data encrypted block contents
+ * @param upload_sig signature stored with the block
+ * @param old_hash hash of the replaced block data
+ * @param refs_hash hash over the object references
+ */
+static void
+block_it (void *cls,
+ const struct SYNC_BlockNonce *nonce,
+ const struct GNUNET_HashCode *block_hash,
+ const struct SYNC_BlockNonce *prev_nonce,
+ const struct SYNC_BlockNonce *next_nonce,
+ size_t data_size,
+ const void *data,
+ const struct SYNC_AccountSignatureP *upload_sig,
+ const struct GNUNET_HashCode *old_hash,
+ const struct GNUNET_HashCode *refs_hash)
+{
+ (void) upload_sig;
+ (void) old_hash;
+ (void) refs_hash;
+ int *cnt = cls;
+
+ (void) nonce;
+ (void) block_hash;
+ (void) prev_nonce;
+ (void) next_nonce;
+ (void) data_size;
+ (void) data;
+ (*cnt)++;
+}
+
+
+/**
+ * Create a paid account, so that the SQL entry points do not reject
+ * it with #SYNC_DB_PAYMENT_REQUIRED.
+ *
+ * @param account_pub account to activate
+ * @param order_id (unique) order ID to use for the fake payment
+ * @return #GNUNET_OK on success
+ */
+static enum GNUNET_GenericReturnValue
+activate_account (const struct SYNC_AccountPublicKeyP *account_pub,
+ const char *order_id)
+{
+ struct TALER_Amount amount;
+ struct TALER_ClaimTokenP token;
+
+ memset (&token, 3, sizeof (token));
+ if (GNUNET_OK !=
+ TALER_string_to_amount ("EUR:1",
+ &amount))
+ {
+ GNUNET_break (0);
+ return GNUNET_SYSERR;
+ }
+ if (SYNC_DB_ONE_RESULT !=
+ SYNCDB_store_payment_TR (account_pub,
+ order_id,
+ &token,
+ &amount,
+ GNUNET_TIME_UNIT_MINUTES))
+ {
+ GNUNET_break (0);
+ return GNUNET_SYSERR;
+ }
+ if (SYNC_DB_ONE_RESULT !=
+ SYNCDB_increment_lifetime_TR (account_pub,
+ order_id,
+ GNUNET_TIME_UNIT_MINUTES))
+ {
+ GNUNET_break (0);
+ return GNUNET_SYSERR;
+ }
+ return GNUNET_OK;
+}
+
+
+/**
+ * Count the blocks currently stored for @a account_pub.
+ *
+ * @param account_pub account to count blocks of
+ * @return number of blocks found
+ */
+static int
+count_blocks (const struct SYNC_AccountPublicKeyP *account_pub)
+{
+ int got = 0;
+
+ SYNCDB_lookup_block_range_TR (account_pub,
+ NULL,
+ 100,
+ &block_it,
+ &got);
+ return got;
+}
+
+
+/**
* Main function that will be run by the scheduler.
*
* @param cls closure with config
@@ -77,21 +186,20 @@ run (void *cls)
struct GNUNET_CONFIGURATION_Handle *cfg = cls;
struct TALER_Amount amount;
struct SYNC_AccountPublicKeyP account_pub;
- struct SYNC_AccountSignatureP account_sig;
- struct SYNC_AccountSignatureP account_sig2;
struct GNUNET_HashCode h;
struct GNUNET_HashCode h2;
- struct GNUNET_HashCode h3;
- struct GNUNET_HashCode r;
- struct GNUNET_HashCode r2;
struct GNUNET_TIME_Absolute ts;
struct TALER_ClaimTokenP token;
- size_t bs;
- void *b = NULL;
+ /* P1: fake signature/refs for the DB-level tests (the procedures
+ store, they do not verify) */
+ struct SYNC_AccountSignatureP fake_sig;
+ struct GNUNET_HashCode empty_refs;
+
+ memset (&fake_sig, 7, sizeof (fake_sig));
+ GNUNET_CRYPTO_hash ("", 0, &empty_refs);
if (GNUNET_OK !=
- SYNCDB_init (cfg,
- true))
+ SYNCDB_init_admin (cfg))
{
result = 77;
return;
@@ -111,127 +219,738 @@ run (void *cls)
GNUNET_assert (GNUNET_OK ==
SYNCDB_preflight ());
memset (&account_pub, 1, sizeof (account_pub));
- memset (&account_sig, 2, sizeof (account_sig));
memset (&token, 3, sizeof (token));
GNUNET_CRYPTO_hash ("data", 4, &h);
GNUNET_CRYPTO_hash ("DATA", 4, &h2);
- GNUNET_CRYPTO_hash ("ATAD", 4, &h3);
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount ("EUR:1",
&amount));
FAILIF (SYNC_DB_ONE_RESULT !=
- SYNCDB_store_payment_TR (&account_pub,
- "fake-order",
- &token,
- &amount));
- FAILIF (SYNC_DB_ONE_RESULT !=
- SYNCDB_increment_lifetime_TR (&account_pub,
- "fake-order",
- GNUNET_TIME_UNIT_MINUTES));
+ SYNCDB_store_payment_TR (
+ &account_pub,
+ "fake-order",
+ &token,
+ &amount,
+ GNUNET_TIME_UNIT_MINUTES));
FAILIF (SYNC_DB_ONE_RESULT !=
- SYNCDB_store_backup_TR (&account_pub,
- &account_sig,
- &h,
- 4,
- "data"));
- FAILIF (SYNC_DB_NO_RESULTS !=
- SYNCDB_store_backup_TR (&account_pub,
- &account_sig,
- &h,
- 4,
- "data"));
- FAILIF (SYNC_DB_ONE_RESULT !=
- SYNCDB_update_backup_TR (&account_pub,
- &h,
- &account_sig,
- &h2,
- 4,
- "DATA"));
- FAILIF (SYNC_DB_OLD_BACKUP_MISMATCH !=
- SYNCDB_update_backup_TR (&account_pub,
- &h,
- &account_sig,
- &h3,
- 4,
- "ATAD"));
- FAILIF (SYNC_DB_NO_RESULTS !=
- SYNCDB_update_backup_TR (&account_pub,
- &h,
- &account_sig,
- &h2,
- 4,
- "DATA"));
- FAILIF (SYNC_DB_ONE_RESULT !=
- SYNCDB_lookup_account_TR (&account_pub,
- &r));
- FAILIF (0 != GNUNET_memcmp (&r,
- &h2));
- FAILIF (SYNC_DB_ONE_RESULT !=
- SYNCDB_lookup_backup_TR (&account_pub,
- &account_sig2,
- &r,
- &r2,
- &bs,
- &b));
- FAILIF (0 != GNUNET_memcmp (&r,
- &h));
- FAILIF (0 != GNUNET_memcmp (&r2,
- &h2));
- FAILIF (0 != GNUNET_memcmp (&account_sig2,
- &account_sig));
- FAILIF (bs != 4);
- FAILIF (0 != memcmp (b,
- "DATA",
- 4));
- GNUNET_free (b);
- b = NULL;
+ SYNCDB_increment_lifetime_TR (
+ &account_pub,
+ "fake-order",
+ GNUNET_TIME_UNIT_MINUTES));
+
FAILIF (0 !=
- SYNCDB_lookup_pending_payments_by_account_TR (&account_pub,
- &payment_it,
- NULL));
+ SYNCDB_lookup_pending_payments_by_account_TR (
+ &account_pub,
+ &payment_it,
+ NULL));
memset (&account_pub, 2, sizeof (account_pub));
FAILIF (SYNC_DB_ONE_RESULT !=
- SYNCDB_store_payment_TR (&account_pub,
- "fake-order-2",
- &token,
- &amount));
+ SYNCDB_store_payment_TR (
+ &account_pub,
+ "fake-order-2",
+ &token,
+ &amount,
+ GNUNET_TIME_UNIT_MINUTES));
FAILIF (1 !=
- SYNCDB_lookup_pending_payments_by_account_TR (&account_pub,
- &payment_it,
- NULL));
- FAILIF (SYNC_DB_PAYMENT_REQUIRED !=
- SYNCDB_store_backup_TR (&account_pub,
- &account_sig,
- &h,
- 4,
- "data"));
+ SYNCDB_lookup_pending_payments_by_account_TR (
+ &account_pub,
+ &payment_it,
+ NULL));
FAILIF (SYNC_DB_ONE_RESULT !=
- SYNCDB_increment_lifetime_TR (&account_pub,
- "fake-order-2",
- GNUNET_TIME_UNIT_MINUTES));
- FAILIF (SYNC_DB_OLD_BACKUP_MISSING !=
- SYNCDB_update_backup_TR (&account_pub,
- &h,
- &account_sig,
- &h2,
- 4,
- "DATA"));
+ SYNCDB_increment_lifetime_TR (
+ &account_pub,
+ "fake-order-2",
+ GNUNET_TIME_UNIT_MINUTES));
+ /* --- DLL tests: append three blocks A->B->C and validate lookup --- */
+ {
+ struct SYNC_BlockNonce nA;
+ struct SYNC_BlockNonce nB;
+ struct SYNC_BlockNonce nC;
+ struct GNUNET_HashCode hA;
+ struct GNUNET_HashCode hB;
+ struct GNUNET_HashCode hC;
+ char dataA[] = "A";
+ char dataB[] = "B";
+ char dataC[] = "C";
+ int got;
+
+ RND_BLK (&nA);
+ RND_BLK (&nB);
+ RND_BLK (&nC);
+ GNUNET_CRYPTO_hash (dataA, sizeof dataA - 1, &hA);
+ GNUNET_CRYPTO_hash (dataB, sizeof dataB - 1, &hB);
+ GNUNET_CRYPTO_hash (dataC, sizeof dataC - 1, &hC);
+
+ /* Append A (prev NULL) */
+ FAILIF (SYNC_DB_ONE_RESULT !=
+ SYNCDB_append_block_TR (
+ &account_pub,
+ &nA,
+ NULL,
+ &hA,
+ sizeof dataA - 1,
+ dataA,
+ 0,
+ NULL,
+ NULL,
+ &fake_sig,
+ &empty_refs,
+ NULL,
+ NULL,
+ NULL));
+
+
+ /* Append B (prev A) */
+ FAILIF (SYNC_DB_ONE_RESULT !=
+ SYNCDB_append_block_TR (
+ &account_pub,
+ &nB,
+ &nA,
+ &hB,
+ sizeof dataB - 1,
+ dataB,
+ 0,
+ NULL,
+ NULL,
+ &fake_sig,
+ &empty_refs,
+ &fake_sig,
+ &hA,
+ NULL));
+
+
+ /* Append C (prev B) */
+ FAILIF (SYNC_DB_ONE_RESULT !=
+ SYNCDB_append_block_TR (
+ &account_pub,
+ &nC,
+ &nB,
+ &hC,
+ sizeof dataC - 1,
+ dataC,
+ 0,
+ NULL,
+ NULL,
+ &fake_sig,
+ &empty_refs,
+ &fake_sig,
+ &hB,
+ &nA));
+
+ /* Verify lookup returns 3 blocks */
+ got = 0;
+ FAILIF (3 !=
+ SYNCDB_lookup_block_range_TR (
+ &account_pub,
+ &nA,
+ 10,
+ &block_it,
+ &got));
+ FAILIF (got != 3);
+
+ /* The relink signature covers the neighbour's other link, so an
+ append whose view of it went stale must be refused rather than
+ stored (the caller reads the neighbour outside the transaction) */
+ {
+ struct SYNC_BlockNonce nS;
+ struct GNUNET_HashCode hS;
+ char dataS[] = "S";
+
+ RND_BLK (&nS);
+ GNUNET_CRYPTO_hash (dataS, sizeof dataS - 1, &hS);
+ FAILIF (SYNC_DB_OUTDATED_WRITE !=
+ SYNCDB_append_block_TR (
+ &account_pub,
+ &nS,
+ &nC,
+ &hS,
+ sizeof dataS - 1,
+ dataS,
+ 0,
+ NULL,
+ NULL,
+ &fake_sig,
+ &empty_refs,
+ &fake_sig,
+ &hC,
+ /* C's predecessor is B, not A */ &nA));
+ FAILIF (3 != count_blocks (&account_pub));
+ }
+
+ /* Delete middle block B (use its prev and next) */
+ FAILIF (SYNC_DB_ONE_RESULT !=
+ SYNCDB_delete_block_TR (
+ &account_pub,
+ &nB,
+ &nA,
+ &nC,
+ &hB,
+ 0,
+ NULL,
+ NULL,
+ &fake_sig,
+ &hA,
+ NULL,
+ &fake_sig,
+ &hC,
+ NULL));
+
+ /* Update block C (test update path) */
+ {
+ struct GNUNET_HashCode newC;
+ char newDataC[] = "C-new";
+
+ GNUNET_CRYPTO_hash (newDataC, sizeof newDataC - 1, &newC);
+ FAILIF (SYNC_DB_ONE_RESULT !=
+ SYNCDB_update_block_TR (
+ &account_pub,
+ &nC,
+ /* prev */ &nA,
+ /* next */ NULL,
+ &hC,
+ &newC,
+ sizeof newDataC - 1,
+ newDataC,
+ 0,
+ NULL,
+ NULL,
+ &fake_sig,
+ &empty_refs));
+ hC = newC;
+ }
+
+ /* --- Outdated-write tests --- */
+ {
+ struct SYNC_BlockNonce nD;
+ struct GNUNET_HashCode hD;
+ char dataD[] = "D";
+ struct GNUNET_HashCode bogus_old;
+ char newDataC2[] = "C-newer";
+ struct GNUNET_HashCode newC2;
+
+ RND_BLK (&nD);
+ GNUNET_CRYPTO_hash (dataD, sizeof dataD - 1, &hD);
+
+ /* Appending with NULL prev when backup is non-empty should be outdated */
+ FAILIF (SYNC_DB_OUTDATED_WRITE !=
+ SYNCDB_append_block_TR (
+ &account_pub,
+ &nD,
+ /* prev */ NULL,
+ &hD,
+ sizeof dataD - 1,
+ dataD,
+ 0,
+ NULL,
+ NULL,
+ &fake_sig,
+ &empty_refs,
+ NULL,
+ NULL,
+ NULL));
+
+ /* Updating C with wrong old-data-hash should be outdated */
+ GNUNET_CRYPTO_hash ("bogus", 5, &bogus_old);
+ GNUNET_CRYPTO_hash (newDataC2, sizeof newDataC2 - 1, &newC2);
+ FAILIF (SYNC_DB_OUTDATED_WRITE !=
+ SYNCDB_update_block_TR (
+ &account_pub,
+ &nC,
+ /* prev */ &nA,
+ /* next */ NULL,
+ &bogus_old,
+ &newC2,
+ sizeof newDataC2 - 1,
+ newDataC2,
+ 0,
+ NULL,
+ NULL,
+ &fake_sig,
+ &empty_refs));
+
+ /* Deleting C with wrong prev nonce should be outdated */
+ FAILIF (SYNC_DB_OUTDATED_WRITE !=
+ SYNCDB_delete_block_TR (
+ &account_pub,
+ &nC,
+ /* prev (wrong) */ &nB,
+ /* next */ NULL,
+ &hC,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL));
+ }
+ }
+
+ /* --- Account status lookup --- */
+ {
+ struct SYNC_AccountPublicKeyP acc;
+ struct SYNC_BlockNonce nonce;
+ struct GNUNET_HashCode bh;
+ char data[] = "account-status-block";
+ struct GNUNET_TIME_Timestamp expiration;
+ uint64_t storage_used_bytes;
+ uint64_t block_count;
+
+ memset (&acc, 20, sizeof (acc));
+
+ /* An account nobody has heard of is missing, not expired */
+ FAILIF (SYNC_DB_TARGET_MISSING !=
+ SYNCDB_lookup_account_TR (&acc,
+ &expiration,
+ &storage_used_bytes,
+ &block_count));
+
+ FAILIF (GNUNET_OK !=
+ activate_account (&acc,
+ "fake-order-account-status"));
+
+ /* A fresh account has an expiration and no storage in use */
+ FAILIF (SYNC_DB_ONE_RESULT !=
+ SYNCDB_lookup_account_TR (&acc,
+ &expiration,
+ &storage_used_bytes,
+ &block_count));
+ FAILIF (GNUNET_TIME_absolute_is_zero (expiration.abs_time));
+ FAILIF (0 != storage_used_bytes);
+ FAILIF (0 != block_count);
+
+ /* ... and the block it stores is reflected in both counters */
+ RND_BLK (&nonce);
+ GNUNET_CRYPTO_hash (data, sizeof data - 1, &bh);
+ FAILIF (SYNC_DB_ONE_RESULT !=
+ SYNCDB_append_block_TR (&acc,
+ &nonce,
+ NULL,
+ &bh,
+ sizeof data - 1,
+ data,
+ 0,
+ NULL,
+ NULL,
+ &fake_sig,
+ &empty_refs,
+ NULL,
+ NULL,
+ NULL));
+ FAILIF (SYNC_DB_ONE_RESULT !=
+ SYNCDB_lookup_account_TR (&acc,
+ &expiration,
+ &storage_used_bytes,
+ &block_count));
+ FAILIF (sizeof data - 1 != storage_used_bytes);
+ FAILIF (1 != block_count);
+
+ /* The blocks of other accounts must not be counted here */
+ FAILIF (SYNC_DB_ONE_RESULT !=
+ SYNCDB_lookup_account_TR (&account_pub,
+ &expiration,
+ &storage_used_bytes,
+ &block_count));
+ FAILIF (1 == block_count);
+ }
+
+ /* --- Object store/lookup tests --- */
+ {
+ struct SYNC_ObjectUID obj_uid;
+ struct GNUNET_HashCode obj_hash;
+ char obj_data[] = "objdata";
+ int64_t refcount = 0;
+ struct GNUNET_TIME_Timestamp exp;
+ size_t data_sz = 0;
+
+ RND_BLK (&obj_uid);
+ GNUNET_CRYPTO_hash (obj_data, sizeof obj_data - 1, &obj_hash);
+ FAILIF (SYNC_DB_ONE_RESULT !=
+ SYNCDB_store_object_TR (
+ &obj_uid,
+ &account_pub,
+ &obj_hash,
+ GNUNET_TIME_UNIT_DAYS,
+ sizeof obj_data - 1,
+ obj_data));
+
+ FAILIF (SYNC_DB_ONE_RESULT !=
+ SYNCDB_lookup_object (
+ &account_pub,
+ &obj_hash,
+ &refcount,
+ &exp,
+ &data_sz,
+ NULL));
+ FAILIF (refcount < 0);
+
+ /* A missing object is a normal outcome, not a failure */
+ {
+ struct GNUNET_HashCode bogus_hash;
+
+ GNUNET_CRYPTO_hash ("no-such-object",
+ 14,
+ &bogus_hash);
+ FAILIF (SYNC_DB_NO_RESULTS !=
+ SYNCDB_lookup_object (&account_pub,
+ &bogus_hash,
+ &refcount,
+ &exp,
+ &data_sz,
+ NULL));
+ }
+ }
+
+ /* --- Referencing an object that was never uploaded --- */
+ {
+ struct SYNC_AccountPublicKeyP acc;
+ struct SYNC_BlockNonce nonce;
+ struct GNUNET_HashCode bh;
+ struct SYNC_ObjectUID unknown_uid;
+ const int16_t inc = 1;
+ char data[] = "orphan";
+
+ memset (&acc, 10, sizeof (acc));
+ FAILIF (GNUNET_OK !=
+ activate_account (&acc,
+ "fake-order-refs-missing"));
+ RND_BLK (&nonce);
+ RND_BLK (&unknown_uid);
+ GNUNET_CRYPTO_hash (data, sizeof data - 1, &bh);
+ FAILIF (SYNC_DB_INCONSISTENT_BACKUP !=
+ SYNCDB_append_block_TR (&acc,
+ &nonce,
+ NULL,
+ &bh,
+ sizeof data - 1,
+ data,
+ 1,
+ &unknown_uid,
+ &inc,
+ &fake_sig,
+ &empty_refs,
+ NULL,
+ NULL,
+ NULL));
+ /* the rejected append must be a no-op */
+ FAILIF (0 != count_blocks (&acc));
+ }
+
+ /* --- A nonce that is already taken is a conflict --- */
+ {
+ struct SYNC_AccountPublicKeyP acc_a;
+ struct SYNC_AccountPublicKeyP acc_b;
+ struct SYNC_BlockNonce nonce;
+ struct GNUNET_HashCode bh;
+ char data[] = "shared-nonce";
+
+ memset (&acc_a, 11, sizeof (acc_a));
+ memset (&acc_b, 12, sizeof (acc_b));
+ FAILIF (GNUNET_OK !=
+ activate_account (&acc_a,
+ "fake-order-nonce-a"));
+ FAILIF (GNUNET_OK !=
+ activate_account (&acc_b,
+ "fake-order-nonce-b"));
+ RND_BLK (&nonce);
+ GNUNET_CRYPTO_hash (data, sizeof data - 1, &bh);
+ FAILIF (SYNC_DB_ONE_RESULT !=
+ SYNCDB_append_block_TR (&acc_a,
+ &nonce,
+ NULL,
+ &bh,
+ sizeof data - 1,
+ data,
+ 0,
+ NULL,
+ NULL,
+ &fake_sig,
+ &empty_refs,
+ NULL,
+ NULL,
+ NULL));
+ FAILIF (SYNC_DB_OUTDATED_WRITE !=
+ SYNCDB_append_block_TR (&acc_b,
+ &nonce,
+ NULL,
+ &bh,
+ sizeof data - 1,
+ data,
+ 0,
+ NULL,
+ NULL,
+ &fake_sig,
+ &empty_refs,
+ NULL,
+ NULL,
+ NULL));
+ /* the rejected append must not have touched account B */
+ FAILIF (0 != count_blocks (&acc_b));
+ }
+
+ /* --- Refcount lifecycle --- */
+ {
+ struct SYNC_AccountPublicKeyP acc;
+ struct SYNC_BlockNonce nonce;
+ struct GNUNET_HashCode bh;
+ struct SYNC_ObjectUID uid;
+ struct GNUNET_HashCode oh;
+ const int16_t inc = 1;
+ const int16_t dec = -1;
+ const int16_t dec2 = -2;
+ char data[] = "refcounted";
+ char obj[] = "payload";
+ int64_t refcount = 0;
+ struct GNUNET_TIME_Timestamp exp;
+ size_t data_sz = 0;
+
+ memset (&acc, 13, sizeof (acc));
+ FAILIF (GNUNET_OK !=
+ activate_account (&acc,
+ "fake-order-refcount"));
+ RND_BLK (&nonce);
+ RND_BLK (&uid);
+ GNUNET_CRYPTO_hash (data, sizeof data - 1, &bh);
+ GNUNET_CRYPTO_hash (obj, sizeof obj - 1, &oh);
+ FAILIF (SYNC_DB_ONE_RESULT !=
+ SYNCDB_store_object_TR (&uid,
+ &acc,
+ &oh,
+ GNUNET_TIME_UNIT_DAYS,
+ sizeof obj - 1,
+ obj));
+ /* re-uploading the same UID is idempotent, not an error */
+ FAILIF (SYNC_DB_ONE_RESULT !=
+ SYNCDB_store_object_TR (&uid,
+ &acc,
+ &oh,
+ GNUNET_TIME_UNIT_DAYS,
+ sizeof obj - 1,
+ obj));
+ FAILIF (SYNC_DB_ONE_RESULT !=
+ SYNCDB_append_block_TR (&acc,
+ &nonce,
+ NULL,
+ &bh,
+ sizeof data - 1,
+ data,
+ 1,
+ &uid,
+ &inc,
+ &fake_sig,
+ &empty_refs,
+ NULL,
+ NULL,
+ NULL));
+ FAILIF (SYNC_DB_ONE_RESULT !=
+ SYNCDB_lookup_object (&acc,
+ &oh,
+ &refcount,
+ &exp,
+ &data_sz,
+ NULL));
+ FAILIF (1 != refcount);
+
+ /* decrementing below zero is refused, refcount untouched */
+ FAILIF (SYNC_DB_INCONSISTENT_BACKUP !=
+ SYNCDB_delete_block_TR (&acc,
+ &nonce,
+ NULL,
+ NULL,
+ &bh,
+ 1,
+ &uid,
+ &dec2,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL));
+ FAILIF (SYNC_DB_ONE_RESULT !=
+ SYNCDB_lookup_object (&acc,
+ &oh,
+ &refcount,
+ &exp,
+ &data_sz,
+ NULL));
+ FAILIF (1 != refcount);
+ /* ... and the block must still be there */
+ FAILIF (1 != count_blocks (&acc));
+
+ /* the matching decrement drops it to 0, deleting the object */
+ FAILIF (SYNC_DB_ONE_RESULT !=
+ SYNCDB_delete_block_TR (&acc,
+ &nonce,
+ NULL,
+ NULL,
+ &bh,
+ 1,
+ &uid,
+ &dec,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL));
+ FAILIF (SYNC_DB_NO_RESULTS !=
+ SYNCDB_lookup_object (&acc,
+ &oh,
+ &refcount,
+ &exp,
+ &data_sz,
+ NULL));
+ FAILIF (0 != count_blocks (&acc));
+ }
+
+ /* --- Increments for the same UID are aggregated --- */
+ {
+ struct SYNC_AccountPublicKeyP acc;
+ struct SYNC_BlockNonce nonce;
+ struct GNUNET_HashCode bh;
+ struct SYNC_ObjectUID uids[2];
+ struct GNUNET_HashCode oh;
+ const int16_t incs[2] = { -1, 1 };
+ char data[] = "net-zero";
+ char obj[] = "aggregated";
+ int64_t refcount = 0;
+ struct GNUNET_TIME_Timestamp exp;
+ size_t data_sz = 0;
+
+ memset (&acc, 14, sizeof (acc));
+ FAILIF (GNUNET_OK !=
+ activate_account (&acc,
+ "fake-order-aggregate"));
+ RND_BLK (&nonce);
+ RND_BLK (&uids[0]);
+ uids[1] = uids[0];
+ GNUNET_CRYPTO_hash (data, sizeof data - 1, &bh);
+ GNUNET_CRYPTO_hash (obj, sizeof obj - 1, &oh);
+ FAILIF (SYNC_DB_ONE_RESULT !=
+ SYNCDB_store_object_TR (&uids[0],
+ &acc,
+ &oh,
+ GNUNET_TIME_UNIT_DAYS,
+ sizeof obj - 1,
+ obj));
+ FAILIF (SYNC_DB_ONE_RESULT !=
+ SYNCDB_append_block_TR (&acc,
+ &nonce,
+ NULL,
+ &bh,
+ sizeof data - 1,
+ data,
+ 2,
+ uids,
+ incs,
+ &fake_sig,
+ &empty_refs,
+ NULL,
+ NULL,
+ NULL));
+ /* refcount unchanged, and the object still exists */
+ FAILIF (SYNC_DB_ONE_RESULT !=
+ SYNCDB_lookup_object (&acc,
+ &oh,
+ &refcount,
+ &exp,
+ &data_sz,
+ NULL));
+ FAILIF (0 != refcount);
+ }
+
+ /* --- GC must not remove still-referenced objects --- */
+ {
+ struct SYNC_AccountPublicKeyP acc;
+ struct SYNC_BlockNonce nonce;
+ struct GNUNET_HashCode bh;
+ struct SYNC_ObjectUID kept_uid;
+ struct SYNC_ObjectUID swept_uid;
+ struct GNUNET_HashCode kept_oh;
+ struct GNUNET_HashCode swept_oh;
+ const int16_t inc = 1;
+ char data[] = "gc-block";
+ char kept[] = "still-referenced";
+ char swept[] = "unreferenced";
+ int64_t refcount = 0;
+ struct GNUNET_TIME_Timestamp exp;
+ size_t data_sz = 0;
+ struct GNUNET_TIME_Absolute future;
+
+ memset (&acc, 15, sizeof (acc));
+ FAILIF (GNUNET_OK !=
+ activate_account (&acc,
+ "fake-order-gc"));
+ RND_BLK (&nonce);
+ RND_BLK (&kept_uid);
+ RND_BLK (&swept_uid);
+ GNUNET_CRYPTO_hash (data, sizeof data - 1, &bh);
+ GNUNET_CRYPTO_hash (kept, sizeof kept - 1, &kept_oh);
+ GNUNET_CRYPTO_hash (swept, sizeof swept - 1, &swept_oh);
+ FAILIF (SYNC_DB_ONE_RESULT !=
+ SYNCDB_store_object_TR (&kept_uid,
+ &acc,
+ &kept_oh,
+ GNUNET_TIME_UNIT_DAYS,
+ sizeof kept - 1,
+ kept));
+ FAILIF (SYNC_DB_ONE_RESULT !=
+ SYNCDB_store_object_TR (&swept_uid,
+ &acc,
+ &swept_oh,
+ GNUNET_TIME_UNIT_DAYS,
+ sizeof swept - 1,
+ swept));
+ FAILIF (SYNC_DB_ONE_RESULT !=
+ SYNCDB_append_block_TR (&acc,
+ &nonce,
+ NULL,
+ &bh,
+ sizeof data - 1,
+ data,
+ 1,
+ &kept_uid,
+ &inc,
+ &fake_sig,
+ &empty_refs,
+ NULL,
+ NULL,
+ NULL));
+ /* Leave accounts and payments alone, only sweep objects */
+ future = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_YEARS);
+ FAILIF (GNUNET_OK !=
+ SYNCDB_gc (GNUNET_TIME_UNIT_ZERO_ABS,
+ GNUNET_TIME_UNIT_ZERO_ABS,
+ future));
+ /* the referenced object survives ... */
+ FAILIF (SYNC_DB_ONE_RESULT !=
+ SYNCDB_lookup_object (&acc,
+ &kept_oh,
+ &refcount,
+ &exp,
+ &data_sz,
+ NULL));
+ FAILIF (1 != refcount);
+ /* ... the unreferenced one is collected */
+ FAILIF (SYNC_DB_NO_RESULTS !=
+ SYNCDB_lookup_object (&acc,
+ &swept_oh,
+ &refcount,
+ &exp,
+ &data_sz,
+ NULL));
+ }
+
ts = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_YEARS);
- FAILIF (0 >
+ FAILIF (GNUNET_OK !=
SYNCDB_gc (ts,
+ ts,
ts));
- memset (&account_pub, 1, sizeof (account_pub));
- FAILIF (SYNC_DB_NO_RESULTS !=
- SYNCDB_lookup_backup_TR (&account_pub,
- &account_sig2,
- &r,
- &r2,
- &bs,
- &b));
-
result = 0;
drop:
- GNUNET_free (b);
GNUNET_break (GNUNET_OK ==
SYNCDB_drop_tables ());
SYNCDB_fini ();
@@ -244,39 +963,36 @@ main (int argc,
{
const char *plugin_name;
char *config_filename;
- char *testname;
struct GNUNET_CONFIGURATION_Handle *cfg;
+ (void) argc;
result = EXIT_FAILURE;
+ /* the database backend is the suffix of our own binary name */
if (NULL == (plugin_name = strrchr (argv[0], (int) '-')))
{
GNUNET_break (0);
return EXIT_FAILURE;
}
+ plugin_name++;
GNUNET_log_setup (argv[0],
"DEBUG",
NULL);
- plugin_name++;
- (void) GNUNET_asprintf (&testname,
- "%s",
- plugin_name);
- (void) GNUNET_asprintf (&config_filename,
- "test_sync_db_%s.conf",
- testname);
+ GNUNET_asprintf (&config_filename,
+ "test_sync_db_%s.conf",
+ plugin_name);
cfg = GNUNET_CONFIGURATION_create (SYNC_project_data ());
if (GNUNET_OK !=
GNUNET_CONFIGURATION_parse (cfg,
config_filename))
{
GNUNET_break (0);
+ GNUNET_CONFIGURATION_destroy (cfg);
GNUNET_free (config_filename);
- GNUNET_free (testname);
return EXIT_NOTCONFIGURED;
}
GNUNET_SCHEDULER_run (&run, cfg);
GNUNET_CONFIGURATION_destroy (cfg);
GNUNET_free (config_filename);
- GNUNET_free (testname);
return result;
}
diff --git a/src/testing/meson.build b/src/testing/meson.build
@@ -2,15 +2,21 @@
libsynctesting = library(
'synctesting',
[
- 'testing_api_cmd_backup_download.c',
- 'testing_api_cmd_backup_upload.c',
'testing_api_traits.c',
+ 'testing_api_cmd_block_common.c',
+ 'testing_api_cmd_block_append.c',
+ 'testing_api_cmd_block_update.c',
+ 'testing_api_cmd_block_delete.c',
+ 'testing_api_cmd_block_list.c',
+ 'testing_api_cmd_object_upload.c',
+ 'testing_api_cmd_object_fetch.c',
],
soversion: solibversions['libsynctesting']['soversion'],
version: solibversions['libsynctesting']['soversion'],
install_rpath: rpath_option,
dependencies: [
libsync_dep,
+ libsyncutil_dep,
talermerchant_dep,
talerutil_dep,
talerexchange_dep,
@@ -41,6 +47,7 @@ test_sync_api = executable(
dependencies: [
libsync_dep,
libsynctesting_dep,
+ libsyncutil_dep,
talerutil_dep,
talerjson_dep,
talermerchant_dep,
@@ -71,3 +78,4 @@ configure_file(
)
subdir('test_sync_api_home' / '.local' / 'share' / 'taler' / 'exchange-offline')
+
diff --git a/src/testing/test_sync_api.c b/src/testing/test_sync_api.c
@@ -17,7 +17,7 @@
<http://www.gnu.org/licenses/>
*/
/**
- * @file lib/test_sync_api.c
+ * @file testing/test_sync_api.c
* @brief testcase to test sync's HTTP API interface
* @author Christian Grothoff
*/
@@ -35,6 +35,8 @@
#include <taler/taler_error_codes.h>
#include "sync/sync_service.h"
#include "sync/sync_testing_lib.h"
+#include "sync_util.h"
+
/**
* Configuration file we use. One (big) configuration is used
@@ -43,16 +45,6 @@
#define CONFIG_FILE "test_sync_api.conf"
/**
- * Exchange base URL. Could also be taken from config.
- */
-#define EXCHANGE_URL "http://localhost:8081/"
-
-/**
- * Account number of the exchange at the bank.
- */
-#define EXCHANGE_ACCOUNT_NAME "2"
-
-/**
* Account number of some user.
*/
#define USER_ACCOUNT_NAME "62"
@@ -68,11 +60,6 @@
static struct TALER_FullPayto payer_payto;
/**
- * Payto URI of the exchange (escrow account).
- */
-static struct TALER_FullPayto exchange_payto;
-
-/**
* Payto URI of the merchant (receiver).
*/
static struct TALER_FullPayto merchant_payto;
@@ -131,133 +118,467 @@ cmd_transfer_to_exchange (const char *label,
* run.
*
* @param cls closure
+ * @param is interpreter state
*/
static void
run (void *cls,
struct TALER_TESTING_Interpreter *is)
{
- struct TALER_TESTING_Command commands[] = {
- TALER_TESTING_cmd_system_start ("start-taler",
- CONFIG_FILE,
- "-efms",
- "-u", "exchange-account-exchange",
- "-r", "merchant-exchange-default",
- NULL),
- TALER_TESTING_cmd_get_exchange ("get-exchange",
- cred.cfg,
- NULL,
- true,
- true),
- TALER_TESTING_cmd_merchant_post_instances ("instance-create-admin",
- merchant_url,
- "admin",
- MHD_HTTP_NO_CONTENT),
- TALER_TESTING_cmd_merchant_post_account (
- "instance-create-default-account",
- merchant_url,
- merchant_payto,
- NULL, NULL,
- MHD_HTTP_OK),
-
- /**
- * Move money to the exchange's bank account.
- */
- cmd_transfer_to_exchange ("create-reserve-1",
- "EUR:10.02"),
- /**
- * Make a reserve exist, according to the previous
- * transfer.
- */
- cmd_exec_wirewatch ("wirewatch-1"),
- TALER_TESTING_cmd_withdraw_amount ("withdraw-coin-1",
- "create-reserve-1",
- "EUR:5",
- 0,
- MHD_HTTP_OK),
- TALER_TESTING_cmd_withdraw_amount ("withdraw-coin-2",
- "create-reserve-1",
- "EUR:5",
- 0,
- MHD_HTTP_OK),
- /* Failed download: no backup exists */
- SYNC_TESTING_cmd_backup_nx ("backup-download-nx",
- sync_url),
- /* Failed upload: need to pay */
- SYNC_TESTING_cmd_backup_upload ("backup-upload-1",
- sync_url,
- NULL /* prev upload */,
- NULL /* last upload */,
- SYNC_TESTING_UO_NONE,
- MHD_HTTP_PAYMENT_REQUIRED,
- "Test-1",
- strlen ("Test-1")),
- /* what would we have to pay? */
- TALER_TESTING_cmd_merchant_claim_order ("fetch-proposal",
+ /* Payload of the blocks we upload. data[0] is the initial content of
+ block A, which is later updated to data[1]; hashes[] holds the
+ hashes of the *final* content of blocks A, C and D. */
+ static const char *const data[] = {
+ "Hello World",
+ "Block B data",
+ "Block C data",
+ "Block D data"
+ };
+ static const char obj_data_A[] = "object-A-data";
+ static const char obj_data_B[] = "object-B-data";
+ /* Nonces of blocks A, C and D, in chain order. */
+ struct SYNC_BlockNonce nonces[3];
+ /* Hashes of blocks A, C and D, in chain order. */
+ struct GNUNET_HashCode hashes[3];
+ /* Nonce of a block that is never successfully uploaded. */
+ struct SYNC_BlockNonce nonce_stale;
+ /* Nonce that never exists on the server. */
+ struct SYNC_BlockNonce bogus_nonce;
+ /* Nonces of the two blocks used for the refcount tests. */
+ struct SYNC_BlockNonce ref_nonce;
+ struct SYNC_BlockNonce ref_nonce2;
+ /* Nonce of a block whose append is rejected for a bad object ref. */
+ struct SYNC_BlockNonce orphan_nonce;
+ /* Nonce of the block appended after that rejected append. */
+ struct SYNC_BlockNonce tail_nonce;
+ /* UIDs of the two objects used for the refcount tests. */
+ struct SYNC_ObjectUID obj_uids[2];
+ /* UID of an object that is never uploaded. */
+ struct SYNC_ObjectUID unknown_uid;
+ const int16_t ref_incs[2] = { 1, 1 };
+ const int16_t ref_decs[2] = { -1, -1 };
+ const int16_t ref_inc1[1] = { 1 };
+ const int16_t ref_dec2[1] = { -2 };
+
+ (void) cls;
+ for (unsigned int i = 0; i < 3; i++)
+ SYNC_block_nonce_generate (&nonces[i]);
+ SYNC_block_nonce_generate (&nonce_stale);
+ SYNC_block_nonce_generate (&bogus_nonce);
+ SYNC_block_nonce_generate (&ref_nonce);
+ SYNC_block_nonce_generate (&ref_nonce2);
+ SYNC_block_nonce_generate (&orphan_nonce);
+ SYNC_block_nonce_generate (&tail_nonce);
+ for (unsigned int i = 0; i < 2; i++)
+ GNUNET_CRYPTO_random_block (&obj_uids[i],
+ sizeof (obj_uids[i]));
+ GNUNET_CRYPTO_random_block (&unknown_uid,
+ sizeof (unknown_uid));
+ for (unsigned int i = 0; i < 3; i++)
+ GNUNET_CRYPTO_hash (data[i + 1],
+ strlen (data[i + 1]),
+ &hashes[i]);
+
+ {
+ struct TALER_TESTING_Command commands[] = {
+ TALER_TESTING_cmd_system_start ("start-taler",
+ CONFIG_FILE,
+ "-efms",
+ "-u", "exchange-account-exchange",
+ "-r", "merchant-exchange-default",
+ NULL),
+ TALER_TESTING_cmd_get_exchange ("get-exchange",
+ cred.cfg,
+ NULL,
+ true,
+ true),
+ TALER_TESTING_cmd_merchant_post_instances ("instance-create-admin",
+ merchant_url,
+ "admin",
+ MHD_HTTP_NO_CONTENT),
+ TALER_TESTING_cmd_merchant_post_account (
+ "instance-create-default-account",
+ merchant_url,
+ merchant_payto,
+ NULL, NULL,
+ MHD_HTTP_OK),
+
+ /**
+ * Move money to the exchange's bank account.
+ */
+ cmd_transfer_to_exchange ("create-reserve-1",
+ "EUR:10.02"),
+ /**
+ * Make a reserve exist, according to the previous
+ * transfer.
+ */
+ cmd_exec_wirewatch ("wirewatch-1"),
+ TALER_TESTING_cmd_withdraw_amount ("withdraw-coin-1",
+ "create-reserve-1",
+ "EUR:5",
+ 0,
+ MHD_HTTP_OK),
+ TALER_TESTING_cmd_withdraw_amount ("withdraw-coin-2",
+ "create-reserve-1",
+ "EUR:5",
+ 0,
+ MHD_HTTP_OK),
+
+ /* Upload first block; no lifetime yet → 402 Payment Required */
+ SYNC_TESTING_cmd_block_append ("upload-block-1",
+ sync_url,
+ NULL,
+ &nonces[0],
+ MHD_HTTP_PAYMENT_REQUIRED,
+ data[0],
+ strlen (data[0]),
+ true,
+ 0,
+ NULL,
+ NULL),
+ /* Claim the order from the merchant */
+ TALER_TESTING_cmd_merchant_claim_order ("fetch-proposal",
+ merchant_url,
+ MHD_HTTP_OK,
+ "upload-block-1",
+ NULL),
+ /* Pay with one withdrawn coin */
+ TALER_TESTING_cmd_merchant_pay_order ("pay-account",
merchant_url,
MHD_HTTP_OK,
- "backup-upload-1",
- NULL),
- /* make the payment */
- TALER_TESTING_cmd_merchant_pay_order ("pay-account",
- merchant_url,
- MHD_HTTP_OK,
- "fetch-proposal",
- "withdraw-coin-1",
- "EUR:5",
- "EUR:4.99", /* must match ANNUAL_FEE in config! */
- "session-id"),
- /* now upload should succeed */
- SYNC_TESTING_cmd_backup_upload ("backup-upload-2",
- sync_url,
- "backup-upload-1",
- NULL,
- SYNC_TESTING_UO_NONE,
- MHD_HTTP_NO_CONTENT,
- "Test-1",
- strlen ("Test-1")),
- /* now updated upload should succeed */
- SYNC_TESTING_cmd_backup_upload ("backup-upload-3",
- sync_url,
- "backup-upload-2",
- NULL,
- SYNC_TESTING_UO_NONE,
- MHD_HTTP_NO_CONTENT,
- "Test-3",
- strlen ("Test-3")),
- /* Test download: succeeds! */
- SYNC_TESTING_cmd_backup_download ("download-3",
+ "fetch-proposal",
+ "withdraw-coin-1",
+ "EUR:5",
+ "EUR:4.99",
+ "session-id"),
+ /* Retry same upload; server finds paid order → 204 */
+ SYNC_TESTING_cmd_block_append ("upload-block-2",
+ sync_url,
+ "upload-block-1",
+ &nonces[0],
+ MHD_HTTP_NO_CONTENT,
+ data[0],
+ strlen (data[0]),
+ true,
+ 0,
+ NULL,
+ NULL),
+ /* Update the existing block with different content */
+ SYNC_TESTING_cmd_block_update ("upload-block-B",
+ sync_url,
+ "upload-block-2",
+ &nonces[0],
+ NULL,
+ MHD_HTTP_NO_CONTENT,
+ data[1],
+ strlen (data[1]),
+ 0,
+ NULL,
+ NULL),
+ /* Append a new block C at the end of block B */
+ SYNC_TESTING_cmd_block_append ("upload-block-C",
+ sync_url,
+ "upload-block-B",
+ &nonces[1],
+ MHD_HTTP_NO_CONTENT,
+ data[2],
+ strlen (data[2]),
+ false,
+ 0,
+ NULL,
+ NULL),
+ /* List blocks for the account */
+ SYNC_TESTING_cmd_block_list ("list-blocks",
+ sync_url,
+ "upload-block-2",
+ 10,
+ NULL,
+ MHD_HTTP_OK,
+ 2,
+ nonces,
+ hashes),
+
+ /* Append with stale prev_nonce (nonces[0]),
+ but last_block is now nonces[1] → 409 */
+ SYNC_TESTING_cmd_block_append ("append-stale-prev",
+ sync_url,
+ "upload-block-B",
+ &nonce_stale,
+ MHD_HTTP_CONFLICT,
+ data[0],
+ strlen (data[0]),
+ false,
+ 0,
+ NULL,
+ NULL),
+ /* Update with stale old_data_hash
+ (references upload-block-2 whose curr_hash is hash("Hello World"),
+ but block now has hash("Block B data")) → 409 */
+ SYNC_TESTING_cmd_block_update ("update-conflict",
+ sync_url,
+ "upload-block-2",
+ &nonces[0],
+ NULL,
+ MHD_HTTP_CONFLICT,
+ data[2],
+ strlen (data[2]),
+ 0,
+ NULL,
+ NULL),
+ /* Re-upload same data → 304 Not Modified */
+ SYNC_TESTING_cmd_block_append ("reupload-same-data",
+ sync_url,
+ "upload-block-B",
+ &nonces[0],
+ MHD_HTTP_NOT_MODIFIED,
+ data[1],
+ strlen (data[1]),
+ true,
+ 0,
+ NULL,
+ NULL),
+ /* List with non-existent start_nonce → 404 */
+ SYNC_TESTING_cmd_block_list ("list-bogus-start",
+ sync_url,
+ "upload-block-2",
+ 10,
+ &bogus_nonce,
+ MHD_HTTP_NOT_FOUND,
+ 0,
+ NULL,
+ NULL),
+ /* Final list confirms error cases left state intact */
+ SYNC_TESTING_cmd_block_list ("list-blocks-final",
+ sync_url,
+ "upload-block-2",
+ 10,
+ NULL,
+ MHD_HTTP_OK,
+ 2,
+ nonces,
+ hashes),
+
+ /* ========== Delete block tests ========== */
+
+ /* Upload a third block D */
+ SYNC_TESTING_cmd_block_append ("upload-block-D",
+ sync_url,
+ "upload-block-C",
+ &nonces[2],
+ MHD_HTTP_NO_CONTENT,
+ data[3],
+ strlen (data[3]),
+ false,
+ 0,
+ NULL,
+ NULL),
+ /* List all 3 blocks */
+ SYNC_TESTING_cmd_block_list ("list-3-blocks",
+ sync_url,
+ "upload-block-D",
+ 10,
+ NULL,
+ MHD_HTTP_OK,
+ 3,
+ nonces,
+ hashes),
+ /* Delete block D */
+ SYNC_TESTING_cmd_block_delete ("delete-block-D",
+ sync_url,
+ "upload-block-D",
+ MHD_HTTP_NO_CONTENT,
+ 0,
+ NULL,
+ NULL),
+ /* List: only A and B remain */
+ SYNC_TESTING_cmd_block_list ("list-after-delete-D",
+ sync_url,
+ "upload-block-2",
+ 10,
+ NULL,
+ MHD_HTTP_OK,
+ 2,
+ nonces,
+ hashes),
+ /* Delete block D again → 404 (already gone) */
+ SYNC_TESTING_cmd_block_delete ("delete-block-D-again",
+ sync_url,
+ "upload-block-D",
+ MHD_HTTP_NOT_FOUND,
+ 0,
+ NULL,
+ NULL),
+ /* Final list confirms all is well */
+ SYNC_TESTING_cmd_block_list ("list-blocks-end",
+ sync_url,
+ "upload-block-2",
+ 10,
+ NULL,
+ MHD_HTTP_OK,
+ 2,
+ nonces,
+ hashes),
+
+ /* ========== Object store and refcount tests ========== */
+
+ /* Upload object A */
+ SYNC_TESTING_cmd_object_upload ("upload-obj-A",
sync_url,
- MHD_HTTP_OK,
- "backup-upload-3"),
- /* now updated upload should fail (conflict) */
- SYNC_TESTING_cmd_backup_upload ("backup-upload-3b",
- sync_url,
- "backup-upload-2",
- "backup-upload-3",
- SYNC_TESTING_UO_NONE,
- MHD_HTTP_CONFLICT,
- "Test-3b",
- strlen ("Test-3b")),
- /* now updated upload should fail (payment requested) */
- SYNC_TESTING_cmd_backup_upload ("backup-upload-4",
- sync_url,
- "backup-upload-3",
- "backup-upload-3",
- SYNC_TESTING_UO_REQUEST_PAYMENT,
- MHD_HTTP_PAYMENT_REQUIRED,
- "Test-4",
- strlen ("Test-4")),
- /* Test download: previous did NOT change the data on the server! */
- SYNC_TESTING_cmd_backup_download ("download-3b",
+ "upload-block-2",
+ &obj_uids[0],
+ MHD_HTTP_NO_CONTENT,
+ obj_data_A,
+ sizeof obj_data_A - 1),
+ /* Upload object B */
+ SYNC_TESTING_cmd_object_upload ("upload-obj-B",
sync_url,
- MHD_HTTP_OK,
- "backup-upload-3"),
+ "upload-block-2",
+ &obj_uids[1],
+ MHD_HTTP_NO_CONTENT,
+ obj_data_B,
+ sizeof obj_data_B - 1),
- TALER_TESTING_cmd_end ()
- };
+ /* Fetch object A and verify content and hash */
+ SYNC_TESTING_cmd_object_fetch ("fetch-obj-A",
+ sync_url,
+ "upload-block-2",
+ &obj_uids[0],
+ MHD_HTTP_OK,
+ obj_data_A,
+ sizeof obj_data_A - 1),
+
+ /* Fetch object B and verify content and hash */
+ SYNC_TESTING_cmd_object_fetch ("fetch-obj-B",
+ sync_url,
+ "upload-block-2",
+ &obj_uids[1],
+ MHD_HTTP_OK,
+ obj_data_B,
+ sizeof obj_data_B - 1),
+
+ /* Try uploading a duplicate UID → idempotent (204) */
+ SYNC_TESTING_cmd_object_upload ("upload-obj-A-dup",
+ sync_url,
+ "upload-block-2",
+ &obj_uids[0],
+ MHD_HTTP_NO_CONTENT,
+ obj_data_B,
+ sizeof obj_data_B - 1),
+
+ /* Upload a block that references objects A (+1) and B (+1) */
+ SYNC_TESTING_cmd_block_append ("upload-block-refs",
+ sync_url,
+ "upload-block-C",
+ &ref_nonce,
+ MHD_HTTP_NO_CONTENT,
+ data[0],
+ strlen (data[0]),
+ false,
+ 2,
+ obj_uids,
+ ref_incs),
+
+ /* Delete the referencing block with matching refs (decrement).
+ Objects reach refcount 0 and are auto-deleted from the DB. */
+ SYNC_TESTING_cmd_block_delete ("delete-block-refs",
+ sync_url,
+ "upload-block-refs",
+ MHD_HTTP_NO_CONTENT,
+ 2,
+ obj_uids,
+ ref_decs),
+
+ /* Re-upload object A (it was deleted when refcount hit 0) */
+ SYNC_TESTING_cmd_object_upload ("upload-obj-A-again",
+ sync_url,
+ "upload-block-2",
+ &obj_uids[0],
+ MHD_HTTP_NO_CONTENT,
+ obj_data_A,
+ sizeof obj_data_A - 1),
+
+ /* Upload another block referencing A (+1) */
+ SYNC_TESTING_cmd_block_append ("upload-block-refs2",
+ sync_url,
+ "upload-block-C",
+ &ref_nonce2,
+ MHD_HTTP_NO_CONTENT,
+ data[0],
+ strlen (data[0]),
+ false,
+ 1,
+ obj_uids,
+ ref_inc1),
+
+ /* Try to delete with refs that would underflow → 409 conflict */
+ SYNC_TESTING_cmd_block_delete ("delete-block-refs-underflow",
+ sync_url,
+ "upload-block-refs2",
+ MHD_HTTP_CONFLICT,
+ 1,
+ obj_uids,
+ ref_dec2),
+ /* The rejected delete left the block listed ... */
+ SYNC_TESTING_cmd_block_list ("list-after-underflow",
+ sync_url,
+ "upload-block-2",
+ 10,
+ NULL,
+ MHD_HTTP_OK,
+ 0,
+ NULL,
+ NULL),
+ /* ... and the object it references is still fetchable */
+ SYNC_TESTING_cmd_object_fetch ("fetch-obj-A-after-underflow",
+ sync_url,
+ "upload-block-2",
+ &obj_uids[0],
+ MHD_HTTP_OK,
+ obj_data_A,
+ sizeof obj_data_A - 1),
+
+ /* ========== Object and reference error paths ========== */
+
+ /* Fetching an object that was never uploaded → 404 */
+ SYNC_TESTING_cmd_object_fetch ("fetch-obj-unknown",
+ sync_url,
+ "upload-block-2",
+ &unknown_uid,
+ MHD_HTTP_NOT_FOUND,
+ NULL,
+ 0),
+
+ /* Referencing an object that was never uploaded → 409 ... */
+ SYNC_TESTING_cmd_block_append ("append-unknown-object-ref",
+ sync_url,
+ "upload-block-refs2",
+ &orphan_nonce,
+ MHD_HTTP_CONFLICT,
+ data[0],
+ strlen (data[0]),
+ false,
+ 1,
+ &unknown_uid,
+ ref_inc1),
+ /* ... and must not have moved the tail, so the next append
+ onto upload-block-refs2 still succeeds */
+ SYNC_TESTING_cmd_block_append ("append-after-rejected-ref",
+ sync_url,
+ "upload-block-refs2",
+ &tail_nonce,
+ MHD_HTTP_NO_CONTENT,
+ data[2],
+ strlen (data[2]),
+ false,
+ 0,
+ NULL,
+ NULL),
+
+ TALER_TESTING_cmd_end ()
+ };
- TALER_TESTING_run (is,
- commands);
+ TALER_TESTING_run (is,
+ commands);
+ }
}
@@ -269,9 +590,6 @@ main (int argc,
payer_payto.full_payto =
(char *) "payto://x-taler-bank/localhost/" USER_ACCOUNT_NAME
"?receiver-name=user";
- exchange_payto.full_payto =
- (char *) "payto://x-taler-bank/localhost/" EXCHANGE_ACCOUNT_NAME
- "?receiver-name=exchange";
merchant_payto.full_payto =
(char *) "payto://x-taler-bank/localhost/" MERCHANT_ACCOUNT_NAME
"?receiver-name=merchant";
diff --git a/src/testing/test_sync_api.conf b/src/testing/test_sync_api.conf
@@ -30,6 +30,9 @@ CURRENCY = EUR
PORT = 8084
PAYMENT_BACKEND_URL = "http://localhost:8080/"
ANNUAL_FEE = EUR:4.99
+# Must be in CURRENCY; the packaged default in config.d/sync.conf
+# is TESTKUDOS, which would be served as the /config liability_limit.
+INSURANCE = EUR:0.0
UPLOAD_LIMIT_MB = 1
SERVE = tcp
diff --git a/src/testing/testing_api_cmd_backup_download.c b/src/testing/testing_api_cmd_backup_download.c
@@ -1,283 +0,0 @@
-/*
- This file is part of SYNC
- Copyright (C) 2014-2019 Taler Systems SA
-
- SYNC 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.
-
- SYNC 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 SYNC; see the file COPYING. If not, see
- <http://www.gnu.org/licenses/>
-*/
-/**
- * @file testing/testing_api_cmd_backup_download.c
- * @brief command to download data to the sync backend service.
- * @author Christian Grothoff
- */
-#include "platform.h"
-#include "sync/sync_service.h"
-#include "sync/sync_testing_lib.h"
-#include <taler/taler_util.h>
-#include <taler/taler_testing_lib.h>
-
-/**
- * State for a "backup download" CMD.
- */
-struct BackupDownloadState
-{
-
- /**
- * Eddsa public key.
- */
- struct SYNC_AccountPublicKeyP sync_pub;
-
- /**
- * Hash of the upload (all zeros if there was no upload).
- */
- const struct GNUNET_HashCode *upload_hash;
-
- /**
- * Hash of the previous upload (all zeros if there was no previous upload).
- */
- const struct GNUNET_HashCode *prev_upload_hash;
-
- /**
- * The /backups POST operation handle.
- */
- struct SYNC_DownloadOperation *download;
-
- /**
- * URL of the sync backend.
- */
- const char *sync_url;
-
- /**
- * The interpreter state.
- */
- struct TALER_TESTING_Interpreter *is;
-
- /**
- * Reference to upload command we expect to download.
- */
- const char *upload_reference;
-
- /**
- * Expected status code.
- */
- unsigned int http_status;
-
-};
-
-
-/**
- * Function called with the results of a #SYNC_download().
- *
- * @param cls closure
- * @param dd details about the download operation
- */
-static void
-backup_download_cb (void *cls,
- const struct SYNC_DownloadDetails *dd)
-{
- struct BackupDownloadState *bds = cls;
-
- bds->download = NULL;
- if (dd->http_status != bds->http_status)
- {
- TALER_TESTING_unexpected_status (bds->is,
- dd->http_status,
- bds->http_status);
- return;
- }
- if (NULL != bds->upload_reference)
- {
- if ( (MHD_HTTP_OK == dd->http_status) &&
- (0 != GNUNET_memcmp (&dd->details.ok.curr_backup_hash,
- bds->upload_hash)) )
- {
- GNUNET_break (0);
- TALER_TESTING_interpreter_fail (bds->is);
- return;
- }
- if ( (MHD_HTTP_OK == dd->http_status) &&
- (0 != GNUNET_memcmp (&dd->details.ok.prev_backup_hash,
- bds->prev_upload_hash)) )
- {
- GNUNET_break (0);
- TALER_TESTING_interpreter_fail (bds->is);
- return;
- }
- }
- TALER_TESTING_interpreter_next (bds->is);
-}
-
-
-/**
- * Run a "backup download" CMD.
- *
- * @param cls closure.
- * @param cmd command currently being run.
- * @param is interpreter state.
- */
-static void
-backup_download_run (void *cls,
- const struct TALER_TESTING_Command *cmd,
- struct TALER_TESTING_Interpreter *is)
-{
- struct BackupDownloadState *bds = cls;
-
- bds->is = is;
- if (NULL != bds->upload_reference)
- {
- const struct TALER_TESTING_Command *upload_cmd;
- const struct SYNC_AccountPublicKeyP *sync_pub;
-
- upload_cmd = TALER_TESTING_interpreter_lookup_command
- (is,
- bds->upload_reference);
- if (NULL == upload_cmd)
- {
- GNUNET_break (0);
- TALER_TESTING_interpreter_fail (bds->is);
- return;
- }
- if (GNUNET_OK !=
- TALER_TESTING_get_trait_curr_hash (upload_cmd,
- &bds->upload_hash))
- {
- GNUNET_break (0);
- TALER_TESTING_interpreter_fail (bds->is);
- return;
- }
- if (GNUNET_OK !=
- TALER_TESTING_get_trait_prev_hash (upload_cmd,
- &bds->prev_upload_hash))
- {
- GNUNET_break (0);
- TALER_TESTING_interpreter_fail (bds->is);
- return;
- }
- if (GNUNET_OK !=
- TALER_TESTING_get_trait_sync_account_pub (upload_cmd,
- &sync_pub))
- {
- GNUNET_break (0);
- TALER_TESTING_interpreter_fail (bds->is);
- return;
- }
- bds->sync_pub = *sync_pub;
- }
- bds->download = SYNC_download (TALER_TESTING_interpreter_get_context (is),
- bds->sync_url,
- &bds->sync_pub,
- &backup_download_cb,
- bds);
- if (NULL == bds->download)
- {
- GNUNET_break (0);
- TALER_TESTING_interpreter_fail (bds->is);
- return;
- }
-}
-
-
-/**
- * Free the state of a "backup download" CMD, and possibly
- * cancel it if it did not complete.
- *
- * @param cls closure.
- * @param cmd command being freed.
- */
-static void
-backup_download_cleanup (void *cls,
- const struct TALER_TESTING_Command *cmd)
-{
- struct BackupDownloadState *bds = cls;
-
- if (NULL != bds->download)
- {
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Command '%s' did not complete (backup download)\n",
- cmd->label);
- SYNC_download_cancel (bds->download);
- bds->download = NULL;
- }
- GNUNET_free (bds);
-}
-
-
-/**
- * Make the "backup download" command.
- *
- * @param label command label
- * @param sync_url base URL of the sync serving
- * the policy store request.
- * @param http_status expected HTTP status.
- * @param upload_ref reference to upload command
- * @return the command
- */
-struct TALER_TESTING_Command
-SYNC_TESTING_cmd_backup_download (const char *label,
- const char *sync_url,
- unsigned int http_status,
- const char *upload_ref)
-{
- struct BackupDownloadState *bds;
-
- GNUNET_assert (NULL != upload_ref);
- bds = GNUNET_new (struct BackupDownloadState);
- bds->http_status = http_status;
- bds->sync_url = sync_url;
- bds->upload_reference = upload_ref;
- {
- struct TALER_TESTING_Command cmd = {
- .cls = bds,
- .label = label,
- .run = &backup_download_run,
- .cleanup = &backup_download_cleanup
- };
-
- return cmd;
- }
-}
-
-
-/**
- * Make the "backup download" command for a non-existent upload.
- *
- * @param label command label
- * @param sync_url base URL of the sync serving
- * the policy store request.
- * @return the command
- */
-struct TALER_TESTING_Command
-SYNC_TESTING_cmd_backup_nx (const char *label,
- const char *sync_url)
-{
- struct BackupDownloadState *bds;
- struct GNUNET_CRYPTO_EddsaPrivateKey priv;
-
- bds = GNUNET_new (struct BackupDownloadState);
- bds->http_status = MHD_HTTP_NOT_FOUND;
- bds->sync_url = sync_url;
- GNUNET_CRYPTO_eddsa_key_create (&priv);
- GNUNET_CRYPTO_eddsa_key_get_public (&priv,
- &bds->sync_pub.eddsa_pub);
- {
- struct TALER_TESTING_Command cmd = {
- .cls = bds,
- .label = label,
- .run = &backup_download_run,
- .cleanup = &backup_download_cleanup
- };
-
- return cmd;
- }
-}
diff --git a/src/testing/testing_api_cmd_backup_upload.c b/src/testing/testing_api_cmd_backup_upload.c
@@ -1,458 +0,0 @@
-/*
- This file is part of SYNC
- Copyright (C) 2014-2023 Taler Systems SA
-
- SYNC 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.
-
- SYNC 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 SYNC; see the file COPYING. If not, see
- <http://www.gnu.org/licenses/>
-*/
-/**
- * @file testing/testing_api_cmd_backup_upload.c
- * @brief command to upload data to the sync backend service.
- * @author Christian Grothoff
- */
-#include "platform.h"
-#include "sync/sync_service.h"
-#include "sync/sync_testing_lib.h"
-#include <taler/taler_util.h>
-#include <taler/taler_testing_lib.h>
-#include <taler/taler_merchant_service.h>
-#include "sync/sync_testing_lib.h"
-
-/**
- * State for a "backup upload" CMD.
- */
-struct BackupUploadState
-{
-
- /**
- * Eddsa private key.
- */
- struct SYNC_AccountPrivateKeyP sync_priv;
-
- /**
- * Eddsa public key.
- */
- struct SYNC_AccountPublicKeyP sync_pub;
-
- /**
- * Hash of the previous upload (maybe bogus if
- * #SYNC_TESTING_UO_PREV_HASH_WRONG is set in @e uo).
- * Maybe all zeros if there was no previous upload.
- */
- struct GNUNET_HashCode prev_hash;
-
- /**
- * Hash of the current upload.
- */
- struct GNUNET_HashCode curr_hash;
-
- /**
- * The /backups POST operation handle.
- */
- struct SYNC_UploadOperation *uo;
-
- /**
- * URL of the sync backend.
- */
- const char *sync_url;
-
- /**
- * Previous upload, or NULL for none. Used to calculate what THIS
- * upload is based on.
- */
- const char *prev_upload;
-
- /**
- * Last upload, or NULL for none, usually same as @e prev_upload.
- * Used to check the response on #MHD_HTTP_CONFLICT.
- */
- const char *last_upload;
-
- /**
- * Payment order ID we got back, if any. Otherwise NULL.
- */
- const char *payment_order_id;
-
- /**
- * Claim token we got back, if any. Otherwise all zeros.
- */
- struct TALER_ClaimTokenP token;
-
- /**
- * Payment order ID we are to provide in the request, may be NULL.
- */
- const char *payment_order_req;
-
- /**
- * The interpreter state.
- */
- struct TALER_TESTING_Interpreter *is;
-
- /**
- * The backup data we are uploading.
- */
- const void *backup;
-
- /**
- * Number of bytes in @e backup.
- */
- size_t backup_size;
-
- /**
- * Expected status code.
- */
- unsigned int http_status;
-
- /**
- * Options for how we are supposed to do the upload.
- */
- enum SYNC_TESTING_UploadOption uopt;
-
-};
-
-
-/**
- * Function called with the results of a #SYNC_upload().
- *
- * @param cls closure
- * @param ud details about the upload operation
- */
-static void
-backup_upload_cb (void *cls,
- const struct SYNC_UploadDetails *ud)
-{
- struct BackupUploadState *bus = cls;
-
- bus->uo = NULL;
- if (ud->http_status != bus->http_status)
- {
- TALER_TESTING_unexpected_status (bus->is,
- ud->http_status,
- bus->http_status);
- return;
- }
- switch (ud->us)
- {
- case SYNC_US_SUCCESS:
- if (0 != GNUNET_memcmp (
- &bus->curr_hash,
- ud->details.success.curr_backup_hash))
- {
- GNUNET_break (0);
- TALER_TESTING_interpreter_fail (bus->is);
- return;
- }
- break;
- case SYNC_US_PAYMENT_REQUIRED:
- {
- struct TALER_MERCHANT_PayUriData pd;
-
- if (GNUNET_OK !=
- TALER_MERCHANT_parse_pay_uri (
- ud->details.payment_required.payment_request,
- &pd))
- {
- GNUNET_break (0);
- TALER_TESTING_interpreter_fail (bus->is);
- return;
- }
- bus->payment_order_id = GNUNET_strdup (pd.order_id);
- if (NULL != pd.claim_token)
- bus->token = *pd.claim_token;
- TALER_MERCHANT_parse_pay_uri_free (&pd);
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Order ID from Sync service is `%s'\n",
- bus->payment_order_id);
- memset (&bus->curr_hash,
- 0,
- sizeof (struct GNUNET_HashCode));
- }
- break;
- case SYNC_US_CONFLICTING_BACKUP:
- {
- const struct TALER_TESTING_Command *ref;
- const struct GNUNET_HashCode *h;
-
- ref = TALER_TESTING_interpreter_lookup_command
- (bus->is,
- bus->last_upload);
- GNUNET_assert (NULL != ref);
- GNUNET_assert (GNUNET_OK ==
- TALER_TESTING_get_trait_curr_hash (ref,
- &h));
- if (0 != GNUNET_memcmp (h,
- &ud->details.recovered_backup.
- existing_backup_hash))
- {
- GNUNET_break (0);
- TALER_TESTING_interpreter_fail (bus->is);
- return;
- }
- }
- case SYNC_US_HTTP_ERROR:
- break;
- case SYNC_US_CLIENT_ERROR:
- GNUNET_break (0);
- TALER_TESTING_interpreter_fail (bus->is);
- return;
- case SYNC_US_SERVER_ERROR:
- GNUNET_break (0);
- TALER_TESTING_interpreter_fail (bus->is);
- return;
- }
- TALER_TESTING_interpreter_next (bus->is);
-}
-
-
-/**
- * Run a "backup upload" CMD.
- *
- * @param cls closure.
- * @param cmd command currently being run.
- * @param is interpreter state.
- */
-static void
-backup_upload_run (void *cls,
- const struct TALER_TESTING_Command *cmd,
- struct TALER_TESTING_Interpreter *is)
-{
- struct BackupUploadState *bus = cls;
-
- bus->is = is;
- if (NULL != bus->prev_upload)
- {
- const struct TALER_TESTING_Command *ref;
-
- ref = TALER_TESTING_interpreter_lookup_command (
- is,
- bus->prev_upload);
- if (NULL == ref)
- {
- GNUNET_break (0);
- TALER_TESTING_interpreter_fail (bus->is);
- return;
- }
- {
- const struct GNUNET_HashCode *h;
-
- if (GNUNET_OK ==
- TALER_TESTING_get_trait_curr_hash (ref,
- &h))
- {
- bus->prev_hash = *h;
- }
- }
- {
- const struct SYNC_AccountPrivateKeyP *priv;
-
- if (GNUNET_OK !=
- TALER_TESTING_get_trait_sync_account_priv (ref,
- &priv))
- {
- GNUNET_break (0);
- TALER_TESTING_interpreter_fail (bus->is);
- return;
- }
- bus->sync_priv = *priv;
- }
- {
- const struct SYNC_AccountPublicKeyP *pub;
-
- if (GNUNET_OK !=
- TALER_TESTING_get_trait_sync_account_pub (ref,
- &pub))
- {
- GNUNET_break (0);
- TALER_TESTING_interpreter_fail (bus->is);
- return;
- }
- bus->sync_pub = *pub;
- }
- if (0 != (SYNC_TESTING_UO_REFERENCE_ORDER_ID & bus->uopt))
- {
- const char *order_id;
-
- if (GNUNET_OK !=
- TALER_TESTING_get_trait_order_id (ref,
- &order_id))
- {
- GNUNET_break (0);
- TALER_TESTING_interpreter_fail (bus->is);
- return;
- }
- bus->payment_order_req = order_id;
- if (NULL == bus->payment_order_req)
- {
- GNUNET_break (0);
- TALER_TESTING_interpreter_fail (bus->is);
- return;
- }
- }
- }
- else
- {
- GNUNET_CRYPTO_eddsa_key_create (&bus->sync_priv.eddsa_priv);
- GNUNET_CRYPTO_eddsa_key_get_public (&bus->sync_priv.eddsa_priv,
- &bus->sync_pub.eddsa_pub);
- }
- if (0 != (SYNC_TESTING_UO_PREV_HASH_WRONG & bus->uopt))
- GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK,
- &bus->prev_hash,
- sizeof (struct GNUNET_HashCode));
- GNUNET_CRYPTO_hash (bus->backup,
- bus->backup_size,
- &bus->curr_hash);
- bus->uo = SYNC_upload (TALER_TESTING_interpreter_get_context (is),
- bus->sync_url,
- &bus->sync_priv,
- ( ( (NULL != bus->prev_upload) &&
- (GNUNET_NO == GNUNET_is_zero (
- &bus->prev_hash)) ) ||
- (0 != (SYNC_TESTING_UO_PREV_HASH_WRONG
- & bus->uopt)) )
- ? &bus->prev_hash
- : NULL,
- bus->backup_size,
- bus->backup,
- (0 != (SYNC_TESTING_UO_REQUEST_PAYMENT & bus->uopt))
- ? SYNC_PO_FORCE_PAYMENT
- : SYNC_PO_NONE,
- bus->payment_order_req,
- &backup_upload_cb,
- bus);
- if (NULL == bus->uo)
- {
- GNUNET_break (0);
- TALER_TESTING_interpreter_fail (bus->is);
- return;
- }
-}
-
-
-/**
- * Free the state of a "backup upload" CMD, and possibly
- * cancel it if it did not complete.
- *
- * @param cls closure.
- * @param cmd command being freed.
- */
-static void
-backup_upload_cleanup (void *cls,
- const struct TALER_TESTING_Command *cmd)
-{
- struct BackupUploadState *bus = cls;
-
- if (NULL != bus->uo)
- {
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Command '%s' did not complete (backup upload)\n",
- cmd->label);
- SYNC_upload_cancel (bus->uo);
- bus->uo = NULL;
- }
- GNUNET_free (bus);
-}
-
-
-/**
- * Offer internal data to other commands.
- *
- * @param cls closure
- * @param[out] ret result (could be anything)
- * @param trait name of the trait
- * @param index index number of the object to extract.
- * @return #GNUNET_OK on success
- */
-static enum GNUNET_GenericReturnValue
-backup_upload_traits (void *cls,
- const void **ret,
- const char *trait,
- unsigned int index)
-{
- struct BackupUploadState *bus = cls;
- struct TALER_TESTING_Trait straits[] = {
- TALER_TESTING_make_trait_curr_hash (&bus->curr_hash),
- TALER_TESTING_make_trait_prev_hash (&bus->prev_hash),
- TALER_TESTING_make_trait_claim_token (&bus->token),
- TALER_TESTING_make_trait_sync_account_pub (&bus->sync_pub),
- TALER_TESTING_make_trait_sync_account_priv (&bus->sync_priv),
- TALER_TESTING_make_trait_order_id (bus->payment_order_id),
- TALER_TESTING_trait_end ()
- };
- struct TALER_TESTING_Trait ftraits[] = {
- TALER_TESTING_make_trait_claim_token (&bus->token),
- TALER_TESTING_make_trait_sync_account_pub (&bus->sync_pub),
- TALER_TESTING_make_trait_sync_account_priv (&bus->sync_priv),
- TALER_TESTING_make_trait_order_id (bus->payment_order_id),
- TALER_TESTING_trait_end ()
- };
-
-
- return TALER_TESTING_get_trait ((NULL != bus->payment_order_req)
- ? ftraits
- : straits,
- ret,
- trait,
- index);
-}
-
-
-/**
- * Make the "backup upload" command.
- *
- * @param label command label
- * @param sync_url base URL of the sync serving
- * the policy store request.
- * @param prev_upload reference to a previous upload we are
- * supposed to update, NULL for none
- * @param last_upload reference to the last upload for the
- * same account, used to check result on MHD_HTTP_CONFLICT
- * @param uo upload options
- * @param http_status expected HTTP status.
- * @param backup_data data to upload
- * @param backup_data_size number of bytes in @a backup_data
- * @return the command
- */
-struct TALER_TESTING_Command
-SYNC_TESTING_cmd_backup_upload (const char *label,
- const char *sync_url,
- const char *prev_upload,
- const char *last_upload,
- enum SYNC_TESTING_UploadOption uo,
- unsigned int http_status,
- const void *backup_data,
- size_t backup_data_size)
-{
- struct BackupUploadState *bus;
-
- bus = GNUNET_new (struct BackupUploadState);
- bus->http_status = http_status;
- bus->prev_upload = prev_upload;
- bus->last_upload = last_upload;
- bus->uopt = uo;
- bus->sync_url = sync_url;
- bus->backup = backup_data;
- bus->backup_size = backup_data_size;
- {
- struct TALER_TESTING_Command cmd = {
- .cls = bus,
- .label = label,
- .run = &backup_upload_run,
- .cleanup = &backup_upload_cleanup,
- .traits = &backup_upload_traits
- };
-
- return cmd;
- }
-}
diff --git a/src/testing/testing_api_cmd_block_append.c b/src/testing/testing_api_cmd_block_append.c
@@ -0,0 +1,241 @@
+/*
+ This file is part of SYNC
+ Copyright (C) 2014-2026 Taler Systems SA
+
+ SYNC 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.
+
+ SYNC 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 SYNC; see the file COPYING. If not, see
+ <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file testing/testing_api_cmd_block_append.c
+ * @brief command to append a block to the sync backend service.
+ * @author Iván Ávalos
+ */
+#include "testing_api_cmd_block_common.h"
+#include <stdbool.h>
+
+
+/**
+ * Process a reference for a retry upload: inherit keys, nonce
+ * (if not explicitly set), and old_data_hash from the ref's
+ * prev_hash trait.
+ *
+ * @param[in,out] bus state to populate
+ * @param ref reference command to inherit from
+ * @return #GNUNET_OK on success, #GNUNET_SYSERR on failure
+ */
+static enum GNUNET_GenericReturnValue
+append_retry_process_ref (
+ struct BlockUploadState *bus,
+ const struct TALER_TESTING_Command *ref)
+{
+ if (GNUNET_OK !=
+ SYNC_TESTING_inherit_account_keys_ (bus,
+ ref))
+ return GNUNET_SYSERR;
+
+ /* Inherit block nonce if not explicitly set */
+ if (GNUNET_is_zero (&bus->nonce))
+ {
+ const struct SYNC_BlockNonce *nonce;
+
+ if (GNUNET_OK ==
+ TALER_TESTING_get_trait_block_nonce (ref,
+ &nonce))
+ bus->nonce = *nonce;
+ }
+
+ /* Inherit old_data_hash from prev_hash for retry */
+ if (GNUNET_is_zero (&bus->old_data_hash))
+ {
+ const struct GNUNET_HashCode *h;
+
+ if (GNUNET_OK ==
+ TALER_TESTING_get_trait_prev_hash (ref,
+ &h))
+ bus->old_data_hash = *h;
+ }
+
+ return GNUNET_OK;
+}
+
+
+/**
+ * Process a reference for an append-after: inherit keys, set
+ * prev_nonce from the ref's block_nonce (to chain the new block after
+ * the referenced one), and collect the tail's data for its relink
+ * signature.
+ *
+ * @param[in,out] bus state to populate
+ * @param ref reference command to inherit from
+ * @return #GNUNET_OK on success, #GNUNET_SYSERR on failure
+ */
+static enum GNUNET_GenericReturnValue
+append_after_process_ref (
+ struct BlockUploadState *bus,
+ const struct TALER_TESTING_Command *ref)
+{
+ const struct SYNC_BlockNonce *nonce;
+ const struct SYNC_BlockNonce *pn;
+ const struct GNUNET_HashCode *h;
+ const struct GNUNET_HashCode *rh;
+
+ if (GNUNET_OK !=
+ SYNC_TESTING_inherit_account_keys_ (bus,
+ ref))
+ return GNUNET_SYSERR;
+ /* Set prev_nonce to chain after the referenced block. If ref has
+ no nonce trait, prev_nonce stays zero (meaning no predecessor —
+ allowed by the API) */
+ if (GNUNET_OK ==
+ TALER_TESTING_get_trait_block_nonce (ref,
+ &nonce))
+ bus->prev_nonce = *nonce;
+
+ /* The tail's data for its relink signature */
+ if (GNUNET_OK ==
+ TALER_TESTING_get_trait_curr_hash (ref,
+ &h))
+ {
+ bus->prev_entry.nonce = bus->prev_nonce;
+ bus->prev_entry.hash = *h;
+ bus->have_relink_target = true;
+ }
+ if (GNUNET_OK ==
+ TALER_TESTING_get_trait_block_refs_hash (ref,
+ &rh))
+ bus->prev_entry.refs_hash = *rh;
+ if (GNUNET_OK ==
+ TALER_TESTING_get_trait_prev_nonce (ref,
+ &pn))
+ {
+ bus->prev_entry.prev_nonce = *pn;
+ bus->prev_entry.have_prev_nonce = ! GNUNET_is_zero (pn);
+ }
+
+ return GNUNET_OK;
+}
+
+
+/**
+ * Start a block append operation.
+ *
+ * @param ctx for HTTP client request processing
+ * @param sync_url base URL of the Sync server
+ * @param sync_priv private key of the account
+ * @param nonce nonce identifying the block
+ * @param prev_nonce nonce of the preceding block, or NULL
+ * @param next_nonce unused, appends are always at the tail
+ * @param old_data_hash unused, appends have no previous data
+ * @param block_data_size number of bytes in @a block_data
+ * @param block_data the block data
+ * @param refs_len number of entries in @a object_uids and @a object_incs
+ * @param object_uids array of object UIDs referenced by this block
+ * @param object_incs array of reference count adjustments
+ * @param prev_entry the tail's entry, for its relink signature, or NULL
+ * @param cb function to call with the result
+ * @param cb_cls closure for @a cb
+ * @return handle for the operation
+ */
+static struct SYNC_BlockOperation *
+append_start_upload (
+ struct GNUNET_CURL_Context *ctx,
+ const char *sync_url,
+ const struct SYNC_AccountPrivateKeyP *sync_priv,
+ const struct SYNC_BlockNonce *nonce,
+ const struct SYNC_BlockNonce *prev_nonce,
+ const struct SYNC_BlockNonce *next_nonce,
+ const struct GNUNET_HashCode *old_data_hash,
+ size_t block_data_size,
+ const void *block_data,
+ size_t refs_len,
+ const struct SYNC_ObjectUID *object_uids,
+ const int16_t *object_incs,
+ const struct SYNC_BlockListEntry *prev_entry,
+ SYNC_BlockOperationCallback cb,
+ void *cb_cls)
+{
+ struct SYNC_BlockOperationArgs args = {
+ .nonce = nonce,
+ .prev_nonce = prev_nonce,
+ .next_nonce = NULL,
+ .old_data_hash = NULL,
+ .block_data_size = block_data_size,
+ .block_data = block_data,
+ .refs_len = refs_len,
+ .object_uids = object_uids,
+ .object_incs = object_incs,
+ .prev_entry = prev_entry,
+ };
+
+ (void) next_nonce;
+ (void) old_data_hash;
+ return SYNC_block_append (ctx,
+ sync_url,
+ sync_priv,
+ &args,
+ cb,
+ cb_cls);
+}
+
+
+struct TALER_TESTING_Command
+SYNC_TESTING_cmd_block_append (const char *label,
+ const char *sync_url,
+ const char *upload_reference,
+ const struct SYNC_BlockNonce *nonce,
+ unsigned int http_status,
+ const void *block_data,
+ size_t block_data_size,
+ bool retry_upload,
+ unsigned int refs_len,
+ const struct SYNC_ObjectUID *object_uids,
+ const int16_t *object_incs)
+{
+ struct BlockUploadState *bus;
+
+ GNUNET_assert (refs_len <= MAX_BLOCK_OBJECT_REFS);
+ bus = GNUNET_new (struct BlockUploadState);
+ bus->sync_url = sync_url;
+ bus->upload_reference = upload_reference;
+ if (NULL != nonce)
+ bus->nonce = *nonce;
+ bus->http_status = http_status;
+ bus->block_data = block_data;
+ bus->block_data_size = block_data_size;
+ bus->refs_len = refs_len;
+ if (0 < refs_len)
+ {
+ GNUNET_memcpy (bus->object_uids,
+ object_uids,
+ refs_len * sizeof (struct SYNC_ObjectUID));
+ GNUNET_memcpy (bus->object_incs,
+ object_incs,
+ refs_len * sizeof (int16_t));
+ }
+ bus->process_ref = retry_upload
+ ? &append_retry_process_ref
+ : &append_after_process_ref;
+ bus->start_upload = &append_start_upload;
+ {
+ struct TALER_TESTING_Command cmd = {
+ .cls = bus,
+ .label = label,
+ .run = &SYNC_TESTING_block_upload_run_,
+ .cleanup = &SYNC_TESTING_block_upload_cleanup_,
+ .traits = &SYNC_TESTING_block_upload_traits_
+ };
+
+ return cmd;
+ }
+}
diff --git a/src/testing/testing_api_cmd_block_common.c b/src/testing/testing_api_cmd_block_common.c
@@ -0,0 +1,224 @@
+/*
+ This file is part of SYNC
+ Copyright (C) 2014-2026 Taler Systems SA
+
+ SYNC 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.
+
+ SYNC 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 SYNC; see the file COPYING. If not, see
+ <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file testing/testing_api_cmd_block_common.c
+ * @brief shared logic for block upload commands.
+ * @author Iván Ávalos
+ */
+#include <sync/sync_util.h>
+#include "testing_api_cmd_block_common.h"
+
+
+/**
+ * Function called when we're done processing the
+ * block upload.
+ *
+ * @param cls the `struct BlockUploadState`
+ * @param ud details about the upload operation
+ */
+static void
+block_upload_finished (void *cls,
+ const struct SYNC_BlockOperationDetails *ud)
+{
+ struct BlockUploadState *bus = cls;
+
+ bus->bop = NULL;
+ if (ud->http_status != bus->http_status)
+ {
+ TALER_TESTING_unexpected_status (bus->is,
+ ud->http_status,
+ bus->http_status);
+ return;
+ }
+ if (MHD_HTTP_PAYMENT_REQUIRED == ud->http_status)
+ {
+ struct TALER_MERCHANT_PayUriData pd;
+
+ if (GNUNET_OK !=
+ TALER_MERCHANT_parse_pay_uri (ud->pay_uri,
+ &pd))
+ {
+ GNUNET_break (0);
+ TALER_TESTING_interpreter_fail (bus->is);
+ return;
+ }
+ bus->payment_order_id = GNUNET_strdup (pd.order_id);
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Order ID from Sync service is `%s'\n",
+ bus->payment_order_id);
+ if (NULL != pd.claim_token)
+ bus->claim_token = *pd.claim_token;
+ TALER_MERCHANT_parse_pay_uri_free (&pd);
+ }
+ TALER_TESTING_interpreter_next (bus->is);
+}
+
+
+enum GNUNET_GenericReturnValue
+SYNC_TESTING_inherit_account_keys_ (
+ struct BlockUploadState *bus,
+ const struct TALER_TESTING_Command *ref)
+{
+ const struct SYNC_AccountPrivateKeyP *priv;
+ const struct SYNC_AccountPublicKeyP *pub;
+
+ if (GNUNET_OK !=
+ TALER_TESTING_get_trait_sync_account_priv (ref,
+ &priv))
+ {
+ GNUNET_break (0);
+ return GNUNET_SYSERR;
+ }
+ if (GNUNET_OK !=
+ TALER_TESTING_get_trait_sync_account_pub (ref,
+ &pub))
+ {
+ GNUNET_break (0);
+ return GNUNET_SYSERR;
+ }
+ bus->sync_priv = *priv;
+ bus->sync_pub = *pub;
+ return GNUNET_OK;
+}
+
+
+void
+SYNC_TESTING_block_upload_run_ (
+ void *cls,
+ const struct TALER_TESTING_Command *cmd,
+ struct TALER_TESTING_Interpreter *is)
+{
+ struct BlockUploadState *bus = cls;
+
+ (void) cmd;
+ bus->is = is;
+
+ if (NULL != bus->upload_reference)
+ {
+ const struct TALER_TESTING_Command *ref;
+
+ ref = TALER_TESTING_interpreter_lookup_command (is,
+ bus->upload_reference);
+ if (NULL == ref)
+ goto fail;
+ if (GNUNET_OK !=
+ bus->process_ref (bus, ref))
+ goto fail;
+ }
+ else
+ {
+ GNUNET_CRYPTO_eddsa_key_create (&bus->sync_priv.eddsa_priv);
+ GNUNET_CRYPTO_eddsa_key_get_public (&bus->sync_priv.eddsa_priv,
+ &bus->sync_pub.eddsa_pub);
+ }
+
+ /* Generate random nonce if still all zeros */
+ if (GNUNET_is_zero (&bus->nonce))
+ SYNC_block_nonce_generate (&bus->nonce);
+
+ /* Compute hash of block data for traits */
+ GNUNET_CRYPTO_hash (bus->block_data,
+ bus->block_data_size,
+ &bus->new_data_hash);
+
+ /* P1: hash over the object references (covered by the signature) */
+ if (GNUNET_OK !=
+ SYNC_object_refs_hash (bus->refs_len,
+ bus->object_uids,
+ bus->object_incs,
+ &bus->refs_hash))
+ {
+ GNUNET_break (0);
+ goto fail;
+ }
+
+ bus->bop = bus->start_upload (
+ TALER_TESTING_interpreter_get_context (is),
+ bus->sync_url,
+ &bus->sync_priv,
+ &bus->nonce,
+ GNUNET_is_zero (&bus->prev_nonce) ? NULL : &bus->prev_nonce,
+ GNUNET_is_zero (&bus->next_nonce) ? NULL : &bus->next_nonce,
+ GNUNET_is_zero (&bus->old_data_hash) ? NULL : &bus->old_data_hash,
+ bus->block_data_size,
+ bus->block_data,
+ bus->refs_len,
+ bus->object_uids,
+ bus->object_incs,
+ bus->have_relink_target ? &bus->prev_entry : NULL,
+ &block_upload_finished,
+ bus);
+ if (NULL == bus->bop)
+ goto fail;
+
+ return;
+
+fail:
+ GNUNET_break (0);
+ TALER_TESTING_interpreter_fail (is);
+}
+
+
+void
+SYNC_TESTING_block_upload_cleanup_ (
+ void *cls,
+ const struct TALER_TESTING_Command *cmd)
+{
+ struct BlockUploadState *bus = cls;
+
+ if (NULL != bus->bop)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Command '%s' did not complete (block upload)\n",
+ cmd->label);
+ SYNC_block_operation_cancel (bus->bop);
+ bus->bop = NULL;
+ }
+ GNUNET_free (bus->payment_order_id);
+ GNUNET_free (bus);
+}
+
+
+enum GNUNET_GenericReturnValue
+SYNC_TESTING_block_upload_traits_ (
+ void *cls,
+ const void **ret,
+ const char *trait,
+ unsigned int index)
+{
+ struct BlockUploadState *bus = cls;
+ struct TALER_TESTING_Trait traits[] = {
+ TALER_TESTING_make_trait_sync_account_pub (&bus->sync_pub),
+ TALER_TESTING_make_trait_sync_account_priv (&bus->sync_priv),
+ TALER_TESTING_make_trait_curr_hash (&bus->new_data_hash),
+ TALER_TESTING_make_trait_prev_hash (&bus->old_data_hash),
+ TALER_TESTING_make_trait_block_nonce (&bus->nonce),
+ TALER_TESTING_make_trait_prev_nonce (&bus->prev_nonce),
+ TALER_TESTING_make_trait_next_nonce (&bus->next_nonce),
+ TALER_TESTING_make_trait_block_refs_hash (&bus->refs_hash),
+ TALER_TESTING_make_trait_order_id (bus->payment_order_id),
+ TALER_TESTING_make_trait_claim_token (&bus->claim_token),
+ TALER_TESTING_trait_end ()
+ };
+
+ return TALER_TESTING_get_trait (traits,
+ ret,
+ trait,
+ index);
+}
diff --git a/src/testing/testing_api_cmd_block_common.h b/src/testing/testing_api_cmd_block_common.h
@@ -0,0 +1,259 @@
+/*
+ This file is part of SYNC
+ Copyright (C) 2014-2026 Taler Systems SA
+
+ SYNC 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.
+
+ SYNC 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 SYNC; see the file COPYING. If not, see
+ <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file testing/testing_api_cmd_block_common.h
+ * @brief shared internal state for block upload commands
+ * @author Iván Ávalos
+ */
+#ifndef TESTING_API_CMD_BLOCK_COMMON_H
+#define TESTING_API_CMD_BLOCK_COMMON_H
+
+#include "platform.h"
+#include "sync/sync_service.h"
+#include "sync/sync_testing_lib.h"
+#include <taler/taler_util.h>
+#include <taler/taler_signatures.h>
+#include <taler/taler_testing_lib.h>
+#include <taler/merchant/common.h>
+#include <gnunet/gnunet_curl_lib.h>
+
+
+/**
+ * Maximum number of object references a block command can carry.
+ */
+#define MAX_BLOCK_OBJECT_REFS 32
+
+
+/**
+ * State for a "block upload" CMD.
+ */
+struct BlockUploadState
+{
+
+ /**
+ * Eddsa private key.
+ */
+ struct SYNC_AccountPrivateKeyP sync_priv;
+
+ /**
+ * Eddsa public key.
+ */
+ struct SYNC_AccountPublicKeyP sync_pub;
+
+ /**
+ * Nonce for the block.
+ */
+ struct SYNC_BlockNonce nonce;
+
+ /**
+ * Nonce of the preceding block, all zeros for none.
+ */
+ struct SYNC_BlockNonce prev_nonce;
+
+ /**
+ * Nonce of the succeeding block, all zeros for none.
+ */
+ struct SYNC_BlockNonce next_nonce;
+
+ /**
+ * Hash of old data, all zeros for append.
+ */
+ struct GNUNET_HashCode old_data_hash;
+
+ /**
+ * Hash of new block data (exposed as curr_hash trait).
+ */
+ struct GNUNET_HashCode new_data_hash;
+
+ /**
+ * URL of the sync backend.
+ */
+ const char *sync_url;
+
+ /**
+ * Reference to a command that can provide account keys,
+ * NULL to generate fresh keys.
+ */
+ const char *upload_reference;
+
+ /**
+ * Function to process a reference command: inherit keys,
+ * nonce, old_data_hash, and/or set prev_nonce depending
+ * on the upload mode (retry, append-after, update, etc.).
+ *
+ * @param[in,out] bus state to populate
+ * @param ref reference command to inherit from
+ * @return #GNUNET_OK on success, #GNUNET_SYSERR on failure
+ */
+ enum GNUNET_GenericReturnValue
+ (*process_ref)(
+ struct BlockUploadState *bus,
+ const struct TALER_TESTING_Command *ref);
+
+ /**
+ * Function to start the upload operation.
+ */
+ struct SYNC_BlockOperation *
+ (*start_upload)(
+ struct GNUNET_CURL_Context *ctx,
+ const char *sync_url,
+ const struct SYNC_AccountPrivateKeyP *sync_priv,
+ const struct SYNC_BlockNonce *nonce,
+ const struct SYNC_BlockNonce *prev_nonce,
+ const struct SYNC_BlockNonce *next_nonce,
+ const struct GNUNET_HashCode *old_data_hash,
+ size_t block_data_size,
+ const void *block_data,
+ size_t refs_len,
+ const struct SYNC_ObjectUID *object_uids,
+ const int16_t *object_incs,
+ const struct SYNC_BlockListEntry *prev_entry,
+ SYNC_BlockOperationCallback cb,
+ void *cb_cls);
+
+ /**
+ * Block data to upload.
+ */
+ const void *block_data;
+
+ /**
+ * Number of bytes in @e block_data.
+ */
+ size_t block_data_size;
+
+ /**
+ * Number of entries in @e object_uids and @e object_incs.
+ */
+ unsigned int refs_len;
+
+ /**
+ * Array of object UIDs referenced by this block (array of
+ * @e refs_len, up to #MAX_BLOCK_OBJECT_REFS).
+ */
+ struct SYNC_ObjectUID object_uids[MAX_BLOCK_OBJECT_REFS];
+
+ /**
+ * Array of reference count adjustments (array of @e refs_len,
+ * up to #MAX_BLOCK_OBJECT_REFS).
+ */
+ int16_t object_incs[MAX_BLOCK_OBJECT_REFS];
+
+ /**
+ * Hash over the object references of this block.
+ */
+ struct GNUNET_HashCode refs_hash;
+
+ /**
+ * Entry of the block at @e prev_nonce (the tail on an append),
+ * used to build its relink signature; NULL if there is no such
+ * block.
+ */
+ struct SYNC_BlockListEntry prev_entry;
+
+ /**
+ * True if the relink target entry is available.
+ */
+ bool have_relink_target;
+
+ /**
+ * Expected HTTP status code.
+ */
+ unsigned int http_status;
+
+ /**
+ * The interpreter state.
+ */
+ struct TALER_TESTING_Interpreter *is;
+
+ /**
+ * Payment order ID extracted from the 402 taler://pay URI.
+ */
+ char *payment_order_id;
+
+ /**
+ * Claim token extracted from the 402 taler://pay URI.
+ */
+ struct TALER_ClaimTokenP claim_token;
+
+ /**
+ * Handle for the library block upload operation.
+ */
+ struct SYNC_BlockOperation *bop;
+};
+
+
+/**
+ * Copy the account key pair offered by @a ref into @a bus.
+ *
+ * @param[in,out] bus state to populate
+ * @param ref reference command to inherit the keys from
+ * @return #GNUNET_OK on success, #GNUNET_SYSERR if @a ref
+ * does not offer the account key traits
+ */
+enum GNUNET_GenericReturnValue
+SYNC_TESTING_inherit_account_keys_ (
+ struct BlockUploadState *bus,
+ const struct TALER_TESTING_Command *ref);
+
+
+/**
+ * Run a "block upload" CMD.
+ *
+ * @param cls closure.
+ * @param cmd command currently being run.
+ * @param is interpreter state.
+ */
+void
+SYNC_TESTING_block_upload_run_ (
+ void *cls,
+ const struct TALER_TESTING_Command *cmd,
+ struct TALER_TESTING_Interpreter *is);
+
+
+/**
+ * Free the state of a "block upload" CMD, and possibly
+ * cancel it if it did not complete.
+ *
+ * @param cls closure.
+ * @param cmd command being freed.
+ */
+void
+SYNC_TESTING_block_upload_cleanup_ (
+ void *cls,
+ const struct TALER_TESTING_Command *cmd);
+
+
+/**
+ * Offer the internal data of a "block upload" CMD to other commands.
+ *
+ * @param cls closure
+ * @param[out] ret result (could be anything)
+ * @param trait name of the trait
+ * @param index index number of the object to extract.
+ * @return #GNUNET_OK on success
+ */
+enum GNUNET_GenericReturnValue
+SYNC_TESTING_block_upload_traits_ (
+ void *cls,
+ const void **ret,
+ const char *trait,
+ unsigned int index);
+
+
+#endif
diff --git a/src/testing/testing_api_cmd_block_delete.c b/src/testing/testing_api_cmd_block_delete.c
@@ -0,0 +1,442 @@
+/*
+ This file is part of SYNC
+ Copyright (C) 2014-2026 Taler Systems SA
+
+ SYNC 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.
+
+ SYNC 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 SYNC; see the file COPYING. If not, see
+ <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file testing/testing_api_cmd_block_delete.c
+ * @brief command to delete a block from the sync backend service.
+ * @author Iván Ávalos
+ */
+#include "platform.h"
+#include "sync/sync_service.h"
+#include "sync/sync_testing_lib.h"
+#include <taler/taler_util.h>
+#include <taler/taler_signatures.h>
+#include <taler/taler_testing_lib.h>
+#include <gnunet/gnunet_curl_lib.h>
+#include "testing_api_cmd_block_common.h"
+
+
+/**
+ * State for a "block delete" CMD.
+ */
+struct BlockDeleteState
+{
+
+ /**
+ * URL of the sync backend.
+ */
+ const char *sync_url;
+
+ /**
+ * Reference to a command that provides account keys,
+ * block nonce and block hash.
+ */
+ const char *account_reference;
+
+ /**
+ * Eddsa private key.
+ */
+ struct SYNC_AccountPrivateKeyP sync_priv;
+
+ /**
+ * Eddsa public key.
+ */
+ struct SYNC_AccountPublicKeyP sync_pub;
+
+ /**
+ * Nonce of the block to delete.
+ */
+ struct SYNC_BlockNonce nonce;
+
+ /**
+ * Prev nonce (zero if first block).
+ */
+ struct SYNC_BlockNonce prev_nonce;
+
+ /**
+ * Next nonce (zero if last block).
+ */
+ struct SYNC_BlockNonce next_nonce;
+
+ /**
+ * Hash of the block to delete.
+ */
+ struct GNUNET_HashCode block_hash;
+
+ /**
+ * Expected HTTP status code.
+ */
+ unsigned int http_status;
+
+ /**
+ * The interpreter state.
+ */
+ struct TALER_TESTING_Interpreter *is;
+
+ /**
+ * Handle for the library block delete operation.
+ */
+ struct SYNC_BlockOperation *bop;
+
+ /**
+ * Handle for the library block list operation (first phase).
+ */
+ struct SYNC_BlockListOperation *blop;
+
+ /**
+ * Number of entries in @e object_uids and @e object_incs.
+ */
+ unsigned int refs_len;
+
+ /**
+ * Array of object UIDs referenced by this block (array of
+ * @e refs_len, up to #MAX_BLOCK_OBJECT_REFS).
+ */
+ struct SYNC_ObjectUID object_uids[MAX_BLOCK_OBJECT_REFS];
+
+ /**
+ * Array of reference count adjustments (array of @e refs_len,
+ * up to #MAX_BLOCK_OBJECT_REFS).
+ */
+ int16_t object_incs[MAX_BLOCK_OBJECT_REFS];
+
+ /**
+ * Entry of the preceding neighbour (for its relink signature).
+ */
+ struct SYNC_BlockListEntry prev_entry;
+
+ /**
+ * True if @e prev_entry is set.
+ */
+ bool have_prev_entry;
+
+ /**
+ * Entry of the succeeding neighbour (for its relink signature).
+ */
+ struct SYNC_BlockListEntry next_entry;
+
+ /**
+ * True if @e next_entry is set.
+ */
+ bool have_next_entry;
+
+};
+
+
+/**
+ * Function called when we're done processing the
+ * block delete.
+ *
+ * @param cls the `struct BlockDeleteState`
+ * @param ud details about the delete operation
+ */
+static void
+block_delete_finished (void *cls,
+ const struct SYNC_BlockOperationDetails *ud)
+{
+ struct BlockDeleteState *bds = cls;
+
+ bds->bop = NULL;
+ if (ud->http_status != bds->http_status)
+ {
+ TALER_TESTING_unexpected_status (bds->is,
+ ud->http_status,
+ bds->http_status);
+ return;
+ }
+ TALER_TESTING_interpreter_next (bds->is);
+}
+
+
+/**
+ * Function called when we're done processing the
+ * block list that precedes the delete.
+ *
+ * @param cls the `struct BlockDeleteState`
+ * @param ld details about the list operation
+ */
+static void
+block_delete_list_finished (void *cls,
+ const struct SYNC_BlockListDetails *ld)
+{
+ struct BlockDeleteState *bds = cls;
+
+ bds->blop = NULL;
+ if (MHD_HTTP_OK != ld->http_status)
+ {
+ GNUNET_break_op (0);
+ TALER_TESTING_interpreter_fail (bds->is);
+ return;
+ }
+
+ /* Find the block to delete and its neighbours in the list */
+ {
+ const struct SYNC_BlockListEntry *prev = NULL;
+ const struct SYNC_BlockListEntry *next = NULL;
+
+ for (unsigned int i = 0; i < ld->num_entries; i++)
+ {
+ const struct SYNC_BlockListEntry *e = &ld->entries[i];
+
+ if ( (0 == GNUNET_memcmp (&e->nonce,
+ &bds->prev_nonce)) ||
+ (0 == GNUNET_memcmp (&e->nonce,
+ &bds->next_nonce)) )
+ {
+ if (0 == GNUNET_memcmp (&e->nonce,
+ &bds->prev_nonce))
+ prev = e;
+ else
+ next = e;
+ }
+ }
+
+ if (NULL != prev)
+ {
+ bds->prev_entry = *prev;
+ bds->have_prev_entry = true;
+ }
+ if (NULL != next)
+ {
+ bds->next_entry = *next;
+ bds->have_next_entry = true;
+ }
+ }
+
+ {
+ struct SYNC_BlockOperationArgs args = {
+ .nonce = &bds->nonce,
+ .prev_nonce = GNUNET_is_zero (&bds->prev_nonce)
+ ? NULL
+ : &bds->prev_nonce,
+ .next_nonce = GNUNET_is_zero (&bds->next_nonce)
+ ? NULL
+ : &bds->next_nonce,
+ .block_hash = &bds->block_hash,
+ .refs_len = bds->refs_len,
+ .object_uids = bds->object_uids,
+ .object_incs = bds->object_incs,
+ .prev_entry = bds->have_prev_entry
+ ? &bds->prev_entry
+ : NULL,
+ .next_entry = bds->have_next_entry
+ ? &bds->next_entry
+ : NULL,
+ };
+
+ bds->bop = SYNC_block_delete (
+ TALER_TESTING_interpreter_get_context (bds->is),
+ bds->sync_url,
+ &bds->sync_priv,
+ &args,
+ &block_delete_finished,
+ bds);
+ if (NULL == bds->bop)
+ {
+ GNUNET_break (0);
+ TALER_TESTING_interpreter_fail (bds->is);
+ }
+ }
+}
+
+
+/**
+ * Run a "block delete" CMD.
+ *
+ * @param cls closure.
+ * @param cmd command currently being run.
+ * @param is interpreter state.
+ */
+static void
+block_delete_run (void *cls,
+ const struct TALER_TESTING_Command *cmd,
+ struct TALER_TESTING_Interpreter *is)
+{
+ struct BlockDeleteState *bds = cls;
+
+ (void) cmd;
+ bds->is = is;
+
+ {
+ const struct TALER_TESTING_Command *ref;
+
+ ref = TALER_TESTING_interpreter_lookup_command (is,
+ bds->account_reference);
+ if (NULL == ref)
+ {
+ GNUNET_break (0);
+ goto fail;
+ }
+ {
+ const struct SYNC_AccountPrivateKeyP *priv;
+ const struct SYNC_AccountPublicKeyP *pub;
+
+ if ( (GNUNET_OK !=
+ TALER_TESTING_get_trait_sync_account_priv (ref,
+ &priv)) ||
+ (GNUNET_OK !=
+ TALER_TESTING_get_trait_sync_account_pub (ref,
+ &pub)) )
+ {
+ GNUNET_break (0);
+ goto fail;
+ }
+ bds->sync_priv = *priv;
+ bds->sync_pub = *pub;
+ }
+ {
+ const struct SYNC_BlockNonce *nonce;
+
+ if (GNUNET_OK !=
+ TALER_TESTING_get_trait_block_nonce (ref,
+ &nonce))
+ {
+ GNUNET_break (0);
+ goto fail;
+ }
+ bds->nonce = *nonce;
+ }
+ {
+ const struct SYNC_BlockNonce *pn;
+
+ if (GNUNET_OK ==
+ TALER_TESTING_get_trait_prev_nonce (ref,
+ &pn))
+ {
+ bds->prev_nonce = *pn;
+ }
+ }
+ {
+ const struct SYNC_BlockNonce *nn;
+
+ if (GNUNET_OK ==
+ TALER_TESTING_get_trait_next_nonce (ref,
+ &nn))
+ {
+ bds->next_nonce = *nn;
+ }
+ }
+ {
+ const struct GNUNET_HashCode *hash;
+
+ if (GNUNET_OK !=
+ TALER_TESTING_get_trait_curr_hash (ref,
+ &hash))
+ {
+ GNUNET_break (0);
+ goto fail;
+ }
+ bds->block_hash = *hash;
+ }
+ }
+
+ /* Phase 1: fetch the linked list to learn the current state of the
+ block and its neighbours before deleting (their entries are needed
+ for the relink signatures). */
+ bds->blop = SYNC_block_list (
+ TALER_TESTING_interpreter_get_context (is),
+ bds->sync_url,
+ &bds->sync_pub,
+ 10000,
+ NULL,
+ &block_delete_list_finished,
+ bds);
+ if (NULL == bds->blop)
+ goto fail;
+
+ return;
+
+fail:
+ GNUNET_break (0);
+ TALER_TESTING_interpreter_fail (is);
+}
+
+
+/**
+ * Free the state of a "block delete" CMD, and possibly
+ * cancel it if it did not complete.
+ *
+ * @param cls closure.
+ * @param cmd command being freed.
+ */
+static void
+block_delete_cleanup (void *cls,
+ const struct TALER_TESTING_Command *cmd)
+{
+ struct BlockDeleteState *bds = cls;
+
+ if (NULL != bds->bop)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Command '%s' did not complete (block delete)\n",
+ cmd->label);
+ SYNC_block_operation_cancel (bds->bop);
+ bds->bop = NULL;
+ }
+ if (NULL != bds->blop)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Command '%s' did not complete (block list)\n",
+ cmd->label);
+ SYNC_block_list_cancel (bds->blop);
+ bds->blop = NULL;
+ }
+ GNUNET_free (bds);
+}
+
+
+struct TALER_TESTING_Command
+SYNC_TESTING_cmd_block_delete (const char *label,
+ const char *sync_url,
+ const char *account_reference,
+ unsigned int http_status,
+ unsigned int refs_len,
+ const struct SYNC_ObjectUID *object_uids,
+ const int16_t *object_incs)
+{
+ struct BlockDeleteState *bds;
+
+ GNUNET_assert (refs_len <= MAX_BLOCK_OBJECT_REFS);
+ bds = GNUNET_new (struct BlockDeleteState);
+ bds->sync_url = sync_url;
+ bds->account_reference = account_reference;
+ bds->http_status = http_status;
+ bds->refs_len = refs_len;
+ if (0 < refs_len)
+ {
+ GNUNET_memcpy (bds->object_uids,
+ object_uids,
+ refs_len * sizeof (struct SYNC_ObjectUID));
+ GNUNET_memcpy (bds->object_incs,
+ object_incs,
+ refs_len * sizeof (int16_t));
+ }
+ {
+ struct TALER_TESTING_Command cmd = {
+ .cls = bds,
+ .label = label,
+ .run = &block_delete_run,
+ .cleanup = &block_delete_cleanup,
+ };
+
+ return cmd;
+ }
+}
+
+
+/* end of testing_api_cmd_block_delete.c */
diff --git a/src/testing/testing_api_cmd_block_list.c b/src/testing/testing_api_cmd_block_list.c
@@ -0,0 +1,445 @@
+/*
+ This file is part of SYNC
+ Copyright (C) 2026 Taler Systems SA
+
+ SYNC 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.
+
+ SYNC 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 SYNC; see the file COPYING. If not, see
+ <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file testing/testing_api_cmd_block_list.c
+ * @brief command to list blocks from the sync backend service.
+ * @author Iván Ávalos
+ */
+#include "platform.h"
+#include "sync/sync_service.h"
+#include "sync/sync_signatures.h"
+#include "sync/sync_testing_lib.h"
+#include <taler/taler_util.h>
+#include <taler/taler_testing_lib.h>
+#include <gnunet/gnunet_curl_lib.h>
+
+
+/**
+ * Maximum number of expected entries in block list.
+ */
+#define MAX_BLOCK_LIST_ENTRIES 256
+
+
+/**
+ * State for a "block list" CMD.
+ */
+struct BlockListState
+{
+
+ /**
+ * URL of the sync backend.
+ */
+ const char *sync_url;
+
+ /**
+ * Reference to a command that provides the account public key.
+ */
+ const char *account_reference;
+
+ /**
+ * Account public key to query.
+ */
+ struct SYNC_AccountPublicKeyP account_pub;
+
+ /**
+ * Maximum number of blocks to return.
+ */
+ int16_t limit;
+
+ /**
+ * Optional starting nonce.
+ */
+ struct SYNC_BlockNonce start_nonce;
+
+ /**
+ * True if @e start_nonce should be used.
+ */
+ bool have_start_nonce;
+
+ /**
+ * Expected HTTP status code.
+ */
+ unsigned int http_status;
+
+ /**
+ * The interpreter state.
+ */
+ struct TALER_TESTING_Interpreter *is;
+
+ /**
+ * Handle for the library block list operation.
+ */
+ struct SYNC_BlockListOperation *blop;
+
+ /**
+ * Result details to expose as trait.
+ */
+ struct SYNC_BlockListDetails details;
+
+ /**
+ * Number of expected entries.
+ */
+ unsigned int num_expected;
+
+ /**
+ * Expected nonces (array of @e num_expected, up to
+ * #MAX_BLOCK_LIST_ENTRIES).
+ */
+ struct SYNC_BlockNonce expected_nonces[MAX_BLOCK_LIST_ENTRIES];
+
+ /**
+ * Expected block hashes (array of @e num_expected, up to
+ * #MAX_BLOCK_LIST_ENTRIES).
+ */
+ struct GNUNET_HashCode expected_hashes[MAX_BLOCK_LIST_ENTRIES];
+
+};
+
+
+/**
+ * Function called when we're done processing the
+ * block list.
+ *
+ * @param cls the `struct BlockListState`
+ * @param ld details about the list operation
+ */
+static void
+handle_block_list_finished (void *cls,
+ const struct SYNC_BlockListDetails *ld)
+{
+ struct BlockListState *bls = cls;
+ bool mismatch = false;
+
+ bls->blop = NULL;
+ bls->details.http_status = ld->http_status;
+ bls->details.ec = ld->ec;
+ if (ld->http_status != bls->http_status)
+ {
+ TALER_TESTING_unexpected_status (bls->is,
+ ld->http_status,
+ bls->http_status);
+ return;
+ }
+ /* Whatever the server returns must carry a signature that verifies
+ against the stored context, or a client has no way to trust the
+ backup it is about to restore */
+ for (unsigned int i = 0; i < ld->num_entries; i++)
+ {
+ const struct SYNC_BlockListEntry *e = &ld->entries[i];
+
+ if (GNUNET_OK !=
+ SYNC_block_upload_verify (&bls->account_pub,
+ &e->nonce,
+ e->have_prev_nonce
+ ? &e->prev_nonce
+ : NULL,
+ e->have_next_nonce
+ ? &e->next_nonce
+ : NULL,
+ &e->old_hash,
+ &e->hash,
+ &e->refs_hash,
+ &e->upload_sig))
+ {
+ char *nonce_s;
+
+ mismatch = true;
+ nonce_s = GNUNET_STRINGS_data_to_string_alloc (
+ &e->nonce,
+ sizeof (struct SYNC_BlockNonce));
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Block list entry %u (%s): stored signature does not"
+ " verify against the stored context\n",
+ i,
+ nonce_s);
+ GNUNET_free (nonce_s);
+ }
+ }
+
+ if (bls->num_expected > 0)
+ {
+ if (ld->num_entries != bls->num_expected)
+ {
+ GNUNET_break_op (0);
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Expected %u entries in block list, got %u\n",
+ bls->num_expected,
+ ld->num_entries);
+ TALER_TESTING_interpreter_fail (bls->is);
+ return;
+ }
+ for (unsigned int i = 0; i < bls->num_expected; i++)
+ {
+ if (0 != GNUNET_memcmp (&ld->entries[i].nonce,
+ &bls->expected_nonces[i]))
+ {
+ char *nonce_a;
+ char *nonce_b;
+
+ mismatch = true;
+ nonce_a = GNUNET_STRINGS_data_to_string_alloc (
+ &ld->entries[i].nonce,
+ sizeof (struct SYNC_BlockNonce));
+ nonce_b = GNUNET_STRINGS_data_to_string_alloc (
+ &bls->expected_nonces[i],
+ sizeof (struct SYNC_BlockNonce));
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Block list entry %u nonce mismatch\n(%s vs.\n %s)\n",
+ i,
+ nonce_a,
+ nonce_b);
+ GNUNET_free (nonce_a);
+ GNUNET_free (nonce_b);
+ }
+ if (0 != GNUNET_memcmp (&ld->entries[i].hash,
+ &bls->expected_hashes[i]))
+ {
+ char *hash_a;
+ char *hash_b;
+
+ mismatch = true;
+ hash_a = GNUNET_STRINGS_data_to_string_alloc (
+ &ld->entries[i].hash,
+ sizeof (struct GNUNET_HashCode));
+ hash_b = GNUNET_STRINGS_data_to_string_alloc (
+ &bls->expected_hashes[i],
+ sizeof (struct GNUNET_HashCode));
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Block list entry %u hash mismatch\n(%s vs.\n %s)\n",
+ i,
+ hash_a,
+ hash_b);
+ GNUNET_free (hash_a);
+ GNUNET_free (hash_b);
+ }
+ /* The payload must hash to the hash reported for the block */
+ {
+ struct GNUNET_HashCode dh;
+
+ GNUNET_CRYPTO_hash (ld->entries[i].data,
+ ld->entries[i].data_size,
+ &dh);
+ if (0 != GNUNET_memcmp (&dh,
+ &ld->entries[i].hash))
+ {
+ mismatch = true;
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Block list entry %u: data of %u bytes does not"
+ " hash to the reported block hash\n",
+ i,
+ (unsigned int) ld->entries[i].data_size);
+ }
+ }
+ }
+ }
+
+ if (mismatch)
+ {
+ GNUNET_break_op (0);
+ TALER_TESTING_interpreter_fail (bls->is);
+ return;
+ }
+
+ TALER_TESTING_interpreter_next (bls->is);
+}
+
+
+/**
+ * Run a "block list" CMD.
+ *
+ * @param cls closure.
+ * @param cmd command currently being run.
+ * @param is interpreter state.
+ */
+static void
+block_list_run (void *cls,
+ const struct TALER_TESTING_Command *cmd,
+ struct TALER_TESTING_Interpreter *is)
+{
+ struct BlockListState *bls = cls;
+
+ (void) cmd;
+ bls->is = is;
+
+ if (NULL != bls->account_reference)
+ {
+ const struct TALER_TESTING_Command *ref;
+
+ ref = TALER_TESTING_interpreter_lookup_command (is,
+ bls->account_reference);
+ if (NULL == ref)
+ {
+ GNUNET_break (0);
+ goto fail;
+ }
+ {
+ const struct SYNC_AccountPublicKeyP *pub;
+
+ if (GNUNET_OK !=
+ TALER_TESTING_get_trait_sync_account_pub (ref,
+ &pub))
+ goto fail;
+ bls->account_pub = *pub;
+ }
+ }
+ else
+ goto fail;
+
+ bls->blop = SYNC_block_list (
+ TALER_TESTING_interpreter_get_context (is),
+ bls->sync_url,
+ &bls->account_pub,
+ bls->limit,
+ bls->have_start_nonce ? &bls->start_nonce : NULL,
+ &handle_block_list_finished,
+ bls);
+ if (NULL == bls->blop)
+ goto fail;
+
+ return;
+
+fail:
+ GNUNET_break (0);
+ TALER_TESTING_interpreter_fail (is);
+}
+
+
+/**
+ * Free the state of a "block list" CMD, and possibly
+ * cancel it if it did not complete.
+ *
+ * @param cls closure.
+ * @param cmd command being freed.
+ */
+static void
+block_list_cleanup (void *cls,
+ const struct TALER_TESTING_Command *cmd)
+{
+ struct BlockListState *bls = cls;
+
+ if (NULL != bls->blop)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Command '%s' did not complete (block list)\n",
+ cmd->label);
+ SYNC_block_list_cancel (bls->blop);
+ bls->blop = NULL;
+ }
+ GNUNET_free (bls);
+}
+
+
+/**
+ * Offer internal data to other commands.
+ *
+ * @param cls closure
+ * @param[out] ret result (could be anything)
+ * @param trait name of the trait
+ * @param index index number of the object to extract.
+ * @return #GNUNET_OK on success
+ */
+static enum GNUNET_GenericReturnValue
+block_list_traits (void *cls,
+ const void **ret,
+ const char *trait,
+ unsigned int index)
+{
+ struct BlockListState *bls = cls;
+ struct TALER_TESTING_Trait traits[] = {
+ TALER_TESTING_make_trait_block_list_result (&bls->details),
+ TALER_TESTING_trait_end ()
+ };
+ const struct TALER_TESTING_Trait *t;
+ enum GNUNET_GenericReturnValue res;
+
+ res = TALER_TESTING_get_trait (traits,
+ ret,
+ trait,
+ index);
+ if (GNUNET_OK == res)
+ return res;
+ if (0 < bls->details.num_entries)
+ {
+ /* Expose the last entry's signed context to the block write
+ commands (needed for relink signatures). */
+ const struct SYNC_BlockListEntry *last =
+ &bls->details.entries[bls->details.num_entries - 1];
+ struct TALER_TESTING_Trait st[] = {
+ TALER_TESTING_make_trait_block_nonce (&last->nonce),
+ TALER_TESTING_make_trait_prev_nonce (&last->prev_nonce),
+ TALER_TESTING_make_trait_next_nonce (&last->next_nonce),
+ TALER_TESTING_make_trait_block_refs_hash (&last->refs_hash),
+ TALER_TESTING_trait_end ()
+ };
+
+ return TALER_TESTING_get_trait (st,
+ ret,
+ trait,
+ index);
+ }
+ return res;
+}
+
+
+struct TALER_TESTING_Command
+SYNC_TESTING_cmd_block_list (const char *label,
+ const char *sync_url,
+ const char *account_reference,
+ int16_t limit,
+ const struct SYNC_BlockNonce *start_nonce,
+ unsigned int http_status,
+ unsigned int num_expected,
+ const struct SYNC_BlockNonce *expected_nonces,
+ const struct GNUNET_HashCode *expected_hashes)
+{
+ struct BlockListState *bls;
+
+ GNUNET_assert (num_expected <= MAX_BLOCK_LIST_ENTRIES);
+ bls = GNUNET_new (struct BlockListState);
+ bls->sync_url = sync_url;
+ bls->account_reference = account_reference;
+ bls->limit = limit;
+ if (NULL != start_nonce)
+ {
+ bls->start_nonce = *start_nonce;
+ bls->have_start_nonce = true;
+ }
+ bls->http_status = http_status;
+ bls->num_expected = num_expected;
+ if (0 < num_expected)
+ {
+ GNUNET_memcpy (bls->expected_nonces,
+ expected_nonces,
+ num_expected * sizeof (struct SYNC_BlockNonce));
+ GNUNET_memcpy (bls->expected_hashes,
+ expected_hashes,
+ num_expected * sizeof (struct GNUNET_HashCode));
+ }
+ {
+ struct TALER_TESTING_Command cmd = {
+ .cls = bls,
+ .label = label,
+ .run = &block_list_run,
+ .cleanup = &block_list_cleanup,
+ .traits = &block_list_traits
+ };
+
+ return cmd;
+ }
+}
diff --git a/src/testing/testing_api_cmd_block_update.c b/src/testing/testing_api_cmd_block_update.c
@@ -0,0 +1,181 @@
+/*
+ This file is part of SYNC
+ Copyright (C) 2014-2026 Taler Systems SA
+
+ SYNC 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.
+
+ SYNC 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 SYNC; see the file COPYING. If not, see
+ <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file testing/testing_api_cmd_block_update.c
+ * @brief command to update a block in the sync backend service.
+ * @author Iván Ávalos
+ */
+#include "testing_api_cmd_block_common.h"
+
+
+/**
+ * Process a reference for an update: inherit keys, nonce
+ * (if not explicitly set), and old_data_hash from the ref's
+ * curr_hash trait.
+ *
+ * @param[in,out] bus state to populate
+ * @param ref reference command to inherit from
+ * @return #GNUNET_OK on success, #GNUNET_SYSERR on failure
+ */
+static enum GNUNET_GenericReturnValue
+update_process_ref (
+ struct BlockUploadState *bus,
+ const struct TALER_TESTING_Command *ref)
+{
+ if (GNUNET_OK !=
+ SYNC_TESTING_inherit_account_keys_ (bus,
+ ref))
+ return GNUNET_SYSERR;
+
+ /* Inherit block nonce if not explicitly set */
+ if (GNUNET_is_zero (&bus->nonce))
+ {
+ const struct SYNC_BlockNonce *nonce;
+
+ if (GNUNET_OK ==
+ TALER_TESTING_get_trait_block_nonce (ref,
+ &nonce))
+ bus->nonce = *nonce;
+ }
+
+ /* Inherit old_data_hash from curr_hash for update */
+ if (GNUNET_is_zero (&bus->old_data_hash))
+ {
+ const struct GNUNET_HashCode *h;
+
+ if (GNUNET_OK ==
+ TALER_TESTING_get_trait_curr_hash (ref,
+ &h))
+ bus->old_data_hash = *h;
+ }
+
+ return GNUNET_OK;
+}
+
+
+/**
+ * Start a block update operation.
+ *
+ * @param ctx for HTTP client request processing
+ * @param sync_url base URL of the Sync server
+ * @param sync_priv private key of the account
+ * @param nonce nonce identifying the block
+ * @param prev_nonce nonce of the preceding block, or NULL
+ * @param next_nonce nonce of the succeeding block, or NULL
+ * @param old_data_hash hash of the existing block data
+ * @param block_data_size number of bytes in @a block_data
+ * @param block_data the block data
+ * @param refs_len number of entries in @a object_uids and @a object_incs
+ * @param object_uids array of object UIDs referenced by this block
+ * @param object_incs array of reference count adjustments
+ * @param prev_entry unused for updates (the links do not change)
+ * @param cb function to call with the result
+ * @param cb_cls closure for @a cb
+ * @return handle for the operation
+ */
+static struct SYNC_BlockOperation *
+update_start_upload (
+ struct GNUNET_CURL_Context *ctx,
+ const char *sync_url,
+ const struct SYNC_AccountPrivateKeyP *sync_priv,
+ const struct SYNC_BlockNonce *nonce,
+ const struct SYNC_BlockNonce *prev_nonce,
+ const struct SYNC_BlockNonce *next_nonce,
+ const struct GNUNET_HashCode *old_data_hash,
+ size_t block_data_size,
+ const void *block_data,
+ size_t refs_len,
+ const struct SYNC_ObjectUID *object_uids,
+ const int16_t *object_incs,
+ const struct SYNC_BlockListEntry *prev_entry,
+ SYNC_BlockOperationCallback cb,
+ void *cb_cls)
+{
+ struct SYNC_BlockOperationArgs args = {
+ .nonce = nonce,
+ .prev_nonce = prev_nonce,
+ .next_nonce = next_nonce,
+ .old_data_hash = old_data_hash,
+ .block_data_size = block_data_size,
+ .block_data = block_data,
+ .refs_len = refs_len,
+ .object_uids = object_uids,
+ .object_incs = object_incs,
+ };
+
+ (void) prev_entry;
+ return SYNC_block_update (ctx,
+ sync_url,
+ sync_priv,
+ &args,
+ cb,
+ cb_cls);
+}
+
+
+struct TALER_TESTING_Command
+SYNC_TESTING_cmd_block_update (const char *label,
+ const char *sync_url,
+ const char *upload_reference,
+ const struct SYNC_BlockNonce *nonce,
+ const struct GNUNET_HashCode *old_data_hash,
+ unsigned int http_status,
+ const void *block_data,
+ size_t block_data_size,
+ unsigned int refs_len,
+ const struct SYNC_ObjectUID *object_uids,
+ const int16_t *object_incs)
+{
+ struct BlockUploadState *bus;
+
+ GNUNET_assert (refs_len <= MAX_BLOCK_OBJECT_REFS);
+ bus = GNUNET_new (struct BlockUploadState);
+ bus->sync_url = sync_url;
+ bus->upload_reference = upload_reference;
+ if (NULL != nonce)
+ bus->nonce = *nonce;
+ if (NULL != old_data_hash)
+ bus->old_data_hash = *old_data_hash;
+ bus->http_status = http_status;
+ bus->block_data = block_data;
+ bus->block_data_size = block_data_size;
+ bus->refs_len = refs_len;
+ if (0 < refs_len)
+ {
+ GNUNET_memcpy (bus->object_uids,
+ object_uids,
+ refs_len * sizeof (struct SYNC_ObjectUID));
+ GNUNET_memcpy (bus->object_incs,
+ object_incs,
+ refs_len * sizeof (int16_t));
+ }
+ bus->process_ref = &update_process_ref;
+ bus->start_upload = &update_start_upload;
+ {
+ struct TALER_TESTING_Command cmd = {
+ .cls = bus,
+ .label = label,
+ .run = &SYNC_TESTING_block_upload_run_,
+ .cleanup = &SYNC_TESTING_block_upload_cleanup_,
+ .traits = &SYNC_TESTING_block_upload_traits_
+ };
+
+ return cmd;
+ }
+}
diff --git a/src/testing/testing_api_cmd_object_fetch.c b/src/testing/testing_api_cmd_object_fetch.c
@@ -0,0 +1,270 @@
+/*
+ This file is part of SYNC
+ Copyright (C) 2026 Taler Systems SA
+
+ SYNC 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.
+
+ SYNC 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 SYNC; see the file COPYING. If not, see
+ <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file testing/testing_api_cmd_object_fetch.c
+ * @brief command to fetch an object from the sync backend service.
+ * @author Iván Ávalos
+ */
+#include "platform.h"
+#include "sync/sync_service.h"
+#include "sync/sync_testing_lib.h"
+#include <sync/sync_util.h>
+#include <taler/taler_util.h>
+#include <taler/taler_signatures.h>
+#include <taler/taler_testing_lib.h>
+#include <gnunet/gnunet_curl_lib.h>
+
+
+/**
+ * State for an "object fetch" CMD.
+ */
+struct ObjectFetchState
+{
+
+ /**
+ * URL of the sync backend.
+ */
+ const char *sync_url;
+
+ /**
+ * Reference to a command that provides account keys.
+ */
+ const char *account_reference;
+
+ /**
+ * Eddsa public key.
+ */
+ struct SYNC_AccountPublicKeyP sync_pub;
+
+ /**
+ * UID of the object to fetch.
+ */
+ struct SYNC_ObjectUID uid;
+
+ /**
+ * Expected object data.
+ */
+ const void *expected_data;
+
+ /**
+ * Number of bytes in @e expected_data.
+ */
+ size_t expected_data_size;
+
+ /**
+ * Expected HTTP status code.
+ */
+ unsigned int http_status;
+
+ /**
+ * The interpreter state.
+ */
+ struct TALER_TESTING_Interpreter *is;
+
+ /**
+ * Handle for the library object fetch operation.
+ */
+ struct SYNC_ObjectFetchOperation *fop;
+
+};
+
+
+/**
+ * Function called when we're done processing the
+ * object fetch.
+ *
+ * @param cls the `struct ObjectFetchState`
+ * @param http_status HTTP status code
+ * @param ec error code
+ * @param uid UID of the fetched object
+ * @param data_size number of bytes in @a data
+ * @param data object data
+ */
+static void
+object_fetch_finished (void *cls,
+ unsigned int http_status,
+ enum TALER_ErrorCode ec,
+ const struct SYNC_ObjectUID *uid,
+ size_t data_size,
+ const void *data)
+{
+ struct ObjectFetchState *ofs = cls;
+
+ ofs->fop = NULL;
+ if (http_status != ofs->http_status)
+ {
+ TALER_TESTING_unexpected_status (ofs->is,
+ http_status,
+ ofs->http_status);
+ return;
+ }
+ if (MHD_HTTP_OK == http_status)
+ {
+ if (data_size != ofs->expected_data_size)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Object fetch data size mismatch: expected %zu, got %zu\n",
+ ofs->expected_data_size,
+ data_size);
+ GNUNET_break_op (0);
+ TALER_TESTING_interpreter_fail (ofs->is);
+ return;
+ }
+ if (0 != memcmp (data,
+ ofs->expected_data,
+ ofs->expected_data_size))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Object fetch data content mismatch\n");
+ GNUNET_break_op (0);
+ TALER_TESTING_interpreter_fail (ofs->is);
+ return;
+ }
+ if (NULL != uid)
+ {
+ if (0 != GNUNET_memcmp (uid,
+ &ofs->uid))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Object fetch UID mismatch\n");
+ GNUNET_break_op (0);
+ TALER_TESTING_interpreter_fail (ofs->is);
+ return;
+ }
+ }
+ }
+ TALER_TESTING_interpreter_next (ofs->is);
+}
+
+
+/**
+ * Run an "object fetch" CMD.
+ *
+ * @param cls closure.
+ * @param cmd command currently being run.
+ * @param is interpreter state.
+ */
+static void
+object_fetch_run (void *cls,
+ const struct TALER_TESTING_Command *cmd,
+ struct TALER_TESTING_Interpreter *is)
+{
+ struct ObjectFetchState *ofs = cls;
+
+ (void) cmd;
+ ofs->is = is;
+
+ {
+ const struct TALER_TESTING_Command *ref;
+ const struct SYNC_AccountPublicKeyP *pub;
+
+ ref = TALER_TESTING_interpreter_lookup_command (is,
+ ofs->account_reference);
+ if (NULL == ref)
+ {
+ GNUNET_break (0);
+ goto fail;
+ }
+ if (GNUNET_OK !=
+ TALER_TESTING_get_trait_sync_account_pub (ref,
+ &pub))
+ {
+ GNUNET_break (0);
+ goto fail;
+ }
+ ofs->sync_pub = *pub;
+ }
+
+ ofs->fop = SYNC_object_fetch (
+ TALER_TESTING_interpreter_get_context (is),
+ ofs->sync_url,
+ &ofs->sync_pub,
+ &ofs->uid,
+ &object_fetch_finished,
+ ofs);
+ if (NULL == ofs->fop)
+ goto fail;
+
+ return;
+
+fail:
+ GNUNET_break (0);
+ TALER_TESTING_interpreter_fail (is);
+}
+
+
+/**
+ * Free the state of an "object fetch" CMD, and possibly
+ * cancel it if it did not complete.
+ *
+ * @param cls closure.
+ * @param cmd command being freed.
+ */
+static void
+object_fetch_cleanup (void *cls,
+ const struct TALER_TESTING_Command *cmd)
+{
+ struct ObjectFetchState *ofs = cls;
+
+ if (NULL != ofs->fop)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Command '%s' did not complete (object fetch)\n",
+ cmd->label);
+ SYNC_object_fetch_cancel (ofs->fop);
+ ofs->fop = NULL;
+ }
+ GNUNET_free (ofs);
+}
+
+
+struct TALER_TESTING_Command
+SYNC_TESTING_cmd_object_fetch (const char *label,
+ const char *sync_url,
+ const char *account_reference,
+ const struct SYNC_ObjectUID *uid,
+ unsigned int http_status,
+ const void *expected_data,
+ size_t expected_data_size)
+{
+ struct ObjectFetchState *ofs;
+
+ ofs = GNUNET_new (struct ObjectFetchState);
+ ofs->sync_url = sync_url;
+ ofs->account_reference = account_reference;
+ if (NULL != uid)
+ ofs->uid = *uid;
+ ofs->http_status = http_status;
+ ofs->expected_data = expected_data;
+ ofs->expected_data_size = expected_data_size;
+ {
+ struct TALER_TESTING_Command cmd = {
+ .cls = ofs,
+ .label = label,
+ .run = &object_fetch_run,
+ .cleanup = &object_fetch_cleanup,
+ .traits = NULL
+ };
+
+ return cmd;
+ }
+}
+
+
+/* end of testing_api_cmd_object_fetch.c */
diff --git a/src/testing/testing_api_cmd_object_upload.c b/src/testing/testing_api_cmd_object_upload.c
@@ -0,0 +1,238 @@
+/*
+ This file is part of SYNC
+ Copyright (C) 2014-2026 Taler Systems SA
+
+ SYNC 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.
+
+ SYNC 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 SYNC; see the file COPYING. If not, see
+ <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file testing/testing_api_cmd_object_upload.c
+ * @brief command to upload an object to the sync backend service.
+ * @author Iván Ávalos
+ */
+#include "platform.h"
+#include "sync/sync_service.h"
+#include "sync/sync_testing_lib.h"
+#include <taler/taler_util.h>
+#include <taler/taler_signatures.h>
+#include <taler/taler_testing_lib.h>
+#include <gnunet/gnunet_curl_lib.h>
+
+
+/**
+ * State for an "object upload" CMD.
+ */
+struct ObjectUploadState
+{
+
+ /**
+ * URL of the sync backend.
+ */
+ const char *sync_url;
+
+ /**
+ * Reference to a command that provides account keys.
+ */
+ const char *account_reference;
+
+ /**
+ * Eddsa private key.
+ */
+ struct SYNC_AccountPrivateKeyP sync_priv;
+
+ /**
+ * Eddsa public key.
+ */
+ struct SYNC_AccountPublicKeyP sync_pub;
+
+ /**
+ * UID of the object.
+ */
+ struct SYNC_ObjectUID uid;
+
+ /**
+ * Object data.
+ */
+ const void *data;
+
+ /**
+ * Number of bytes in @e data.
+ */
+ size_t data_size;
+
+ /**
+ * Expected HTTP status code.
+ */
+ unsigned int http_status;
+
+ /**
+ * The interpreter state.
+ */
+ struct TALER_TESTING_Interpreter *is;
+
+ /**
+ * Handle for the library object upload operation.
+ */
+ struct SYNC_BlockOperation *bop;
+
+};
+
+
+/**
+ * Function called when we're done processing the
+ * object upload.
+ *
+ * @param cls the `struct ObjectUploadState`
+ * @param ud details about the upload operation
+ */
+static void
+object_upload_finished (void *cls,
+ const struct SYNC_BlockOperationDetails *ud)
+{
+ struct ObjectUploadState *ous = cls;
+
+ ous->bop = NULL;
+ if (ud->http_status != ous->http_status)
+ {
+ TALER_TESTING_unexpected_status (ous->is,
+ ud->http_status,
+ ous->http_status);
+ return;
+ }
+ TALER_TESTING_interpreter_next (ous->is);
+}
+
+
+/**
+ * Run an "object upload" CMD.
+ *
+ * @param cls closure.
+ * @param cmd command currently being run.
+ * @param is interpreter state.
+ */
+static void
+object_upload_run (void *cls,
+ const struct TALER_TESTING_Command *cmd,
+ struct TALER_TESTING_Interpreter *is)
+{
+ struct ObjectUploadState *ous = cls;
+
+ (void) cmd;
+ ous->is = is;
+
+ {
+ const struct TALER_TESTING_Command *ref;
+ const struct SYNC_AccountPrivateKeyP *priv;
+ const struct SYNC_AccountPublicKeyP *pub;
+
+ ref = TALER_TESTING_interpreter_lookup_command (is,
+ ous->account_reference);
+ if (NULL == ref)
+ {
+ GNUNET_break (0);
+ goto fail;
+ }
+ if ( (GNUNET_OK !=
+ TALER_TESTING_get_trait_sync_account_priv (ref,
+ &priv)) ||
+ (GNUNET_OK !=
+ TALER_TESTING_get_trait_sync_account_pub (ref,
+ &pub)) )
+ {
+ GNUNET_break (0);
+ goto fail;
+ }
+ ous->sync_priv = *priv;
+ ous->sync_pub = *pub;
+ }
+
+ ous->bop = SYNC_object_upload (
+ TALER_TESTING_interpreter_get_context (is),
+ ous->sync_url,
+ &ous->sync_priv,
+ &ous->uid,
+ ous->data_size,
+ ous->data,
+ &object_upload_finished,
+ ous);
+ if (NULL == ous->bop)
+ goto fail;
+
+ return;
+
+fail:
+ GNUNET_break (0);
+ TALER_TESTING_interpreter_fail (is);
+}
+
+
+/**
+ * Free the state of an "object upload" CMD, and possibly
+ * cancel it if it did not complete.
+ *
+ * @param cls closure.
+ * @param cmd command being freed.
+ */
+static void
+object_upload_cleanup (void *cls,
+ const struct TALER_TESTING_Command *cmd)
+{
+ struct ObjectUploadState *ous = cls;
+
+ if (NULL != ous->bop)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Command '%s' did not complete (object upload)\n",
+ cmd->label);
+ SYNC_block_operation_cancel (ous->bop);
+ ous->bop = NULL;
+ }
+ GNUNET_free (ous);
+}
+
+
+struct TALER_TESTING_Command
+SYNC_TESTING_cmd_object_upload (const char *label,
+ const char *sync_url,
+ const char *account_reference,
+ const struct SYNC_ObjectUID *uid,
+ unsigned int http_status,
+ const void *data,
+ size_t data_size)
+{
+ struct ObjectUploadState *ous;
+
+ ous = GNUNET_new (struct ObjectUploadState);
+ ous->sync_url = sync_url;
+ ous->account_reference = account_reference;
+ if (NULL != uid)
+ ous->uid = *uid;
+ ous->http_status = http_status;
+ ous->data = data;
+ ous->data_size = data_size;
+ {
+ struct TALER_TESTING_Command cmd = {
+ .cls = ous,
+ .label = label,
+ .run = &object_upload_run,
+ .cleanup = &object_upload_cleanup,
+ .traits = NULL
+ };
+
+ return cmd;
+ }
+}
+
+
+/* end of testing_api_cmd_object_upload.c */
diff --git a/src/util/meson.build b/src/util/meson.build
@@ -10,11 +10,17 @@ configure_file(
libsyncutil = library(
'syncutil',
- ['os_installation.c'],
+ ['os_installation.c', 'sync_signatures.c', 'sync_block.c', 'sync_object.c'],
soversion: solibversions['libsyncutil']['soversion'],
version: solibversions['libsyncutil']['soversion'],
install_rpath: rpath_option,
- dependencies: [gnunetutil_dep],
+ dependencies: [
+ gnunetutil_dep,
+ talerutil_dep,
+ gnunetjson_dep,
+ talerjson_dep,
+ json_dep,
+ ],
include_directories: [incdir, configuration_inc],
install: true,
install_dir: get_option('libdir'),
@@ -36,3 +42,27 @@ executable(
)
+test_sync_signatures = executable(
+ 'test_sync_signatures',
+ ['test_sync_signatures.c'],
+ install_rpath: rpath_option,
+ dependencies: [
+ libsyncutil_dep,
+ talerutil_dep,
+ gnunetutil_dep,
+ gnunetjson_dep,
+ json_dep,
+ ],
+ include_directories: [incdir, configuration_inc],
+ install: false,
+)
+# Not in 'installcheck', which the default test setup excludes: this
+# test needs neither an installed tree nor a database.
+test(
+ 'test_sync_signatures',
+ test_sync_signatures,
+ workdir: meson.current_build_dir(),
+ suite: ['util'],
+)
+
+
diff --git a/src/util/os_installation.c b/src/util/os_installation.c
@@ -19,7 +19,7 @@
*/
/**
- * @file os_installation.c
+ * @file util/os_installation.c
* @brief initialize libgnunet OS subsystem for Sync.
* @author Christian Grothoff
*/
diff --git a/src/util/sync_block.c b/src/util/sync_block.c
@@ -0,0 +1,129 @@
+/*
+ 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/>
+*/
+/**
+ * @file util/sync_block.c
+ * @brief shared logic for block JSON parsing and serialization
+ * @author Iván Ávalos
+ */
+#include "platform.h"
+#include <gnunet/gnunet_common.h>
+#include <gnunet/gnunet_json_lib.h>
+#include <jansson.h>
+#include <taler/taler_json_lib.h>
+#include <sync/sync_util.h>
+
+
+void
+SYNC_block_nonce_generate (
+ struct SYNC_BlockNonce *nonce)
+{
+ GNUNET_CRYPTO_random_block (nonce,
+ sizeof (struct SYNC_BlockNonce));
+}
+
+
+enum GNUNET_GenericReturnValue
+SYNC_block_entry_parse (
+ json_t *input,
+ struct SYNC_BlockListEntry *entry)
+{
+ struct GNUNET_JSON_Specification espec[] = {
+ GNUNET_JSON_spec_fixed_auto ("nonce",
+ &entry->nonce),
+ GNUNET_JSON_spec_fixed_auto ("block_hash",
+ &entry->hash),
+ GNUNET_JSON_spec_varsize ("data",
+ &entry->data,
+ &entry->data_size),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_fixed_auto ("prev_nonce",
+ &entry->prev_nonce),
+ &entry->have_prev_nonce),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_fixed_auto ("next_nonce",
+ &entry->next_nonce),
+ &entry->have_next_nonce),
+ GNUNET_JSON_spec_fixed_auto ("upload_sig",
+ &entry->upload_sig),
+ GNUNET_JSON_spec_fixed_auto ("old_hash",
+ &entry->old_hash),
+ GNUNET_JSON_spec_fixed_auto ("refs_hash",
+ &entry->refs_hash),
+ GNUNET_JSON_spec_end ()
+ };
+ enum GNUNET_GenericReturnValue res;
+ const char *ename;
+ unsigned int eline;
+
+ res = GNUNET_JSON_parse (input,
+ espec,
+ &ename,
+ &eline);
+ if (GNUNET_OK != res)
+ {
+ GNUNET_break_op (0);
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Failed to parse block entry at field %s\n",
+ ename);
+ return GNUNET_SYSERR;
+ }
+ return GNUNET_OK;
+}
+
+
+void
+SYNC_block_entry_clear (
+ struct SYNC_BlockListEntry *entry)
+{
+ GNUNET_free (entry->data);
+ entry->data = NULL;
+ entry->data_size = 0;
+}
+
+
+json_t *
+SYNC_block_entry_serialize (
+ const struct SYNC_BlockNonce *nonce,
+ const struct GNUNET_HashCode *hash,
+ const struct SYNC_BlockNonce *prev_nonce,
+ const struct SYNC_BlockNonce *next_nonce,
+ size_t data_size,
+ const void *data,
+ const struct SYNC_AccountSignatureP *upload_sig,
+ const struct GNUNET_HashCode *old_hash,
+ const struct GNUNET_HashCode *refs_hash)
+{
+ return GNUNET_JSON_PACK (
+ GNUNET_JSON_pack_data_auto ("nonce",
+ nonce),
+ GNUNET_JSON_pack_data_auto ("block_hash",
+ hash),
+ GNUNET_JSON_pack_data_varsize ("data",
+ data,
+ data_size),
+ GNUNET_JSON_pack_allow_null (
+ GNUNET_JSON_pack_data_auto ("prev_nonce",
+ prev_nonce)),
+ GNUNET_JSON_pack_allow_null (
+ GNUNET_JSON_pack_data_auto ("next_nonce",
+ next_nonce)),
+ GNUNET_JSON_pack_data_auto ("upload_sig",
+ upload_sig),
+ GNUNET_JSON_pack_data_auto ("old_hash",
+ old_hash),
+ GNUNET_JSON_pack_data_auto ("refs_hash",
+ refs_hash));
+}
diff --git a/src/util/sync_object.c b/src/util/sync_object.c
@@ -0,0 +1,276 @@
+/*
+ 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/>
+*/
+/**
+ * @file util/sync_object.c
+ * @brief shared logic for object JSON parsing and serialization
+ * @author Iván Ávalos
+ */
+#include "platform.h"
+#include <gnunet/gnunet_common.h>
+#include <gnunet/gnunet_json_lib.h>
+#include <jansson.h>
+#include <taler/taler_json_lib.h>
+#include <sync/sync_util.h>
+
+
+enum GNUNET_GenericReturnValue
+SYNC_object_entry_parse (
+ json_t *input,
+ struct SYNC_ObjectEntry *entry)
+{
+ struct GNUNET_JSON_Specification espec[] = {
+ GNUNET_JSON_spec_fixed_auto ("uid",
+ &entry->uid),
+ GNUNET_JSON_spec_varsize ("data",
+ &entry->data,
+ &entry->data_size),
+ GNUNET_JSON_spec_end ()
+ };
+ enum GNUNET_GenericReturnValue res;
+ const char *ename;
+ unsigned int eline;
+
+ res = GNUNET_JSON_parse (input,
+ espec,
+ &ename,
+ &eline);
+ if (GNUNET_OK != res)
+ {
+ GNUNET_break_op (0);
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Failed to parse object entry at field %s\n",
+ ename);
+ return GNUNET_SYSERR;
+ }
+ return GNUNET_OK;
+}
+
+
+void
+SYNC_object_entry_clear (
+ struct SYNC_ObjectEntry *entry)
+{
+ GNUNET_free (entry->data);
+ entry->data = NULL;
+ entry->data_size = 0;
+}
+
+
+json_t *
+SYNC_object_entry_serialize (
+ const struct SYNC_ObjectUID *uid,
+ size_t data_size,
+ const void *data)
+{
+ return GNUNET_JSON_PACK (
+ GNUNET_JSON_pack_data_auto ("uid",
+ uid),
+ GNUNET_JSON_pack_data_varsize ("data",
+ data,
+ data_size));
+}
+
+
+json_t *
+SYNC_object_refs_serialize (
+ size_t refs_len,
+ const struct SYNC_ObjectUID *object_uids,
+ const int16_t *object_incs)
+{
+ json_t *refs;
+
+ if (0 == refs_len)
+ return NULL;
+ refs = json_object ();
+ GNUNET_assert (NULL != refs);
+ for (size_t i = 0; i < refs_len; i++)
+ {
+ char *key;
+
+ key = GNUNET_STRINGS_data_to_string_alloc (&object_uids[i],
+ sizeof (object_uids[i]));
+ GNUNET_assert (0 ==
+ json_object_set_new (refs,
+ key,
+ json_integer (object_incs[i])));
+ GNUNET_free (key);
+ }
+ return refs;
+}
+
+
+GNUNET_NETWORK_STRUCT_BEGIN
+
+/**
+ * One object reference, as hashed for the signatures.
+ */
+struct SYNC_ObjectRefP
+{
+ /**
+ * UID of the referenced object.
+ */
+ struct SYNC_ObjectUID uid;
+
+ /**
+ * Reference count adjustment, in network byte order.
+ */
+ int16_t inc GNUNET_PACKED;
+};
+
+GNUNET_NETWORK_STRUCT_END
+
+
+/**
+ * Compare two object references by UID, for #qsort().
+ *
+ * @param a first `struct SYNC_ObjectRefP`
+ * @param b second `struct SYNC_ObjectRefP`
+ * @return negative/zero/positive as @a a sorts before/equal/after @a b
+ */
+static int
+ref_cmp (const void *a,
+ const void *b)
+{
+ const struct SYNC_ObjectRefP *ra = a;
+ const struct SYNC_ObjectRefP *rb = b;
+
+ return memcmp (&ra->uid,
+ &rb->uid,
+ sizeof (ra->uid));
+}
+
+
+enum GNUNET_GenericReturnValue
+SYNC_object_refs_hash (
+ size_t refs_len,
+ const struct SYNC_ObjectUID *object_uids,
+ const int16_t *object_incs,
+ struct GNUNET_HashCode *refs_hash)
+{
+ struct SYNC_ObjectRefP *refs;
+
+ if (0 == refs_len)
+ {
+ GNUNET_CRYPTO_hash ("",
+ 0,
+ refs_hash);
+ return GNUNET_OK;
+ }
+ refs = GNUNET_new_array (refs_len,
+ struct SYNC_ObjectRefP);
+ for (size_t i = 0; i < refs_len; i++)
+ {
+ refs[i].uid = object_uids[i];
+ refs[i].inc = htons ((uint16_t) object_incs[i]);
+ }
+ qsort (refs,
+ refs_len,
+ sizeof (struct SYNC_ObjectRefP),
+ &ref_cmp);
+ for (size_t i = 1; i < refs_len; i++)
+ {
+ if (0 == ref_cmp (&refs[i - 1],
+ &refs[i]))
+ {
+ GNUNET_break (0);
+ GNUNET_free (refs);
+ return GNUNET_SYSERR;
+ }
+ }
+ GNUNET_CRYPTO_hash (refs,
+ refs_len * sizeof (struct SYNC_ObjectRefP),
+ refs_hash);
+ GNUNET_free (refs);
+ return GNUNET_OK;
+}
+
+
+enum GNUNET_GenericReturnValue
+SYNC_object_refs_parse (
+ const json_t *refs,
+ size_t *refs_len,
+ struct SYNC_ObjectUID **object_uids,
+ int16_t **object_incs)
+{
+ struct SYNC_ObjectUID *uids;
+ int16_t *incs;
+ size_t len;
+ size_t i;
+ const char *key;
+ json_t *value;
+
+ if (NULL == refs)
+ {
+ *refs_len = 0;
+ *object_uids = NULL;
+ *object_incs = NULL;
+ return GNUNET_OK;
+ }
+ if (! json_is_object (refs))
+ {
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
+ len = json_object_size (refs);
+ if (0 == len)
+ {
+ *refs_len = 0;
+ *object_uids = NULL;
+ *object_incs = NULL;
+ return GNUNET_OK;
+ }
+ uids = GNUNET_new_array (len,
+ struct SYNC_ObjectUID);
+ incs = GNUNET_new_array (len,
+ int16_t);
+ i = 0;
+ json_object_foreach ((json_t *) refs, key, value)
+ {
+ json_int_t inc;
+
+ if (GNUNET_OK !=
+ GNUNET_STRINGS_string_to_data (key,
+ strlen (key),
+ &uids[i],
+ sizeof (uids[i])))
+ {
+ GNUNET_break_op (0);
+ goto fail;
+ }
+ if (! json_is_integer (value))
+ {
+ GNUNET_break_op (0);
+ goto fail;
+ }
+ inc = json_integer_value (value);
+ if ( (inc < INT16_MIN) ||
+ (inc > INT16_MAX) )
+ {
+ GNUNET_break_op (0);
+ goto fail;
+ }
+ incs[i] = (int16_t) inc;
+ i++;
+ }
+ *refs_len = len;
+ *object_uids = uids;
+ *object_incs = incs;
+ return GNUNET_OK;
+fail:
+ GNUNET_free (uids);
+ GNUNET_free (incs);
+ return GNUNET_SYSERR;
+}
diff --git a/src/util/sync_signatures.c b/src/util/sync_signatures.c
@@ -0,0 +1,260 @@
+/*
+ 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/>
+*/
+/**
+ * @file util/sync_signatures.c
+ * @brief Utility functions for sync signatures
+ * @author Iván Ávalos
+ */
+#include "platform.h"
+#include <taler/taler_signatures.h>
+#include <sync/sync_signatures.h>
+
+/* FIXME: implement in exchange */
+#ifndef TALER_SIGNATURE_SYNC_BLOCK_UPLOAD
+#define TALER_SIGNATURE_SYNC_BLOCK_UPLOAD 1452
+#endif
+#ifndef TALER_SIGNATURE_SYNC_BLOCK_DELETE
+#define TALER_SIGNATURE_SYNC_BLOCK_DELETE 1453
+#endif
+#ifndef TALER_SIGNATURE_SYNC_OBJECT_UPLOAD
+#define TALER_SIGNATURE_SYNC_OBJECT_UPLOAD 1454
+#endif
+
+
+GNUNET_NETWORK_STRUCT_BEGIN
+
+/**
+ * Data signed by the account public key of a sync client to
+ * authorize the append/update of a block.
+ */
+struct SYNC_BlockUploadSignaturePS
+{
+ struct GNUNET_CRYPTO_SignaturePurpose purpose;
+
+ struct SYNC_BlockNonce prev_nonce;
+
+ struct SYNC_BlockNonce next_nonce;
+
+ struct SYNC_BlockNonce nonce;
+
+ struct GNUNET_HashCode old_hash;
+
+ struct GNUNET_HashCode new_hash;
+
+ struct GNUNET_HashCode refs_hash;
+};
+
+/**
+ * Data signed by the account public key of a sync client to
+ * authorize the deletion of a block.
+ */
+struct SYNC_BlockDeleteSignaturePS
+{
+ struct GNUNET_CRYPTO_SignaturePurpose purpose;
+
+ struct SYNC_BlockNonce nonce;
+
+ struct SYNC_BlockNonce prev_nonce;
+
+ struct SYNC_BlockNonce next_nonce;
+
+ struct GNUNET_HashCode hash;
+
+ struct GNUNET_HashCode refs_hash;
+};
+
+/**
+ * Data signed by the account public key of a sync client to
+ * authorize the upload of an object.
+ */
+struct SYNC_ObjectUploadSignaturePS
+{
+ struct GNUNET_CRYPTO_SignaturePurpose purpose;
+
+ struct SYNC_ObjectUID uid;
+
+ struct GNUNET_HashCode hash;
+};
+
+GNUNET_NETWORK_STRUCT_END
+
+
+void
+SYNC_block_upload_sign (
+ const struct SYNC_AccountPrivateKeyP *account_priv,
+ const struct SYNC_BlockNonce *nonce,
+ const struct SYNC_BlockNonce *prev_nonce,
+ const struct SYNC_BlockNonce *next_nonce,
+ const struct GNUNET_HashCode *old_hash,
+ const struct GNUNET_HashCode *new_hash,
+ const struct GNUNET_HashCode *refs_hash,
+ struct SYNC_AccountSignatureP *upload_sig)
+{
+ struct SYNC_BlockUploadSignaturePS busp = {
+ .purpose.size = htonl (sizeof (busp)),
+ .purpose.purpose = htonl (TALER_SIGNATURE_SYNC_BLOCK_UPLOAD),
+ .nonce = *nonce,
+ .new_hash = *new_hash,
+ .refs_hash = *refs_hash,
+ };
+
+ if (NULL != prev_nonce)
+ busp.prev_nonce = *prev_nonce;
+ if (NULL != next_nonce)
+ busp.next_nonce = *next_nonce;
+ if (NULL != old_hash)
+ busp.old_hash = *old_hash;
+
+ GNUNET_CRYPTO_eddsa_sign (&account_priv->eddsa_priv,
+ &busp,
+ &upload_sig->eddsa_sig);
+}
+
+
+enum GNUNET_GenericReturnValue
+SYNC_block_upload_verify (
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const struct SYNC_BlockNonce *nonce,
+ const struct SYNC_BlockNonce *prev_nonce,
+ const struct SYNC_BlockNonce *next_nonce,
+ const struct GNUNET_HashCode *old_hash,
+ const struct GNUNET_HashCode *new_hash,
+ const struct GNUNET_HashCode *refs_hash,
+ const struct SYNC_AccountSignatureP *upload_sig)
+{
+ struct SYNC_BlockUploadSignaturePS busp = {
+ .purpose.size = htonl (sizeof (busp)),
+ .purpose.purpose = htonl (TALER_SIGNATURE_SYNC_BLOCK_UPLOAD),
+ .nonce = *nonce,
+ .new_hash = *new_hash,
+ .refs_hash = *refs_hash,
+ };
+
+ if (NULL != prev_nonce)
+ busp.prev_nonce = *prev_nonce;
+ if (NULL != next_nonce)
+ busp.next_nonce = *next_nonce;
+ if (NULL != old_hash)
+ busp.old_hash = *old_hash;
+
+ return GNUNET_CRYPTO_eddsa_verify (
+ TALER_SIGNATURE_SYNC_BLOCK_UPLOAD,
+ &busp,
+ &upload_sig->eddsa_sig,
+ &account_pub->eddsa_pub);
+}
+
+
+void
+SYNC_block_delete_sign (
+ const struct SYNC_AccountPrivateKeyP *account_priv,
+ const struct SYNC_BlockNonce *nonce,
+ const struct SYNC_BlockNonce *prev_nonce,
+ const struct SYNC_BlockNonce *next_nonce,
+ const struct GNUNET_HashCode *hash,
+ const struct GNUNET_HashCode *refs_hash,
+ struct SYNC_AccountSignatureP *delete_sig)
+{
+ struct SYNC_BlockDeleteSignaturePS bdsp = {
+ .purpose.size = htonl (sizeof (bdsp)),
+ .purpose.purpose = htonl (TALER_SIGNATURE_SYNC_BLOCK_DELETE),
+ .nonce = *nonce,
+ .hash = *hash,
+ .refs_hash = *refs_hash,
+ };
+
+ if (NULL != prev_nonce)
+ bdsp.prev_nonce = *prev_nonce;
+ if (NULL != next_nonce)
+ bdsp.next_nonce = *next_nonce;
+
+ GNUNET_CRYPTO_eddsa_sign (&account_priv->eddsa_priv,
+ &bdsp,
+ &delete_sig->eddsa_sig);
+}
+
+
+enum GNUNET_GenericReturnValue
+SYNC_block_delete_verify (
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const struct SYNC_BlockNonce *nonce,
+ const struct SYNC_BlockNonce *prev_nonce,
+ const struct SYNC_BlockNonce *next_nonce,
+ const struct GNUNET_HashCode *hash,
+ const struct GNUNET_HashCode *refs_hash,
+ const struct SYNC_AccountSignatureP *delete_sig)
+{
+ struct SYNC_BlockDeleteSignaturePS bdsp = {
+ .purpose.size = htonl (sizeof (bdsp)),
+ .purpose.purpose = htonl (TALER_SIGNATURE_SYNC_BLOCK_DELETE),
+ .nonce = *nonce,
+ .hash = *hash,
+ .refs_hash = *refs_hash,
+ };
+
+ if (NULL != prev_nonce)
+ bdsp.prev_nonce = *prev_nonce;
+ if (NULL != next_nonce)
+ bdsp.next_nonce = *next_nonce;
+
+ return GNUNET_CRYPTO_eddsa_verify (
+ TALER_SIGNATURE_SYNC_BLOCK_DELETE,
+ &bdsp,
+ &delete_sig->eddsa_sig,
+ &account_pub->eddsa_pub);
+}
+
+
+void
+SYNC_object_upload_sign (
+ const struct SYNC_AccountPrivateKeyP *account_priv,
+ const struct SYNC_ObjectUID *uid,
+ const struct GNUNET_HashCode *hash,
+ struct SYNC_AccountSignatureP *upload_sig)
+{
+ struct SYNC_ObjectUploadSignaturePS ousp = {
+ .purpose.size = htonl (sizeof (ousp)),
+ .purpose.purpose = htonl (TALER_SIGNATURE_SYNC_OBJECT_UPLOAD),
+ .uid = *uid,
+ .hash = *hash,
+ };
+
+ GNUNET_CRYPTO_eddsa_sign (&account_priv->eddsa_priv,
+ &ousp,
+ &upload_sig->eddsa_sig);
+}
+
+
+enum GNUNET_GenericReturnValue
+SYNC_object_upload_verify (
+ const struct SYNC_AccountPublicKeyP *account_pub,
+ const struct SYNC_ObjectUID *uid,
+ const struct GNUNET_HashCode *hash,
+ const struct SYNC_AccountSignatureP *upload_sig)
+{
+ struct SYNC_ObjectUploadSignaturePS ousp = {
+ .purpose.size = htonl (sizeof (ousp)),
+ .purpose.purpose = htonl (TALER_SIGNATURE_SYNC_OBJECT_UPLOAD),
+ .uid = *uid,
+ .hash = *hash,
+ };
+
+ return GNUNET_CRYPTO_eddsa_verify (
+ TALER_SIGNATURE_SYNC_OBJECT_UPLOAD,
+ &ousp,
+ &upload_sig->eddsa_sig,
+ &account_pub->eddsa_pub);
+}
diff --git a/src/util/test_sync_signatures.c b/src/util/test_sync_signatures.c
@@ -0,0 +1,413 @@
+/*
+ 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/>
+*/
+/**
+ * @file util/test_sync_signatures.c
+ * @brief tests for the sync signatures and the object reference hash
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include <gnunet/gnunet_util_lib.h>
+#include "sync/sync_service.h"
+#include "sync/sync_signatures.h"
+#include "sync/sync_util.h"
+
+
+#define FAILIF(cond) \
+ do { \
+ if (! (cond)) break; \
+ GNUNET_break (0); \
+ return 1; \
+ } while (0)
+
+
+/**
+ * Check #SYNC_object_refs_hash(): insensitive to the order the
+ * references arrive in, sensitive to every UID and adjustment.
+ *
+ * @return 0 on success
+ */
+static int
+test_refs_hash (void)
+{
+ struct SYNC_ObjectUID uids[3];
+ struct SYNC_ObjectUID shuffled[3];
+ const int16_t incs[3] = { 1, -2, 3 };
+ int16_t shuffled_incs[3];
+ struct GNUNET_HashCode h;
+ struct GNUNET_HashCode h2;
+
+ for (unsigned int i = 0; i < 3; i++)
+ GNUNET_CRYPTO_random_block (&uids[i],
+ sizeof (uids[i]));
+
+ /* An empty reference set hashes deterministically */
+ FAILIF (GNUNET_OK !=
+ SYNC_object_refs_hash (0,
+ NULL,
+ NULL,
+ &h));
+ FAILIF (GNUNET_OK !=
+ SYNC_object_refs_hash (0,
+ uids,
+ incs,
+ &h2));
+ FAILIF (0 != GNUNET_memcmp (&h,
+ &h2));
+
+ /* ... and differs from any non-empty set */
+ FAILIF (GNUNET_OK !=
+ SYNC_object_refs_hash (3,
+ uids,
+ incs,
+ &h));
+ FAILIF (0 == GNUNET_memcmp (&h,
+ &h2));
+
+ /* JSON object member order is not preserved, so reordering the
+ references must not change the hash */
+ shuffled[0] = uids[2];
+ shuffled[1] = uids[0];
+ shuffled[2] = uids[1];
+ shuffled_incs[0] = incs[2];
+ shuffled_incs[1] = incs[0];
+ shuffled_incs[2] = incs[1];
+ FAILIF (GNUNET_OK !=
+ SYNC_object_refs_hash (3,
+ shuffled,
+ shuffled_incs,
+ &h2));
+ FAILIF (0 != GNUNET_memcmp (&h,
+ &h2));
+
+ /* Changing an adjustment changes the hash */
+ shuffled_incs[0] = (int16_t) (shuffled_incs[0] + 1);
+ FAILIF (GNUNET_OK !=
+ SYNC_object_refs_hash (3,
+ shuffled,
+ shuffled_incs,
+ &h2));
+ FAILIF (0 == GNUNET_memcmp (&h,
+ &h2));
+
+ /* Changing a UID changes the hash */
+ shuffled_incs[0] = incs[2];
+ GNUNET_CRYPTO_random_block (&shuffled[0],
+ sizeof (shuffled[0]));
+ FAILIF (GNUNET_OK !=
+ SYNC_object_refs_hash (3,
+ shuffled,
+ shuffled_incs,
+ &h2));
+ FAILIF (0 == GNUNET_memcmp (&h,
+ &h2));
+
+ /* Dropping a reference changes the hash */
+ FAILIF (GNUNET_OK !=
+ SYNC_object_refs_hash (2,
+ uids,
+ incs,
+ &h2));
+ FAILIF (0 == GNUNET_memcmp (&h,
+ &h2));
+
+ /* A repeated UID cannot be carried on the wire */
+ shuffled[0] = uids[0];
+ shuffled[1] = uids[0];
+ shuffled[2] = uids[1];
+ FAILIF (GNUNET_SYSERR !=
+ SYNC_object_refs_hash (3,
+ shuffled,
+ shuffled_incs,
+ &h2));
+ return 0;
+}
+
+
+/**
+ * Check that references survive the wire encoding with their hash
+ * intact; this is what makes the signature verify server-side.
+ *
+ * @return 0 on success
+ */
+static int
+test_refs_json_roundtrip (void)
+{
+ struct SYNC_ObjectUID uids[3];
+ const int16_t incs[3] = { 5, -1, 32767 };
+ struct GNUNET_HashCode h;
+ struct GNUNET_HashCode h2;
+ json_t *refs;
+ size_t parsed_len;
+ struct SYNC_ObjectUID *parsed_uids;
+ int16_t *parsed_incs;
+
+ for (unsigned int i = 0; i < 3; i++)
+ GNUNET_CRYPTO_random_block (&uids[i],
+ sizeof (uids[i]));
+ FAILIF (GNUNET_OK !=
+ SYNC_object_refs_hash (3,
+ uids,
+ incs,
+ &h));
+ refs = SYNC_object_refs_serialize (3,
+ uids,
+ incs);
+ FAILIF (NULL == refs);
+ FAILIF (GNUNET_OK !=
+ SYNC_object_refs_parse (refs,
+ &parsed_len,
+ &parsed_uids,
+ &parsed_incs));
+ json_decref (refs);
+ FAILIF (3 != parsed_len);
+ FAILIF (GNUNET_OK !=
+ SYNC_object_refs_hash (parsed_len,
+ parsed_uids,
+ parsed_incs,
+ &h2));
+ GNUNET_free (parsed_uids);
+ GNUNET_free (parsed_incs);
+ FAILIF (0 != GNUNET_memcmp (&h,
+ &h2));
+ return 0;
+}
+
+
+/**
+ * Check that the block upload signature covers the object references.
+ *
+ * @return 0 on success
+ */
+static int
+test_upload_sig (void)
+{
+ struct SYNC_AccountPrivateKeyP priv;
+ struct SYNC_AccountPublicKeyP pub;
+ struct SYNC_AccountSignatureP sig;
+ struct SYNC_BlockNonce nonce;
+ struct SYNC_BlockNonce prev;
+ struct GNUNET_HashCode old_hash;
+ struct GNUNET_HashCode new_hash;
+ struct SYNC_ObjectUID uid;
+ int16_t inc = 1;
+ int16_t tampered_inc = 2;
+ struct GNUNET_HashCode refs_hash;
+ struct GNUNET_HashCode other_refs_hash;
+
+ GNUNET_CRYPTO_eddsa_key_create (&priv.eddsa_priv);
+ GNUNET_CRYPTO_eddsa_key_get_public (&priv.eddsa_priv,
+ &pub.eddsa_pub);
+ GNUNET_CRYPTO_random_block (&nonce,
+ sizeof (nonce));
+ GNUNET_CRYPTO_random_block (&prev,
+ sizeof (prev));
+ GNUNET_CRYPTO_random_block (&uid,
+ sizeof (uid));
+ GNUNET_CRYPTO_hash ("old", 3, &old_hash);
+ GNUNET_CRYPTO_hash ("new", 3, &new_hash);
+ FAILIF (GNUNET_OK !=
+ SYNC_object_refs_hash (1,
+ &uid,
+ &inc,
+ &refs_hash));
+ FAILIF (GNUNET_OK !=
+ SYNC_object_refs_hash (1,
+ &uid,
+ &tampered_inc,
+ &other_refs_hash));
+ SYNC_block_upload_sign (&priv,
+ &nonce,
+ &prev,
+ NULL,
+ &old_hash,
+ &new_hash,
+ &refs_hash,
+ &sig);
+ FAILIF (GNUNET_OK !=
+ SYNC_block_upload_verify (&pub,
+ &nonce,
+ &prev,
+ NULL,
+ &old_hash,
+ &new_hash,
+ &refs_hash,
+ &sig));
+ /* Altering the adjustment invalidates the signature */
+ FAILIF (GNUNET_OK ==
+ SYNC_block_upload_verify (&pub,
+ &nonce,
+ &prev,
+ NULL,
+ &old_hash,
+ &new_hash,
+ &other_refs_hash,
+ &sig));
+ /* So does stripping the references altogether */
+ FAILIF (GNUNET_OK !=
+ SYNC_object_refs_hash (0,
+ NULL,
+ NULL,
+ &other_refs_hash));
+ FAILIF (GNUNET_OK ==
+ SYNC_block_upload_verify (&pub,
+ &nonce,
+ &prev,
+ NULL,
+ &old_hash,
+ &new_hash,
+ &other_refs_hash,
+ &sig));
+ /* The rest of the signed data is still covered */
+ FAILIF (GNUNET_OK ==
+ SYNC_block_upload_verify (&pub,
+ &nonce,
+ NULL,
+ NULL,
+ &old_hash,
+ &new_hash,
+ &refs_hash,
+ &sig));
+ return 0;
+}
+
+
+/**
+ * Check that the block delete signature covers the object references.
+ *
+ * @return 0 on success
+ */
+static int
+test_delete_sig (void)
+{
+ struct SYNC_AccountPrivateKeyP priv;
+ struct SYNC_AccountPublicKeyP pub;
+ struct SYNC_AccountSignatureP sig;
+ struct SYNC_BlockNonce nonce;
+ struct GNUNET_HashCode block_hash;
+ struct SYNC_ObjectUID uid;
+ int16_t dec = -1;
+ struct GNUNET_HashCode refs_hash;
+ struct GNUNET_HashCode empty_refs_hash;
+
+ GNUNET_CRYPTO_eddsa_key_create (&priv.eddsa_priv);
+ GNUNET_CRYPTO_eddsa_key_get_public (&priv.eddsa_priv,
+ &pub.eddsa_pub);
+ GNUNET_CRYPTO_random_block (&nonce,
+ sizeof (nonce));
+ GNUNET_CRYPTO_random_block (&uid,
+ sizeof (uid));
+ GNUNET_CRYPTO_hash ("block", 5, &block_hash);
+ FAILIF (GNUNET_OK !=
+ SYNC_object_refs_hash (1,
+ &uid,
+ &dec,
+ &refs_hash));
+ FAILIF (GNUNET_OK !=
+ SYNC_object_refs_hash (0,
+ NULL,
+ NULL,
+ &empty_refs_hash));
+ SYNC_block_delete_sign (&priv,
+ &nonce,
+ NULL,
+ NULL,
+ &block_hash,
+ &refs_hash,
+ &sig);
+ FAILIF (GNUNET_OK !=
+ SYNC_block_delete_verify (&pub,
+ &nonce,
+ NULL,
+ NULL,
+ &block_hash,
+ &refs_hash,
+ &sig));
+ FAILIF (GNUNET_OK ==
+ SYNC_block_delete_verify (&pub,
+ &nonce,
+ NULL,
+ NULL,
+ &block_hash,
+ &empty_refs_hash,
+ &sig));
+ return 0;
+}
+
+
+/**
+ * Check the object upload signature round trip.
+ *
+ * @return 0 on success
+ */
+static int
+test_object_sig (void)
+{
+ struct SYNC_AccountPrivateKeyP priv;
+ struct SYNC_AccountPublicKeyP pub;
+ struct SYNC_AccountSignatureP sig;
+ struct SYNC_ObjectUID uid;
+ struct SYNC_ObjectUID other_uid;
+ struct GNUNET_HashCode hash;
+
+ GNUNET_CRYPTO_eddsa_key_create (&priv.eddsa_priv);
+ GNUNET_CRYPTO_eddsa_key_get_public (&priv.eddsa_priv,
+ &pub.eddsa_pub);
+ GNUNET_CRYPTO_random_block (&uid,
+ sizeof (uid));
+ GNUNET_CRYPTO_random_block (&other_uid,
+ sizeof (other_uid));
+ GNUNET_CRYPTO_hash ("object", 6, &hash);
+ SYNC_object_upload_sign (&priv,
+ &uid,
+ &hash,
+ &sig);
+ FAILIF (GNUNET_OK !=
+ SYNC_object_upload_verify (&pub,
+ &uid,
+ &hash,
+ &sig));
+ FAILIF (GNUNET_OK ==
+ SYNC_object_upload_verify (&pub,
+ &other_uid,
+ &hash,
+ &sig));
+ return 0;
+}
+
+
+int
+main (int argc,
+ char *const argv[])
+{
+ (void) argc;
+ GNUNET_log_setup (argv[0],
+ "WARNING",
+ NULL);
+ if (0 != test_refs_hash ())
+ return 1;
+ if (0 != test_refs_json_roundtrip ())
+ return 1;
+ if (0 != test_upload_sig ())
+ return 1;
+ if (0 != test_delete_sig ())
+ return 1;
+ if (0 != test_object_sig ())
+ return 1;
+ return 0;
+}
+
+
+/* end of test_sync_signatures.c */