diff --git a/contrib/auditor-report.tex.j2 b/contrib/auditor-report.tex.j2 index 380733037..75f7c6023 100644 --- a/contrib/auditor-report.tex.j2 +++ b/contrib/auditor-report.tex.j2 @@ -148,6 +148,61 @@ the tiny threshold. Below, we report {\em non-tiny} wire transfers that are lagg {% endif %} + +\section{Deposit confirmation lag} + +This section analyzes the lag, which is by how much the exchange's +database reporting is behind in providing us with information about +deposit confirmations. Merchants probabilisitcally report deposit +confirmations to the auditor directly, so if the exchange is slow at +synchronizing its database with the auditor, some deposit +confirmations may be known at the auditor only directly. However, any +delta not accounted for by database synchronization delays is an +indicator of a malicious exchange (or online singing key compromise) +and should be answered by revoking the exchange's online siging keys. +% FIXME: reference PhD thesis? + +The total amount the exchange currently lags behind is +{\bf {{ data.missing_deposit_confirmation_total.value }}.{{ data.missing_deposit_confirmation_total.fraction }} + {{ data.missing_deposit_confirmation_total.currency }}} or +{\bf {{ data.total_missed_deposit_confirmations}} } deposit confirmations. + +Note that some lag is perfectly normal. +Below, we report {\em all} deposit confirmations that are lagging behind. + +{% if data.deposit_confirmation_inconsistencies|length() == 0 %} + {\bf No deposit confirmations that are lagging behind detected.} +{% else %} + \begin{longtable}{p{1.5cm}|rl|c|rl} + {\bf Timestamp} & {\bf Amount} & {\bf Row} \\ + \multicolumn{3}{l}{\bf Target account} \\ \hline \hline +\endfirsthead + {\bf Timestamp} & {\bf Amount} & {\bf Row} \\ + \multicolumn{3}{l}{\bf Target account} \\ \hline \hline +\endhead + \hline \hline + {\bf Timestamp} & {\bf Amount} & {\bf Row} \\ + \multicolumn{3}{l}{\bf Target account} \\ +\endfoot + \hline \hline + {\bf Timestamp} & {\bf Amount} & {\bf Row} \\ + \multicolumn{3}{l}{\bf Target account} \\ + \caption{Missing deposit confirmations.} + \label{table:missing_dc} +\endlastfoot +{% for item in data.deposit_confirmation_inconsistencies %} + & + {{ item.timestamp }} & + {{ item.amount.value }}.{{ item.amount.fraction }} & + {{ item.amount.currency }} & + {{ item.row }} \\ +\nopagebreak + \multicolumn{3}{l}{ {\tt {{ item.account }} } } \\ \hline +{% endfor %} + \end{longtable} +{% endif %} + + \section{Major irregularities} This section describes the possible major irregularities that the diff --git a/src/auditor/taler-auditor.c b/src/auditor/taler-auditor.c index 8c98c3c22..c1aaa614a 100644 --- a/src/auditor/taler-auditor.c +++ b/src/auditor/taler-auditor.c @@ -182,6 +182,11 @@ static struct TALER_Amount total_balance_reserve_not_closed; */ static json_t *report_wire_out_inconsistencies; +/** + * Array of reports about missing deposit confirmations. + */ +static json_t *report_deposit_confirmation_inconsistencies; + /** * Total delta between calculated and stored wire out transfers, * for positive deltas. @@ -235,6 +240,16 @@ static struct TALER_Amount total_arithmetic_delta_plus; */ static struct TALER_Amount total_arithmetic_delta_minus; +/** + * 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; + /** * Total amount reported in all calls to #report_emergency(). */ @@ -4036,6 +4051,202 @@ analyze_coins (void *cls) } +/* *************************** 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) + 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:s, s:o, s:I, s:o}", + "timestamp", + GNUNET_STRINGS_absolute_time_to_string (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; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "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; + } + 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; +} + + + /* *************************** General transaction logic ****************** */ /** @@ -4151,6 +4362,8 @@ setup_sessions_and_run () NULL); transact (&analyze_coins, NULL); + transact (&analyze_deposit_confirmations, + NULL); } @@ -4347,6 +4560,8 @@ run (void *cls, (report_reserve_not_closed_inconsistencies = json_array ())); GNUNET_assert (NULL != (report_wire_out_inconsistencies = json_array ())); + GNUNET_assert (NULL != + (report_deposit_confirmation_inconsistencies = json_array ())); GNUNET_assert (NULL != (report_coin_inconsistencies = json_array ())); GNUNET_assert (NULL != @@ -4383,7 +4598,8 @@ run (void *cls, " 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:o, s:o, s:I," + " s:o }", /* blocks of 5 for easier counting/matching to format string */ /* block */ "reserve_balance_insufficient_inconsistencies", @@ -4457,7 +4673,14 @@ run (void *cls, "total_refresh_hanging", TALER_JSON_from_amount (&total_refresh_hanging), "refresh_hanging", - report_refreshs_hanging); + report_refreshs_hanging, + "deposit_confirmation_inconsistencies", + report_deposit_confirmation_inconsistencies, + "missing_deposit_confirmation_count", + (json_int_t) number_missed_deposit_confirmations, + /* block */ + "missing_deposit_confirmation_total", + TALER_JSON_from_amount (&total_missed_deposit_confirmations)); GNUNET_break (NULL != report); json_dumpf (report, stdout, diff --git a/src/auditordb/plugin_auditordb_postgres.c b/src/auditordb/plugin_auditordb_postgres.c index 6f2aa2909..aeb96df5d 100644 --- a/src/auditordb/plugin_auditordb_postgres.c +++ b/src/auditordb/plugin_auditordb_postgres.c @@ -390,6 +390,7 @@ postgres_create_tables (void *cls) we must check that the exchange reported these properly. */ GNUNET_PQ_make_execute ("CREATE TABLE IF NOT EXISTS deposit_confirmations " "(master_pub BYTEA CONSTRAINT master_pub_ref REFERENCES auditor_exchanges(master_pub) ON DELETE CASCADE" + ",serial_id BIGSERIAL UNIQUE" ",h_contract_terms BYTEA CHECK (LENGTH(h_contract_terms)=64)" ",h_wire BYTEA CHECK (LENGTH(h_wire)=64)" ",timestamp INT8 NOT NULL" @@ -567,6 +568,25 @@ postgres_prepare (PGconn *db_conn) ",master_sig" /* master_sig could be normalized... */ ") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13);", 11), + /* Used in #postgres_get_deposit_confirmations() */ + GNUNET_PQ_make_prepare ("auditor_deposit_confirmation_select", + "SELECT" + " h_contract_terms" + ",h_wire" + ",timestamp" + ",refund_deadline" + ",amount_without_fee_val" + ",amount_without_fee_frac" + ",amount_without_fee_curr" + ",coin_pub" + ",merchant_pub" + ",exchange_sig" + ",exchange_pub" + ",master_sig" /* master_sig could be normalized... */ + " FROM deposit_confirmations" + " WHERE master_pub=$1" + " AND serial_id>$2", + 2), /* Used in #postgres_update_auditor_progress_reserve() */ GNUNET_PQ_make_prepare ("auditor_progress_update_reserve", "UPDATE auditor_progress_reserve SET " @@ -1416,6 +1436,144 @@ postgres_insert_deposit_confirmation (void *cls, } +/** + * Closure for #deposit_confirmation_cb(). + */ +struct DepositConfirmationContext +{ + + /** + * Master public key that is being used. + */ + const struct TALER_MasterPublicKeyP *master_pub; + + /** + * Function to call for each deposit confirmation. + */ + TALER_AUDITORDB_DepositConfirmationCallback cb; + + /** + * Closure for @e cb + */ + void *cb_cls; + + /** + * Query status to return. + */ + enum GNUNET_DB_QueryStatus qs; +}; + + +/** + * Helper function for #postgres_get_deposit_confirmations(). + * To be called with the results of a SELECT statement + * that has returned @a num_results results. + * + * @param cls closure of type `struct DepositConfirmationContext *` + * @param result the postgres result + * @param num_result the number of results in @a result + */ +static void +deposit_confirmation_cb (void *cls, + PGresult *result, + unsigned int num_results) +{ + struct DepositConfirmationContext *dcc = cls; + + for (unsigned int i = 0; i < num_results; i++) + { + uint64_t serial_id; + struct TALER_AUDITORDB_DepositConfirmation dc = { + .master_public_key = *dcc->master_pub + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_uint64 ("serial_id", + &serial_id), + GNUNET_PQ_result_spec_auto_from_type ("h_contract_terms", + &dc.h_contract_terms), + GNUNET_PQ_result_spec_auto_from_type ("h_wire", + &dc.h_wire), + GNUNET_PQ_result_spec_absolute_time ("timetamp", + &dc.timestamp), + GNUNET_PQ_result_spec_absolute_time ("refund_deadline", + &dc.refund_deadline), + TALER_PQ_result_spec_amount ("amount_without_fee", + &dc.amount_without_fee), + GNUNET_PQ_result_spec_auto_from_type ("coin_pub", + &dc.coin_pub), + GNUNET_PQ_result_spec_auto_from_type ("merchant_pub", + &dc.merchant), + GNUNET_PQ_result_spec_auto_from_type ("exchange_sig", + &dc.exchange_sig), + GNUNET_PQ_result_spec_auto_from_type ("exchange_pub", + &dc.exchange_pub), + GNUNET_PQ_result_spec_auto_from_type ("master_sig", + &dc.master_sig), + GNUNET_PQ_result_spec_end + }; + + if (GNUNET_OK != + GNUNET_PQ_extract_result (result, + rs, + i)) + { + GNUNET_break (0); + dcc->qs = GNUNET_DB_STATUS_HARD_ERROR; + return; + } + dcc->qs = i + 1; + dcc->cb (dcc->cb_cls, + serial_id, + &dc); + } +} + + +/** + * Get information about deposit confirmations from the database. + * + * @param cls the @e cls of this struct with the plugin-specific state + * @param session connection to the database + * @param master_pub for which exchange do we want to get deposit confirmations + * @param start_id row/serial ID where to start the iteration (0 from + * the start, exclusive, i.e. serial_ids must start from 1) + * @param cb function to call with results + * @param cb_cls closure for @a cb + * @return query result status + */ +static enum GNUNET_DB_QueryStatus +postgres_get_deposit_confirmations (void *cls, + struct TALER_AUDITORDB_Session *session, + const struct TALER_MasterPublicKeyP *master_public_key, + uint64_t start_id, + TALER_AUDITORDB_DepositConfirmationCallback cb, + void *cb_cls) +{ + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (master_public_key), + GNUNET_PQ_query_param_uint64 (&start_id), + GNUNET_PQ_query_param_end + }; + struct DepositConfirmationContext dcc = { + .master_pub = master_public_key, + .cb = cb, + .cb_cls = cb_cls + }; + enum GNUNET_DB_QueryStatus qs; + + qs = GNUNET_PQ_eval_prepared_multi_select (session->conn, + "auditor_deposit_confirmation_select", + params, + &deposit_confirmation_cb, + &dcc); + if (qs > 0) + return dcc.qs; + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR != qs); + return qs; +} + + + /** * Insert information about a denomination key and in particular * the properties (value, fees, expiration times) the coins signed @@ -3264,6 +3422,7 @@ libtaler_plugin_auditordb_postgres_init (void *cls) plugin->list_exchanges = &postgres_list_exchanges; plugin->insert_exchange_signkey = &postgres_insert_exchange_signkey; plugin->insert_deposit_confirmation = &postgres_insert_deposit_confirmation; + plugin->get_deposit_confirmations = &postgres_get_deposit_confirmations; plugin->select_denomination_info = &postgres_select_denomination_info; plugin->insert_denomination_info = &postgres_insert_denomination_info; diff --git a/src/exchange/taler-exchange-httpd_deposit.c b/src/exchange/taler-exchange-httpd_deposit.c index 52344d645..3e91218ce 100644 --- a/src/exchange/taler-exchange-httpd_deposit.c +++ b/src/exchange/taler-exchange-httpd_deposit.c @@ -142,7 +142,8 @@ deposit_transaction (void *cls, qs = TEH_plugin->have_deposit (TEH_plugin->cls, session, - deposit); + deposit, + GNUNET_YES /* check refund deadline */); if (qs < 0) { if (GNUNET_DB_STATUS_HARD_ERROR == qs) diff --git a/src/exchangedb/plugin_exchangedb_postgres.c b/src/exchangedb/plugin_exchangedb_postgres.c index 3cd467c01..2ce865ae3 100644 --- a/src/exchangedb/plugin_exchangedb_postgres.c +++ b/src/exchangedb/plugin_exchangedb_postgres.c @@ -2730,6 +2730,7 @@ postgres_get_reserve_history (void *cls, * @param cls the `struct PostgresClosure` with the plugin-specific state * @param session database connection * @param deposit deposit to search for + * @param check_extras wether to check extra fields match or not * @return 1 if we know this operation, * 0 if this exact deposit is unknown to us, * otherwise transaction error status @@ -2737,7 +2738,8 @@ postgres_get_reserve_history (void *cls, static enum GNUNET_DB_QueryStatus postgres_have_deposit (void *cls, struct TALER_EXCHANGEDB_Session *session, - const struct TALER_EXCHANGEDB_Deposit *deposit) + const struct TALER_EXCHANGEDB_Deposit *deposit, + int check_extras) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_auto_from_type (&deposit->coin.coin_pub), @@ -2755,8 +2757,6 @@ postgres_have_deposit (void *cls, &deposit2.refund_deadline), TALER_PQ_result_spec_absolute_time ("wire_deadline", &deposit2.wire_deadline), - GNUNET_PQ_result_spec_auto_from_type ("h_contract_terms", - &deposit2.h_contract_terms), GNUNET_PQ_result_spec_auto_from_type ("h_wire", &deposit2.h_wire), GNUNET_PQ_result_spec_end @@ -2774,18 +2774,16 @@ postgres_have_deposit (void *cls, return qs; /* Now we check that the other information in @a deposit also matches, and if not report inconsistencies. */ - if ( (0 != TALER_amount_cmp (&deposit->amount_with_fee, - &deposit2.amount_with_fee)) || - (deposit->timestamp.abs_value_us != - deposit2.timestamp.abs_value_us) || + if ( ( (check_extras) && + ( (0 != TALER_amount_cmp (&deposit->amount_with_fee, + &deposit2.amount_with_fee)) || + (deposit->timestamp.abs_value_us != + deposit2.timestamp.abs_value_us) ) ) || (deposit->refund_deadline.abs_value_us != - deposit2.refund_deadline.abs_value_us) || - (0 != memcmp (&deposit->h_contract_terms, - &deposit2.h_contract_terms, - sizeof (struct GNUNET_HashCode))) || + deposit2.refund_deadline.abs_value_us) || (0 != memcmp (&deposit->h_wire, - &deposit2.h_wire, - sizeof (struct GNUNET_HashCode))) ) + &deposit2.h_wire, + sizeof (struct GNUNET_HashCode)) ) ) { /* Inconsistencies detected! Does not match! (We might want to expand the API with a 'get_deposit' function to return the diff --git a/src/include/taler_auditordb_plugin.h b/src/include/taler_auditordb_plugin.h index 76dcc3507..92494ab32 100644 --- a/src/include/taler_auditordb_plugin.h +++ b/src/include/taler_auditordb_plugin.h @@ -345,6 +345,20 @@ struct TALER_AUDITORDB_DepositConfirmation }; +/** + * Function called with deposit confirmations stored in + * the auditor's database. + * + * @param cls closure + * @param serial_id location of the @a dc in the database + * @param dc the deposit confirmation itself + */ +typedef void +(*TALER_AUDITORDB_DepositConfirmationCallback)(void *cls, + uint64_t serial_id, + const struct TALER_AUDITORDB_DepositConfirmation *dc); + + /** * Handle for one session with the database. */ @@ -525,6 +539,27 @@ struct TALER_AUDITORDB_Plugin const struct TALER_AUDITORDB_DepositConfirmation *dc); + /** + * Get information about a deposit confirmations from the database. + * + * @param cls the @e cls of this struct with the plugin-specific state + * @param session connection to the database + * @param master_pub for which exchange do we want to get deposit confirmations + * @param start_id row/serial ID where to start the iteration (0 from + * the start, exclusive, i.e. serial_ids must start from 1) + * @param cb function to call with results + * @param cb_cls closure for @a cb + * @return query result status + */ + enum GNUNET_DB_QueryStatus + (*get_deposit_confirmations) (void *cls, + struct TALER_AUDITORDB_Session *session, + const struct TALER_MasterPublicKeyP *master_public_key, + uint64_t start_id, + TALER_AUDITORDB_DepositConfirmationCallback cb, + void *cb_cls); + + /** * Insert information about a denomination key and in particular * the properties (value, fees, expiration times) the coins signed diff --git a/src/include/taler_exchangedb_plugin.h b/src/include/taler_exchangedb_plugin.h index c8417a3a7..89df42190 100644 --- a/src/include/taler_exchangedb_plugin.h +++ b/src/include/taler_exchangedb_plugin.h @@ -1431,6 +1431,7 @@ struct TALER_EXCHANGEDB_Plugin * @param cls the @e cls of this struct with the plugin-specific state * @param session database connection * @param deposit deposit to search for + * @param check_extras wether to check extra fields or not * @return 1 if we know this operation, * 0 if this exact deposit is unknown to us, * otherwise transaction error status @@ -1438,7 +1439,8 @@ struct TALER_EXCHANGEDB_Plugin enum GNUNET_DB_QueryStatus (*have_deposit) (void *cls, struct TALER_EXCHANGEDB_Session *session, - const struct TALER_EXCHANGEDB_Deposit *deposit); + const struct TALER_EXCHANGEDB_Deposit *deposit, + int check_extras); /**