/* 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-helper-auditor0coins.c * @brief audits coins in an exchange database. * @author Christian Grothoff * * 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 TALER_ARL_reports about denomination keys with an * emergency (more value deposited than withdrawn) */ static json_t *report_emergencies; /** * Array of TALER_ARL_reports about denomination keys with an * emergency (more coins deposited than withdrawn) */ static json_t *report_emergencies_by_count; /** * Array of TALER_ARL_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 TALER_ARL_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) { TALER_ARL_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", TALER_ARL_json_from_time_abs_nbo ( issue->start), "deposit_end", TALER_ARL_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; TALER_ARL_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", TALER_ARL_json_from_time_abs_nbo ( issue->start), "deposit_end", TALER_ARL_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)); } TALER_ARL_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) { TALER_ARL_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 TALER_ARL_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 = TALER_ARL_adb->get_denomination_balance (TALER_ARL_adb->cls, TALER_ARL_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 = TALER_ARL_edb->get_denomination_revocation (TALER_ARL_edb->cls, TALER_ARL_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, &TALER_ARL_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 (TALER_ARL_currency, &ds->denom_balance)); GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (TALER_ARL_currency, &ds->denom_loss)); GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (TALER_ARL_currency, &ds->denom_risk)); GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (TALER_ARL_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 TALER_ARL_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 = TALER_ARL_adb->del_denomination_balance (TALER_ARL_adb->cls, TALER_ARL_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 = TALER_ARL_adb->insert_historic_denom_revenue ( TALER_ARL_adb->cls, TALER_ARL_asession, & TALER_ARL_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 = TALER_ARL_edb->count_known_coins (TALER_ARL_edb->cls, TALER_ARL_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 = TALER_ARL_adb->update_denomination_balance (TALER_ARL_adb->cls, TALER_ARL_asession, denom_hash, &ds->denom_balance, &ds->denom_loss, &ds->denom_risk, &ds->denom_recoup, ds->num_issued); else qs = TALER_ARL_adb->insert_denomination_balance (TALER_ARL_adb->cls, TALER_ARL_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 = TALER_ARL_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, TALER_ARL_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 = TALER_ARL_edb->get_known_coin (TALER_ARL_edb->cls, TALER_ARL_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)) { TALER_ARL_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 = TALER_ARL_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)) { TALER_ARL_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 = TALER_ARL_edb->get_refresh_reveal (TALER_ARL_edb->cls, TALER_ARL_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 TALER_ARL_report it. */ TALER_ARL_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 = TALER_ARL_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)) { TALER_ARL_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; /* NOTE: This is one of the operations we might eventually want to do in parallel in the background to improve auditor performance! */ if (GNUNET_OK != GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_WALLET_COIN_DEPOSIT, &dr.purpose, &coin_sig->eddsa_signature, &coin_pub->eddsa_pub)) { TALER_ARL_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 = TALER_ARL_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)) { TALER_ARL_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)) { TALER_ARL_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 = TALER_ARL_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)) { TALER_ARL_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!? */ TALER_ARL_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 = TALER_ARL_adb->get_auditor_progress_coin (TALER_ARL_adb->cls, TALER_ARL_asession, &TALER_ARL_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 = TALER_ARL_adb->get_balance_summary (TALER_ARL_adb->cls, TALER_ARL_asession, &TALER_ARL_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 = TALER_ARL_edb->select_withdrawals_above_serial_id ( TALER_ARL_edb->cls, TALER_ARL_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 = TALER_ARL_edb->select_refunds_above_serial_id (TALER_ARL_edb->cls, TALER_ARL_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 = TALER_ARL_edb->select_refreshes_above_serial_id (TALER_ARL_edb->cls, TALER_ARL_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 = TALER_ARL_edb->select_deposits_above_serial_id (TALER_ARL_edb->cls, TALER_ARL_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 = TALER_ARL_edb->select_recoup_above_serial_id (TALER_ARL_edb->cls, TALER_ARL_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 = TALER_ARL_edb->select_recoup_refresh_above_serial_id ( TALER_ARL_edb->cls, TALER_ARL_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 = TALER_ARL_adb->update_balance_summary (TALER_ARL_adb->cls, TALER_ARL_asession, &TALER_ARL_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 = TALER_ARL_adb->insert_balance_summary (TALER_ARL_adb->cls, TALER_ARL_asession, &TALER_ARL_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 = TALER_ARL_adb->update_auditor_progress_coin (TALER_ARL_adb->cls, TALER_ARL_asession, &TALER_ARL_master_pub, &ppc); else qs = TALER_ARL_adb->insert_auditor_progress_coin (TALER_ARL_adb->cls, TALER_ARL_asession, &TALER_ARL_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 TALER_ARL_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 *TALER_ARL_cfgfile, const struct GNUNET_CONFIGURATION_Handle *c) { json_t *report; (void) cls; (void) args; (void) TALER_ARL_cfgfile; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Launching auditor\n"); if (GNUNET_OK != TALER_ARL_init (c)) { global_ret = 1; return; } GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Starting audit\n"); GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (TALER_ARL_currency, &reported_emergency_loss)); GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (TALER_ARL_currency, & reported_emergency_risk_by_amount)); GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (TALER_ARL_currency, & reported_emergency_risk_by_count)); GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (TALER_ARL_currency, & reported_emergency_loss_by_count)); GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (TALER_ARL_currency, &total_escrow_balance)); GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (TALER_ARL_currency, &total_risk)); GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (TALER_ARL_currency, &total_recoup_loss)); GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (TALER_ARL_currency, &total_irregular_recoups)); GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (TALER_ARL_currency, &total_deposit_fee_income)); GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (TALER_ARL_currency, &total_melt_fee_income)); GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (TALER_ARL_currency, &total_refund_fee_income)); GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (TALER_ARL_currency, &total_arithmetic_delta_plus)); GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (TALER_ARL_currency, &total_arithmetic_delta_minus)); GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (TALER_ARL_currency, &total_bad_sig_loss)); GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (TALER_ARL_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 != TALER_ARL_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/#13/#26 */ "bad_sig_losses", report_bad_sig_losses, /* Tested in test-auditor.sh #4/#5/#6/#13/#26 */ "total_bad_sig_loss", TALER_JSON_from_amount (&total_bad_sig_loss), /* FIXME: Tested in test-auditor.sh #?? */ "row_inconsistencies", report_row_inconsistencies, /* Block #3 */ /* Tested in test-auditor.sh #18 */ "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", TALER_ARL_json_from_time_abs ( start_time), "auditor_end_time", TALER_ARL_json_from_time_abs ( GNUNET_TIME_absolute_get ()), "total_irregular_recoups", TALER_JSON_from_amount ( &total_irregular_recoups) ); GNUNET_break (NULL != report); TALER_ARL_done (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)", &TALER_ARL_master_pub), 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-helper-auditor-coins", "MESSAGE", NULL)); if (GNUNET_OK != GNUNET_PROGRAM_run (argc, argv, "taler-helper-auditor-coins", "Audit Taler coin processing", options, &run, NULL)) return 1; return global_ret; } /* end of taler-auditor.c */