From 4b952b30374d73692ecdea7063df4d2e816ebb56 Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Tue, 14 Mar 2017 15:13:50 +0100 Subject: more work on auditor, still very incomplete --- src/auditor/taler-auditor.c | 506 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 448 insertions(+), 58 deletions(-) (limited to 'src/auditor/taler-auditor.c') diff --git a/src/auditor/taler-auditor.c b/src/auditor/taler-auditor.c index 483f30353..51a4eda46 100644 --- a/src/auditor/taler-auditor.c +++ b/src/auditor/taler-auditor.c @@ -17,11 +17,18 @@ * @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 XXX table. This needs to be checked separately! */ #include "platform.h" #include #include "taler_auditordb_plugin.h" #include "taler_exchangedb_plugin.h" +#include "taler_signatures.h" /** @@ -67,7 +74,7 @@ static uint64_t reserve_out_serial_id; /** * Last deposit serial ID seen. */ -static uint64_t last_deposit_serial_id; +static uint64_t deposit_serial_id; /** * Last melt serial ID seen. @@ -77,13 +84,158 @@ static uint64_t melt_serial_id; /** * Last deposit refund ID seen. */ -static uint64_t last_refund_serial_id; +static uint64_t refund_serial_id; /** * Last prewire serial ID seen. */ -static uint64_t last_prewire_serial_id; +static uint64_t prewire_serial_id; + + +/* ***************************** Report logic **************************** */ + +/** + * 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) +{ + // TODO: implement proper reporting logic writing to file. + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Database inconsistency detected in table %s at row %llu: %s\n", + table, + (unsigned long long) rowid, + diagnostic); +} + + +/** + * Report a global inconsistency with respect to a reserve. + * + * @param reserve_pub the affected reserve + * @param expected expected amount + * @param observed observed amount + * @param diagnostic message explaining what @a expected and @a observed refer to + */ +static void +report_reserve_inconsistency (const struct TALER_ReservePublicKeyP *reserve_pub, + const struct TALER_Amount *expected, + const struct TALER_Amount *observed, + const char *diagnostic) +{ + // TODO: implement proper reporting logic writing to file, include amounts. + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Reserve inconsistency detected affecting reserve %s: %s\n", + TALER_B2S (reserve_pub), + diagnostic); +} + + +/* ************************* Transaction-global state ************************ */ + +/** + * Results about denominations, cached per-transaction. + */ +static struct GNUNET_CONTAINER_MultiHashMap *denominations; + + +/** + * Obtain information about a @a denom_pub. + * + * @param denom_pub key to look up + * @param[out] set to the hash of @a denom_pub, may be NULL + * @param[out] dki set to detailed information about @a denom_pub, NULL if not found, must + * NOT be freed by caller + * @return #GNUNET_OK on success, #GNUNET_NO for not found, #GNUNET_SYSERR for DB error + */ +static int +get_denomination_info (const struct TALER_DenominationPublicKey *denom_pub, + const struct TALER_EXCHANGEDB_DenominationKeyInformationP **dki, + struct GNUNET_HashCode *dh) +{ + struct GNUNET_HashCode hc; + struct TALER_EXCHANGEDB_DenominationKeyInformationP *dkip; + int ret; + + if (NULL == dh) + dh = &hc; + GNUNET_CRYPTO_rsa_public_key_hash (denom_pub->rsa_public_key, + dh); + dkip = GNUNET_CONTAINER_multihashmap_get (denominations, + dh); + if (NULL != dkip) + { + /* cache hit */ + *dki = dkip; + return GNUNET_OK; + } + dkip = GNUNET_new (struct TALER_EXCHANGEDB_DenominationKeyInformationP); + ret = edb->get_denomination_info (edb->cls, + esession, + denom_pub, + dkip); + if (GNUNET_OK != ret) + { + GNUNET_free (dkip); + GNUNET_break (GNUNET_NO == ret); + *dki = NULL; + return ret; + } + *dki = dkip; + GNUNET_assert (GNUNET_OK == + GNUNET_CONTAINER_multihashmap_put (denominations, + dh, + dkip, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)); + return GNUNET_OK; +} + + +/** + * Free denomination key information. + * + * @param cls NULL + * @param key unused + * @param value the `struct TALER_EXCHANGEDB_DenominationKeyInformationP *` to free + * @return #GNUNET_OK (continue to iterate) + */ +static int +free_dk_info (void *cls, + const struct GNUNET_HashCode *key, + void *value) +{ + struct TALER_EXCHANGEDB_DenominationKeyInformationP *dki = value; + + GNUNET_free (dki); + return GNUNET_OK; +} + + +/** + * Purge transaction global state cache, the transaction is + * done and we do not want to have the state cross over to + * the next transaction. + */ +static void +clear_transaction_state_cache () +{ + if (NULL == denominations) + return; + GNUNET_CONTAINER_multihashmap_iterate (denominations, + &free_dk_info, + NULL); + GNUNET_CONTAINER_multihashmap_destroy (denominations); + denominations = NULL; +} + +/* ***************************** Analyze reserves ************************ */ /** * Summary data we keep per reserve. @@ -105,19 +257,80 @@ struct ReserveSummary */ struct TALER_Amount total_out; + /** + * Previous balance of the reserve as remembered by the auditor. + */ + struct TALER_Amount a_balance; + + /** + * Previous withdraw fee balance of the reserve, as remembered by the auditor. + */ + struct TALER_Amount a_withdraw_fee_balance; + + /** + * Previous reserve expiration data, as remembered by the auditor. + */ + struct GNUNET_TIME_Absolute a_expiration_date; + + /** + * Previous last processed reserve_in serial ID, as remembered by the auditor. + */ + uint64_t a_last_reserve_in_serial_id; + + /** + * Previous last processed reserve_out serial ID, as remembered by the auditor. + */ + uint64_t a_last_reserve_out_serial_id; + + /** + * Did we have a previous reserve info? + */ + int had_ri; + }; /** - * Map from hash of reserve's public key to a `struct ReserveSummary`. + * Load the auditor's remembered state about the reserve into @a rs. + * + * @param[in|out] rs reserve summary to (fully) initialize + * @return #GNUNET_OK on success, #GNUNET_SYSERR on DB errors */ -static struct GNUNET_CONTAINER_MultiHashMap *reserves; +static int +load_auditor_reserve_summary (struct ReserveSummary *rs) +{ + int ret; + + ret = adb->get_reserve_info (adb->cls, + asession, + &rs->reserve_pub, + &master_pub, + &rs->a_balance, + &rs->a_withdraw_fee_balance, + &rs->a_expiration_date, + &rs->a_last_reserve_in_serial_id, + &rs->a_last_reserve_out_serial_id); + if (GNUNET_SYSERR == ret) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (GNUNET_NO == ret) + { + rs->had_ri = GNUNET_NO; + // FIXME: set rs->a-values to sane defaults! + return GNUNET_OK; + } + rs->had_ri = GNUNET_YES; + /* TODO: check values we got are sane? */ + return GNUNET_OK; +} /** * Function called with details about incoming wire transfers. * - * @param cls NULL + * @param cls our `struct GNUNET_CONTAINER_MultiHashMap` with the reserves * @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 @@ -135,13 +348,10 @@ handle_reserve_in (void *cls, const json_t *transfer_details, struct GNUNET_TIME_Absolute execution_date) { + struct GNUNET_CONTAINER_MultiHashMap *reserves = cls; struct GNUNET_HashCode key; struct ReserveSummary *rs; - /* TODO: somewhere we need to check that 'reserve_in' data actually - matches wire transfers from the bank. Not sure if this should be - done within the core auditor logic though... */ - GNUNET_assert (rowid >= reserve_in_serial_id); /* should be monotonically increasing */ reserve_in_serial_id = rowid + 1; GNUNET_CRYPTO_hash (reserve_pub, @@ -157,6 +367,13 @@ handle_reserve_in (void *cls, GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (credit->currency, &rs->total_out)); + if (GNUNET_OK != + load_auditor_reserve_summary (rs)) + { + GNUNET_break (0); + GNUNET_free (rs); + return GNUNET_SYSERR; + } GNUNET_assert (GNUNET_OK == GNUNET_CONTAINER_multihashmap_put (reserves, &key, @@ -177,7 +394,7 @@ handle_reserve_in (void *cls, /** * Function called with details about withdraw operations. * - * @param cls closure + * @param cls our `struct GNUNET_CONTAINER_MultiHashMap` with the reserves * @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 @@ -199,12 +416,55 @@ handle_reserve_out (void *cls, struct GNUNET_TIME_Absolute execution_date, const struct TALER_Amount *amount_with_fee) { + struct GNUNET_CONTAINER_MultiHashMap *reserves = cls; + struct TALER_WithdrawRequestPS wsrd; struct GNUNET_HashCode key; struct ReserveSummary *rs; + const struct TALER_EXCHANGEDB_DenominationKeyInformationP *dki; + int ret; - /* TODO: check signatures, in particluar the reserve_sig! */ - GNUNET_assert (rowid >= reserve_out_serial_id); /* should be monotonically increasing */ + /* should be monotonically increasing */ + GNUNET_assert (rowid >= reserve_out_serial_id); reserve_out_serial_id = rowid + 1; + + /* lookup denomination pub data (make sure denom_pub is valid, establish fees) */ + ret = get_denomination_info (denom_pub, + &dki, + &wsrd.h_denomination_pub); + if (GNUNET_SYSERR == ret) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (GNUNET_NO == ret) + { + report_row_inconsistency ("reserve_out", + rowid, + "denomination key not found (foreign key constraint violated)"); + return GNUNET_OK; + } + + /* check that execution date is within withdraw range for denom_pub (?) */ + + /* check reserve_sig */ + wsrd.purpose.purpose = htonl (TALER_SIGNATURE_WALLET_RESERVE_WITHDRAW); + wsrd.purpose.size = htonl (sizeof (wsrd)); + TALER_amount_hton (&wsrd.amount_with_fee, + amount_with_fee); + wsrd.withdraw_fee = dki->properties.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_row_inconsistency ("reserve_out", + rowid, + "invalid signature for reserve withdrawal"); + return GNUNET_OK; + } + GNUNET_CRYPTO_hash (reserve_pub, sizeof (*reserve_pub), &key); @@ -218,6 +478,13 @@ handle_reserve_out (void *cls, GNUNET_assert (GNUNET_OK == TALER_amount_get_zero (amount_with_fee->currency, &rs->total_in)); + if (GNUNET_OK != + load_auditor_reserve_summary (rs)) + { + GNUNET_break (0); + GNUNET_free (rs); + return GNUNET_SYSERR; + } GNUNET_assert (GNUNET_OK == GNUNET_CONTAINER_multihashmap_put (reserves, &key, @@ -241,7 +508,7 @@ handle_reserve_out (void *cls, * * Remove all reserves that we are happy with from the DB. * - * @param cls NULL + * @param cls our `struct GNUNET_CONTAINER_MultiHashMap` with the reserves * @param key hash of the reserve public key * @param value a `struct ReserveSummary` * @return #GNUNET_OK to process more entries @@ -251,53 +518,90 @@ verify_reserve_balance (void *cls, const struct GNUNET_HashCode *key, void *value) { + struct GNUNET_CONTAINER_MultiHashMap *reserves = cls; struct ReserveSummary *rs = value; struct TALER_EXCHANGEDB_Reserve reserve; struct TALER_Amount balance; + int ret; + ret = GNUNET_OK; reserve.pub = rs->reserve_pub; if (GNUNET_OK != edb->reserve_get (edb->cls, esession, &reserve)) { - GNUNET_break (0); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to find summary for reserve `%s'\n", - TALER_B2S (&rs->reserve_pub)); - + 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); return GNUNET_OK; } - /* TODO: check reserve.expiry? */ - /* FIXME: get previous reserve state from auditor DB */ + /* TODO: check reserve.expiry */ /* FIXME: simplified computation as we have no previous reserve state yet */ + /* FIXME: actually update withdraw fee balance, expiration data and serial IDs! */ if (GNUNET_SYSERR == TALER_amount_subtract (&balance, &rs->total_in, &rs->total_out)) { - GNUNET_break (0); - return GNUNET_OK; + report_reserve_inconsistency (&rs->reserve_pub, + &rs->total_in, + &rs->total_out, + "available balance insufficient to cover transfers"); + goto cleanup; } if (0 != TALER_amount_cmp (&balance, &reserve.balance)) { - GNUNET_break (0); - return GNUNET_OK; + report_reserve_inconsistency (&rs->reserve_pub, + &balance, + &reserve.balance, + "computed balance does not match stored balance"); + goto cleanup; } + + /* FIXME: if balance is zero, create reserve summary and drop reserve details! */ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Reserve balance `%s' OK\n", TALER_B2S (&rs->reserve_pub)); - /* FIXME: commit new reserve state from auditor DB */ - + if (rs->had_ri) + ret = adb->update_reserve_info (adb->cls, + asession, + &rs->reserve_pub, + &master_pub, + &balance, + &rs->a_withdraw_fee_balance, + rs->a_expiration_date, + rs->a_last_reserve_in_serial_id, + rs->a_last_reserve_out_serial_id); + else + ret = adb->insert_reserve_info (adb->cls, + asession, + &rs->reserve_pub, + &master_pub, + &balance, + &rs->a_withdraw_fee_balance, + rs->a_expiration_date, + rs->a_last_reserve_in_serial_id, + rs->a_last_reserve_out_serial_id); + + + cleanup: GNUNET_assert (GNUNET_YES == GNUNET_CONTAINER_multihashmap_remove (reserves, key, rs)); GNUNET_free (rs); - return GNUNET_OK; + return ret; } @@ -310,52 +614,71 @@ verify_reserve_balance (void *cls, static int analyze_reserves (void *cls) { + /* Map from hash of reserve's public key to a `struct ReserveSummary`. */ + struct GNUNET_CONTAINER_MultiHashMap *reserves; + reserves = GNUNET_CONTAINER_multihashmap_create (512, GNUNET_NO); - /* FIXME: check return values... */ - edb->select_reserves_in_above_serial_id (edb->cls, - esession, - reserve_in_serial_id, - &handle_reserve_in, - NULL); - edb->select_reserves_out_above_serial_id (edb->cls, - esession, - reserve_out_serial_id, - &handle_reserve_out, - NULL); + if (GNUNET_OK != + edb->select_reserves_in_above_serial_id (edb->cls, + esession, + reserve_in_serial_id, + &handle_reserve_in, + reserves)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + edb->select_reserves_out_above_serial_id (edb->cls, + esession, + reserve_out_serial_id, + &handle_reserve_out, + reserves)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } GNUNET_CONTAINER_multihashmap_iterate (reserves, &verify_reserve_balance, - NULL); - /* FIXME: any values left in #reserves indicate errors! */ + reserves); + GNUNET_break (0 == + GNUNET_CONTAINER_multihashmap_size (reserves)); GNUNET_CONTAINER_multihashmap_destroy (reserves); return GNUNET_OK; } +/* *************************** General transaction logic ****************** */ + /** - * Type of an analysis function. + * Type of an analysis function. Each analysis function runs in + * its own transaction scope and must thus be internally consistent. * * @param cls closure - * @param int #GNUNET_OK on success + * @param int #GNUNET_OK on success, #GNUNET_SYSERR on hard errors */ typedef int (*Analysis)(void *cls); /** - * Perform the given @a analysis within a transaction scope. - * Commit on success. + * Perform the given @a analysis incrementally, checkpointing our + * progress in the auditor DB. * * @param analysis analysis to run * @param analysis_cls closure for @a analysis - * @return #GNUNET_OK if @a analysis succeessfully committed + * @return #GNUNET_OK if @a analysis succeessfully committed, + * #GNUNET_SYSERR on hard errors */ static int -transact (Analysis analysis, - void *analysis_cls) +incremental_processing (Analysis analysis, + void *analysis_cls) { + int ret; + ret = adb->get_auditor_progress (adb->cls, asession, &master_pub, @@ -368,7 +691,7 @@ transact (Analysis analysis, if (GNUNET_SYSERR == ret) { GNUNET_break (0); - return; + return GNUNET_SYSERR; } if (GNUNET_NO == ret) { @@ -386,10 +709,13 @@ transact (Analysis analysis, (unsigned long long) refund_serial_id, (unsigned long long) prewire_serial_id); } - ret = analysis (analysis_cls); - // FIXME: add other 'analyze' calls here... - + if (GNUNET_OK != ret) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Analysis phase failed, not recording progress\n"); + return GNUNET_SYSERR; + } ret = adb->update_auditor_progress (adb->cls, asession, &master_pub, @@ -402,10 +728,8 @@ transact (Analysis analysis, if (GNUNET_OK != ret) { GNUNET_break (0); - global_ret = 1; - return; + return GNUNET_SYSERR; } - GNUNET_log (GNUNET_ERROR_TYPE_MESSAGE, _("Resuming audit at %llu/%llu/%llu/%llu/%llu/%llu\n\n"), (unsigned long long) reserve_in_serial_id, @@ -414,7 +738,75 @@ transact (Analysis analysis, (unsigned long long) melt_serial_id, (unsigned long long) refund_serial_id, (unsigned long long) prewire_serial_id); + return GNUNET_OK; +} + + +/** + * 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 + */ +static int +transact (Analysis analysis, + void *analysis_cls) +{ + int ret; + ret = adb->start (adb->cls, + asession); + if (GNUNET_OK != ret) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + ret = edb->start (edb->cls, + esession); + if (GNUNET_OK != ret) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + ret = incremental_processing (analysis, + analysis_cls); + if (GNUNET_OK == ret) + { + ret = edb->commit (edb->cls, + esession); + if (GNUNET_OK != ret) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Exchange DB commit failed, rolling back transaction\n"); + adb->rollback (adb->cls, + asession); + } + else + { + ret = adb->commit (adb->cls, + asession); + if (GNUNET_OK != ret) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Auditor DB commit failed!\n"); + } + } + } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Processing failed, rolling back transaction\n"); + adb->rollback (adb->cls, + asession); + edb->rollback (edb->cls, + esession); + } + clear_transaction_state_cache (); + return ret; } @@ -424,8 +816,6 @@ transact (Analysis analysis, static void setup_sessions_and_run () { - int ret; - esession = edb->get_session (edb->cls); if (NULL == esession) { @@ -445,7 +835,7 @@ setup_sessions_and_run () transact (&analyze_reserves, NULL); - // FIXME: add other 'analyze' calls here... + // NOTE: add other 'transact (&analyze_*)'-calls here as they are implemented. } -- cgit v1.2.3