diff --git a/src/auditor/taler-helper-auditor-coins.c b/src/auditor/taler-helper-auditor-coins.c index 9f9f6dc93..5bcf636b7 100644 --- a/src/auditor/taler-helper-auditor-coins.c +++ b/src/auditor/taler-helper-auditor-coins.c @@ -38,7 +38,7 @@ * Set to a VERY low value here for testing. Practical values may be * in the millions. */ -#define MAX_COIN_SUMMARIES 4 +#define MAX_COIN_HISTORIES 4 /** * Use a 1 day grace period to deal with clocks not being perfectly synchronized. @@ -169,6 +169,84 @@ static json_t *report_refreshs_hanging; */ static struct TALER_Amount total_refresh_hanging; +/** + * Coin and associated transaction history. + */ +struct CoinHistory +{ + /** + * Public key of the coin. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; + + /** + * The transaction list for the @a coin_pub. + */ + struct TALER_EXCHANGEDB_TransactionList *tl; +}; + +/** + * Array of transaction histories for coins. The index is based on the coin's + * public key. Entries are replaced whenever we have a collision. + */ +static struct CoinHistory coin_histories[MAX_COIN_HISTORIES]; + + +/** + * Return the index we should use for @a coin_pub in #coin_histories. + * + * @param coin_pub a coin's public key + * @return index for caching this coin's history in #coin_histories + */ +static unsigned int +coin_history_index (const struct TALER_CoinSpendPublicKeyP *coin_pub) +{ + uint32_t i; + + memcpy (&i, + coin_pub, + sizeof (i)); + return i % MAX_COIN_HISTORIES; +} + + +/** + * Add a coin history to our in-memory cache. + * + * @param coin_pub public key of the coin to cache + * @param tl history to store + */ +static void +cache_history (const struct TALER_CoinSpendPublicKeyP *coin_pub, + struct TALER_EXCHANGEDB_TransactionList *tl) +{ + unsigned int i = coin_history_index (coin_pub); + + if (NULL != coin_histories[i].tl) + TALER_ARL_edb->free_coin_transaction_list (TALER_ARL_edb->cls, + coin_histories[i].tl); + coin_histories[i].coin_pub = *coin_pub; + coin_histories[i].tl = tl; +} + + +/** + * Obtain a coin's history from our in-memory cache. + * + * @param coin_pub public key of the coin to cache + * @return NULL if @a coin_pub is not in the cache + */ +static struct TALER_EXCHANGEDB_TransactionList * +get_cached_history (const struct TALER_CoinSpendPublicKeyP *coin_pub) +{ + unsigned int i = coin_history_index (coin_pub); + + if (GNUNET_memcmp (coin_pub, + &coin_histories[i].coin_pub)) + return coin_histories[i].tl; + return NULL; +} + /* ***************************** Report logic **************************** */ @@ -362,6 +440,136 @@ report_row_inconsistency (const char *table, } +/* ************* Analyze history of a coin ******************** */ + + +/** + * Obtain @a coin_pub's history, verify it, report inconsistencies + * and store the result in our cache. + * + * @param coin_pub public key of the coin to check the history of + * @param rowid a row identifying the transaction + * @param operation operation matching @a rowid + * @param value value of the respective coin's denomination + * @return database status code, negative on failures + */ +static enum GNUNET_DB_QueryStatus +check_coin_history (const struct TALER_CoinSpendPublicKeyP *coin_pub, + uint64_t rowid, + const char *operation, + const struct TALER_Amount *value) +{ + struct TALER_EXCHANGEDB_TransactionList *tl; + enum GNUNET_DB_QueryStatus qs; + struct TALER_Amount total; + struct TALER_Amount spent; + struct TALER_Amount refunded; + struct TALER_Amount deposit_fee; + int have_refund; + + qs = TALER_ARL_edb->get_coin_transactions (TALER_ARL_edb->cls, + TALER_ARL_esession, + coin_pub, + GNUNET_YES, + &tl); + if (0 >= qs) + return qs; + + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (value->currency, + &refunded)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (value->currency, + &spent)); + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (value->currency, + &deposit_fee)); + for (struct TALER_EXCHANGEDB_TransactionList *pos = tl; + NULL != pos; + pos = pos->next) + { + switch (pos->type) + { + case TALER_EXCHANGEDB_TT_DEPOSIT: + /* spent += pos->amount_with_fee */ + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&spent, + &spent, + &pos->details.deposit->amount_with_fee)); + deposit_fee = pos->details.deposit->deposit_fee; + break; + case TALER_EXCHANGEDB_TT_MELT: + /* spent += pos->amount_with_fee */ + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&spent, + &spent, + &pos->details.melt->amount_with_fee)); + break; + case TALER_EXCHANGEDB_TT_REFUND: + /* refunded += pos->refund_amount - pos->refund_fee */ + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&refunded, + &refunded, + &pos->details.refund->refund_amount)); + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&spent, + &spent, + &pos->details.refund->refund_fee)); + have_refund = GNUNET_YES; + break; + case TALER_EXCHANGEDB_TT_OLD_COIN_RECOUP: + /* refunded += pos->value */ + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&refunded, + &refunded, + &pos->details.old_coin_recoup->value)); + case TALER_EXCHANGEDB_TT_RECOUP: + /* spent += pos->value */ + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&spent, + &spent, + &pos->details.recoup->value)); + break; + case TALER_EXCHANGEDB_TT_RECOUP_REFRESH: + /* spent += pos->value */ + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&spent, + &spent, + &pos->details.recoup_refresh->value)); + break; + } + } + + if (have_refund) + { + /* If we gave any refund, also discount ONE deposit fee */ + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&refunded, + &refunded, + &deposit_fee)); + } + /* total coin value = original value plus refunds */ + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&total, + &refunded, + value)); + if (1 == + TALER_amount_cmp (&spent, + &total)) + { + /* spent > total: bad */ + report_amount_arithmetic_inconsistency (operation, + rowid, + &spent, + &total, + -1); + } + cache_history (coin_pub, + tl); + return qs; +} + + /* ************************* Analyze coins ******************** */ /* This logic checks that the exchange did the right thing for each coin, checking deposits, refunds, refresh* and known_coins @@ -776,6 +984,7 @@ withdraw_cb (void *cls, (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; @@ -930,9 +1139,12 @@ reveal_data_cb (void *cls, /** * Check that the @a coin_pub is a known coin with a proper - * signature for denominatinon @a denom_pub. If not, TALER_ARL_report + * signature for denominatinon @a denom_pub. If not, report * a loss of @a loss_potential. * + * @param operation which operation is this about + * @param issue denomination key information about the coin + * @param rowid which row is this operation in * @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 @@ -941,13 +1153,35 @@ reveal_data_cb (void *cls, * #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT */ static enum GNUNET_DB_QueryStatus -check_known_coin (const struct TALER_CoinSpendPublicKeyP *coin_pub, +check_known_coin (const char *operation, + const struct TALER_DenominationKeyValidityPS *issue, + uint64_t rowid, + 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; + if (NULL == get_cached_history (coin_pub)) + { + struct TALER_Amount value; + + TALER_amount_ntoh (&value, + &issue->value); + qs = check_coin_history (coin_pub, + rowid, + operation, + &value); + if (0 > qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs); + } + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Checking denomination signature on %s\n", TALER_B2S (coin_pub)); @@ -966,11 +1200,11 @@ check_known_coin (const struct TALER_CoinSpendPublicKeyP *coin_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, + "operation", operation, + "row", (json_int_t) rowid, "loss", TALER_JSON_from_amount ( loss_potential), - "key_pub", GNUNET_JSON_from_data_auto ( + "coin_pub", GNUNET_JSON_from_data_auto ( coin_pub))); GNUNET_assert (GNUNET_OK == TALER_amount_add (&total_bad_sig_loss, @@ -1037,7 +1271,10 @@ refresh_session_cb (void *cls, cc->qs = qs; return GNUNET_SYSERR; } - qs = check_known_coin (coin_pub, + qs = check_known_coin ("melt", + issue, + rowid, + coin_pub, denom_pub, amount_with_fee); if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) @@ -1377,7 +1614,10 @@ deposit_cb (void *cls, cc->qs = qs; return GNUNET_SYSERR; } - qs = check_known_coin (coin_pub, + qs = check_known_coin ("deposit", + issue, + rowid, + coin_pub, denom_pub, amount_with_fee); if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) @@ -1738,6 +1978,18 @@ check_recoup (struct CoinContext *cc, cc->qs = qs; return GNUNET_SYSERR; } + qs = check_known_coin ("recoup", + issue, + rowid, + &coin->coin_pub, + denom_pub, + amount); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + cc->qs = qs; + return GNUNET_SYSERR; + } { struct TALER_RecoupRequestPS pr = { .purpose.purpose = htonl (TALER_SIGNATURE_WALLET_COIN_RECOUP), @@ -1789,6 +2041,10 @@ check_recoup (struct CoinContext *cc, "loss", TALER_JSON_from_amount (amount), "coin_pub", GNUNET_JSON_from_data_auto ( &coin->coin_pub))); + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&total_bad_sig_loss, + &total_bad_sig_loss, + amount)); } GNUNET_assert (GNUNET_OK == TALER_amount_add (&ds->denom_recoup,