From 66616a97d77d37ab0a1358f3678a07223e624636 Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Fri, 20 Mar 2020 22:34:17 +0100 Subject: working on splitting auditor --- src/auditor/.gitignore | 1 + src/auditor/Makefile.am | 69 +- src/auditor/report-lib.c | 549 ++++++++ src/auditor/report-lib.h | 188 +++ src/auditor/taler-auditor-aggregation.c | 1511 ++++++++++++++++++++ src/auditor/taler-auditor-coins.c | 2346 +++++++++++++++++++++++++++++++ src/auditor/taler-auditor-deposits.c | 360 +++++ src/auditor/taler-auditor-reserves.c | 1641 +++++++++++++++++++++ src/auditor/taler-auditor.c | 7 + 9 files changed, 6671 insertions(+), 1 deletion(-) create mode 100644 src/auditor/report-lib.c create mode 100644 src/auditor/report-lib.h create mode 100644 src/auditor/taler-auditor-aggregation.c create mode 100644 src/auditor/taler-auditor-coins.c create mode 100644 src/auditor/taler-auditor-deposits.c create mode 100644 src/auditor/taler-auditor-reserves.c (limited to 'src/auditor') diff --git a/src/auditor/.gitignore b/src/auditor/.gitignore index fac12fb12..fe067a536 100644 --- a/src/auditor/.gitignore +++ b/src/auditor/.gitignore @@ -12,3 +12,4 @@ test-audit-inc.json test-wire-audit-inc.json wirefees/ bank.err +libauditor.a diff --git a/src/auditor/Makefile.am b/src/auditor/Makefile.am index e129fb46a..819789efd 100644 --- a/src/auditor/Makefile.am +++ b/src/auditor/Makefile.am @@ -13,12 +13,22 @@ pkgcfg_DATA = \ bin_PROGRAMS = \ taler-auditor \ + taler-auditor-reserves \ + taler-auditor-coins \ + taler-auditor-aggregation \ + taler-auditor-deposits \ + taler-wire-auditor \ taler-auditor-exchange \ taler-auditor-httpd \ - taler-wire-auditor \ taler-auditor-sign \ taler-auditor-dbinit +noinst_LIBRARIES = \ + libauditor.a + +libauditor_a_SOURCES = \ + report-lib.c report-lib.h + taler_auditor_dbinit_SOURCES = \ taler-auditor-dbinit.c taler_auditor_dbinit_LDADD = \ @@ -34,6 +44,62 @@ taler_auditor_dbinit_CPPFLAGS = \ -I$(top_srcdir)/src/pq/ \ $(POSTGRESQL_CPPFLAGS) +taler_auditor_reserves_SOURCES = \ + taler-auditor-reserves.c +taler_auditor_reserves_LDADD = \ + $(LIBGCRYPT_LIBS) \ + $(top_builddir)/src/util/libtalerutil.la \ + $(top_builddir)/src/json/libtalerjson.la \ + $(top_builddir)/src/bank-lib/libtalerbank.la \ + $(top_builddir)/src/exchangedb/libtalerexchangedb.la \ + $(top_builddir)/src/auditordb/libtalerauditordb.la \ + libauditor.a \ + -ljansson \ + -lgnunetjson \ + -lgnunetutil + +taler_auditor_coins_SOURCES = \ + taler-auditor-coins.c +taler_auditor_coins_LDADD = \ + $(LIBGCRYPT_LIBS) \ + $(top_builddir)/src/util/libtalerutil.la \ + $(top_builddir)/src/json/libtalerjson.la \ + $(top_builddir)/src/bank-lib/libtalerbank.la \ + $(top_builddir)/src/exchangedb/libtalerexchangedb.la \ + $(top_builddir)/src/auditordb/libtalerauditordb.la \ + libauditor.a \ + -ljansson \ + -lgnunetjson \ + -lgnunetutil + +taler_auditor_aggregation_SOURCES = \ + taler-auditor-aggregation.c +taler_auditor_aggregation_LDADD = \ + $(LIBGCRYPT_LIBS) \ + $(top_builddir)/src/util/libtalerutil.la \ + $(top_builddir)/src/json/libtalerjson.la \ + $(top_builddir)/src/bank-lib/libtalerbank.la \ + $(top_builddir)/src/exchangedb/libtalerexchangedb.la \ + $(top_builddir)/src/auditordb/libtalerauditordb.la \ + libauditor.a \ + -ljansson \ + -lgnunetjson \ + -lgnunetutil + +taler_auditor_deposits_SOURCES = \ + taler-auditor-deposits.c +taler_auditor_deposits_LDADD = \ + $(LIBGCRYPT_LIBS) \ + $(top_builddir)/src/util/libtalerutil.la \ + $(top_builddir)/src/json/libtalerjson.la \ + $(top_builddir)/src/bank-lib/libtalerbank.la \ + $(top_builddir)/src/exchangedb/libtalerexchangedb.la \ + $(top_builddir)/src/auditordb/libtalerauditordb.la \ + libauditor.a \ + -ljansson \ + -lgnunetjson \ + -lgnunetutil + taler_auditor_SOURCES = \ taler-auditor.c taler_auditor_LDADD = \ @@ -47,6 +113,7 @@ taler_auditor_LDADD = \ -lgnunetjson \ -lgnunetutil + taler_auditor_httpd_SOURCES = \ taler-auditor-httpd.c taler-auditor-httpd.h \ taler-auditor-httpd_deposit-confirmation.c taler-auditor-httpd_deposit-confirmation.h \ diff --git a/src/auditor/report-lib.c b/src/auditor/report-lib.c new file mode 100644 index 000000000..b2df8a14d --- /dev/null +++ b/src/auditor/report-lib.c @@ -0,0 +1,549 @@ +/* + This file is part of TALER + Copyright (C) 2016-2020 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero 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 Public License for more details. + + You should have received a copy of the GNU Affero Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file auditor/report-lib.c + * @brief helper library to facilitate generation of audit reports + * @author Christian Grothoff + */ +#include "platform.h" +#include "report-lib.h" + +/** + * Command-line option "-r": restart audit from scratch + */ +int restart; + +/** + * Handle to access the exchange's database. + */ +struct TALER_EXCHANGEDB_Plugin *edb; + +/** + * Which currency are we doing the audit for? + */ +char *currency; + +/** + * How many fractional digits does the currency use? + */ +struct TALER_Amount currency_round_unit; + +/** + * Our configuration. + */ +const struct GNUNET_CONFIGURATION_Handle *cfg; + +/** + * Our session with the #edb. + */ +struct TALER_EXCHANGEDB_Session *esession; + +/** + * Handle to access the auditor's database. + */ +struct TALER_AUDITORDB_Plugin *adb; + +/** + * Our session with the #adb. + */ +struct TALER_AUDITORDB_Session *asession; + +/** + * Master public key of the exchange to audit. + */ +struct TALER_MasterPublicKeyP master_pub; + +/** + * At what time did the auditor process start? + */ +struct GNUNET_TIME_Absolute start_time; + +/** + * Results about denominations, cached per-transaction, maps denomination pub hashes + * to `struct TALER_DenominationKeyValidityPS`. + */ +static struct GNUNET_CONTAINER_MultiHashMap *denominations; + + +/** + * Convert absolute time to human-readable JSON string. + * + * @param at time to convert + * @return human-readable string representing the time + */ +json_t * +json_from_time_abs_nbo (struct GNUNET_TIME_AbsoluteNBO at) +{ + return json_string + (GNUNET_STRINGS_absolute_time_to_string + (GNUNET_TIME_absolute_ntoh (at))); +} + + +/** + * Convert absolute time to human-readable JSON string. + * + * @param at time to convert + * @return human-readable string representing the time + */ +json_t * +json_from_time_abs (struct GNUNET_TIME_Absolute at) +{ + return json_string + (GNUNET_STRINGS_absolute_time_to_string (at)); +} + + +/** + * Add @a object to the report @a array. Fail hard if this fails. + * + * @param array report array to append @a object to + * @param object object to append, should be check that it is not NULL + */ +void +report (json_t *array, + json_t *object) +{ + GNUNET_assert (NULL != object); + GNUNET_assert (0 == + json_array_append_new (array, + object)); +} + + +/** + * Function called with the results of select_denomination_info() + * + * @param cls closure, NULL + * @param issue issuing information with value, fees and other info about the denomination. + * @return #GNUNET_OK (to continue) + */ +static int +add_denomination (void *cls, + const struct TALER_DenominationKeyValidityPS *issue) +{ + struct TALER_DenominationKeyValidityPS *i; + + (void) cls; + if (NULL != + GNUNET_CONTAINER_multihashmap_get (denominations, + &issue->denom_hash)) + return GNUNET_OK; /* value already known */ + { + struct TALER_Amount value; + + TALER_amount_ntoh (&value, + &issue->value); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Tracking denomination `%s' (%s)\n", + GNUNET_h2s (&issue->denom_hash), + TALER_amount2s (&value)); + TALER_amount_ntoh (&value, + &issue->fee_withdraw); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Withdraw fee is %s\n", + TALER_amount2s (&value)); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Start time is %s\n", + GNUNET_STRINGS_absolute_time_to_string + (GNUNET_TIME_absolute_ntoh (issue->start))); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Expire deposit time is %s\n", + GNUNET_STRINGS_absolute_time_to_string + (GNUNET_TIME_absolute_ntoh (issue->expire_deposit))); + } + i = GNUNET_new (struct TALER_DenominationKeyValidityPS); + *i = *issue; + GNUNET_assert (GNUNET_OK == + GNUNET_CONTAINER_multihashmap_put (denominations, + &issue->denom_hash, + i, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); + return GNUNET_OK; +} + + +/** + * Obtain information about a @a denom_pub. + * + * @param dh hash of the denomination public key to look up + * @param[out] issue set to detailed information about @a denom_pub, NULL if not found, must + * NOT be freed by caller + * @return transaction status code + */ +enum GNUNET_DB_QueryStatus +get_denomination_info_by_hash (const struct GNUNET_HashCode *dh, + const struct + TALER_DenominationKeyValidityPS **issue) +{ + const struct TALER_DenominationKeyValidityPS *i; + enum GNUNET_DB_QueryStatus qs; + + if (NULL == denominations) + { + denominations = GNUNET_CONTAINER_multihashmap_create (256, + GNUNET_NO); + qs = adb->select_denomination_info (adb->cls, + asession, + &master_pub, + &add_denomination, + NULL); + if (0 > qs) + { + *issue = NULL; + return qs; + } + } + i = GNUNET_CONTAINER_multihashmap_get (denominations, + dh); + if (NULL != i) + { + /* cache hit */ + *issue = i; + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; + } + /* maybe database changed since we last iterated, give it one more shot */ + qs = adb->select_denomination_info (adb->cls, + asession, + &master_pub, + &add_denomination, + NULL); + if (qs <= 0) + { + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Denomination %s not found\n", + TALER_B2S (dh)); + return qs; + } + i = GNUNET_CONTAINER_multihashmap_get (denominations, + dh); + if (NULL != i) + { + /* cache hit */ + *issue = i; + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; + } + /* We found more keys, but not the denomination we are looking for :-( */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Denomination %s not found\n", + TALER_B2S (dh)); + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; +} + + +/** + * Obtain information about a @a denom_pub. + * + * @param denom_pub key to look up + * @param[out] issue set to detailed information about @a denom_pub, NULL if not found, must + * NOT be freed by caller + * @param[out] dh set to the hash of @a denom_pub, may be NULL + * @return transaction status code + */ +enum GNUNET_DB_QueryStatus +get_denomination_info (const struct TALER_DenominationPublicKey *denom_pub, + const struct + TALER_DenominationKeyValidityPS **issue, + struct GNUNET_HashCode *dh) +{ + struct GNUNET_HashCode hc; + + if (NULL == dh) + dh = &hc; + GNUNET_CRYPTO_rsa_public_key_hash (denom_pub->rsa_public_key, + dh); + return get_denomination_info_by_hash (dh, + issue); +} + + +/** + * Perform the given @a analysis within a transaction scope. + * Commit on success. + * + * @param analysis analysis to run + * @param analysis_cls closure for @a analysis + * @return #GNUNET_OK if @a analysis succeessfully committed, + * #GNUNET_NO if we had an error on commit (retry may help) + * #GNUNET_SYSERR on hard errors + */ +int +transact (Analysis analysis, + void *analysis_cls) +{ + int ret; + enum GNUNET_DB_QueryStatus qs; + + ret = adb->start (adb->cls, + asession); + if (GNUNET_OK != ret) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + edb->preflight (edb->cls, + esession); + ret = edb->start (edb->cls, + esession, + "auditor"); + if (GNUNET_OK != ret) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + qs = analysis (analysis_cls); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + { + qs = edb->commit (edb->cls, + esession); + if (0 > qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Exchange DB commit failed, rolling back transaction\n"); + adb->rollback (adb->cls, + asession); + } + else + { + qs = adb->commit (adb->cls, + asession); + if (0 > qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Auditor DB commit failed!\n"); + } + } + } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Processing failed (or no changes), rolling back transaction\n"); + adb->rollback (adb->cls, + asession); + edb->rollback (edb->cls, + esession); + } + switch (qs) + { + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + return GNUNET_OK; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return GNUNET_OK; + case GNUNET_DB_STATUS_SOFT_ERROR: + return GNUNET_NO; + case GNUNET_DB_STATUS_HARD_ERROR: + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Initialize DB sessions and run the analysis. + * + * @param ana analysis to run + * @param ana_cls closure for @ana + * @return #GNUNET_OK on success + */ +int +setup_sessions_and_run (Analysis ana, + void *ana_cls) +{ + esession = edb->get_session (edb->cls); + if (NULL == esession) + { + fprintf (stderr, + "Failed to initialize exchange session.\n"); + return GNUNET_SYSERR; + } + asession = adb->get_session (adb->cls); + if (NULL == asession) + { + fprintf (stderr, + "Failed to initialize auditor session.\n"); + return GNUNET_SYSERR; + } + + GNUNET_break (GNUNET_SYSERR != + transact (ana, + ana_cls)); + return GNUNET_OK; +} + + +/** + * Test if the given @a mpub matches the #master_pub. + * If so, set "found" to GNUNET_YES. + * + * @param cls a `int *` pointing to "found" + * @param mpub exchange master public key to compare + * @param exchange_url URL of the exchange (ignored) + */ +static void +test_master_present (void *cls, + const struct TALER_MasterPublicKeyP *mpub, + const char *exchange_url) +{ + int *found = cls; + + (void) exchange_url; + if (0 == GNUNET_memcmp (mpub, + &master_pub)) + *found = GNUNET_YES; +} + + +int +setup_globals (const struct GNUNET_CONFIGURATION_Handle *c) +{ + int found; + struct TALER_AUDITORDB_Session *as; + + cfg = c; + start_time = GNUNET_TIME_absolute_get (); + if (0 == GNUNET_is_zero (&master_pub)) + { + /* -m option not given, try configuration */ + char *master_public_key_str; + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + "exchange", + "MASTER_PUBLIC_KEY", + &master_public_key_str)) + { + fprintf (stderr, + "Pass option -m or set it in the configuration!\n"); + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "exchange", + "MASTER_PUBLIC_KEY"); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + GNUNET_CRYPTO_eddsa_public_key_from_string (master_public_key_str, + strlen ( + master_public_key_str), + &master_pub.eddsa_pub)) + { + fprintf (stderr, + "Invalid master public key given in configuration file."); + GNUNET_free (master_public_key_str); + return GNUNET_SYSERR; + } + GNUNET_free (master_public_key_str); + } /* end of -m not given */ + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Taler auditor running for exchange master public key %s\n", + TALER_B2S (&master_pub)); + + if (GNUNET_OK != + TALER_config_get_currency (cfg, + ¤cy)) + { + return GNUNET_SYSERR; + } + { + if (GNUNET_OK != + TALER_config_get_amount (cfg, + "taler", + "CURRENCY_ROUND_UNIT", + ¤cy_round_unit)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Invalid or missing amount in `TALER' under `CURRENCY_ROUND_UNIT'\n"); + return GNUNET_SYSERR; + } + } + if (NULL == + (edb = TALER_EXCHANGEDB_plugin_load (cfg))) + { + fprintf (stderr, + "Failed to initialize exchange database plugin.\n"); + return GNUNET_SYSERR; + } + if (NULL == + (adb = TALER_AUDITORDB_plugin_load (cfg))) + { + fprintf (stderr, + "Failed to initialize auditor database plugin.\n"); + TALER_EXCHANGEDB_plugin_unload (edb); + return GNUNET_SYSERR; + } + found = GNUNET_NO; + as = adb->get_session (adb->cls); + if (NULL == as) + { + fprintf (stderr, + "Failed to start session with auditor database.\n"); + TALER_AUDITORDB_plugin_unload (adb); + TALER_EXCHANGEDB_plugin_unload (edb); + return GNUNET_SYSERR; + } + (void) adb->list_exchanges (adb->cls, + as, + &test_master_present, + &found); + if (GNUNET_NO == found) + { + fprintf (stderr, + "Exchange's master public key `%s' not known to auditor DB. Did you forget to run `taler-auditor-exchange`?\n", + GNUNET_p2s (&master_pub.eddsa_pub)); + TALER_AUDITORDB_plugin_unload (adb); + TALER_EXCHANGEDB_plugin_unload (edb); + return GNUNET_SYSERR; + } + if (restart) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Full audit restart requested, dropping old audit data.\n"); + GNUNET_break (GNUNET_OK == + adb->drop_tables (adb->cls, + GNUNET_NO)); + TALER_AUDITORDB_plugin_unload (adb); + if (NULL == + (adb = TALER_AUDITORDB_plugin_load (cfg))) + { + fprintf (stderr, + "Failed to initialize auditor database plugin after drop.\n"); + TALER_EXCHANGEDB_plugin_unload (edb); + return GNUNET_SYSERR; + } + GNUNET_break (GNUNET_OK == + adb->create_tables (adb->cls)); + } + + return GNUNET_OK; +} + + +void +finish_report (json_t *report) +{ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Audit complete\n"); + TALER_AUDITORDB_plugin_unload (adb); + adb = NULL; + TALER_EXCHANGEDB_plugin_unload (edb); + edb = NULL; + json_dumpf (report, + stdout, + JSON_INDENT (2)); + json_decref (report); +} diff --git a/src/auditor/report-lib.h b/src/auditor/report-lib.h new file mode 100644 index 000000000..334ac198e --- /dev/null +++ b/src/auditor/report-lib.h @@ -0,0 +1,188 @@ +/* + This file is part of TALER + Copyright (C) 2016-2020 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero 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 Public License for more details. + + You should have received a copy of the GNU Affero Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file auditor/report-lib.h + * @brief helper library to facilitate generation of audit reports + * @author Christian Grothoff + */ +#ifndef REPORT_LIB_H +#define REPORT_LIB_H + +#include +#include "taler_auditordb_plugin.h" +#include "taler_exchangedb_lib.h" +#include "taler_json_lib.h" +#include "taler_bank_service.h" +#include "taler_signatures.h" + + +/** + * Command-line option "-r": restart audit from scratch + */ +extern int restart; + +/** + * Handle to access the exchange's database. + */ +extern struct TALER_EXCHANGEDB_Plugin *edb; + +/** + * Which currency are we doing the audit for? + */ +extern char *currency; + +/** + * How many fractional digits does the currency use? + */ +extern struct TALER_Amount currency_round_unit; + +/** + * Our configuration. + */ +extern const struct GNUNET_CONFIGURATION_Handle *cfg; + +/** + * Our session with the #edb. + */ +extern struct TALER_EXCHANGEDB_Session *esession; + +/** + * Handle to access the auditor's database. + */ +extern struct TALER_AUDITORDB_Plugin *adb; + +/** + * Our session with the #adb. + */ +extern struct TALER_AUDITORDB_Session *asession; + +/** + * Master public key of the exchange to audit. + */ +extern struct TALER_MasterPublicKeyP master_pub; + +/** + * At what time did the auditor process start? + */ +extern struct GNUNET_TIME_Absolute start_time; + + +/** + * Convert absolute time to human-readable JSON string. + * + * @param at time to convert + * @return human-readable string representing the time + */ +json_t * +json_from_time_abs_nbo (struct GNUNET_TIME_AbsoluteNBO at); + + +/** + * Convert absolute time to human-readable JSON string. + * + * @param at time to convert + * @return human-readable string representing the time + */ +json_t * +json_from_time_abs (struct GNUNET_TIME_Absolute at); + + +/** + * Add @a object to the report @a array. Fail hard if this fails. + * + * @param array report array to append @a object to + * @param object object to append, should be check that it is not NULL + */ +void +report (json_t *array, + json_t *object); + + +/** + * Obtain information about a @a denom_pub. + * + * @param dh hash of the denomination public key to look up + * @param[out] issue set to detailed information about @a denom_pub, NULL if not found, must + * NOT be freed by caller + * @return transaction status code + */ +enum GNUNET_DB_QueryStatus +get_denomination_info_by_hash ( + const struct GNUNET_HashCode *dh, + const struct TALER_DenominationKeyValidityPS **issue); + + +/** + * Obtain information about a @a denom_pub. + * + * @param denom_pub key to look up + * @param[out] issue set to detailed information about @a denom_pub, NULL if not found, must + * NOT be freed by caller + * @param[out] dh set to the hash of @a denom_pub, may be NULL + * @return transaction status code + */ +enum GNUNET_DB_QueryStatus +get_denomination_info ( + const struct TALER_DenominationPublicKey *denom_pub, + const struct TALER_DenominationKeyValidityPS **issue, + struct GNUNET_HashCode *dh); + +/** + * Type of an analysis function. Each analysis function runs in + * its own transaction scope and must thus be internally consistent. + * + * @param cls closure + * @return transaction status code + */ +typedef enum GNUNET_DB_QueryStatus +(*Analysis)(void *cls); + + +/** + * Perform the given @a analysis within a transaction scope. + * Commit on success. + * + * @param analysis analysis to run + * @param analysis_cls closure for @a analysis + * @return #GNUNET_OK if @a analysis succeessfully committed, + * #GNUNET_NO if we had an error on commit (retry may help) + * #GNUNET_SYSERR on hard errors + */ +int +transact (Analysis analysis, + void *analysis_cls); + + +/** + * Initialize DB sessions and run the analysis. + * + * @param ana analysis to run + * @param ana_cls closure for @ana + * @return #GNUNET_OK on success + */ +int +setup_sessions_and_run (Analysis ana, + void *ana_cls); + + +int +setup_globals (const struct GNUNET_CONFIGURATION_Handle *c); + + +void +finish_report (json_t *report); + +#endif diff --git a/src/auditor/taler-auditor-aggregation.c b/src/auditor/taler-auditor-aggregation.c new file mode 100644 index 000000000..de249ed52 --- /dev/null +++ b/src/auditor/taler-auditor-aggregation.c @@ -0,0 +1,1511 @@ +/* + This file is part of TALER + Copyright (C) 2016-2020 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero 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 Public License for more details. + + You should have received a copy of the GNU Affero Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file auditor/taler-auditor-aggregation.c + * @brief audits an exchange's aggregations. + * @author Christian Grothoff + */ +#include "platform.h" +#include +#include "taler_auditordb_plugin.h" +#include "taler_exchangedb_lib.h" +#include "taler_json_lib.h" +#include "taler_bank_service.h" +#include "taler_signatures.h" +#include "report-lib.h" + + +/** + * Return value from main(). + */ +static int global_ret; + +/** + * Checkpointing our progress for aggregations. + */ +static struct TALER_AUDITORDB_ProgressPointAggregation ppa; + +/** + * Checkpointing our progress for aggregations. + */ +static struct TALER_AUDITORDB_ProgressPointAggregation ppa_start; + +/** + * Array of reports about row inconsitencies. + */ +static json_t *report_row_inconsistencies; + +/** + * Array of reports about irregular wire out entries. + */ +static json_t *report_wire_out_inconsistencies; + +/** + * Total delta between calculated and stored wire out transfers, + * for positive deltas. + */ +static struct TALER_Amount total_wire_out_delta_plus; + +/** + * Total delta between calculated and stored wire out transfers + * for negative deltas. + */ +static struct TALER_Amount total_wire_out_delta_minus; + +/** + * Array of reports about inconsistencies about coins. + */ +static json_t *report_coin_inconsistencies; + +/** + * Profits the exchange made by bad amount calculations on coins. + */ +static struct TALER_Amount total_coin_delta_plus; + +/** + * Losses the exchange made by bad amount calculations on coins. + */ +static struct TALER_Amount total_coin_delta_minus; + +/** + * Report about amount calculation differences (causing profit + * or loss at the exchange). + */ +static json_t *report_amount_arithmetic_inconsistencies; + +/** + * Array of reports about wire fees being ambiguous in terms of validity periods. + */ +static json_t *report_fee_time_inconsistencies; + +/** + * Profits the exchange made by bad amount calculations. + */ +static struct TALER_Amount total_arithmetic_delta_plus; + +/** + * Losses the exchange made by bad amount calculations. + */ +static struct TALER_Amount total_arithmetic_delta_minus; + +/** + * Total aggregation fees earned. + */ +static struct TALER_Amount total_aggregation_fee_income; + +/** + * Array of reports about coin operations with bad signatures. + */ +static json_t *report_bad_sig_losses; + +/** + * Total amount lost by operations for which signatures were invalid. + */ +static struct TALER_Amount total_bad_sig_loss; + + +/** + * Report a (serious) inconsistency in the exchange's database with + * respect to calculations involving amounts. + * + * @param operation what operation had the inconsistency + * @param rowid affected row, UINT64_MAX if row is missing + * @param exchange amount calculated by exchange + * @param auditor amount calculated by auditor + * @param profitable 1 if @a exchange being larger than @a auditor is + * profitable for the exchange for this operation, + * -1 if @a exchange being smaller than @a auditor is + * profitable for the exchange, and 0 if it is unclear + */ +static void +report_amount_arithmetic_inconsistency (const char *operation, + uint64_t rowid, + const struct TALER_Amount *exchange, + const struct TALER_Amount *auditor, + int profitable) +{ + struct TALER_Amount delta; + struct TALER_Amount *target; + + if (0 < TALER_amount_cmp (exchange, + auditor)) + { + /* exchange > auditor */ + GNUNET_break (GNUNET_OK == + TALER_amount_subtract (&delta, + exchange, + auditor)); + } + else + { + /* auditor < exchange */ + profitable = -profitable; + GNUNET_break (GNUNET_OK == + TALER_amount_subtract (&delta, + auditor, + exchange)); + } + report (report_amount_arithmetic_inconsistencies, + json_pack ("{s:s, s:I, s:o, s:o, s:I}", + "operation", operation, + "rowid", (json_int_t) rowid, + "exchange", TALER_JSON_from_amount (exchange), + "auditor", TALER_JSON_from_amount (auditor), + "profitable", (json_int_t) profitable)); + if (0 != profitable) + { + target = (1 == profitable) + ? &total_arithmetic_delta_plus + : &total_arithmetic_delta_minus; + GNUNET_break (GNUNET_OK == + TALER_amount_add (target, + target, + &delta)); + } +} + + +/** + * Report a (serious) inconsistency in the exchange's database with + * respect to calculations involving amounts of a coin. + * + * @param operation what operation had the inconsistency + * @param coin_pub affected coin + * @param exchange amount calculated by exchange + * @param auditor amount calculated by auditor + * @param profitable 1 if @a exchange being larger than @a auditor is + * profitable for the exchange for this operation, + * -1 if @a exchange being smaller than @a auditor is + * profitable for the exchange, and 0 if it is unclear + */ +static void +report_coin_arithmetic_inconsistency (const char *operation, + const struct + TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_Amount *exchange, + const struct TALER_Amount *auditor, + int profitable) +{ + struct TALER_Amount delta; + struct TALER_Amount *target; + + if (0 < TALER_amount_cmp (exchange, + auditor)) + { + /* exchange > auditor */ + GNUNET_break (GNUNET_OK == + TALER_amount_subtract (&delta, + exchange, + auditor)); + } + else + { + /* auditor < exchange */ + profitable = -profitable; + GNUNET_break (GNUNET_OK == + TALER_amount_subtract (&delta, + auditor, + exchange)); + } + report (report_coin_inconsistencies, + json_pack ("{s:s, s:o, s:o, s:o, s:I}", + "operation", operation, + "coin_pub", GNUNET_JSON_from_data_auto (coin_pub), + "exchange", TALER_JSON_from_amount (exchange), + "auditor", TALER_JSON_from_amount (auditor), + "profitable", (json_int_t) profitable)); + if (0 != profitable) + { + target = (1 == profitable) + ? &total_coin_delta_plus + : &total_coin_delta_minus; + GNUNET_break (GNUNET_OK == + TALER_amount_add (target, + target, + &delta)); + } +} + + +/** + * Report a (serious) inconsistency in the exchange's database. + * + * @param table affected table + * @param rowid affected row, UINT64_MAX if row is missing + * @param diagnostic message explaining the problem + */ +static void +report_row_inconsistency (const char *table, + uint64_t rowid, + const char *diagnostic) +{ + report (report_row_inconsistencies, + json_pack ("{s:s, s:I, s:s}", + "table", table, + "row", (json_int_t) rowid, + "diagnostic", diagnostic)); +} + + +/* *********************** Analyze aggregations ******************** */ +/* This logic checks that the aggregator did the right thing + paying each merchant what they were due (and on time). */ + + +/** + * Information about wire fees charged by the exchange. + */ +struct WireFeeInfo +{ + + /** + * Kept in a DLL. + */ + struct WireFeeInfo *next; + + /** + * Kept in a DLL. + */ + struct WireFeeInfo *prev; + + /** + * When does the fee go into effect (inclusive). + */ + struct GNUNET_TIME_Absolute start_date; + + /** + * When does the fee stop being in effect (exclusive). + */ + struct GNUNET_TIME_Absolute end_date; + + /** + * How high is the wire fee. + */ + struct TALER_Amount wire_fee; + + /** + * How high is the closing fee. + */ + struct TALER_Amount closing_fee; + +}; + + +/** + * Closure for callbacks during #analyze_merchants(). + */ +struct AggregationContext +{ + + /** + * DLL of wire fees charged by the exchange. + */ + struct WireFeeInfo *fee_head; + + /** + * DLL of wire fees charged by the exchange. + */ + struct WireFeeInfo *fee_tail; + + /** + * Final result status. + */ + enum GNUNET_DB_QueryStatus qs; +}; + + +/** + * Closure for #wire_transfer_information_cb. + */ +struct WireCheckContext +{ + + /** + * Corresponding merchant context. + */ + struct AggregationContext *ac; + + /** + * Total deposits claimed by all transactions that were aggregated + * under the given @e wtid. + */ + struct TALER_Amount total_deposits; + + /** + * Hash of the wire transfer details of the receiver. + */ + struct GNUNET_HashCode h_wire; + + /** + * Execution time of the wire transfer. + */ + struct GNUNET_TIME_Absolute date; + + /** + * Database transaction status. + */ + enum GNUNET_DB_QueryStatus qs; + +}; + + +/** + * Check coin's transaction history for plausibility. Does NOT check + * the signatures (those are checked independently), but does calculate + * the amounts for the aggregation table and checks that the total + * claimed coin value is within the value of the coin's denomination. + * + * @param coin_pub public key of the coin (for reporting) + * @param h_contract_terms hash of the proposal for which we calculate the amount + * @param merchant_pub public key of the merchant (who is allowed to issue refunds) + * @param issue denomination information about the coin + * @param tl_head head of transaction history to verify + * @param[out] merchant_gain amount the coin contributes to the wire transfer to the merchant + * @param[out] deposit_gain amount the coin contributes excluding refunds + * @return #GNUNET_OK on success, #GNUNET_SYSERR on error + */ +static int +check_transaction_history_for_deposit (const struct + TALER_CoinSpendPublicKeyP *coin_pub, + const struct + GNUNET_HashCode *h_contract_terms, + const struct + TALER_MerchantPublicKeyP *merchant_pub, + const struct + TALER_DenominationKeyValidityPS *issue, + const struct + TALER_EXCHANGEDB_TransactionList *tl_head, + struct TALER_Amount *merchant_gain, + struct TALER_Amount *deposit_gain) +{ + struct TALER_Amount expenditures; + struct TALER_Amount refunds; + struct TALER_Amount spent; + struct TALER_Amount value; + struct TALER_Amount merchant_loss; + struct TALER_Amount final_gain; + const struct TALER_Amount *deposit_fee; + int refund_deposit_fee; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Checking transaction history of coin %s\n", + TALER_B2S (coin_pub)); + + GNUNET_assert (NULL != tl_head); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &expenditures)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &refunds)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + merchant_gain)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &merchant_loss)); + /* Go over transaction history to compute totals; note that we do not + know the order, so instead of subtracting we compute positive + (deposit, melt) and negative (refund) values separately here, + and then subtract the negative from the positive after the loop. */ + refund_deposit_fee = GNUNET_NO; + deposit_fee = NULL; + for (const struct TALER_EXCHANGEDB_TransactionList *tl = tl_head; + NULL != tl; + tl = tl->next) + { + const struct TALER_Amount *amount_with_fee; + const struct TALER_Amount *fee; + const struct TALER_AmountNBO *fee_dki; + struct TALER_Amount tmp; + + switch (tl->type) + { + case TALER_EXCHANGEDB_TT_DEPOSIT: + /* check wire and h_wire are consistent */ + { + struct GNUNET_HashCode hw; + + if (GNUNET_OK != + TALER_JSON_merchant_wire_signature_hash ( + tl->details.deposit->receiver_wire_account, + &hw)) + { + report_row_inconsistency ("deposits", + tl->serial_id, + "wire value malformed"); + } + else if (0 != + GNUNET_memcmp (&hw, + &tl->details.deposit->h_wire)) + { + report_row_inconsistency ("deposits", + tl->serial_id, + "h(wire) does not match wire"); + } + } + amount_with_fee = &tl->details.deposit->amount_with_fee; + fee = &tl->details.deposit->deposit_fee; + fee_dki = &issue->fee_deposit; + if (GNUNET_OK != + TALER_amount_add (&expenditures, + &expenditures, + amount_with_fee)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + /* Check if this deposit is within the remit of the aggregation + we are investigating, if so, include it in the totals. */ + if ( (0 == GNUNET_memcmp (merchant_pub, + &tl->details.deposit->merchant_pub)) && + (0 == GNUNET_memcmp (h_contract_terms, + &tl->details.deposit->h_contract_terms)) ) + { + struct TALER_Amount amount_without_fee; + + if (GNUNET_OK != + TALER_amount_subtract (&amount_without_fee, + amount_with_fee, + fee)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_amount_add (merchant_gain, + merchant_gain, + &amount_without_fee)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Detected applicable deposit of %s\n", + TALER_amount2s (&amount_without_fee)); + deposit_fee = fee; + } + /* Check that the fees given in the transaction list and in dki match */ + TALER_amount_ntoh (&tmp, + fee_dki); + if (0 != + TALER_amount_cmp (&tmp, + fee)) + { + /* Disagreement in fee structure within DB, should be impossible! */ + GNUNET_break (0); + return GNUNET_SYSERR; + } + break; + case TALER_EXCHANGEDB_TT_MELT: + amount_with_fee = &tl->details.melt->amount_with_fee; + fee = &tl->details.melt->melt_fee; + fee_dki = &issue->fee_refresh; + if (GNUNET_OK != + TALER_amount_add (&expenditures, + &expenditures, + amount_with_fee)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + /* Check that the fees given in the transaction list and in dki match */ + TALER_amount_ntoh (&tmp, + fee_dki); + if (0 != + TALER_amount_cmp (&tmp, + fee)) + { + /* Disagreement in fee structure within DB, should be impossible! */ + GNUNET_break (0); + return GNUNET_SYSERR; + } + break; + case TALER_EXCHANGEDB_TT_REFUND: + amount_with_fee = &tl->details.refund->refund_amount; + fee = &tl->details.refund->refund_fee; + fee_dki = &issue->fee_refund; + if (GNUNET_OK != + TALER_amount_add (&refunds, + &refunds, + amount_with_fee)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_amount_add (&expenditures, + &expenditures, + fee)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + /* Check if this refund is within the remit of the aggregation + we are investigating, if so, include it in the totals. */ + if ( (0 == GNUNET_memcmp (merchant_pub, + &tl->details.refund->merchant_pub)) && + (0 == GNUNET_memcmp (h_contract_terms, + &tl->details.refund->h_contract_terms)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Detected applicable refund of %s\n", + TALER_amount2s (amount_with_fee)); + if (GNUNET_OK != + TALER_amount_add (&merchant_loss, + &merchant_loss, + amount_with_fee)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + refund_deposit_fee = GNUNET_YES; + } + /* Check that the fees given in the transaction list and in dki match */ + TALER_amount_ntoh (&tmp, + fee_dki); + if (0 != + TALER_amount_cmp (&tmp, + fee)) + { + /* Disagreement in fee structure within DB, should be impossible! */ + GNUNET_break (0); + return GNUNET_SYSERR; + } + break; + case TALER_EXCHANGEDB_TT_OLD_COIN_RECOUP: + amount_with_fee = &tl->details.old_coin_recoup->value; + if (GNUNET_OK != + TALER_amount_add (&refunds, + &refunds, + amount_with_fee)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + break; + case TALER_EXCHANGEDB_TT_RECOUP: + amount_with_fee = &tl->details.recoup->value; + if (GNUNET_OK != + TALER_amount_add (&expenditures, + &expenditures, + amount_with_fee)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + break; + case TALER_EXCHANGEDB_TT_RECOUP_REFRESH: + amount_with_fee = &tl->details.recoup_refresh->value; + if (GNUNET_OK != + TALER_amount_add (&expenditures, + &expenditures, + amount_with_fee)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + break; + } + } /* for 'tl' */ + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Deposits without fees are %s\n", + TALER_amount2s (merchant_gain)); + + /* Calculate total balance change, i.e. expenditures (recoup, deposit, refresh) + minus refunds (refunds, recoup-to-old) */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Subtracting refunds of %s from coin value loss\n", + TALER_amount2s (&refunds)); + if (GNUNET_SYSERR == + TALER_amount_subtract (&spent, + &expenditures, + &refunds)) + { + /* refunds above expenditures? Bad! */ + report_coin_arithmetic_inconsistency ("refund (balance)", + coin_pub, + &expenditures, + &refunds, + 1); + return GNUNET_SYSERR; + } + + /* Now check that 'spent' is less or equal than the total coin value */ + TALER_amount_ntoh (&value, + &issue->value); + if (1 == TALER_amount_cmp (&spent, + &value)) + { + /* spent > value */ + report_coin_arithmetic_inconsistency ("spend", + coin_pub, + &spent, + &value, + -1); + return GNUNET_SYSERR; + } + + /* Finally, update @a merchant_gain by subtracting what he "lost" + from refunds */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Merchant 'loss' due to refunds is %s\n", + TALER_amount2s (&merchant_loss)); + *deposit_gain = *merchant_gain; + if ( (GNUNET_YES == refund_deposit_fee) && + (NULL != deposit_fee) ) + { + /* We had a /deposit operation AND a /refund operation, + and should thus not charge the merchant the /deposit fee */ + GNUNET_assert (GNUNET_OK == + TALER_amount_add (merchant_gain, + merchant_gain, + deposit_fee)); + } + if (GNUNET_SYSERR == + TALER_amount_subtract (&final_gain, + merchant_gain, + &merchant_loss)) + { + /* refunds above deposits? Bad! */ + report_coin_arithmetic_inconsistency ("refund (merchant)", + coin_pub, + merchant_gain, + &merchant_loss, + 1); + return GNUNET_SYSERR; + } + *merchant_gain = final_gain; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Final merchant gain after refunds is %s\n", + TALER_amount2s (deposit_gain)); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Coin %s contributes %s to contract %s\n", + TALER_B2S (coin_pub), + TALER_amount2s (merchant_gain), + GNUNET_h2s (h_contract_terms)); + return GNUNET_OK; +} + + +/** + * Function called with the results of the lookup of the + * transaction data associated with a wire transfer identifier. + * + * @param cls a `struct WireCheckContext` + * @param rowid which row in the table is the information from (for diagnostics) + * @param merchant_pub public key of the merchant (should be same for all callbacks with the same @e cls) + * @param h_wire hash of wire transfer details of the merchant (should be same for all callbacks with the same @e cls) + * @param account_details where did we transfer the funds? + * @param exec_time execution time of the wire transfer (should be same for all callbacks with the same @e cls) + * @param h_contract_terms which proposal was this payment about + * @param denom_pub denomination of @a coin_pub + * @param coin_pub which public key was this payment about + * @param coin_value amount contributed by this coin in total (with fee), + * but excluding refunds by this coin + * @param deposit_fee applicable deposit fee for this coin, actual + * fees charged may differ if coin was refunded + */ +static void +wire_transfer_information_cb ( + void *cls, + uint64_t rowid, + const struct TALER_MerchantPublicKeyP *merchant_pub, + const struct GNUNET_HashCode *h_wire, + const json_t *account_details, + struct GNUNET_TIME_Absolute exec_time, + const struct GNUNET_HashCode *h_contract_terms, + const struct TALER_DenominationPublicKey *denom_pub, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_Amount *coin_value, + const struct TALER_Amount *deposit_fee) +{ + struct WireCheckContext *wcc = cls; + const struct TALER_DenominationKeyValidityPS *issue; + struct TALER_Amount computed_value; + struct TALER_Amount coin_value_without_fee; + struct TALER_Amount total_deposit_without_refunds; + struct TALER_EXCHANGEDB_TransactionList *tl; + struct TALER_CoinPublicInfo coin; + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_HashCode hw; + + if (GNUNET_OK != + TALER_JSON_merchant_wire_signature_hash (account_details, + &hw)) + { + report_row_inconsistency ("aggregation", + rowid, + "failed to compute hash of given wire data"); + } + else if (0 != + GNUNET_memcmp (&hw, + h_wire)) + { + report_row_inconsistency ("aggregation", + rowid, + "database contains wrong hash code for wire details"); + } + + /* Obtain coin's transaction history */ + qs = edb->get_coin_transactions (edb->cls, + esession, + coin_pub, + GNUNET_YES, + &tl); + if ( (qs < 0) || + (NULL == tl) ) + { + wcc->qs = qs; + report_row_inconsistency ("aggregation", + rowid, + "no transaction history for coin claimed in aggregation"); + return; + } + qs = edb->get_known_coin (edb->cls, + esession, + coin_pub, + &coin); + if (qs < 0) + { + GNUNET_break (0); /* this should be a foreign key violation at this point! */ + wcc->qs = qs; + report_row_inconsistency ("aggregation", + rowid, + "could not get coin details for coin claimed in aggregation"); + return; + } + + qs = get_denomination_info_by_hash (&coin.denom_pub_hash, + &issue); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + { + GNUNET_CRYPTO_rsa_signature_free (coin.denom_sig.rsa_signature); + edb->free_coin_transaction_list (edb->cls, + tl); + if (0 == qs) + report_row_inconsistency ("aggregation", + rowid, + "could not find denomination key for coin claimed in aggregation"); + else + wcc->qs = qs; + return; + } + if (GNUNET_OK != + TALER_test_coin_valid (&coin, + denom_pub)) + { + report (report_bad_sig_losses, + json_pack ("{s:s, s:I, s:o, s:o}", + "operation", "wire", + "row", (json_int_t) rowid, + "loss", TALER_JSON_from_amount (coin_value), + "key_pub", GNUNET_JSON_from_data_auto ( + &issue->denom_hash))); + GNUNET_break (GNUNET_OK == + TALER_amount_add (&total_bad_sig_loss, + &total_bad_sig_loss, + coin_value)); + GNUNET_CRYPTO_rsa_signature_free (coin.denom_sig.rsa_signature); + edb->free_coin_transaction_list (edb->cls, + tl); + wcc->qs = GNUNET_DB_STATUS_HARD_ERROR; + report_row_inconsistency ("deposit", + rowid, + "coin denomination signature invalid"); + return; + } + GNUNET_CRYPTO_rsa_signature_free (coin.denom_sig.rsa_signature); + coin.denom_sig.rsa_signature = NULL; /* just to be sure */ + GNUNET_assert (NULL != issue); /* mostly to help static analysis */ + /* Check transaction history to see if it supports aggregate + valuation */ + if (GNUNET_OK != + check_transaction_history_for_deposit (coin_pub, + h_contract_terms, + merchant_pub, + issue, + tl, + &computed_value, + &total_deposit_without_refunds)) + { + wcc->qs = GNUNET_DB_STATUS_HARD_ERROR; + report_row_inconsistency ("coin history", + rowid, + "failed to verify coin history (for deposit)"); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Coin contributes %s to aggregate (deposits after fees and refunds)\n", + TALER_amount2s (&computed_value)); + if (GNUNET_SYSERR == + TALER_amount_subtract (&coin_value_without_fee, + coin_value, + deposit_fee)) + { + wcc->qs = GNUNET_DB_STATUS_HARD_ERROR; + report_amount_arithmetic_inconsistency ("aggregation (fee structure)", + rowid, + coin_value, + deposit_fee, + -1); + return; + } + if (0 != + TALER_amount_cmp (&total_deposit_without_refunds, + &coin_value_without_fee)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Expected coin contribution of %s to aggregate\n", + TALER_amount2s (&coin_value_without_fee)); + wcc->qs = GNUNET_DB_STATUS_HARD_ERROR; + report_amount_arithmetic_inconsistency ("aggregation (contribution)", + rowid, + &coin_value_without_fee, + &total_deposit_without_refunds, + -1); + } + edb->free_coin_transaction_list (edb->cls, + tl); + + /* Check other details of wire transfer match */ + if (0 != GNUNET_memcmp (h_wire, + &wcc->h_wire)) + { + wcc->qs = GNUNET_DB_STATUS_HARD_ERROR; + report_row_inconsistency ("aggregation", + rowid, + "target of outgoing wire transfer do not match hash of wire from deposit"); + } + if (exec_time.abs_value_us != wcc->date.abs_value_us) + { + /* This should be impossible from database constraints */ + GNUNET_break (0); + wcc->qs = GNUNET_DB_STATUS_HARD_ERROR; + report_row_inconsistency ("aggregation", + rowid, + "date given in aggregate does not match wire transfer date"); + } + + /* Add coin's contribution to total aggregate value */ + { + struct TALER_Amount res; + + if (GNUNET_OK != + TALER_amount_add (&res, + &wcc->total_deposits, + &computed_value)) + { + GNUNET_break (0); + wcc->qs = GNUNET_DB_STATUS_HARD_ERROR; + return; + } + wcc->total_deposits = res; + } +} + + +/** + * Lookup the wire fee that the exchange charges at @a timestamp. + * + * @param ac context for caching the result + * @param method method of the wire plugin + * @param timestamp time for which we need the fee + * @return NULL on error (fee unknown) + */ +static const struct TALER_Amount * +get_wire_fee (struct AggregationContext *ac, + const char *method, + struct GNUNET_TIME_Absolute timestamp) +{ + struct WireFeeInfo *wfi; + struct WireFeeInfo *pos; + struct TALER_MasterSignatureP master_sig; + + /* Check if fee is already loaded in cache */ + for (pos = ac->fee_head; NULL != pos; pos = pos->next) + { + if ( (pos->start_date.abs_value_us <= timestamp.abs_value_us) && + (pos->end_date.abs_value_us > timestamp.abs_value_us) ) + return &pos->wire_fee; + if (pos->start_date.abs_value_us > timestamp.abs_value_us) + break; + } + + /* Lookup fee in exchange database */ + wfi = GNUNET_new (struct WireFeeInfo); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + edb->get_wire_fee (edb->cls, + esession, + method, + timestamp, + &wfi->start_date, + &wfi->end_date, + &wfi->wire_fee, + &wfi->closing_fee, + &master_sig)) + { + GNUNET_break (0); + GNUNET_free (wfi); + return NULL; + } + + /* Check signature. (This is not terribly meaningful as the exchange can + easily make this one up, but it means that we have proof that the master + key was used for inconsistent wire fees if a merchant complains.) */ + { + struct TALER_MasterWireFeePS wf; + + wf.purpose.purpose = htonl (TALER_SIGNATURE_MASTER_WIRE_FEES); + wf.purpose.size = htonl (sizeof (wf)); + GNUNET_CRYPTO_hash (method, + strlen (method) + 1, + &wf.h_wire_method); + wf.start_date = GNUNET_TIME_absolute_hton (wfi->start_date); + wf.end_date = GNUNET_TIME_absolute_hton (wfi->end_date); + TALER_amount_hton (&wf.wire_fee, + &wfi->wire_fee); + TALER_amount_hton (&wf.closing_fee, + &wfi->closing_fee); + if (GNUNET_OK != + GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_MASTER_WIRE_FEES, + &wf.purpose, + &master_sig.eddsa_signature, + &master_pub.eddsa_pub)) + { + report_row_inconsistency ("wire-fee", + timestamp.abs_value_us, + "wire fee signature invalid at given time"); + } + } + + /* Established fee, keep in sorted list */ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Wire fee is %s starting at %s\n", + TALER_amount2s (&wfi->wire_fee), + GNUNET_STRINGS_absolute_time_to_string (wfi->start_date)); + if ( (NULL == pos) || + (NULL == pos->prev) ) + GNUNET_CONTAINER_DLL_insert (ac->fee_head, + ac->fee_tail, + wfi); + else + GNUNET_CONTAINER_DLL_insert_after (ac->fee_head, + ac->fee_tail, + pos->prev, + wfi); + /* Check non-overlaping fee invariant */ + if ( (NULL != wfi->prev) && + (wfi->prev->end_date.abs_value_us > wfi->start_date.abs_value_us) ) + { + report (report_fee_time_inconsistencies, + json_pack ("{s:s, s:s, s:o}", + "type", method, + "diagnostic", "start date before previous end date", + "time", json_from_time_abs (wfi->start_date))); + } + if ( (NULL != wfi->next) && + (wfi->next->start_date.abs_value_us >= wfi->end_date.abs_value_us) ) + { + report (report_fee_time_inconsistencies, + json_pack ("{s:s, s:s, s:o}", + "type", method, + "diagnostic", "end date date after next start date", + "time", json_from_time_abs (wfi->end_date))); + } + return &wfi->wire_fee; +} + + +/** + * Check that a wire transfer made by the exchange is valid + * (has matching deposits). + * + * @param cls a `struct AggregationContext` + * @param rowid identifier of the respective row in the database + * @param date timestamp of the wire transfer (roughly) + * @param wtid wire transfer subject + * @param wire wire transfer details of the receiver + * @param amount amount that was wired + * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop iteration + */ +static int +check_wire_out_cb (void *cls, + uint64_t rowid, + struct GNUNET_TIME_Absolute date, + const struct TALER_WireTransferIdentifierRawP *wtid, + const json_t *wire, + const struct TALER_Amount *amount) +{ + struct AggregationContext *ac = cls; + struct WireCheckContext wcc; + struct TALER_Amount final_amount; + struct TALER_Amount exchange_gain; + enum GNUNET_DB_QueryStatus qs; + char *method; + + /* should be monotonically increasing */ + GNUNET_assert (rowid >= ppa.last_wire_out_serial_id); + ppa.last_wire_out_serial_id = rowid + 1; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Checking wire transfer %s over %s performed on %s\n", + TALER_B2S (wtid), + TALER_amount2s (amount), + GNUNET_STRINGS_absolute_time_to_string (date)); + if (NULL == (method = TALER_JSON_wire_to_method (wire))) + { + report_row_inconsistency ("wire_out", + rowid, + "specified wire address lacks method"); + return GNUNET_OK; + } + + wcc.ac = ac; + wcc.qs = GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; + wcc.date = date; + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (amount->currency, + &wcc.total_deposits)); + if (GNUNET_OK != + TALER_JSON_merchant_wire_signature_hash (wire, + &wcc.h_wire)) + { + GNUNET_break (0); + GNUNET_free (method); + return GNUNET_SYSERR; + } + qs = edb->lookup_wire_transfer (edb->cls, + esession, + wtid, + &wire_transfer_information_cb, + &wcc); + if (0 > qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + ac->qs = qs; + GNUNET_free (method); + return GNUNET_SYSERR; + } + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != wcc.qs) + { + /* Note: detailed information was already logged + in #wire_transfer_information_cb, so here we + only log for debugging */ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Inconsitency for wire_out %llu (WTID %s) detected\n", + (unsigned long long) rowid, + TALER_B2S (wtid)); + } + + + /* Subtract aggregation fee from total (if possible) */ + { + const struct TALER_Amount *wire_fee; + + wire_fee = get_wire_fee (ac, + method, + date); + if (NULL == wire_fee) + { + report_row_inconsistency ("wire-fee", + date.abs_value_us, + "wire fee unavailable for given time"); + /* If fee is unknown, we just assume the fee is zero */ + final_amount = wcc.total_deposits; + } + else if (GNUNET_SYSERR == + TALER_amount_subtract (&final_amount, + &wcc.total_deposits, + wire_fee)) + { + report_amount_arithmetic_inconsistency ("wire out (fee structure)", + rowid, + &wcc.total_deposits, + wire_fee, + -1); + /* If fee arithmetic fails, we just assume the fee is zero */ + final_amount = wcc.total_deposits; + } + } + GNUNET_free (method); + + /* Round down to amount supported by wire method */ + GNUNET_break (GNUNET_SYSERR != + TALER_amount_round_down (&final_amount, + ¤cy_round_unit)); + + /* Calculate the exchange's gain as the fees plus rounding differences! */ + if (GNUNET_SYSERR == + TALER_amount_subtract (&exchange_gain, + &wcc.total_deposits, + &final_amount)) + { + GNUNET_break (0); + ac->qs = GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_SYSERR; + } + + /* Sum up aggregation fees (we simply include the rounding gains) */ + if (GNUNET_OK != + TALER_amount_add (&total_aggregation_fee_income, + &total_aggregation_fee_income, + &exchange_gain)) + { + GNUNET_break (0); + ac->qs = GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_SYSERR; + } + + /* Check that calculated amount matches actual amount */ + if (0 != TALER_amount_cmp (amount, + &final_amount)) + { + struct TALER_Amount delta; + + if (0 < TALER_amount_cmp (amount, + &final_amount)) + { + /* amount > final_amount */ + GNUNET_assert (GNUNET_OK == + TALER_amount_subtract (&delta, + amount, + &final_amount)); + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&total_wire_out_delta_plus, + &total_wire_out_delta_plus, + &delta)); + } + else + { + /* amount < final_amount */ + GNUNET_assert (GNUNET_OK == + TALER_amount_subtract (&delta, + &final_amount, + amount)); + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&total_wire_out_delta_minus, + &total_wire_out_delta_minus, + &delta)); + } + + report (report_wire_out_inconsistencies, + json_pack ("{s:O, s:I, s:o, s:o}", + "destination_account", wire, + "rowid", (json_int_t) rowid, + "expected", + TALER_JSON_from_amount (&final_amount), + "claimed", + TALER_JSON_from_amount (amount))); + return GNUNET_OK; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Wire transfer %s is OK\n", + TALER_B2S (wtid)); + return GNUNET_OK; +} + + +/** + * Analyze the exchange aggregator's payment processing. + * + * @param cls closure + * @return transaction status code + */ +static enum GNUNET_DB_QueryStatus +analyze_aggregations (void *cls) +{ + struct AggregationContext ac; + struct WireFeeInfo *wfi; + enum GNUNET_DB_QueryStatus qsx; + enum GNUNET_DB_QueryStatus qs; + enum GNUNET_DB_QueryStatus qsp; + + (void) cls; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Analyzing aggregations\n"); + qsp = adb->get_auditor_progress_aggregation (adb->cls, + asession, + &master_pub, + &ppa); + if (0 > qsp) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qsp); + return qsp; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qsp) + { + GNUNET_log (GNUNET_ERROR_TYPE_MESSAGE, + _ ( + "First analysis using this auditor, starting audit from scratch\n")); + } + else + { + ppa_start = ppa; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + _ ("Resuming aggregation audit at %llu\n"), + (unsigned long long) ppa.last_wire_out_serial_id); + } + + memset (&ac, + 0, + sizeof (ac)); + qsx = adb->get_wire_fee_summary (adb->cls, + asession, + &master_pub, + &total_aggregation_fee_income); + if (0 > qsx) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qsx); + return qsx; + } + ac.qs = GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; + qs = edb->select_wire_out_above_serial_id (edb->cls, + esession, + ppa.last_wire_out_serial_id, + &check_wire_out_cb, + &ac); + if (0 > qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + ac.qs = qs; + } + while (NULL != (wfi = ac.fee_head)) + { + GNUNET_CONTAINER_DLL_remove (ac.fee_head, + ac.fee_tail, + wfi); + GNUNET_free (wfi); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + /* there were no wire out entries to be looked at, we are done */ + return qs; + } + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != ac.qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == ac.qs); + return ac.qs; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qsx) + ac.qs = adb->insert_wire_fee_summary (adb->cls, + asession, + &master_pub, + &total_aggregation_fee_income); + else + ac.qs = adb->update_wire_fee_summary (adb->cls, + asession, + &master_pub, + &total_aggregation_fee_income); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != ac.qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == ac.qs); + return ac.qs; + } + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qsp) + qs = adb->update_auditor_progress_aggregation (adb->cls, + asession, + &master_pub, + &ppa); + else + qs = adb->insert_auditor_progress_aggregation (adb->cls, + asession, + &master_pub, + &ppa); + if (0 >= qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Failed to update auditor DB, not recording progress\n"); + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + _ ("Concluded aggregation audit step at %llu\n"), + (unsigned long long) ppa.last_wire_out_serial_id); + + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; +} + + +/** + * Main function that will be run. + * + * @param cls closure + * @param args remaining command-line arguments + * @param cfgfile name of the configuration file used (for saving, can be NULL!) + * @param c configuration + */ +static void +run (void *cls, + char *const *args, + const char *cfgfile, + const struct GNUNET_CONFIGURATION_Handle *c) +{ + json_t *report; + + (void) cls; + (void) args; + (void) cfgfile; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Launching auditor\n"); + if (GNUNET_OK != + setup_globals (c)) + { + global_ret = 1; + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Starting audit\n"); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_aggregation_fee_income)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_wire_out_delta_plus)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_wire_out_delta_minus)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_arithmetic_delta_plus)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_arithmetic_delta_minus)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_coin_delta_plus)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_coin_delta_minus)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_bad_sig_loss)); + GNUNET_assert (NULL != + (report_row_inconsistencies = json_array ())); + GNUNET_assert (NULL != + (report_wire_out_inconsistencies = json_array ())); + GNUNET_assert (NULL != + (report_coin_inconsistencies = json_array ())); + GNUNET_assert (NULL != + (report_amount_arithmetic_inconsistencies = json_array ())); + GNUNET_assert (NULL != + (report_bad_sig_losses = json_array ())); + GNUNET_assert (NULL != + (report_fee_time_inconsistencies = json_array ())); + setup_sessions_and_run (&analyze_aggregations, + NULL); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Audit complete\n"); + report = json_pack ("{s:o, s:o, s:o, s:o, s:o," + " s:o, s:o, s:o, s:o, s:o," + " s:o, s:o, s:o, s:I, s:I," + " s:o, s:o }", + /* blocks #1 */ + "wire_out_inconsistencies", + report_wire_out_inconsistencies, + "total_wire_out_delta_plus", + TALER_JSON_from_amount (&total_wire_out_delta_plus), + "total_wire_out_delta_minus", + TALER_JSON_from_amount (&total_wire_out_delta_minus), + /* Tested in test-auditor.sh #4/#5/#6/#7/#13 */ + "bad_sig_losses", + report_bad_sig_losses, + /* Tested in test-auditor.sh #4/#5/#6/#7/#13 */ + "total_bad_sig_loss", + TALER_JSON_from_amount (&total_bad_sig_loss), + /* block #2 */ + /* Tested in test-auditor.sh #14/#15 */ + "row_inconsistencies", + report_row_inconsistencies, + "coin_inconsistencies", + report_coin_inconsistencies, + "total_coin_delta_plus", + TALER_JSON_from_amount (&total_coin_delta_plus), + "total_coin_delta_minus", + TALER_JSON_from_amount (&total_coin_delta_minus), + "amount_arithmetic_inconsistencies", + report_amount_arithmetic_inconsistencies, + /* block #3 */ + "total_arithmetic_delta_plus", + TALER_JSON_from_amount (&total_arithmetic_delta_plus), + "total_arithmetic_delta_minus", + TALER_JSON_from_amount (&total_arithmetic_delta_minus), + "total_aggregation_fee_income", + TALER_JSON_from_amount (&total_aggregation_fee_income), + "start_ppa_wire_out_serial_id", + (json_int_t) ppa_start.last_wire_out_serial_id, + "end_ppa_wire_out_serial_id", + (json_int_t) ppa.last_wire_out_serial_id, + /* block #4 */ + "auditor_start_time", json_from_time_abs (start_time), + "auditor_end_time", json_from_time_abs ( + GNUNET_TIME_absolute_get ()) + ); + GNUNET_break (NULL != report); + finish_report (report); +} + + +/** + * The main function of the database initialization tool. + * Used to initialize the Taler Exchange's database. + * + * @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) +{ + const struct GNUNET_GETOPT_CommandLineOption options[] = { + GNUNET_GETOPT_option_base32_auto ('m', + "exchange-key", + "KEY", + "public key of the exchange (Crockford base32 encoded)", + &master_pub), + GNUNET_GETOPT_option_flag ('r', + "restart", + "restart audit from the beginning (required on first run)", + &restart), + GNUNET_GETOPT_option_timetravel ('T', + "timetravel"), + GNUNET_GETOPT_OPTION_END + }; + + /* force linker to link against libtalerutil; if we do + not do this, the linker may "optimize" libtalerutil + away and skip #TALER_OS_init(), which we do need */ + (void) TALER_project_data_default (); + GNUNET_assert (GNUNET_OK == + GNUNET_log_setup ("taler-auditor-aggregation", + "MESSAGE", + NULL)); + if (GNUNET_OK != + GNUNET_PROGRAM_run (argc, + argv, + "taler-auditor-aggregation", + "Audit Taler exchange aggregation activity", + options, + &run, + NULL)) + return 1; + return global_ret; +} + + +/* end of taler-auditor-aggregation.c */ diff --git a/src/auditor/taler-auditor-coins.c b/src/auditor/taler-auditor-coins.c new file mode 100644 index 000000000..d895c0433 --- /dev/null +++ b/src/auditor/taler-auditor-coins.c @@ -0,0 +1,2346 @@ +/* + This file is part of TALER + Copyright (C) 2016-2020 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero 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 Public License for more details. + + You should have received a copy of the GNU Affero Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file auditor/taler-auditor.c + * @brief audits an exchange database. + * @author Christian Grothoff + * + * NOTE: + * - This auditor does not verify that 'reserves_in' actually matches + * the wire transfers from the bank. This needs to be checked separately! + * - Similarly, we do not check that the outgoing wire transfers match those + * given in the 'wire_out' table. This needs to be checked separately! + * + * TODO: + * - reorganize: different passes are combined in one tool and one + * file here, we should split this up! + * - likely should do an iteration over known_coins instead of checking + * those signatures again and again + * - might want to bite the bullet and do asynchronous signature + * verification to improve parallelism / speed -- we'll need to scale + * this eventually anyway! + * + * UNDECIDED: + * - do we care about checking the 'done' flag in deposit_cb? + */ +#include "platform.h" +#include +#include "taler_auditordb_plugin.h" +#include "taler_exchangedb_lib.h" +#include "taler_json_lib.h" +#include "taler_bank_service.h" +#include "taler_signatures.h" +#include "report-lib.h" + +/** + * How many coin histories do we keep in RAM at any given point in + * time? Used bound memory consumption of the auditor. Larger values + * reduce database accesses. + * + * Set to a VERY low value here for testing. Practical values may be + * in the millions. + */ +#define MAX_COIN_SUMMARIES 4 + +/** + * Use a 1 day grace period to deal with clocks not being perfectly synchronized. + */ +#define DEPOSIT_GRACE_PERIOD GNUNET_TIME_UNIT_DAYS + +/** + * Return value from main(). + */ +static int global_ret; + +/** + * Checkpointing our progress for coins. + */ +static struct TALER_AUDITORDB_ProgressPointCoin ppc; + +/** + * Checkpointing our progress for coins. + */ +static struct TALER_AUDITORDB_ProgressPointCoin ppc_start; + +/** + * Array of reports about denomination keys with an + * emergency (more value deposited than withdrawn) + */ +static json_t *report_emergencies; + +/** + * Array of reports about denomination keys with an + * emergency (more coins deposited than withdrawn) + */ +static json_t *report_emergencies_by_count; + +/** + * Array of reports about row inconsitencies. + */ +static json_t *report_row_inconsistencies; + +/** + * Report about amount calculation differences (causing profit + * or loss at the exchange). + */ +static json_t *report_amount_arithmetic_inconsistencies; + +/** + * Profits the exchange made by bad amount calculations. + */ +static struct TALER_Amount total_arithmetic_delta_plus; + +/** + * Losses the exchange made by bad amount calculations. + */ +static struct TALER_Amount total_arithmetic_delta_minus; + +/** + * Total amount reported in all calls to #report_emergency_by_count(). + */ +static struct TALER_Amount reported_emergency_risk_by_count; + +/** + * Total amount reported in all calls to #report_emergency_by_amount(). + */ +static struct TALER_Amount reported_emergency_risk_by_amount; + +/** + * Total amount in losses reported in all calls to #report_emergency_by_amount(). + */ +static struct TALER_Amount reported_emergency_loss; + +/** + * Total amount in losses reported in all calls to #report_emergency_by_count(). + */ +static struct TALER_Amount reported_emergency_loss_by_count; + +/** + * Expected balance in the escrow account. + */ +static struct TALER_Amount total_escrow_balance; + +/** + * Active risk exposure. + */ +static struct TALER_Amount total_risk; + +/** + * Actualized risk (= loss) from recoups. + */ +static struct TALER_Amount total_recoup_loss; + +/** + * Recoups we made on denominations that were not revoked (!?). + */ +static struct TALER_Amount total_irregular_recoups; + +/** + * Total deposit fees earned. + */ +static struct TALER_Amount total_deposit_fee_income; + +/** + * Total melt fees earned. + */ +static struct TALER_Amount total_melt_fee_income; + +/** + * Total refund fees earned. + */ +static struct TALER_Amount total_refund_fee_income; + +/** + * Array of reports about coin operations with bad signatures. + */ +static json_t *report_bad_sig_losses; + +/** + * Total amount lost by operations for which signatures were invalid. + */ +static struct TALER_Amount total_bad_sig_loss; + +/** + * Array of refresh transactions where the /refresh/reveal has not yet + * happened (and may of course never happen). + */ +static json_t *report_refreshs_hanging; + +/** + * Total amount lost by operations for which signatures were invalid. + */ +static struct TALER_Amount total_refresh_hanging; + + +/* ***************************** Report logic **************************** */ + +/** + * Called in case we detect an emergency situation where the exchange + * is paying out a larger amount on a denomination than we issued in + * that denomination. This means that the exchange's private keys + * might have gotten compromised, and that we need to trigger an + * emergency request to all wallets to deposit pending coins for the + * denomination (and as an exchange suffer a huge financial loss). + * + * @param issue denomination key where the loss was detected + * @param risk maximum risk that might have just become real (coins created by this @a issue) + * @param loss actual losses already (actualized before denomination was revoked) + */ +static void +report_emergency_by_amount (const struct TALER_DenominationKeyValidityPS *issue, + const struct TALER_Amount *risk, + const struct TALER_Amount *loss) +{ + report (report_emergencies, + json_pack ("{s:o, s:o, s:o, s:o, s:o, s:o}", + "denompub_hash", + GNUNET_JSON_from_data_auto (&issue->denom_hash), + "denom_risk", + TALER_JSON_from_amount (risk), + "denom_loss", + TALER_JSON_from_amount (loss), + "start", + json_from_time_abs_nbo (issue->start), + "deposit_end", + json_from_time_abs_nbo (issue->expire_deposit), + "value", + TALER_JSON_from_amount_nbo (&issue->value))); + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&reported_emergency_risk_by_amount, + &reported_emergency_risk_by_amount, + risk)); + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&reported_emergency_loss, + &reported_emergency_loss, + loss)); +} + + +/** + * Called in case we detect an emergency situation where the exchange + * is paying out a larger NUMBER of coins of a denomination than we + * issued in that denomination. This means that the exchange's + * private keys might have gotten compromised, and that we need to + * trigger an emergency request to all wallets to deposit pending + * coins for the denomination (and as an exchange suffer a huge + * financial loss). + * + * @param issue denomination key where the loss was detected + * @param num_issued number of coins that were issued + * @param num_known number of coins that have been deposited + * @param risk amount that is at risk + */ +static void +report_emergency_by_count (const struct TALER_DenominationKeyValidityPS *issue, + uint64_t num_issued, + uint64_t num_known, + const struct TALER_Amount *risk) +{ + struct TALER_Amount denom_value; + + report (report_emergencies_by_count, + json_pack ("{s:o, s:I, s:I, s:o, s:o, s:o, s:o}", + "denompub_hash", + GNUNET_JSON_from_data_auto (&issue->denom_hash), + "num_issued", + (json_int_t) num_issued, + "num_known", + (json_int_t) num_known, + "denom_risk", + TALER_JSON_from_amount (risk), + "start", + json_from_time_abs_nbo (issue->start), + "deposit_end", + json_from_time_abs_nbo (issue->expire_deposit), + "value", + TALER_JSON_from_amount_nbo (&issue->value))); + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&reported_emergency_risk_by_count, + &reported_emergency_risk_by_count, + risk)); + TALER_amount_ntoh (&denom_value, + &issue->value); + for (uint64_t i = num_issued; i auditor */ + GNUNET_break (GNUNET_OK == + TALER_amount_subtract (&delta, + exchange, + auditor)); + } + else + { + /* auditor < exchange */ + profitable = -profitable; + GNUNET_break (GNUNET_OK == + TALER_amount_subtract (&delta, + auditor, + exchange)); + } + report (report_amount_arithmetic_inconsistencies, + json_pack ("{s:s, s:I, s:o, s:o, s:I}", + "operation", operation, + "rowid", (json_int_t) rowid, + "exchange", TALER_JSON_from_amount (exchange), + "auditor", TALER_JSON_from_amount (auditor), + "profitable", (json_int_t) profitable)); + if (0 != profitable) + { + target = (1 == profitable) + ? &total_arithmetic_delta_plus + : &total_arithmetic_delta_minus; + GNUNET_break (GNUNET_OK == + TALER_amount_add (target, + target, + &delta)); + } +} + + +/** + * Report a (serious) inconsistency in the exchange's database. + * + * @param table affected table + * @param rowid affected row, UINT64_MAX if row is missing + * @param diagnostic message explaining the problem + */ +static void +report_row_inconsistency (const char *table, + uint64_t rowid, + const char *diagnostic) +{ + report (report_row_inconsistencies, + json_pack ("{s:s, s:I, s:s}", + "table", table, + "row", (json_int_t) rowid, + "diagnostic", diagnostic)); +} + + +/* ************************* Analyze coins ******************** */ +/* This logic checks that the exchange did the right thing for each + coin, checking deposits, refunds, refresh* and known_coins + tables */ + + +/** + * Summary data we keep per denomination. + */ +struct DenominationSummary +{ + /** + * Total value of outstanding (not deposited) coins issued with this + * denomination key. + */ + struct TALER_Amount denom_balance; + + /** + * Total losses made (once coins deposited exceed + * coins withdrawn and thus the @e denom_balance is + * effectively negative). + */ + struct TALER_Amount denom_loss; + + /** + * Total value of coins issued with this denomination key. + */ + struct TALER_Amount denom_risk; + + /** + * Total value of coins subjected to recoup with this denomination key. + */ + struct TALER_Amount denom_recoup; + + /** + * How many coins (not their amount!) of this denomination + * did the exchange issue overall? + */ + uint64_t num_issued; + + /** + * Denomination key information for this denomination. + */ + const struct TALER_DenominationKeyValidityPS *issue; + + /** + * #GNUNET_YES if this record already existed in the DB. + * Used to decide between insert/update in + * #sync_denomination(). + */ + int in_db; + + /** + * Should we report an emergency for this denomination? + */ + int report_emergency; + + /** + * #GNUNET_YES if this denomination was revoked. + */ + int was_revoked; +}; + + +/** + * Closure for callbacks during #analyze_coins(). + */ +struct CoinContext +{ + + /** + * Map for tracking information about denominations. + */ + struct GNUNET_CONTAINER_MultiHashMap *denom_summaries; + + /** + * Current write/replace offset in the circular @e summaries buffer. + */ + unsigned int summaries_off; + + /** + * Transaction status code. + */ + enum GNUNET_DB_QueryStatus qs; + +}; + + +/** + * Initialize information about denomination from the database. + * + * @param denom_hash hash of the public key of the denomination + * @param[out] ds summary to initialize + * @return transaction status code + */ +static enum GNUNET_DB_QueryStatus +init_denomination (const struct GNUNET_HashCode *denom_hash, + struct DenominationSummary *ds) +{ + enum GNUNET_DB_QueryStatus qs; + struct TALER_MasterSignatureP msig; + uint64_t rowid; + + qs = adb->get_denomination_balance (adb->cls, + asession, + denom_hash, + &ds->denom_balance, + &ds->denom_loss, + &ds->denom_risk, + &ds->denom_recoup, + &ds->num_issued); + if (0 > qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + { + ds->in_db = GNUNET_YES; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Starting balance for denomination `%s' is %s\n", + GNUNET_h2s (denom_hash), + TALER_amount2s (&ds->denom_balance)); + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; + } + qs = edb->get_denomination_revocation (edb->cls, + esession, + denom_hash, + &msig, + &rowid); + if (0 > qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + if (0 < qs) + { + /* check revocation signature */ + struct TALER_MasterDenominationKeyRevocationPS rm; + + rm.purpose.purpose = htonl ( + TALER_SIGNATURE_MASTER_DENOMINATION_KEY_REVOKED); + rm.purpose.size = htonl (sizeof (rm)); + rm.h_denom_pub = *denom_hash; + if (GNUNET_OK != + GNUNET_CRYPTO_eddsa_verify ( + TALER_SIGNATURE_MASTER_DENOMINATION_KEY_REVOKED, + &rm.purpose, + &msig.eddsa_signature, + &master_pub.eddsa_pub)) + { + report_row_inconsistency ("denomination revocation table", + rowid, + "revocation signature invalid"); + } + else + { + ds->was_revoked = GNUNET_YES; + } + } + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &ds->denom_balance)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &ds->denom_loss)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &ds->denom_risk)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &ds->denom_recoup)); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Starting balance for denomination `%s' is %s\n", + GNUNET_h2s (denom_hash), + TALER_amount2s (&ds->denom_balance)); + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; +} + + +/** + * Obtain the denomination summary for the given @a dh + * + * @param cc our execution context + * @param issue denomination key information for @a dh + * @param dh the denomination hash to use for the lookup + * @return NULL on error + */ +static struct DenominationSummary * +get_denomination_summary (struct CoinContext *cc, + const struct TALER_DenominationKeyValidityPS *issue, + const struct GNUNET_HashCode *dh) +{ + struct DenominationSummary *ds; + + ds = GNUNET_CONTAINER_multihashmap_get (cc->denom_summaries, + dh); + if (NULL != ds) + return ds; + ds = GNUNET_new (struct DenominationSummary); + ds->issue = issue; + if (0 > (cc->qs = init_denomination (dh, + ds))) + { + GNUNET_break (0); + GNUNET_free (ds); + return NULL; + } + GNUNET_assert (GNUNET_OK == + GNUNET_CONTAINER_multihashmap_put (cc->denom_summaries, + dh, + ds, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); + return ds; +} + + +/** + * Write information about the current knowledge about a denomination key + * back to the database and update our global reporting data about the + * denomination. Also remove and free the memory of @a value. + * + * @param cls the `struct CoinContext` + * @param denom_hash the hash of the denomination key + * @param value a `struct DenominationSummary` + * @return #GNUNET_OK (continue to iterate) + */ +static int +sync_denomination (void *cls, + const struct GNUNET_HashCode *denom_hash, + void *value) +{ + struct CoinContext *cc = cls; + struct DenominationSummary *ds = value; + const struct TALER_DenominationKeyValidityPS *issue = ds->issue; + struct GNUNET_TIME_Absolute now; + struct GNUNET_TIME_Absolute expire_deposit; + struct GNUNET_TIME_Absolute expire_deposit_grace; + enum GNUNET_DB_QueryStatus qs; + + now = GNUNET_TIME_absolute_get (); + expire_deposit = GNUNET_TIME_absolute_ntoh (issue->expire_deposit); + /* add day grace period to deal with clocks not being perfectly synchronized */ + expire_deposit_grace = GNUNET_TIME_absolute_add (expire_deposit, + DEPOSIT_GRACE_PERIOD); + if (now.abs_value_us > expire_deposit_grace.abs_value_us) + { + /* Denominationkey has expired, book remaining balance of + outstanding coins as revenue; and reduce cc->risk exposure. */ + if (ds->in_db) + qs = adb->del_denomination_balance (adb->cls, + asession, + denom_hash); + else + qs = GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; + if ( (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) && + ( (0 != ds->denom_risk.value) || + (0 != ds->denom_risk.fraction) ) ) + { + /* The denomination expired and carried a balance; we can now + book the remaining balance as profit, and reduce our risk + exposure by the accumulated risk of the denomination. */ + if (GNUNET_SYSERR == + TALER_amount_subtract (&total_risk, + &total_risk, + &ds->denom_risk)) + { + /* Holy smokes, our risk assessment was inconsistent! + This is really, really bad. */ + GNUNET_break (0); + cc->qs = GNUNET_DB_STATUS_HARD_ERROR; + } + } + if ( (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) && + ( (0 != ds->denom_balance.value) || + (0 != ds->denom_balance.fraction) ) ) + { + /* book denom_balance coin expiration profits! */ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Denomination `%s' expired, booking %s in expiration profits\n", + GNUNET_h2s (denom_hash), + TALER_amount2s (&ds->denom_balance)); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + (qs = adb->insert_historic_denom_revenue (adb->cls, + asession, + &master_pub, + denom_hash, + expire_deposit, + &ds->denom_balance, + &ds->denom_recoup))) + { + /* Failed to store profits? Bad database */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + cc->qs = qs; + } + } + } + else + { + long long cnt; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Final balance for denomination `%s' is %s (%llu)\n", + GNUNET_h2s (denom_hash), + TALER_amount2s (&ds->denom_balance), + (unsigned long long) ds->num_issued); + cnt = edb->count_known_coins (edb->cls, + esession, + denom_hash); + if (0 > cnt) + { + /* Failed to obtain count? Bad database */ + qs = (enum GNUNET_DB_QueryStatus) cnt; + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + cc->qs = qs; + } + else + { + if (ds->num_issued < (uint64_t) cnt) + { + report_emergency_by_count (issue, + ds->num_issued, + cnt, + &ds->denom_risk); + } + if (GNUNET_YES == ds->report_emergency) + { + report_emergency_by_amount (issue, + &ds->denom_risk, + &ds->denom_loss); + + } + if (ds->in_db) + qs = adb->update_denomination_balance (adb->cls, + asession, + denom_hash, + &ds->denom_balance, + &ds->denom_loss, + &ds->denom_risk, + &ds->denom_recoup, + ds->num_issued); + else + qs = adb->insert_denomination_balance (adb->cls, + asession, + denom_hash, + &ds->denom_balance, + &ds->denom_loss, + &ds->denom_risk, + &ds->denom_recoup, + ds->num_issued); + } + } + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + cc->qs = qs; + } + GNUNET_assert (GNUNET_YES == + GNUNET_CONTAINER_multihashmap_remove (cc->denom_summaries, + denom_hash, + ds)); + GNUNET_free (ds); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != cc->qs) + return GNUNET_SYSERR; + return GNUNET_OK; +} + + +/** + * Function called with details about all withdraw operations. + * Updates the denomination balance and the overall balance as + * we now have additional coins that have been issued. + * + * Note that the signature was already checked in + * #handle_reserve_out(), so we do not check it again here. + * + * @param cls our `struct CoinContext` + * @param rowid unique serial ID for the refresh session in our DB + * @param h_blind_ev blinded hash of the coin's public key + * @param denom_pub public denomination key of the deposited coin + * @param reserve_pub public key of the reserve + * @param reserve_sig signature over the withdraw operation (verified elsewhere) + * @param execution_date when did the wallet withdraw the coin + * @param amount_with_fee amount that was withdrawn + * @return #GNUNET_OK to continue to iterate, #GNUNET_SYSERR to stop + */ +static int +withdraw_cb (void *cls, + uint64_t rowid, + const struct GNUNET_HashCode *h_blind_ev, + const struct TALER_DenominationPublicKey *denom_pub, + const struct TALER_ReservePublicKeyP *reserve_pub, + const struct TALER_ReserveSignatureP *reserve_sig, + struct GNUNET_TIME_Absolute execution_date, + const struct TALER_Amount *amount_with_fee) +{ + struct CoinContext *cc = cls; + struct DenominationSummary *ds; + struct GNUNET_HashCode dh; + const struct TALER_DenominationKeyValidityPS *issue; + struct TALER_Amount value; + enum GNUNET_DB_QueryStatus qs; + + (void) h_blind_ev; + (void) reserve_pub; + (void) reserve_sig; + (void) execution_date; + (void) amount_with_fee; + GNUNET_assert (rowid >= ppc.last_withdraw_serial_id); /* should be monotonically increasing */ + ppc.last_withdraw_serial_id = rowid + 1; + + qs = get_denomination_info (denom_pub, + &issue, + &dh); + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + report_row_inconsistency ("withdraw", + rowid, + "denomination key not found"); + return GNUNET_OK; + } + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + { + /* This really ought to be a transient DB error. */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + cc->qs = qs; + return GNUNET_SYSERR; + } + ds = get_denomination_summary (cc, + issue, + &dh); + if (NULL == ds) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + TALER_amount_ntoh (&value, + &issue->value); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Issued coin in denomination `%s' of total value %s\n", + GNUNET_h2s (&dh), + TALER_amount2s (&value)); + ds->num_issued++; + if (GNUNET_OK != + TALER_amount_add (&ds->denom_balance, + &ds->denom_balance, + &value)) + { + GNUNET_break (0); + cc->qs = GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_SYSERR; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "New balance of denomination `%s' is %s\n", + GNUNET_h2s (&dh), + TALER_amount2s (&ds->denom_balance)); + if (GNUNET_OK != + TALER_amount_add (&total_escrow_balance, + &total_escrow_balance, + &value)) + { + GNUNET_break (0); + cc->qs = GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_amount_add (&total_risk, + &total_risk, + &value)) + { + GNUNET_break (0); + cc->qs = GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_amount_add (&ds->denom_risk, + &ds->denom_risk, + &value)) + { + GNUNET_break (0); + cc->qs = GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Closure for #reveal_data_cb(). + */ +struct RevealContext +{ + + /** + * Denomination public keys of the new coins. + */ + struct TALER_DenominationPublicKey *new_dps; + + /** + * Size of the @a new_dp and @a new_dps arrays. + */ + unsigned int num_freshcoins; +}; + + +/** + * Function called with information about a refresh order. + * + * @param cls closure + * @param num_freshcoins size of the @a rrcs array + * @param rrcs array of @a num_freshcoins information about coins to be created + * @param num_tprivs number of entries in @a tprivs, should be #TALER_CNC_KAPPA - 1 + * @param tprivs array of @e num_tprivs transfer private keys + * @param tp transfer public key information + */ +static void +reveal_data_cb (void *cls, + uint32_t num_freshcoins, + const struct TALER_EXCHANGEDB_RefreshRevealedCoin *rrcs, + unsigned int num_tprivs, + const struct TALER_TransferPrivateKeyP *tprivs, + const struct TALER_TransferPublicKeyP *tp) +{ + struct RevealContext *rctx = cls; + + (void) num_tprivs; + (void) tprivs; + (void) tp; + rctx->num_freshcoins = num_freshcoins; + rctx->new_dps = GNUNET_new_array (num_freshcoins, + struct TALER_DenominationPublicKey); + for (unsigned int i = 0; inew_dps[i].rsa_public_key + = GNUNET_CRYPTO_rsa_public_key_dup (rrcs[i].denom_pub.rsa_public_key); +} + + +/** + * Check that the @a coin_pub is a known coin with a proper + * signature for denominatinon @a denom_pub. If not, report + * a loss of @a loss_potential. + * + * @param coin_pub public key of a coin + * @param denom_pub expected denomination of the coin + * @param loss_potential how big could the loss be if the coin is + * not properly signed + * @return database transaction status, on success + * #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT + */ +static enum GNUNET_DB_QueryStatus +check_known_coin (const struct TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_DenominationPublicKey *denom_pub, + const struct TALER_Amount *loss_potential) +{ + struct TALER_CoinPublicInfo ci; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Checking denomination signature on %s\n", + TALER_B2S (coin_pub)); + qs = edb->get_known_coin (edb->cls, + esession, + coin_pub, + &ci); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + if (GNUNET_YES != + TALER_test_coin_valid (&ci, + denom_pub)) + { + report (report_bad_sig_losses, + json_pack ("{s:s, s:I, s:o, s:o}", + "operation", "known-coin", + "row", (json_int_t) -1, + "loss", TALER_JSON_from_amount (loss_potential), + "key_pub", GNUNET_JSON_from_data_auto (coin_pub))); + GNUNET_break (GNUNET_OK == + TALER_amount_add (&total_bad_sig_loss, + &total_bad_sig_loss, + loss_potential)); + + } + GNUNET_CRYPTO_rsa_signature_free (ci.denom_sig.rsa_signature); + return qs; +} + + +/** + * Function called with details about coins that were melted, with the + * goal of auditing the refresh's execution. Verifies the signature + * and updates our information about coins outstanding (the old coin's + * denomination has less, the fresh coins increased outstanding + * balances). + * + * @param cls closure + * @param rowid unique serial ID for the refresh session in our DB + * @param denom_pub denomination public key of @a coin_pub + * @param coin_pub public key of the coin + * @param coin_sig signature from the coin + * @param amount_with_fee amount that was deposited including fee + * @param noreveal_index which index was picked by the exchange in cut-and-choose + * @param rc what is the refresh commitment + * @return #GNUNET_OK to continue to iterate, #GNUNET_SYSERR to stop + */ +static int +refresh_session_cb (void *cls, + uint64_t rowid, + const struct TALER_DenominationPublicKey *denom_pub, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_CoinSpendSignatureP *coin_sig, + const struct TALER_Amount *amount_with_fee, + uint32_t noreveal_index, + const struct TALER_RefreshCommitmentP *rc) +{ + struct CoinContext *cc = cls; + struct TALER_RefreshMeltCoinAffirmationPS rmc; + const struct TALER_DenominationKeyValidityPS *issue; + struct DenominationSummary *dso; + struct TALER_Amount amount_without_fee; + struct TALER_Amount tmp; + enum GNUNET_DB_QueryStatus qs; + + (void) noreveal_index; + GNUNET_assert (rowid >= ppc.last_melt_serial_id); /* should be monotonically increasing */ + ppc.last_melt_serial_id = rowid + 1; + + qs = get_denomination_info (denom_pub, + &issue, + NULL); + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + report_row_inconsistency ("melt", + rowid, + "denomination key not found"); + return GNUNET_OK; + } + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + cc->qs = qs; + return GNUNET_SYSERR; + } + qs = check_known_coin (coin_pub, + denom_pub, + amount_with_fee); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + cc->qs = qs; + return GNUNET_SYSERR; + } + + /* verify melt signature */ + rmc.purpose.purpose = htonl (TALER_SIGNATURE_WALLET_COIN_MELT); + rmc.purpose.size = htonl (sizeof (rmc)); + rmc.rc = *rc; + TALER_amount_hton (&rmc.amount_with_fee, + amount_with_fee); + rmc.melt_fee = issue->fee_refresh; + rmc.coin_pub = *coin_pub; + if (GNUNET_OK != + GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_WALLET_COIN_MELT, + &rmc.purpose, + &coin_sig->eddsa_signature, + &coin_pub->eddsa_pub)) + { + report (report_bad_sig_losses, + json_pack ("{s:s, s:I, s:o, s:o}", + "operation", "melt", + "row", (json_int_t) rowid, + "loss", TALER_JSON_from_amount (amount_with_fee), + "key_pub", GNUNET_JSON_from_data_auto (coin_pub))); + GNUNET_break (GNUNET_OK == + TALER_amount_add (&total_bad_sig_loss, + &total_bad_sig_loss, + amount_with_fee)); + return GNUNET_OK; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Melting coin %s in denomination `%s' of value %s\n", + TALER_B2S (coin_pub), + GNUNET_h2s (&issue->denom_hash), + TALER_amount2s (amount_with_fee)); + + { + struct RevealContext reveal_ctx; + struct TALER_Amount refresh_cost; + int err; + + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (amount_with_fee->currency, + &refresh_cost)); + memset (&reveal_ctx, + 0, + sizeof (reveal_ctx)); + qs = edb->get_refresh_reveal (edb->cls, + esession, + rc, + &reveal_data_cb, + &reveal_ctx); + if (0 > qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + cc->qs = GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_SYSERR; + } + if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) || + (0 == reveal_ctx.num_freshcoins) ) + { + /* This can happen if /refresh/reveal was not yet called or only + with invalid data, even if the exchange is correctly + operating. We still report it. */ + report (report_refreshs_hanging, + json_pack ("{s:I, s:o, s:o}", + "row", (json_int_t) rowid, + "amount", TALER_JSON_from_amount (amount_with_fee), + "coin_pub", GNUNET_JSON_from_data_auto (coin_pub))); + GNUNET_break (GNUNET_OK == + TALER_amount_add (&total_refresh_hanging, + &total_refresh_hanging, + amount_with_fee)); + return GNUNET_OK; + } + + { + const struct TALER_DenominationKeyValidityPS *new_issues[reveal_ctx. + num_freshcoins]; + + /* Update outstanding amounts for all new coin's denominations, and check + that the resulting amounts are consistent with the value being refreshed. */ + err = GNUNET_OK; + for (unsigned int i = 0; iqs = qs; + err = GNUNET_SYSERR; /* terminate, return GNUNET_SYSERR */ + } + GNUNET_CRYPTO_rsa_public_key_free ( + reveal_ctx.new_dps[i].rsa_public_key); + reveal_ctx.new_dps[i].rsa_public_key = NULL; + } + GNUNET_free (reveal_ctx.new_dps); + reveal_ctx.new_dps = NULL; + + if (GNUNET_OK != err) + return (GNUNET_SYSERR == err) ? GNUNET_SYSERR : GNUNET_OK; + + /* calculate total refresh cost */ + for (unsigned int i = 0; ifee_withdraw); + TALER_amount_ntoh (&value, + &new_issues[i]->value); + if ( (GNUNET_OK != + TALER_amount_add (&refresh_cost, + &refresh_cost, + &fee)) || + (GNUNET_OK != + TALER_amount_add (&refresh_cost, + &refresh_cost, + &value)) ) + { + GNUNET_break (0); + cc->qs = GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_SYSERR; + } + } + + /* compute contribution of old coin */ + { + struct TALER_Amount melt_fee; + + TALER_amount_ntoh (&melt_fee, + &issue->fee_refresh); + if (GNUNET_OK != + TALER_amount_subtract (&amount_without_fee, + amount_with_fee, + &melt_fee)) + { + GNUNET_break (0); + cc->qs = GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_SYSERR; + } + } + + /* check old coin covers complete expenses */ + if (1 == TALER_amount_cmp (&refresh_cost, + &amount_without_fee)) + { + /* refresh_cost > amount_without_fee */ + report_amount_arithmetic_inconsistency ("melt (fee)", + rowid, + &amount_without_fee, + &refresh_cost, + -1); + return GNUNET_OK; + } + + /* update outstanding denomination amounts */ + for (unsigned int i = 0; idenom_hash); + if (NULL == dsi) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + TALER_amount_ntoh (&value, + &new_issues[i]->value); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Created fresh coin in denomination `%s' of value %s\n", + GNUNET_h2s (&new_issues[i]->denom_hash), + TALER_amount2s (&value)); + dsi->num_issued++; + if (GNUNET_OK != + TALER_amount_add (&dsi->denom_balance, + &dsi->denom_balance, + &value)) + { + GNUNET_break (0); + cc->qs = GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_amount_add (&dsi->denom_risk, + &dsi->denom_risk, + &value)) + { + GNUNET_break (0); + cc->qs = GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_SYSERR; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "New balance of denomination `%s' is %s\n", + GNUNET_h2s (&new_issues[i]->denom_hash), + TALER_amount2s (&dsi->denom_balance)); + if (GNUNET_OK != + TALER_amount_add (&total_escrow_balance, + &total_escrow_balance, + &value)) + { + GNUNET_break (0); + cc->qs = GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_amount_add (&total_risk, + &total_risk, + &value)) + { + GNUNET_break (0); + cc->qs = GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_SYSERR; + } + } + } + } + + /* update old coin's denomination balance */ + dso = get_denomination_summary (cc, + issue, + &issue->denom_hash); + if (NULL == dso) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (GNUNET_SYSERR == + TALER_amount_subtract (&tmp, + &dso->denom_balance, + amount_with_fee)) + { + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&dso->denom_loss, + &dso->denom_loss, + amount_with_fee)); + dso->report_emergency = GNUNET_YES; + } + else + { + dso->denom_balance = tmp; + } + if (-1 == TALER_amount_cmp (&total_escrow_balance, + amount_with_fee)) + { + /* This can theoretically happen if for example the exchange + never issued any coins (i.e. escrow balance is zero), but + accepted a forged coin (i.e. emergency situation after + private key compromise). In that case, we cannot even + subtract the profit we make from the fee from the escrow + balance. Tested as part of test-auditor.sh, case #18 */report_amount_arithmetic_inconsistency ( + "subtracting refresh fee from escrow balance", + rowid, + &total_escrow_balance, + amount_with_fee, + 0); + } + else + { + GNUNET_assert (GNUNET_SYSERR != + TALER_amount_subtract (&total_escrow_balance, + &total_escrow_balance, + amount_with_fee)); + } + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "New balance of denomination `%s' after melt is %s\n", + GNUNET_h2s (&issue->denom_hash), + TALER_amount2s (&dso->denom_balance)); + + /* update global melt fees */ + { + struct TALER_Amount rfee; + + TALER_amount_ntoh (&rfee, + &issue->fee_refresh); + if (GNUNET_OK != + TALER_amount_add (&total_melt_fee_income, + &total_melt_fee_income, + &rfee)) + { + GNUNET_break (0); + cc->qs = GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_SYSERR; + } + } + + /* We're good! */ + return GNUNET_OK; +} + + +/** + * Function called with details about deposits that have been made, + * with the goal of auditing the deposit's execution. + * + * @param cls closure + * @param rowid unique serial ID for the deposit in our DB + * @param timestamp when did the deposit happen + * @param merchant_pub public key of the merchant + * @param denom_pub denomination public key of @a coin_pub + * @param coin_pub public key of the coin + * @param coin_sig signature from the coin + * @param amount_with_fee amount that was deposited including fee + * @param h_contract_terms hash of the proposal data known to merchant and customer + * @param refund_deadline by which the merchant adviced that he might want + * to get a refund + * @param wire_deadline by which the merchant adviced that he would like the + * wire transfer to be executed + * @param receiver_wire_account wire details for the merchant, NULL from iterate_matching_deposits() + * @param done flag set if the deposit was already executed (or not) + * @return #GNUNET_OK to continue to iterate, #GNUNET_SYSERR to stop + */ +static int +deposit_cb (void *cls, + uint64_t rowid, + struct GNUNET_TIME_Absolute timestamp, + const struct TALER_MerchantPublicKeyP *merchant_pub, + const struct TALER_DenominationPublicKey *denom_pub, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_CoinSpendSignatureP *coin_sig, + const struct TALER_Amount *amount_with_fee, + const struct GNUNET_HashCode *h_contract_terms, + struct GNUNET_TIME_Absolute refund_deadline, + struct GNUNET_TIME_Absolute wire_deadline, + const json_t *receiver_wire_account, + int done) +{ + struct CoinContext *cc = cls; + const struct TALER_DenominationKeyValidityPS *issue; + struct DenominationSummary *ds; + struct TALER_DepositRequestPS dr; + struct TALER_Amount tmp; + enum GNUNET_DB_QueryStatus qs; + + (void) wire_deadline; + (void) done; + GNUNET_assert (rowid >= ppc.last_deposit_serial_id); /* should be monotonically increasing */ + ppc.last_deposit_serial_id = rowid + 1; + + qs = get_denomination_info (denom_pub, + &issue, + NULL); + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + report_row_inconsistency ("deposits", + rowid, + "denomination key not found"); + return GNUNET_OK; + } + + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + cc->qs = qs; + return GNUNET_SYSERR; + } + qs = check_known_coin (coin_pub, + denom_pub, + amount_with_fee); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + cc->qs = qs; + return GNUNET_SYSERR; + } + + /* Verify deposit signature */ + dr.purpose.purpose = htonl (TALER_SIGNATURE_WALLET_COIN_DEPOSIT); + dr.purpose.size = htonl (sizeof (dr)); + dr.h_contract_terms = *h_contract_terms; + if (GNUNET_OK != + TALER_JSON_merchant_wire_signature_hash (receiver_wire_account, + &dr.h_wire)) + { + report (report_bad_sig_losses, + json_pack ("{s:s, s:I, s:o, s:o}", + "operation", "deposit", + "row", (json_int_t) rowid, + "loss", TALER_JSON_from_amount (amount_with_fee), + "key_pub", GNUNET_JSON_from_data_auto (coin_pub))); + GNUNET_break (GNUNET_OK == + TALER_amount_add (&total_bad_sig_loss, + &total_bad_sig_loss, + amount_with_fee)); + return GNUNET_OK; + } + dr.timestamp = GNUNET_TIME_absolute_hton (timestamp); + dr.refund_deadline = GNUNET_TIME_absolute_hton (refund_deadline); + TALER_amount_hton (&dr.amount_with_fee, + amount_with_fee); + dr.deposit_fee = issue->fee_deposit; + dr.merchant = *merchant_pub; + dr.coin_pub = *coin_pub; + if (GNUNET_OK != + GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_WALLET_COIN_DEPOSIT, + &dr.purpose, + &coin_sig->eddsa_signature, + &coin_pub->eddsa_pub)) + { + report (report_bad_sig_losses, + json_pack ("{s:s, s:I, s:o, s:o}", + "operation", "deposit", + "row", (json_int_t) rowid, + "loss", TALER_JSON_from_amount (amount_with_fee), + "key_pub", GNUNET_JSON_from_data_auto (coin_pub))); + GNUNET_break (GNUNET_OK == + TALER_amount_add (&total_bad_sig_loss, + &total_bad_sig_loss, + amount_with_fee)); + return GNUNET_OK; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Deposited coin %s in denomination `%s' of value %s\n", + TALER_B2S (coin_pub), + GNUNET_h2s (&issue->denom_hash), + TALER_amount2s (amount_with_fee)); + + /* update old coin's denomination balance */ + ds = get_denomination_summary (cc, + issue, + &issue->denom_hash); + if (NULL == ds) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (GNUNET_SYSERR == + TALER_amount_subtract (&tmp, + &ds->denom_balance, + amount_with_fee)) + { + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&ds->denom_loss, + &ds->denom_loss, + amount_with_fee)); + ds->report_emergency = GNUNET_YES; + } + else + { + ds->denom_balance = tmp; + } + + if (-1 == TALER_amount_cmp (&total_escrow_balance, + amount_with_fee)) + { + /* This can theoretically happen if for example the exchange + never issued any coins (i.e. escrow balance is zero), but + accepted a forged coin (i.e. emergency situation after + private key compromise). In that case, we cannot even + subtract the profit we make from the fee from the escrow + balance. Tested as part of test-auditor.sh, case #18 */report_amount_arithmetic_inconsistency ( + "subtracting deposit fee from escrow balance", + rowid, + &total_escrow_balance, + amount_with_fee, + 0); + } + else + { + GNUNET_assert (GNUNET_SYSERR != + TALER_amount_subtract (&total_escrow_balance, + &total_escrow_balance, + amount_with_fee)); + } + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "New balance of denomination `%s' after deposit is %s\n", + GNUNET_h2s (&issue->denom_hash), + TALER_amount2s (&ds->denom_balance)); + + /* update global up melt fees */ + { + struct TALER_Amount dfee; + + TALER_amount_ntoh (&dfee, + &issue->fee_deposit); + if (GNUNET_OK != + TALER_amount_add (&total_deposit_fee_income, + &total_deposit_fee_income, + &dfee)) + { + GNUNET_break (0); + cc->qs = GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_SYSERR; + } + } + + return GNUNET_OK; +} + + +/** + * Function called with details about coins that were refunding, + * with the goal of auditing the refund's execution. Adds the + * refunded amount back to the outstanding balance of the respective + * denomination. + * + * @param cls closure + * @param rowid unique serial ID for the refund in our DB + * @param denom_pub denomination public key of @a coin_pub + * @param coin_pub public key of the coin + * @param merchant_pub public key of the merchant + * @param merchant_sig signature of the merchant + * @param h_contract_terms hash of the proposal data known to merchant and customer + * @param rtransaction_id refund transaction ID chosen by the merchant + * @param amount_with_fee amount that was deposited including fee + * @return #GNUNET_OK to continue to iterate, #GNUNET_SYSERR to stop + */ +static int +refund_cb (void *cls, + uint64_t rowid, + const struct TALER_DenominationPublicKey *denom_pub, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_MerchantPublicKeyP *merchant_pub, + const struct TALER_MerchantSignatureP *merchant_sig, + const struct GNUNET_HashCode *h_contract_terms, + uint64_t rtransaction_id, + const struct TALER_Amount *amount_with_fee) +{ + struct CoinContext *cc = cls; + const struct TALER_DenominationKeyValidityPS *issue; + struct DenominationSummary *ds; + struct TALER_RefundRequestPS rr; + struct TALER_Amount amount_without_fee; + struct TALER_Amount refund_fee; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_assert (rowid >= ppc.last_refund_serial_id); /* should be monotonically increasing */ + ppc.last_refund_serial_id = rowid + 1; + + qs = get_denomination_info (denom_pub, + &issue, + NULL); + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + report_row_inconsistency ("refunds", + rowid, + "denomination key not found"); + return GNUNET_SYSERR; + } + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return GNUNET_SYSERR; + } + + /* verify refund signature */ + rr.purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_REFUND); + rr.purpose.size = htonl (sizeof (rr)); + rr.h_contract_terms = *h_contract_terms; + rr.coin_pub = *coin_pub; + rr.merchant = *merchant_pub; + rr.rtransaction_id = GNUNET_htonll (rtransaction_id); + TALER_amount_hton (&rr.refund_amount, + amount_with_fee); + rr.refund_fee = issue->fee_refund; + if (GNUNET_OK != + GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_MERCHANT_REFUND, + &rr.purpose, + &merchant_sig->eddsa_sig, + &merchant_pub->eddsa_pub)) + { + report (report_bad_sig_losses, + json_pack ("{s:s, s:I, s:o, s:o}", + "operation", "refund", + "row", (json_int_t) rowid, + "loss", TALER_JSON_from_amount (amount_with_fee), + "key_pub", GNUNET_JSON_from_data_auto (merchant_pub))); + GNUNET_break (GNUNET_OK == + TALER_amount_add (&total_bad_sig_loss, + &total_bad_sig_loss, + amount_with_fee)); + return GNUNET_OK; + } + + TALER_amount_ntoh (&refund_fee, + &issue->fee_refund); + if (GNUNET_OK != + TALER_amount_subtract (&amount_without_fee, + amount_with_fee, + &refund_fee)) + { + report_amount_arithmetic_inconsistency ("refund (fee)", + rowid, + &amount_without_fee, + &refund_fee, + -1); + return GNUNET_OK; + } + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Refunding coin %s in denomination `%s' value %s\n", + TALER_B2S (coin_pub), + GNUNET_h2s (&issue->denom_hash), + TALER_amount2s (amount_with_fee)); + + /* update coin's denomination balance */ + ds = get_denomination_summary (cc, + issue, + &issue->denom_hash); + if (NULL == ds) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_amount_add (&ds->denom_balance, + &ds->denom_balance, + &amount_without_fee)) + { + GNUNET_break (0); + cc->qs = GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_amount_add (&ds->denom_risk, + &ds->denom_risk, + &amount_without_fee)) + { + GNUNET_break (0); + cc->qs = GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_amount_add (&total_escrow_balance, + &total_escrow_balance, + &amount_without_fee)) + { + GNUNET_break (0); + cc->qs = GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_amount_add (&total_risk, + &total_risk, + &amount_without_fee)) + { + GNUNET_break (0); + cc->qs = GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_SYSERR; + } + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "New balance of denomination `%s' after refund is %s\n", + GNUNET_h2s (&issue->denom_hash), + TALER_amount2s (&ds->denom_balance)); + + /* update total refund fee balance */ + if (GNUNET_OK != + TALER_amount_add (&total_refund_fee_income, + &total_refund_fee_income, + &refund_fee)) + { + GNUNET_break (0); + cc->qs = GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_SYSERR; + } + + return GNUNET_OK; +} + + +/** + * Check that the recoup operation was properly initiated by a coin + * and update the denomination's losses accordingly. + * + * @param cc the context with details about the coin + * @param rowid row identifier used to uniquely identify the recoup operation + * @param amount how much should be added back to the reserve + * @param coin public information about the coin + * @param denom_pub public key of the denomionation of @a coin + * @param coin_sig signature with @e coin_pub of type #TALER_SIGNATURE_WALLET_COIN_RECOUP + * @param coin_blind blinding factor used to blind the coin + * @return #GNUNET_OK to continue to iterate, #GNUNET_SYSERR to stop + */ +static int +check_recoup (struct CoinContext *cc, + uint64_t rowid, + const struct TALER_Amount *amount, + const struct TALER_CoinPublicInfo *coin, + const struct TALER_DenominationPublicKey *denom_pub, + const struct TALER_CoinSpendSignatureP *coin_sig, + const struct TALER_DenominationBlindingKeyP *coin_blind) +{ + struct TALER_RecoupRequestPS pr; + struct DenominationSummary *ds; + enum GNUNET_DB_QueryStatus qs; + const struct TALER_DenominationKeyValidityPS *issue; + + if (GNUNET_OK != + TALER_test_coin_valid (coin, + denom_pub)) + { + report (report_bad_sig_losses, + json_pack ("{s:s, s:I, s:o, s:o}", + "operation", "recoup", + "row", (json_int_t) rowid, + "loss", TALER_JSON_from_amount (amount), + "key_pub", GNUNET_JSON_from_data_auto ( + &pr.h_denom_pub))); + GNUNET_break (GNUNET_OK == + TALER_amount_add (&total_bad_sig_loss, + &total_bad_sig_loss, + amount)); + } + qs = get_denomination_info (denom_pub, + &issue, + &pr.h_denom_pub); + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + report_row_inconsistency ("recoup", + rowid, + "denomination key not found"); + return GNUNET_OK; + } + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + { + /* The key not existing should be prevented by foreign key constraints, + so must be a transient DB error. */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + cc->qs = qs; + return GNUNET_SYSERR; + } + pr.purpose.purpose = htonl (TALER_SIGNATURE_WALLET_COIN_RECOUP); + pr.purpose.size = htonl (sizeof (pr)); + pr.coin_pub = coin->coin_pub; + pr.coin_blind = *coin_blind; + if (GNUNET_OK != + GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_WALLET_COIN_RECOUP, + &pr.purpose, + &coin_sig->eddsa_signature, + &coin->coin_pub.eddsa_pub)) + { + report (report_bad_sig_losses, + json_pack ("{s:s, s:I, s:o, s:o}", + "operation", "recoup", + "row", (json_int_t) rowid, + "loss", TALER_JSON_from_amount (amount), + "coin_pub", GNUNET_JSON_from_data_auto ( + &coin->coin_pub))); + GNUNET_break (GNUNET_OK == + TALER_amount_add (&total_bad_sig_loss, + &total_bad_sig_loss, + amount)); + return GNUNET_OK; + } + ds = get_denomination_summary (cc, + issue, + &issue->denom_hash); + if (GNUNET_NO == ds->was_revoked) + { + /* Woopsie, we allowed recoup on non-revoked denomination!? */ + report (report_bad_sig_losses, + json_pack ("{s:s, s:I, s:o, s:o}", + "operation", "recoup (denomination not revoked)", + "row", (json_int_t) rowid, + "loss", TALER_JSON_from_amount (amount), + "coin_pub", GNUNET_JSON_from_data_auto ( + &coin->coin_pub))); + } + GNUNET_break (GNUNET_OK == + TALER_amount_add (&ds->denom_recoup, + &ds->denom_recoup, + amount)); + GNUNET_break (GNUNET_OK == + TALER_amount_add (&total_recoup_loss, + &total_recoup_loss, + amount)); + return GNUNET_OK; +} + + +/** + * Function called about recoups the exchange has to perform. + * + * @param cls a `struct CoinContext *` + * @param rowid row identifier used to uniquely identify the recoup operation + * @param timestamp when did we receive the recoup request + * @param amount how much should be added back to the reserve + * @param reserve_pub public key of the reserve + * @param coin public information about the coin + * @param denom_pub denomination public key of @a coin + * @param coin_sig signature with @e coin_pub of type #TALER_SIGNATURE_WALLET_COIN_RECOUP + * @param coin_blind blinding factor used to blind the coin + * @return #GNUNET_OK to continue to iterate, #GNUNET_SYSERR to stop + */ +static int +recoup_cb (void *cls, + uint64_t rowid, + struct GNUNET_TIME_Absolute timestamp, + const struct TALER_Amount *amount, + const struct TALER_ReservePublicKeyP *reserve_pub, + const struct TALER_CoinPublicInfo *coin, + const struct TALER_DenominationPublicKey *denom_pub, + const struct TALER_CoinSpendSignatureP *coin_sig, + const struct TALER_DenominationBlindingKeyP *coin_blind) +{ + struct CoinContext *cc = cls; + + (void) timestamp; + (void) reserve_pub; + return check_recoup (cc, + rowid, + amount, + coin, + denom_pub, + coin_sig, + coin_blind); +} + + +/** + * Function called about recoups on refreshed coins the exchange has to + * perform. + * + * @param cls a `struct CoinContext *` + * @param rowid row identifier used to uniquely identify the recoup operation + * @param timestamp when did we receive the recoup request + * @param amount how much should be added back to the reserve + * @param old_coin_pub original coin that was refreshed to create @a coin + * @param coin public information about the coin + * @param denom_pub denomination public key of @a coin + * @param coin_sig signature with @e coin_pub of type #TALER_SIGNATURE_WALLET_COIN_RECOUP + * @param coin_blind blinding factor used to blind the coin + * @return #GNUNET_OK to continue to iterate, #GNUNET_SYSERR to stop + */ +static int +recoup_refresh_cb (void *cls, + uint64_t rowid, + struct GNUNET_TIME_Absolute timestamp, + const struct TALER_Amount *amount, + const struct TALER_CoinSpendPublicKeyP *old_coin_pub, + const struct TALER_CoinPublicInfo *coin, + const struct TALER_DenominationPublicKey *denom_pub, + const struct TALER_CoinSpendSignatureP *coin_sig, + const struct TALER_DenominationBlindingKeyP *coin_blind) +{ + struct CoinContext *cc = cls; + + (void) timestamp; + (void) old_coin_pub; + return check_recoup (cc, + rowid, + amount, + coin, + denom_pub, + coin_sig, + coin_blind); +} + + +/** + * Analyze the exchange's processing of coins. + * + * @param cls closure + * @return transaction status code + */ +static enum GNUNET_DB_QueryStatus +analyze_coins (void *cls) +{ + struct CoinContext cc; + enum GNUNET_DB_QueryStatus qs; + enum GNUNET_DB_QueryStatus qsx; + enum GNUNET_DB_QueryStatus qsp; + + (void) cls; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Analyzing coins\n"); + qsp = adb->get_auditor_progress_coin (adb->cls, + asession, + &master_pub, + &ppc); + if (0 > qsp) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qsp); + return qsp; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qsp) + { + GNUNET_log (GNUNET_ERROR_TYPE_MESSAGE, + "First analysis using this auditor, starting from scratch\n"); + } + else + { + ppc_start = ppc; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Resuming coin audit at %llu/%llu/%llu/%llu/%llu\n", + (unsigned long long) ppc.last_deposit_serial_id, + (unsigned long long) ppc.last_melt_serial_id, + (unsigned long long) ppc.last_refund_serial_id, + (unsigned long long) ppc.last_withdraw_serial_id, + (unsigned long long) ppc.last_recoup_refresh_serial_id); + } + + /* setup 'cc' */ + cc.qs = GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; + cc.denom_summaries = GNUNET_CONTAINER_multihashmap_create (256, + GNUNET_NO); + qsx = adb->get_balance_summary (adb->cls, + asession, + &master_pub, + &total_escrow_balance, + &total_deposit_fee_income, + &total_melt_fee_income, + &total_refund_fee_income, + &total_risk, + &total_recoup_loss, + &total_irregular_recoups); + if (0 > qsx) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qsx); + return qsx; + } + + /* process withdrawals */ + if (0 > + (qs = edb->select_withdrawals_above_serial_id (edb->cls, + esession, + ppc. + last_withdraw_serial_id, + &withdraw_cb, + &cc)) ) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + if (0 > cc.qs) + return cc.qs; + + /* process refunds */ + if (0 > + (qs = edb->select_refunds_above_serial_id (edb->cls, + esession, + ppc.last_refund_serial_id, + &refund_cb, + &cc))) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + if (0 > cc.qs) + return cc.qs; + + /* process refreshs */ + if (0 > + (qs = edb->select_refreshes_above_serial_id (edb->cls, + esession, + ppc.last_melt_serial_id, + &refresh_session_cb, + &cc))) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + if (0 > cc.qs) + return cc.qs; + + /* process deposits */ + if (0 > + (qs = edb->select_deposits_above_serial_id (edb->cls, + esession, + ppc.last_deposit_serial_id, + &deposit_cb, + &cc))) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + if (0 > cc.qs) + return cc.qs; + + /* process recoups */ + if (0 > + (qs = edb->select_recoup_above_serial_id (edb->cls, + esession, + ppc.last_recoup_serial_id, + &recoup_cb, + &cc))) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + if (0 > cc.qs) + return cc.qs; + if (0 > + (qs = edb->select_recoup_refresh_above_serial_id (edb->cls, + esession, + ppc. + last_recoup_refresh_serial_id, + &recoup_refresh_cb, + &cc))) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + if (0 > cc.qs) + return cc.qs; + + /* sync 'cc' back to disk */ + cc.qs = GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; + GNUNET_CONTAINER_multihashmap_iterate (cc.denom_summaries, + &sync_denomination, + &cc); + GNUNET_CONTAINER_multihashmap_destroy (cc.denom_summaries); + if (0 > cc.qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == cc.qs); + return cc.qs; + } + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qsx) + qs = adb->update_balance_summary (adb->cls, + asession, + &master_pub, + &total_escrow_balance, + &total_deposit_fee_income, + &total_melt_fee_income, + &total_refund_fee_income, + &total_risk, + &total_recoup_loss, + &total_irregular_recoups); + else + qs = adb->insert_balance_summary (adb->cls, + asession, + &master_pub, + &total_escrow_balance, + &total_deposit_fee_income, + &total_melt_fee_income, + &total_refund_fee_income, + &total_risk, + &total_recoup_loss, + &total_irregular_recoups); + if (0 >= qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qsp) + qs = adb->update_auditor_progress_coin (adb->cls, + asession, + &master_pub, + &ppc); + else + qs = adb->insert_auditor_progress_coin (adb->cls, + asession, + &master_pub, + &ppc); + if (0 >= qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Failed to update auditor DB, not recording progress\n"); + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + _ ("Concluded coin audit step at %llu/%llu/%llu/%llu/%llu\n"), + (unsigned long long) ppc.last_deposit_serial_id, + (unsigned long long) ppc.last_melt_serial_id, + (unsigned long long) ppc.last_refund_serial_id, + (unsigned long long) ppc.last_withdraw_serial_id, + (unsigned long long) ppc.last_recoup_refresh_serial_id); + return qs; +} + + +/** + * Main function that will be run. + * + * @param cls closure + * @param args remaining command-line arguments + * @param cfgfile name of the configuration file used (for saving, can be NULL!) + * @param c configuration + */ +static void +run (void *cls, + char *const *args, + const char *cfgfile, + const struct GNUNET_CONFIGURATION_Handle *c) +{ + json_t *report; + + (void) cls; + (void) args; + (void) cfgfile; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Launching auditor\n"); + if (GNUNET_OK != + setup_globals (c)) + { + global_ret = 1; + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Starting audit\n"); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &reported_emergency_loss)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &reported_emergency_risk_by_amount)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &reported_emergency_risk_by_count)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &reported_emergency_loss_by_count)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_escrow_balance)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_risk)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_recoup_loss)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_irregular_recoups)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_deposit_fee_income)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_melt_fee_income)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_refund_fee_income)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_arithmetic_delta_plus)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_arithmetic_delta_minus)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_bad_sig_loss)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_refresh_hanging)); + GNUNET_assert (NULL != + (report_emergencies = json_array ())); + GNUNET_assert (NULL != + (report_emergencies_by_count = json_array ())); + GNUNET_assert (NULL != + (report_row_inconsistencies = json_array ())); + GNUNET_assert (NULL != + (report_amount_arithmetic_inconsistencies = json_array ())); + GNUNET_assert (NULL != + (report_bad_sig_losses = json_array ())); + GNUNET_assert (NULL != + (report_refreshs_hanging = json_array ())); + if (GNUNET_OK != + setup_sessions_and_run (&analyze_coins, + NULL)) + { + global_ret = 1; + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Audit complete\n"); + report = json_pack ("{s:o, s:o, s:o, s:o, s:o," + " s:o, s:o, s:o, s:o, s:o," + " s:o, s:o, s:o, s:o, s:o," + " s:o, s:o, s:o, s:o, s:o," + " s:I, s:I, s:I, s:I, s:I," + " s:I, s:I, s:I, s:I, s:I," + " s:I, s:I, s:o, s:o, s:o}", + /* Block #1 */ + "total_escrow_balance", + TALER_JSON_from_amount (&total_escrow_balance), + "total_active_risk", + TALER_JSON_from_amount (&total_risk), + "total_deposit_fee_income", + TALER_JSON_from_amount (&total_deposit_fee_income), + "total_melt_fee_income", + TALER_JSON_from_amount (&total_melt_fee_income), + "total_refund_fee_income", + TALER_JSON_from_amount (&total_refund_fee_income), + /* Block #2 */ + /* Tested in test-auditor.sh #18 */ + "emergencies", + report_emergencies, + /* Tested in test-auditor.sh #18 */ + "emergencies_risk_by_amount", + TALER_JSON_from_amount ( + &reported_emergency_risk_by_amount), + /* Tested in test-auditor.sh #4/#5/#6/#7/#13 */ + "bad_sig_losses", + report_bad_sig_losses, + /* Tested in test-auditor.sh #4/#5/#6/#7/#13 */ + "total_bad_sig_loss", + TALER_JSON_from_amount (&total_bad_sig_loss), + /* Tested in test-auditor.sh #14/#15 */ + "row_inconsistencies", + report_row_inconsistencies, + /* Block #3 */ + "amount_arithmetic_inconsistencies", + report_amount_arithmetic_inconsistencies, + "total_arithmetic_delta_plus", + TALER_JSON_from_amount (&total_arithmetic_delta_plus), + "total_arithmetic_delta_minus", + TALER_JSON_from_amount (&total_arithmetic_delta_minus), + /* Tested in test-auditor.sh #12 */ + "total_refresh_hanging", + TALER_JSON_from_amount (&total_refresh_hanging), + /* Tested in test-auditor.sh #12 */ + "refresh_hanging", + report_refreshs_hanging, + /* Block #4 */ + "total_recoup_loss", + TALER_JSON_from_amount (&total_recoup_loss), + /* Tested in test-auditor.sh #18 */ + "emergencies_by_count", + report_emergencies_by_count, + /* Tested in test-auditor.sh #18 */ + "emergencies_risk_by_count", + TALER_JSON_from_amount ( + &reported_emergency_risk_by_count), + /* Tested in test-auditor.sh #18 */ + "emergencies_loss", + TALER_JSON_from_amount (&reported_emergency_loss), + /* Tested in test-auditor.sh #18 */ + "emergencies_loss_by_count", + TALER_JSON_from_amount ( + &reported_emergency_loss_by_count), + /* Block #5 */ + "start_ppc_withdraw_serial_id", + (json_int_t) ppc_start.last_withdraw_serial_id, + "start_ppc_deposit_serial_id", + (json_int_t) ppc_start.last_deposit_serial_id, + "start_ppc_melt_serial_id", + (json_int_t) ppc_start.last_melt_serial_id, + "start_ppc_refund_serial_id", + (json_int_t) ppc_start.last_refund_serial_id, + "start_ppc_recoup_serial_id", + (json_int_t) ppc_start.last_recoup_serial_id, + /* Block #6 */ + "start_ppc_recoup_refresh_serial_id", + (json_int_t) ppc_start.last_recoup_refresh_serial_id, + "end_ppc_withdraw_serial_id", + (json_int_t) ppc.last_withdraw_serial_id, + "end_ppc_deposit_serial_id", + (json_int_t) ppc.last_deposit_serial_id, + "end_ppc_melt_serial_id", + (json_int_t) ppc.last_melt_serial_id, + "end_ppc_refund_serial_id", + (json_int_t) ppc.last_refund_serial_id, + /* Block #7 */ + "end_ppc_recoup_serial_id", + (json_int_t) ppc.last_recoup_serial_id, + "end_ppc_recoup_refresh_serial_id", + (json_int_t) ppc.last_recoup_refresh_serial_id, + "auditor_start_time", json_string ( + GNUNET_STRINGS_absolute_time_to_string (start_time)), + "auditor_end_time", json_string ( + GNUNET_STRINGS_absolute_time_to_string ( + GNUNET_TIME_absolute_get ())), + "total_irregular_recoups", + TALER_JSON_from_amount (&total_irregular_recoups) + ); + GNUNET_break (NULL != report); + finish_report (report); +} + + +/** + * The main function of the database initialization tool. + * Used to initialize the Taler Exchange's database. + * + * @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) +{ + const struct GNUNET_GETOPT_CommandLineOption options[] = { + GNUNET_GETOPT_option_base32_auto ('m', + "exchange-key", + "KEY", + "public key of the exchange (Crockford base32 encoded)", + &master_pub), + GNUNET_GETOPT_option_flag ('r', + "restart", + "restart audit from the beginning (required on first run)", + &restart), + GNUNET_GETOPT_option_timetravel ('T', + "timetravel"), + GNUNET_GETOPT_OPTION_END + }; + + /* force linker to link against libtalerutil; if we do + not do this, the linker may "optimize" libtalerutil + away and skip #TALER_OS_init(), which we do need */ + (void) TALER_project_data_default (); + GNUNET_assert (GNUNET_OK == + GNUNET_log_setup ("taler-auditor", + "MESSAGE", + NULL)); + if (GNUNET_OK != + GNUNET_PROGRAM_run (argc, + argv, + "taler-auditor", + "Audit Taler exchange database", + options, + &run, + NULL)) + return 1; + return global_ret; +} + + +/* end of taler-auditor.c */ diff --git a/src/auditor/taler-auditor-deposits.c b/src/auditor/taler-auditor-deposits.c new file mode 100644 index 000000000..ac8a0b624 --- /dev/null +++ b/src/auditor/taler-auditor-deposits.c @@ -0,0 +1,360 @@ +/* + This file is part of TALER + Copyright (C) 2016-2020 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero 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 Public License for more details. + + You should have received a copy of the GNU Affero Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file auditor/taler-auditor-deposits.c + * @brief audits an exchange database for deposit confirmation consistency + * @author Christian Grothoff + */ +#include "platform.h" +#include +#include "taler_auditordb_plugin.h" +#include "taler_exchangedb_lib.h" +#include "taler_json_lib.h" +#include "taler_bank_service.h" +#include "taler_signatures.h" +#include "report-lib.h" + + +/** + * Return value from main(). + */ +static int global_ret; + +/** + * Array of reports about missing deposit confirmations. + */ +static json_t *report_deposit_confirmation_inconsistencies; + +/** + * Total number of deposit confirmations that we did not get. + */ +static json_int_t number_missed_deposit_confirmations; + +/** + * Total amount involved in deposit confirmations that we did not get. + */ +static struct TALER_Amount total_missed_deposit_confirmations; + + +/* *************************** Analysis of deposit-confirmations ********** */ + +/** + * Closure for #test_dc. + */ +struct DepositConfirmationContext +{ + + /** + * How many deposit confirmations did we NOT find in the #edb? + */ + unsigned long long missed_count; + + /** + * What is the total amount missing? + */ + struct TALER_Amount missed_amount; + + /** + * Lowest SerialID of the first coin we missed? (This is where we + * should resume next time). + */ + uint64_t first_missed_coin_serial; + + /** + * Lowest SerialID of the first coin we missed? (This is where we + * should resume next time). + */ + uint64_t last_seen_coin_serial; + + /** + * Success or failure of (exchange) database operations within + * #test_dc. + */ + enum GNUNET_DB_QueryStatus qs; + +}; + + +/** + * Given a deposit confirmation from #adb, check that it is also + * in #edb. Update the deposit confirmation context accordingly. + * + * @param cls our `struct DepositConfirmationContext` + * @param serial_id row of the @a dc in the database + * @param dc the deposit confirmation we know + */ +static void +test_dc (void *cls, + uint64_t serial_id, + const struct TALER_AUDITORDB_DepositConfirmation *dc) +{ + struct DepositConfirmationContext *dcc = cls; + enum GNUNET_DB_QueryStatus qs; + struct TALER_EXCHANGEDB_Deposit dep; + + dcc->last_seen_coin_serial = serial_id; + memset (&dep, + 0, + sizeof (dep)); + dep.coin.coin_pub = dc->coin_pub; + dep.h_contract_terms = dc->h_contract_terms; + dep.merchant_pub = dc->merchant; + dep.h_wire = dc->h_wire; + dep.refund_deadline = dc->refund_deadline; + + qs = edb->have_deposit (edb->cls, + esession, + &dep, + GNUNET_NO /* do not check refund deadline */); + if (qs > 0) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Found deposit %s in exchange database\n", + GNUNET_h2s (&dc->h_contract_terms)); + return; /* found, all good */ + } + if (qs < 0) + { + GNUNET_break (0); /* DB error, complain */ + dcc->qs = qs; + return; + } + /* deposit confirmation missing! report! */ + report (report_deposit_confirmation_inconsistencies, + json_pack ("{s:o, s:o, s:I, s:o}", + "timestamp", + json_from_time_abs (dc->timestamp), + "amount", + TALER_JSON_from_amount (&dc->amount_without_fee), + "rowid", + (json_int_t) serial_id, + "account", + GNUNET_JSON_from_data_auto (&dc->h_wire))); + dcc->first_missed_coin_serial = GNUNET_MIN (dcc->first_missed_coin_serial, + serial_id); + dcc->missed_count++; + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&dcc->missed_amount, + &dcc->missed_amount, + &dc->amount_without_fee)); +} + + +/** + * Check that the deposit-confirmations that were reported to + * us by merchants are also in the exchange's database. + * + * @param cls closure + * @return transaction status code + */ +static enum GNUNET_DB_QueryStatus +analyze_deposit_confirmations (void *cls) +{ + struct TALER_AUDITORDB_ProgressPointDepositConfirmation ppdc; + struct DepositConfirmationContext dcc; + enum GNUNET_DB_QueryStatus qs; + enum GNUNET_DB_QueryStatus qsx; + enum GNUNET_DB_QueryStatus qsp; + + (void) cls; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Analyzing deposit confirmations\n"); + ppdc.last_deposit_confirmation_serial_id = 0; + qsp = adb->get_auditor_progress_deposit_confirmation (adb->cls, + asession, + &master_pub, + &ppdc); + if (0 > qsp) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qsp); + return qsp; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qsp) + { + GNUNET_log (GNUNET_ERROR_TYPE_MESSAGE, + _ ( + "First analysis using this auditor, starting audit from scratch\n")); + } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + _ ("Resuming deposit confirmation audit at %llu\n"), + (unsigned long long) ppdc.last_deposit_confirmation_serial_id); + } + + /* setup 'cc' */ + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &dcc.missed_amount)); + dcc.qs = GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; + dcc.missed_count = 0LLU; + dcc.first_missed_coin_serial = UINT64_MAX; + qsx = adb->get_deposit_confirmations (adb->cls, + asession, + &master_pub, + ppdc.last_deposit_confirmation_serial_id, + &test_dc, + &dcc); + if (0 > qsx) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qsx); + return qsx; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Analyzed %d deposit confirmations (above serial ID %llu)\n", + (int) qsx, + (unsigned long long) ppdc.last_deposit_confirmation_serial_id); + if (0 > dcc.qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == dcc.qs); + return dcc.qs; + } + if (UINT64_MAX == dcc.first_missed_coin_serial) + ppdc.last_deposit_confirmation_serial_id = dcc.last_seen_coin_serial; + else + ppdc.last_deposit_confirmation_serial_id = dcc.first_missed_coin_serial - 1; + + /* sync 'cc' back to disk */ + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qsp) + qs = adb->update_auditor_progress_deposit_confirmation (adb->cls, + asession, + &master_pub, + &ppdc); + else + qs = adb->insert_auditor_progress_deposit_confirmation (adb->cls, + asession, + &master_pub, + &ppdc); + if (0 >= qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Failed to update auditor DB, not recording progress\n"); + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + number_missed_deposit_confirmations = (json_int_t) dcc.missed_count; + total_missed_deposit_confirmations = dcc.missed_amount; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + _ ("Concluded deposit confirmation audit step at %llu\n"), + (unsigned long long) ppdc.last_deposit_confirmation_serial_id); + return qs; +} + + +/** + * Main function that will be run. + * + * @param cls closure + * @param args remaining command-line arguments + * @param cfgfile name of the configuration file used (for saving, can be NULL!) + * @param c configuration + */ +static void +run (void *cls, + char *const *args, + const char *cfgfile, + const struct GNUNET_CONFIGURATION_Handle *c) +{ + json_t *report; + + (void) cls; + (void) args; + (void) cfgfile; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Launching auditor\n"); + if (GNUNET_OK != + setup_globals (c)) + { + global_ret = 1; + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Starting audit\n"); + GNUNET_assert (NULL != + (report_deposit_confirmation_inconsistencies = json_array ())); + if (GNUNET_OK != + setup_sessions_and_run (&analyze_deposit_confirmations, + NULL)) + { + global_ret = 1; + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Audit complete\n"); + report = json_pack ("{s:o, s:o, s:I}", + "deposit_confirmation_inconsistencies", + report_deposit_confirmation_inconsistencies, + "missing_deposit_confirmation_count", + (json_int_t) number_missed_deposit_confirmations, + "missing_deposit_confirmation_total", + TALER_JSON_from_amount ( + &total_missed_deposit_confirmations) + ); + GNUNET_break (NULL != report); + finish_report (report); +} + + +/** + * The main function of the database initialization tool. + * Used to initialize the Taler Exchange's database. + * + * @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) +{ + const struct GNUNET_GETOPT_CommandLineOption options[] = { + GNUNET_GETOPT_option_base32_auto ('m', + "exchange-key", + "KEY", + "public key of the exchange (Crockford base32 encoded)", + &master_pub), + GNUNET_GETOPT_option_flag ('r', + "restart", + "restart audit from the beginning (required on first run)", + &restart), + GNUNET_GETOPT_option_timetravel ('T', + "timetravel"), + GNUNET_GETOPT_OPTION_END + }; + + /* force linker to link against libtalerutil; if we do + not do this, the linker may "optimize" libtalerutil + away and skip #TALER_OS_init(), which we do need */ + (void) TALER_project_data_default (); + GNUNET_assert (GNUNET_OK == + GNUNET_log_setup ("taler-auditor-deposits", + "MESSAGE", + NULL)); + if (GNUNET_OK != + GNUNET_PROGRAM_run (argc, + argv, + "taler-auditor-deposits", + "Audit Taler exchange database for deposit confirmation consistency", + options, + &run, + NULL)) + return 1; + return global_ret; +} + + +/* end of taler-auditor-deposits.c */ diff --git a/src/auditor/taler-auditor-reserves.c b/src/auditor/taler-auditor-reserves.c new file mode 100644 index 000000000..2fe103c81 --- /dev/null +++ b/src/auditor/taler-auditor-reserves.c @@ -0,0 +1,1641 @@ +/* + This file is part of TALER + Copyright (C) 2016-2020 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero 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 Public License for more details. + + You should have received a copy of the GNU Affero Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file auditor/taler-auditor-reserves.c + * @brief audits the reserves of an exchange database + * @author Christian Grothoff + */ +#include "platform.h" +#include +#include "taler_auditordb_plugin.h" +#include "taler_exchangedb_lib.h" +#include "taler_json_lib.h" +#include "taler_bank_service.h" +#include "taler_signatures.h" +#include "report-lib.h" + + +/** + * Use a 1 day grace period to deal with clocks not being perfectly synchronized. + */ +#define CLOSING_GRACE_PERIOD GNUNET_TIME_UNIT_DAYS + +/** + * Return value from main(). + */ +static int global_ret; + +/** + * After how long should idle reserves be closed? + */ +static struct GNUNET_TIME_Relative idle_reserve_expiration_time; + +/** + * Checkpointing our progress for reserves. + */ +static struct TALER_AUDITORDB_ProgressPointReserve ppr; + +/** + * Checkpointing our progress for reserves. + */ +static struct TALER_AUDITORDB_ProgressPointReserve ppr_start; + +/** + * Array of reports about row inconsitencies. + */ +static json_t *report_row_inconsistencies; + +/** + * Array of reports about the denomination key not being + * valid at the time of withdrawal. + */ +static json_t *denomination_key_validity_withdraw_inconsistencies; + +/** + * Array of reports about reserve balance insufficient inconsitencies. + */ +static json_t *report_reserve_balance_insufficient_inconsistencies; + +/** + * Total amount reserves were charged beyond their balance. + */ +static struct TALER_Amount total_balance_insufficient_loss; + +/** + * Array of reports about reserve balance summary wrong in database. + */ +static json_t *report_reserve_balance_summary_wrong_inconsistencies; + +/** + * Total delta between expected and stored reserve balance summaries, + * for positive deltas. + */ +static struct TALER_Amount total_balance_summary_delta_plus; + +/** + * Total delta between expected and stored reserve balance summaries, + * for negative deltas. + */ +static struct TALER_Amount total_balance_summary_delta_minus; + +/** + * Array of reports about reserve's not being closed inconsitencies. + */ +static json_t *report_reserve_not_closed_inconsistencies; + +/** + * Total amount affected by reserves not having been closed on time. + */ +static struct TALER_Amount total_balance_reserve_not_closed; + +/** + * Report about amount calculation differences (causing profit + * or loss at the exchange). + */ +static json_t *report_amount_arithmetic_inconsistencies; + +/** + * Profits the exchange made by bad amount calculations. + */ +static struct TALER_Amount total_arithmetic_delta_plus; + +/** + * Losses the exchange made by bad amount calculations. + */ +static struct TALER_Amount total_arithmetic_delta_minus; + +/** + * Expected balance in the escrow account. + */ +static struct TALER_Amount total_escrow_balance; + +/** + * Recoups we made on denominations that were not revoked (!?). + */ +static struct TALER_Amount total_irregular_recoups; + +/** + * Total withdraw fees earned. + */ +static struct TALER_Amount total_withdraw_fee_income; + +/** + * Array of reports about coin operations with bad signatures. + */ +static json_t *report_bad_sig_losses; + +/** + * Total amount lost by operations for which signatures were invalid. + */ +static struct TALER_Amount total_bad_sig_loss; + + +/* ***************************** Report logic **************************** */ + + +/** + * Report a (serious) inconsistency in the exchange's database with + * respect to calculations involving amounts. + * + * @param operation what operation had the inconsistency + * @param rowid affected row, UINT64_MAX if row is missing + * @param exchange amount calculated by exchange + * @param auditor amount calculated by auditor + * @param profitable 1 if @a exchange being larger than @a auditor is + * profitable for the exchange for this operation, + * -1 if @a exchange being smaller than @a auditor is + * profitable for the exchange, and 0 if it is unclear + */ +static void +report_amount_arithmetic_inconsistency (const char *operation, + uint64_t rowid, + const struct TALER_Amount *exchange, + const struct TALER_Amount *auditor, + int profitable) +{ + struct TALER_Amount delta; + struct TALER_Amount *target; + + if (0 < TALER_amount_cmp (exchange, + auditor)) + { + /* exchange > auditor */ + GNUNET_break (GNUNET_OK == + TALER_amount_subtract (&delta, + exchange, + auditor)); + } + else + { + /* auditor < exchange */ + profitable = -profitable; + GNUNET_break (GNUNET_OK == + TALER_amount_subtract (&delta, + auditor, + exchange)); + } + report (report_amount_arithmetic_inconsistencies, + json_pack ("{s:s, s:I, s:o, s:o, s:I}", + "operation", operation, + "rowid", (json_int_t) rowid, + "exchange", TALER_JSON_from_amount (exchange), + "auditor", TALER_JSON_from_amount (auditor), + "profitable", (json_int_t) profitable)); + if (0 != profitable) + { + target = (1 == profitable) + ? &total_arithmetic_delta_plus + : &total_arithmetic_delta_minus; + GNUNET_break (GNUNET_OK == + TALER_amount_add (target, + target, + &delta)); + } +} + + +/** + * Report a (serious) inconsistency in the exchange's database. + * + * @param table affected table + * @param rowid affected row, UINT64_MAX if row is missing + * @param diagnostic message explaining the problem + */ +static void +report_row_inconsistency (const char *table, + uint64_t rowid, + const char *diagnostic) +{ + report (report_row_inconsistencies, + json_pack ("{s:s, s:I, s:s}", + "table", table, + "row", (json_int_t) rowid, + "diagnostic", diagnostic)); +} + + +/* ***************************** Analyze reserves ************************ */ +/* This logic checks the reserves_in, reserves_out and reserves-tables */ + +/** + * Summary data we keep per reserve. + */ +struct ReserveSummary +{ + /** + * Public key of the reserve. + * Always set when the struct is first initialized. + */ + struct TALER_ReservePublicKeyP reserve_pub; + + /** + * Sum of all incoming transfers during this transaction. + * Updated only in #handle_reserve_in(). + */ + struct TALER_Amount total_in; + + /** + * Sum of all outgoing transfers during this transaction (includes fees). + * Updated only in #handle_reserve_out(). + */ + struct TALER_Amount total_out; + + /** + * Sum of withdraw fees encountered during this transaction. + */ + struct TALER_Amount total_fee; + + /** + * Previous balance of the reserve as remembered by the auditor. + * (updated based on @e total_in and @e total_out at the end). + */ + struct TALER_Amount a_balance; + + /** + * Previous withdraw fee balance of the reserve, as remembered by the auditor. + * (updated based on @e total_fee at the end). + */ + struct TALER_Amount a_withdraw_fee_balance; + + /** + * Previous reserve expiration data, as remembered by the auditor. + * (updated on-the-fly in #handle_reserve_in()). + */ + struct GNUNET_TIME_Absolute a_expiration_date; + + /** + * Which account did originally put money into the reserve? + */ + char *sender_account; + + /** + * Did we have a previous reserve info? Used to decide between + * UPDATE and INSERT later. Initialized in + * #load_auditor_reserve_summary() together with the a-* values + * (if available). + */ + int had_ri; + +}; + + +/** + * Load the auditor's remembered state about the reserve into @a rs. + * The "total_in" and "total_out" amounts of @a rs must already be + * initialized (so we can determine the currency). + * + * @param[in,out] rs reserve summary to (fully) initialize + * @return transaction status code + */ +static enum GNUNET_DB_QueryStatus +load_auditor_reserve_summary (struct ReserveSummary *rs) +{ + enum GNUNET_DB_QueryStatus qs; + uint64_t rowid; + + qs = adb->get_reserve_info (adb->cls, + asession, + &rs->reserve_pub, + &master_pub, + &rowid, + &rs->a_balance, + &rs->a_withdraw_fee_balance, + &rs->a_expiration_date, + &rs->sender_account); + if (0 > qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + rs->had_ri = GNUNET_NO; + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (rs->total_in.currency, + &rs->a_balance)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (rs->total_in.currency, + &rs->a_withdraw_fee_balance)); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Creating fresh reserve `%s' with starting balance %s\n", + TALER_B2S (&rs->reserve_pub), + TALER_amount2s (&rs->a_balance)); + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } + rs->had_ri = GNUNET_YES; + if ( (GNUNET_YES != + TALER_amount_cmp_currency (&rs->a_balance, + &rs->a_withdraw_fee_balance)) || + (GNUNET_YES != + TALER_amount_cmp_currency (&rs->total_in, + &rs->a_balance)) ) + { + GNUNET_break (0); + return GNUNET_DB_STATUS_HARD_ERROR; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Auditor remembers reserve `%s' has balance %s\n", + TALER_B2S (&rs->reserve_pub), + TALER_amount2s (&rs->a_balance)); + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; +} + + +/** + * Closure to the various callbacks we make while checking a reserve. + */ +struct ReserveContext +{ + /** + * Map from hash of reserve's public key to a `struct ReserveSummary`. + */ + struct GNUNET_CONTAINER_MultiHashMap *reserves; + + /** + * Map from hash of denomination's public key to a + * static string "revoked" for keys that have been revoked, + * or "master signature invalid" in case the revocation is + * there but bogus. + */ + struct GNUNET_CONTAINER_MultiHashMap *revoked; + + /** + * Transaction status code, set to error codes if applicable. + */ + enum GNUNET_DB_QueryStatus qs; + +}; + + +/** + * Function called with details about incoming wire transfers. + * + * @param cls our `struct ReserveContext` + * @param rowid unique serial ID for the refresh session in our DB + * @param reserve_pub public key of the reserve (also the WTID) + * @param credit amount that was received + * @param sender_account_details information about the sender's bank account + * @param wire_reference unique reference identifying the wire transfer + * @param execution_date when did we receive the funds + * @return #GNUNET_OK to continue to iterate, #GNUNET_SYSERR to stop + */ +static int +handle_reserve_in (void *cls, + uint64_t rowid, + const struct TALER_ReservePublicKeyP *reserve_pub, + const struct TALER_Amount *credit, + const char *sender_account_details, + uint64_t wire_reference, + struct GNUNET_TIME_Absolute execution_date) +{ + struct ReserveContext *rc = cls; + struct GNUNET_HashCode key; + struct ReserveSummary *rs; + struct GNUNET_TIME_Absolute expiry; + enum GNUNET_DB_QueryStatus qs; + + (void) wire_reference; + /* should be monotonically increasing */ + GNUNET_assert (rowid >= ppr.last_reserve_in_serial_id); + ppr.last_reserve_in_serial_id = rowid + 1; + + GNUNET_CRYPTO_hash (reserve_pub, + sizeof (*reserve_pub), + &key); + rs = GNUNET_CONTAINER_multihashmap_get (rc->reserves, + &key); + if (NULL == rs) + { + rs = GNUNET_new (struct ReserveSummary); + rs->sender_account = GNUNET_strdup (sender_account_details); + rs->reserve_pub = *reserve_pub; + rs->total_in = *credit; + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (credit->currency, + &rs->total_out)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (credit->currency, + &rs->total_fee)); + if (0 > (qs = load_auditor_reserve_summary (rs))) + { + GNUNET_break (0); + GNUNET_free (rs); + rc->qs = qs; + return GNUNET_SYSERR; + } + GNUNET_assert (GNUNET_OK == + GNUNET_CONTAINER_multihashmap_put (rc->reserves, + &key, + rs, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); + } + else + { + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&rs->total_in, + &rs->total_in, + credit)); + if (NULL == rs->sender_account) + rs->sender_account = GNUNET_strdup (sender_account_details); + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Additional incoming wire transfer for reserve `%s' of %s\n", + TALER_B2S (reserve_pub), + TALER_amount2s (credit)); + expiry = GNUNET_TIME_absolute_add (execution_date, + idle_reserve_expiration_time); + rs->a_expiration_date = GNUNET_TIME_absolute_max (rs->a_expiration_date, + expiry); + return GNUNET_OK; +} + + +/** + * Function called with details about withdraw operations. Verifies + * the signature and updates the reserve's balance. + * + * @param cls our `struct ReserveContext` + * @param rowid unique serial ID for the refresh session in our DB + * @param h_blind_ev blinded hash of the coin's public key + * @param denom_pub public denomination key of the deposited coin + * @param reserve_pub public key of the reserve + * @param reserve_sig signature over the withdraw operation + * @param execution_date when did the wallet withdraw the coin + * @param amount_with_fee amount that was withdrawn + * @return #GNUNET_OK to continue to iterate, #GNUNET_SYSERR to stop + */ +static int +handle_reserve_out (void *cls, + uint64_t rowid, + const struct GNUNET_HashCode *h_blind_ev, + const struct TALER_DenominationPublicKey *denom_pub, + const struct TALER_ReservePublicKeyP *reserve_pub, + const struct TALER_ReserveSignatureP *reserve_sig, + struct GNUNET_TIME_Absolute execution_date, + const struct TALER_Amount *amount_with_fee) +{ + struct ReserveContext *rc = cls; + struct TALER_WithdrawRequestPS wsrd; + struct GNUNET_HashCode key; + struct ReserveSummary *rs; + const struct TALER_DenominationKeyValidityPS *issue; + struct TALER_Amount withdraw_fee; + struct GNUNET_TIME_Absolute valid_start; + struct GNUNET_TIME_Absolute expire_withdraw; + enum GNUNET_DB_QueryStatus qs; + + /* should be monotonically increasing */ + GNUNET_assert (rowid >= ppr.last_reserve_out_serial_id); + ppr.last_reserve_out_serial_id = rowid + 1; + + /* lookup denomination pub data (make sure denom_pub is valid, establish fees) */ + qs = get_denomination_info (denom_pub, + &issue, + &wsrd.h_denomination_pub); + if (0 > qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Hard database error trying to get denomination %s (%s) from database!\n", + TALER_B2S (denom_pub), + TALER_amount2s (amount_with_fee)); + rc->qs = qs; + return GNUNET_SYSERR; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + report_row_inconsistency ("withdraw", + rowid, + "denomination key not found"); + return GNUNET_OK; + } + + /* check that execution date is within withdraw range for denom_pub */ + valid_start = GNUNET_TIME_absolute_ntoh (issue->start); + expire_withdraw = GNUNET_TIME_absolute_ntoh (issue->expire_withdraw); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Checking withdraw timing: %llu, expire: %llu, timing: %llu\n", + (unsigned long long) valid_start.abs_value_us, + (unsigned long long) expire_withdraw.abs_value_us, + (unsigned long long) execution_date.abs_value_us); + if ( (valid_start.abs_value_us > execution_date.abs_value_us) || + (expire_withdraw.abs_value_us < execution_date.abs_value_us) ) + { + report (denomination_key_validity_withdraw_inconsistencies, + json_pack ("{s:I, s:o, s:o, s:o}", + "row", (json_int_t) rowid, + "execution_date", + json_from_time_abs (execution_date), + "reserve_pub", GNUNET_JSON_from_data_auto (reserve_pub), + "denompub_h", GNUNET_JSON_from_data_auto ( + &wsrd.h_denomination_pub))); + } + + /* check reserve_sig */ + wsrd.purpose.purpose = htonl (TALER_SIGNATURE_WALLET_RESERVE_WITHDRAW); + wsrd.purpose.size = htonl (sizeof (wsrd)); + wsrd.reserve_pub = *reserve_pub; + TALER_amount_hton (&wsrd.amount_with_fee, + amount_with_fee); + wsrd.withdraw_fee = issue->fee_withdraw; + wsrd.h_coin_envelope = *h_blind_ev; + if (GNUNET_OK != + GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_WALLET_RESERVE_WITHDRAW, + &wsrd.purpose, + &reserve_sig->eddsa_signature, + &reserve_pub->eddsa_pub)) + { + report (report_bad_sig_losses, + json_pack ("{s:s, s:I, s:o, s:o}", + "operation", "withdraw", + "row", (json_int_t) rowid, + "loss", TALER_JSON_from_amount (amount_with_fee), + "key_pub", GNUNET_JSON_from_data_auto (reserve_pub))); + GNUNET_break (GNUNET_OK == + TALER_amount_add (&total_bad_sig_loss, + &total_bad_sig_loss, + amount_with_fee)); + return GNUNET_OK; + } + + GNUNET_CRYPTO_hash (reserve_pub, + sizeof (*reserve_pub), + &key); + rs = GNUNET_CONTAINER_multihashmap_get (rc->reserves, + &key); + if (NULL == rs) + { + rs = GNUNET_new (struct ReserveSummary); + rs->reserve_pub = *reserve_pub; + rs->total_out = *amount_with_fee; + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (amount_with_fee->currency, + &rs->total_in)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (amount_with_fee->currency, + &rs->total_fee)); + qs = load_auditor_reserve_summary (rs); + if (0 > qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + GNUNET_free (rs); + rc->qs = qs; + return GNUNET_SYSERR; + } + GNUNET_assert (GNUNET_OK == + GNUNET_CONTAINER_multihashmap_put (rc->reserves, + &key, + rs, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); + } + else + { + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&rs->total_out, + &rs->total_out, + amount_with_fee)); + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Reserve `%s' reduced by %s from withdraw\n", + TALER_B2S (reserve_pub), + TALER_amount2s (amount_with_fee)); + TALER_amount_ntoh (&withdraw_fee, + &issue->fee_withdraw); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Increasing withdraw profits by fee %s\n", + TALER_amount2s (&withdraw_fee)); + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&rs->total_fee, + &rs->total_fee, + &withdraw_fee)); + + return GNUNET_OK; +} + + +/** + * Function called with details about withdraw operations. Verifies + * the signature and updates the reserve's balance. + * + * @param cls our `struct ReserveContext` + * @param rowid unique serial ID for the refresh session in our DB + * @param timestamp when did we receive the recoup request + * @param amount how much should be added back to the reserve + * @param reserve_pub public key of the reserve + * @param coin public information about the coin, denomination signature is + * already verified in #check_recoup() + * @param denom_pub public key of the denomionation of @a coin + * @param coin_sig signature with @e coin_pub of type #TALER_SIGNATURE_WALLET_COIN_RECOUP + * @param coin_blind blinding factor used to blind the coin + * @return #GNUNET_OK to continue to iterate, #GNUNET_SYSERR to stop + */ +static int +handle_recoup_by_reserve (void *cls, + uint64_t rowid, + struct GNUNET_TIME_Absolute timestamp, + const struct TALER_Amount *amount, + const struct TALER_ReservePublicKeyP *reserve_pub, + const struct TALER_CoinPublicInfo *coin, + const struct TALER_DenominationPublicKey *denom_pub, + const struct TALER_CoinSpendSignatureP *coin_sig, + const struct + TALER_DenominationBlindingKeyP *coin_blind) +{ + struct ReserveContext *rc = cls; + struct GNUNET_HashCode key; + struct ReserveSummary *rs; + struct GNUNET_TIME_Absolute expiry; + struct TALER_RecoupRequestPS pr; + struct TALER_MasterSignatureP msig; + uint64_t rev_rowid; + enum GNUNET_DB_QueryStatus qs; + const char *rev; + + (void) denom_pub; + /* should be monotonically increasing */ + GNUNET_assert (rowid >= ppr.last_reserve_recoup_serial_id); + ppr.last_reserve_recoup_serial_id = rowid + 1; + /* We know that denom_pub matches denom_pub_hash because this + is how the SQL statement joined the tables. */ + pr.h_denom_pub = coin->denom_pub_hash; + pr.purpose.purpose = htonl (TALER_SIGNATURE_WALLET_COIN_RECOUP); + pr.purpose.size = htonl (sizeof (pr)); + pr.coin_pub = coin->coin_pub; + pr.coin_blind = *coin_blind; + if (GNUNET_OK != + GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_WALLET_COIN_RECOUP, + &pr.purpose, + &coin_sig->eddsa_signature, + &coin->coin_pub.eddsa_pub)) + { + report (report_bad_sig_losses, + json_pack ("{s:s, s:I, s:o, s:o}", + "operation", "recoup", + "row", (json_int_t) rowid, + "loss", TALER_JSON_from_amount (amount), + "key_pub", GNUNET_JSON_from_data_auto ( + &coin->coin_pub))); + GNUNET_break (GNUNET_OK == + TALER_amount_add (&total_bad_sig_loss, + &total_bad_sig_loss, + amount)); + } + + /* check that the coin was eligible for recoup!*/ + rev = GNUNET_CONTAINER_multihashmap_get (rc->revoked, + &pr.h_denom_pub); + if (NULL == rev) + { + qs = edb->get_denomination_revocation (edb->cls, + esession, + &pr.h_denom_pub, + &msig, + &rev_rowid); + if (0 > qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + rc->qs = qs; + return GNUNET_SYSERR; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + report_row_inconsistency ("recoup", + rowid, + "denomination key not in revocation set"); + GNUNET_break (GNUNET_OK == + TALER_amount_add (&total_irregular_recoups, + &total_irregular_recoups, + amount)); + } + else + { + /* verify msig */ + struct TALER_MasterDenominationKeyRevocationPS kr; + + kr.purpose.purpose = htonl ( + TALER_SIGNATURE_MASTER_DENOMINATION_KEY_REVOKED); + kr.purpose.size = htonl (sizeof (kr)); + kr.h_denom_pub = pr.h_denom_pub; + if (GNUNET_OK != + GNUNET_CRYPTO_eddsa_verify ( + TALER_SIGNATURE_MASTER_DENOMINATION_KEY_REVOKED, + &kr.purpose, + &msig.eddsa_signature, + &master_pub.eddsa_pub)) + { + rev = "master signature invalid"; + } + else + { + rev = "revoked"; + } + GNUNET_assert (GNUNET_OK == + GNUNET_CONTAINER_multihashmap_put (rc->revoked, + &pr.h_denom_pub, + (void *) rev, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); + } + } + else + { + rev_rowid = 0; /* reported elsewhere */ + } + if ( (NULL != rev) && + (0 == strcmp (rev, "master signature invalid")) ) + { + report (report_bad_sig_losses, + json_pack ("{s:s, s:I, s:o, s:o}", + "operation", "recoup-master", + "row", (json_int_t) rev_rowid, + "loss", TALER_JSON_from_amount (amount), + "key_pub", GNUNET_JSON_from_data_auto (&master_pub))); + GNUNET_break (GNUNET_OK == + TALER_amount_add (&total_bad_sig_loss, + &total_bad_sig_loss, + amount)); + } + + GNUNET_CRYPTO_hash (reserve_pub, + sizeof (*reserve_pub), + &key); + rs = GNUNET_CONTAINER_multihashmap_get (rc->reserves, + &key); + if (NULL == rs) + { + rs = GNUNET_new (struct ReserveSummary); + rs->reserve_pub = *reserve_pub; + rs->total_in = *amount; + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (amount->currency, + &rs->total_out)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (amount->currency, + &rs->total_fee)); + qs = load_auditor_reserve_summary (rs); + if (0 > qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + GNUNET_free (rs); + rc->qs = qs; + return GNUNET_SYSERR; + } + GNUNET_assert (GNUNET_OK == + GNUNET_CONTAINER_multihashmap_put (rc->reserves, + &key, + rs, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); + } + else + { + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&rs->total_in, + &rs->total_in, + amount)); + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Additional /recoup value to for reserve `%s' of %s\n", + TALER_B2S (reserve_pub), + TALER_amount2s (amount)); + expiry = GNUNET_TIME_absolute_add (timestamp, + idle_reserve_expiration_time); + rs->a_expiration_date = GNUNET_TIME_absolute_max (rs->a_expiration_date, + expiry); + return GNUNET_OK; +} + + +/** + * Obtain the closing fee for a transfer at @a time for target + * @a receiver_account. + * + * @param receiver_account payto:// URI of the target account + * @param atime when was the transfer made + * @param[out] fee set to the closing fee + * @return #GNUNET_OK on success + */ +static int +get_closing_fee (const char *receiver_account, + struct GNUNET_TIME_Absolute atime, + struct TALER_Amount *fee) +{ + struct TALER_MasterSignatureP master_sig; + struct GNUNET_TIME_Absolute start_date; + struct GNUNET_TIME_Absolute end_date; + struct TALER_Amount wire_fee; + char *method; + + method = TALER_payto_get_method (receiver_account); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Method is `%s'\n", + method); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + edb->get_wire_fee (edb->cls, + esession, + method, + atime, + &start_date, + &end_date, + &wire_fee, + fee, + &master_sig)) + { + report_row_inconsistency ("closing-fee", + atime.abs_value_us, + "closing fee unavailable at given time"); + GNUNET_free (method); + return GNUNET_SYSERR; + } + GNUNET_free (method); + return GNUNET_OK; +} + + +/** + * Function called about reserve closing operations + * the aggregator triggered. + * + * @param cls closure + * @param rowid row identifier used to uniquely identify the reserve closing operation + * @param execution_date when did we execute the close operation + * @param amount_with_fee how much did we debit the reserve + * @param closing_fee how much did we charge for closing the reserve + * @param reserve_pub public key of the reserve + * @param receiver_account where did we send the funds + * @param transfer_details details about the wire transfer + * @return #GNUNET_OK to continue to iterate, #GNUNET_SYSERR to stop + */ +static int +handle_reserve_closed (void *cls, + uint64_t rowid, + struct GNUNET_TIME_Absolute execution_date, + const struct TALER_Amount *amount_with_fee, + const struct TALER_Amount *closing_fee, + const struct TALER_ReservePublicKeyP *reserve_pub, + const char *receiver_account, + const struct + TALER_WireTransferIdentifierRawP *transfer_details) +{ + struct ReserveContext *rc = cls; + struct GNUNET_HashCode key; + struct ReserveSummary *rs; + enum GNUNET_DB_QueryStatus qs; + + (void) transfer_details; + /* should be monotonically increasing */ + GNUNET_assert (rowid >= ppr.last_reserve_close_serial_id); + ppr.last_reserve_close_serial_id = rowid + 1; + + GNUNET_CRYPTO_hash (reserve_pub, + sizeof (*reserve_pub), + &key); + rs = GNUNET_CONTAINER_multihashmap_get (rc->reserves, + &key); + if (NULL == rs) + { + rs = GNUNET_new (struct ReserveSummary); + rs->reserve_pub = *reserve_pub; + rs->total_out = *amount_with_fee; + rs->total_fee = *closing_fee; + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (amount_with_fee->currency, + &rs->total_in)); + qs = load_auditor_reserve_summary (rs); + if (0 > qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + GNUNET_free (rs); + rc->qs = qs; + return GNUNET_SYSERR; + } + GNUNET_assert (GNUNET_OK == + GNUNET_CONTAINER_multihashmap_put (rc->reserves, + &key, + rs, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); + } + else + { + struct TALER_Amount expected_fee; + + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&rs->total_out, + &rs->total_out, + amount_with_fee)); + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&rs->total_fee, + &rs->total_fee, + closing_fee)); + /* verify closing_fee is correct! */ + if (GNUNET_OK != + get_closing_fee (receiver_account, + execution_date, + &expected_fee)) + { + GNUNET_break (0); + } + else if (0 != TALER_amount_cmp (&expected_fee, + closing_fee)) + { + report_amount_arithmetic_inconsistency ("closing aggregation fee", + rowid, + closing_fee, + &expected_fee, + 1); + } + } + if (NULL == rs->sender_account) + { + GNUNET_break (GNUNET_NO == rs->had_ri); + report_row_inconsistency ("reserves_close", + rowid, + "target account not verified, auditor does not know reserve"); + } + else if (0 != strcmp (rs->sender_account, + receiver_account)) + { + report_row_inconsistency ("reserves_close", + rowid, + "target account does not match origin account"); + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Additional closing operation for reserve `%s' of %s\n", + TALER_B2S (reserve_pub), + TALER_amount2s (amount_with_fee)); + return GNUNET_OK; +} + + +/** + * Check that the reserve summary matches what the exchange database + * thinks about the reserve, and update our own state of the reserve. + * + * Remove all reserves that we are happy with from the DB. + * + * @param cls our `struct ReserveContext` + * @param key hash of the reserve public key + * @param value a `struct ReserveSummary` + * @return #GNUNET_OK to process more entries + */ +static int +verify_reserve_balance (void *cls, + const struct GNUNET_HashCode *key, + void *value) +{ + struct ReserveContext *rc = cls; + struct ReserveSummary *rs = value; + struct TALER_EXCHANGEDB_Reserve reserve; + struct TALER_Amount balance; + struct TALER_Amount nbalance; + struct TALER_Amount cfee; + enum GNUNET_DB_QueryStatus qs; + int ret; + + ret = GNUNET_OK; + reserve.pub = rs->reserve_pub; + qs = edb->reserves_get (edb->cls, + esession, + &reserve); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + { + char *diag; + + GNUNET_asprintf (&diag, + "Failed to find summary for reserve `%s'\n", + TALER_B2S (&rs->reserve_pub)); + report_row_inconsistency ("reserve-summary", + UINT64_MAX, + diag); + GNUNET_free (diag); + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + GNUNET_break (0); + qs = GNUNET_DB_STATUS_HARD_ERROR; + } + rc->qs = qs; + return GNUNET_OK; + } + + if (GNUNET_OK != + TALER_amount_add (&balance, + &rs->total_in, + &rs->a_balance)) + { + GNUNET_break (0); + goto cleanup; + } + + if (GNUNET_SYSERR == + TALER_amount_subtract (&nbalance, + &balance, + &rs->total_out)) + { + struct TALER_Amount loss; + + GNUNET_break (GNUNET_SYSERR != + TALER_amount_subtract (&loss, + &rs->total_out, + &balance)); + GNUNET_break (GNUNET_OK == + TALER_amount_add (&total_balance_insufficient_loss, + &total_balance_insufficient_loss, + &loss)); + report (report_reserve_balance_insufficient_inconsistencies, + json_pack ("{s:o, s:o}", + "reserve_pub", + GNUNET_JSON_from_data_auto (&rs->reserve_pub), + "loss", + TALER_JSON_from_amount (&loss))); + goto cleanup; + } + if (0 != TALER_amount_cmp (&nbalance, + &reserve.balance)) + { + struct TALER_Amount delta; + + if (0 < TALER_amount_cmp (&nbalance, + &reserve.balance)) + { + /* balance > reserve.balance */ + GNUNET_assert (GNUNET_OK == + TALER_amount_subtract (&delta, + &nbalance, + &reserve.balance)); + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&total_balance_summary_delta_plus, + &total_balance_summary_delta_plus, + &delta)); + } + else + { + /* balance < reserve.balance */ + GNUNET_assert (GNUNET_OK == + TALER_amount_subtract (&delta, + &reserve.balance, + &nbalance)); + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&total_balance_summary_delta_minus, + &total_balance_summary_delta_minus, + &delta)); + } + report (report_reserve_balance_summary_wrong_inconsistencies, + json_pack ("{s:o, s:o, s:o}", + "reserve_pub", + GNUNET_JSON_from_data_auto (&rs->reserve_pub), + "exchange", + TALER_JSON_from_amount (&reserve.balance), + "auditor", + TALER_JSON_from_amount (&nbalance))); + goto cleanup; + } + + /* Check that reserve is being closed if it is past its expiration date */ + + if (CLOSING_GRACE_PERIOD.rel_value_us < + GNUNET_TIME_absolute_get_duration (rs->a_expiration_date).rel_value_us) + { + if ( (NULL != rs->sender_account) && + (GNUNET_OK == + get_closing_fee (rs->sender_account, + rs->a_expiration_date, + &cfee)) ) + { + if (1 == TALER_amount_cmp (&nbalance, + &cfee)) + { + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&total_balance_reserve_not_closed, + &total_balance_reserve_not_closed, + &nbalance)); + report (report_reserve_not_closed_inconsistencies, + json_pack ("{s:o, s:o, s:o}", + "reserve_pub", + GNUNET_JSON_from_data_auto (&rs->reserve_pub), + "balance", + TALER_JSON_from_amount (&nbalance), + "expiration_time", + json_from_time_abs (rs->a_expiration_date))); + } + } + else + { + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&total_balance_reserve_not_closed, + &total_balance_reserve_not_closed, + &nbalance)); + report (report_reserve_not_closed_inconsistencies, + json_pack ("{s:o, s:o, s:o, s:s}", + "reserve_pub", + GNUNET_JSON_from_data_auto (&rs->reserve_pub), + "balance", + TALER_JSON_from_amount (&nbalance), + "expiration_time", + json_from_time_abs (rs->a_expiration_date), + "diagnostic", + "could not determine closing fee")); + } + } + + /* Add withdraw fees we encountered to totals */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Reserve reserve `%s' made %s in withdraw fees\n", + TALER_B2S (&rs->reserve_pub), + TALER_amount2s (&rs->total_fee)); + if (GNUNET_YES != + TALER_amount_add (&rs->a_withdraw_fee_balance, + &rs->a_withdraw_fee_balance, + &rs->total_fee)) + { + GNUNET_break (0); + ret = GNUNET_SYSERR; + goto cleanup; + } + if ( (GNUNET_YES != + TALER_amount_add (&total_escrow_balance, + &total_escrow_balance, + &rs->total_in)) || + (GNUNET_SYSERR == + TALER_amount_subtract (&total_escrow_balance, + &total_escrow_balance, + &rs->total_out)) || + (GNUNET_YES != + TALER_amount_add (&total_withdraw_fee_income, + &total_withdraw_fee_income, + &rs->total_fee)) ) + { + GNUNET_break (0); + ret = GNUNET_SYSERR; + goto cleanup; + } + + if ( (0ULL == balance.value) && + (0U == balance.fraction) ) + { + /* balance is zero, drop reserve details (and then do not update/insert) */ + if (rs->had_ri) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Final balance of reserve `%s' is %s, dropping it\n", + TALER_B2S (&rs->reserve_pub), + TALER_amount2s (&nbalance)); + qs = adb->del_reserve_info (adb->cls, + asession, + &rs->reserve_pub, + &master_pub); + if (0 >= qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + ret = GNUNET_SYSERR; + rc->qs = qs; + goto cleanup; + } + } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Final balance of reserve `%s' is %s, no need to remember it\n", + TALER_B2S (&rs->reserve_pub), + TALER_amount2s (&nbalance)); + } + ret = GNUNET_OK; + goto cleanup; + } + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Remembering final balance of reserve `%s' as %s\n", + TALER_B2S (&rs->reserve_pub), + TALER_amount2s (&nbalance)); + + if (rs->had_ri) + qs = adb->update_reserve_info (adb->cls, + asession, + &rs->reserve_pub, + &master_pub, + &nbalance, + &rs->a_withdraw_fee_balance, + rs->a_expiration_date); + else + qs = adb->insert_reserve_info (adb->cls, + asession, + &rs->reserve_pub, + &master_pub, + &nbalance, + &rs->a_withdraw_fee_balance, + rs->a_expiration_date, + rs->sender_account); + if (0 >= qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + ret = GNUNET_SYSERR; + rc->qs = qs; + } +cleanup: + GNUNET_assert (GNUNET_YES == + GNUNET_CONTAINER_multihashmap_remove (rc->reserves, + key, + rs)); + GNUNET_free_non_null (rs->sender_account); + GNUNET_free (rs); + return ret; +} + + +/** + * Analyze reserves for being well-formed. + * + * @param cls NULL + * @return transaction status code + */ +static enum GNUNET_DB_QueryStatus +analyze_reserves (void *cls) +{ + struct ReserveContext rc; + enum GNUNET_DB_QueryStatus qsx; + enum GNUNET_DB_QueryStatus qs; + enum GNUNET_DB_QueryStatus qsp; + + (void) cls; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Analyzing reserves\n"); + qsp = adb->get_auditor_progress_reserve (adb->cls, + asession, + &master_pub, + &ppr); + if (0 > qsp) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qsp); + return qsp; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qsp) + { + GNUNET_log (GNUNET_ERROR_TYPE_MESSAGE, + _ ( + "First analysis using this auditor, starting audit from scratch\n")); + } + else + { + ppr_start = ppr; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + _ ("Resuming reserve audit at %llu/%llu/%llu/%llu\n"), + (unsigned long long) ppr.last_reserve_in_serial_id, + (unsigned long long) ppr.last_reserve_out_serial_id, + (unsigned long long) ppr.last_reserve_recoup_serial_id, + (unsigned long long) ppr.last_reserve_close_serial_id); + } + rc.qs = GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; + qsx = adb->get_reserve_summary (adb->cls, + asession, + &master_pub, + &total_escrow_balance, + &total_withdraw_fee_income); + if (qsx < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qsx); + return qsx; + } + rc.reserves = GNUNET_CONTAINER_multihashmap_create (512, + GNUNET_NO); + rc.revoked = GNUNET_CONTAINER_multihashmap_create (4, + GNUNET_NO); + + qs = edb->select_reserves_in_above_serial_id (edb->cls, + esession, + ppr.last_reserve_in_serial_id, + &handle_reserve_in, + &rc); + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + qs = edb->select_withdrawals_above_serial_id (edb->cls, + esession, + ppr.last_reserve_out_serial_id, + &handle_reserve_out, + &rc); + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + qs = edb->select_recoup_above_serial_id (edb->cls, + esession, + ppr.last_reserve_recoup_serial_id, + &handle_recoup_by_reserve, + &rc); + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + qs = edb->select_reserve_closed_above_serial_id (edb->cls, + esession, + ppr. + last_reserve_close_serial_id, + &handle_reserve_closed, + &rc); + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + + GNUNET_CONTAINER_multihashmap_iterate (rc.reserves, + &verify_reserve_balance, + &rc); + GNUNET_break (0 == + GNUNET_CONTAINER_multihashmap_size (rc.reserves)); + GNUNET_CONTAINER_multihashmap_destroy (rc.reserves); + GNUNET_CONTAINER_multihashmap_destroy (rc.revoked); + + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != rc.qs) + return qs; + + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qsx) + { + qs = adb->insert_reserve_summary (adb->cls, + asession, + &master_pub, + &total_escrow_balance, + &total_withdraw_fee_income); + } + else + { + qs = adb->update_reserve_summary (adb->cls, + asession, + &master_pub, + &total_escrow_balance, + &total_withdraw_fee_income); + } + if (0 >= qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qsp) + qs = adb->update_auditor_progress_reserve (adb->cls, + asession, + &master_pub, + &ppr); + else + qs = adb->insert_auditor_progress_reserve (adb->cls, + asession, + &master_pub, + &ppr); + if (0 >= qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Failed to update auditor DB, not recording progress\n"); + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + _ ("Concluded reserve audit step at %llu/%llu/%llu/%llu\n"), + (unsigned long long) ppr.last_reserve_in_serial_id, + (unsigned long long) ppr.last_reserve_out_serial_id, + (unsigned long long) ppr.last_reserve_recoup_serial_id, + (unsigned long long) ppr.last_reserve_close_serial_id); + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; +} + + +/** + * Main function that will be run. + * + * @param cls closure + * @param args remaining command-line arguments + * @param cfgfile name of the configuration file used (for saving, can be NULL!) + * @param c configuration + */ +static void +run (void *cls, + char *const *args, + const char *cfgfile, + const struct GNUNET_CONFIGURATION_Handle *c) +{ + json_t *report; + + (void) cls; + (void) args; + (void) cfgfile; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Launching auditor\n"); + if (GNUNET_OK != + setup_globals (cfg)) + { + global_ret = 1; + return; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_time (cfg, + "exchangedb", + "IDLE_RESERVE_EXPIRATION_TIME", + &idle_reserve_expiration_time)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "exchangedb", + "IDLE_RESERVE_EXPIRATION_TIME"); + global_ret = 1; + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Starting audit\n"); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_escrow_balance)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_irregular_recoups)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_withdraw_fee_income)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_balance_insufficient_loss)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_balance_summary_delta_plus)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_balance_summary_delta_minus)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_arithmetic_delta_plus)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_arithmetic_delta_minus)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_balance_reserve_not_closed)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (currency, + &total_bad_sig_loss)); + GNUNET_assert (NULL != + (report_row_inconsistencies = json_array ())); + GNUNET_assert (NULL != + (denomination_key_validity_withdraw_inconsistencies = + json_array ())); + GNUNET_assert (NULL != + (report_reserve_balance_summary_wrong_inconsistencies = + json_array ())); + GNUNET_assert (NULL != + (report_reserve_balance_insufficient_inconsistencies = + json_array ())); + GNUNET_assert (NULL != + (report_reserve_not_closed_inconsistencies = json_array ())); + GNUNET_assert (NULL != + (report_amount_arithmetic_inconsistencies = json_array ())); + GNUNET_assert (NULL != + (report_bad_sig_losses = json_array ())); + if (GNUNET_OK != + setup_sessions_and_run (&analyze_reserves, + NULL)) + { + global_ret = 1; + return; + } + report = json_pack ("{s:o, s:o, s:o, s:o, s:o," + " s:o, s:o, s:o, s:o, s:o," + " s:o, s:o, s:o, s:o, s:o," + " s:o, s:o, s:o, s:o, s:I," + " s:I, s:I, s:I, s:I, s:I," + " s:I, s:I }", + /* blocks #1 */ + "reserve_balance_insufficient_inconsistencies", + report_reserve_balance_insufficient_inconsistencies, + /* Tested in test-auditor.sh #3 */ + "total_loss_balance_insufficient", + TALER_JSON_from_amount (&total_balance_insufficient_loss), + /* Tested in test-auditor.sh #3 */ + "reserve_balance_summary_wrong_inconsistencies", + report_reserve_balance_summary_wrong_inconsistencies, + "total_balance_summary_delta_plus", + TALER_JSON_from_amount ( + &total_balance_summary_delta_plus), + "total_balance_summary_delta_minus", + TALER_JSON_from_amount ( + &total_balance_summary_delta_minus), + /* blocks #2 */ + "total_escrow_balance", + TALER_JSON_from_amount (&total_escrow_balance), + "total_withdraw_fee_income", + TALER_JSON_from_amount (&total_withdraw_fee_income), + /* Tested in test-auditor.sh #21 */ + "reserve_not_closed_inconsistencies", + report_reserve_not_closed_inconsistencies, + /* Tested in test-auditor.sh #21 */ + "total_balance_reserve_not_closed", + TALER_JSON_from_amount ( + &total_balance_reserve_not_closed), + /* Tested in test-auditor.sh #4/#5/#6/#7/#13 */ + "bad_sig_losses", + report_bad_sig_losses, + /* blocks #3 */ + /* Tested in test-auditor.sh #4/#5/#6/#7/#13 */ + "total_bad_sig_loss", + TALER_JSON_from_amount (&total_bad_sig_loss), + /* Tested in test-auditor.sh #14/#15 */ + "row_inconsistencies", + report_row_inconsistencies, + /* Tested in test-auditor.sh #23 */ + "denomination_key_validity_withdraw_inconsistencies", + denomination_key_validity_withdraw_inconsistencies, + "amount_arithmetic_inconsistencies", + report_amount_arithmetic_inconsistencies, + "total_arithmetic_delta_plus", + TALER_JSON_from_amount (&total_arithmetic_delta_plus), + /* blocks #4 */ + "total_arithmetic_delta_minus", + TALER_JSON_from_amount (&total_arithmetic_delta_minus), + "auditor_start_time", + json_from_time_abs (start_time), + "auditor_end_time", + json_from_time_abs (GNUNET_TIME_absolute_get ()), + "total_irregular_recoups", + TALER_JSON_from_amount (&total_irregular_recoups), + "start_ppr_reserve_in_serial_id", + (json_int_t) ppr_start.last_reserve_in_serial_id, + /* blocks #5 */ + "start_ppr_reserve_out_serial_id", + (json_int_t) ppr_start.last_reserve_out_serial_id, + "start_ppr_reserve_recoup_serial_id", + (json_int_t) ppr_start.last_reserve_recoup_serial_id, + "start_ppr_reserve_close_serial_id", + (json_int_t) ppr_start.last_reserve_close_serial_id, + "end_ppr_reserve_in_serial_id", + (json_int_t) ppr.last_reserve_in_serial_id, + "end_ppr_reserve_out_serial_id", + (json_int_t) ppr.last_reserve_out_serial_id, + /* blocks #6 */ + "end_ppr_reserve_recoup_serial_id", + (json_int_t) ppr.last_reserve_recoup_serial_id, + "end_ppr_reserve_close_serial_id", + (json_int_t) ppr.last_reserve_close_serial_id + ); + GNUNET_break (NULL != report); + finish_report (report); +} + + +/** + * The main function of the database initialization tool. + * Used to initialize the Taler Exchange's database. + * + * @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) +{ + const struct GNUNET_GETOPT_CommandLineOption options[] = { + GNUNET_GETOPT_option_base32_auto ('m', + "exchange-key", + "KEY", + "public key of the exchange (Crockford base32 encoded)", + &master_pub), + GNUNET_GETOPT_option_flag ('r', + "restart", + "restart audit from the beginning (required on first run)", + &restart), + GNUNET_GETOPT_option_timetravel ('T', + "timetravel"), + GNUNET_GETOPT_OPTION_END + }; + + /* force linker to link against libtalerutil; if we do + not do this, the linker may "optimize" libtalerutil + away and skip #TALER_OS_init(), which we do need */ + (void) TALER_project_data_default (); + GNUNET_assert (GNUNET_OK == + GNUNET_log_setup ("taler-auditor-reserves", + "MESSAGE", + NULL)); + if (GNUNET_OK != + GNUNET_PROGRAM_run (argc, + argv, + "taler-auditor-reserves", + "Audit Taler exchange reserve handling", + options, + &run, + NULL)) + return 1; + return global_ret; +} + + +/* end of taler-auditor-reserves.c */ diff --git a/src/auditor/taler-auditor.c b/src/auditor/taler-auditor.c index d9d91fcfb..dedf828f1 100644 --- a/src/auditor/taler-auditor.c +++ b/src/auditor/taler-auditor.c @@ -18,6 +18,13 @@ * @brief audits an exchange database. * @author Christian Grothoff * + * README-FIRST: + * + * This code is being split up into + * taler-auditor-{aggregation,coins,deposits,reserves}. It is still here as a + * reference, but this file should be obsolete once the split has been + * completed. DO NOT EDIT THIS FILE ANYMORE! -CG + * * NOTE: * - This auditor does not verify that 'reserves_in' actually matches * the wire transfers from the bank. This needs to be checked separately! -- cgit v1.2.3