/* This file is part of TALER Copyright (C) 2017-2024 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General 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 General Public License for more details. You should have received a copy of the GNU General Public License along with TALER; see the file COPYING. If not, see */ /** * @file auditor/taler-helper-auditor-wire-credit.c * @brief audits that wire transfers match those from an exchange database. * @author Christian Grothoff * * This auditor verifies that 'reserves_in' actually matches * the incoming wire transfers from the bank. */ #include "platform.h" #include #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" #include "taler_dbevents.h" /** * How much time do we allow the aggregator to lag behind? If * wire transfers should have been made more than #GRACE_PERIOD * before, we issue warnings. */ #define GRACE_PERIOD GNUNET_TIME_UNIT_HOURS /** * Maximum number of wire transfers we process per * (database) transaction. */ #define MAX_PER_TRANSACTION 1024 /** * How much do we allow the bank and the exchange to disagree about * timestamps? Should be sufficiently large to avoid bogus reports from deltas * created by imperfect clock synchronization and network delay. */ #define TIME_TOLERANCE GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MINUTES, \ 15) /** * Run in test mode. Exit when idle instead of * going to sleep and waiting for more work. */ static int test_mode; /** * Information we keep for each supported account. */ struct WireAccount { /** * Accounts are kept in a DLL. */ struct WireAccount *next; /** * Plugins are kept in a DLL. */ struct WireAccount *prev; /** * Account details. */ const struct TALER_EXCHANGEDB_AccountInfo *ai; /** * Active wire request for the transaction history. */ struct TALER_BANK_CreditHistoryHandle *chh; /** * Progress point for this account. */ uint64_t last_reserve_in_serial_id; /** * Initial progress point for this account. */ uint64_t start_reserve_in_serial_id; /** * Where we are in the inbound transaction history. */ uint64_t wire_off_in; /** * Label under which we store our pp's reserve_in_serial_id. */ char *label_reserve_in_serial_id; /** * Label under which we store our wire_off_in. */ char *label_wire_off_in; }; /** * Return value from main(). */ static int global_ret; /** * State of the current database transaction with * the auditor DB. */ static enum GNUNET_DB_QueryStatus global_qs; /** * Map with information about incoming wire transfers. * Maps hashes of the wire offsets to `struct ReserveInInfo`s. */ static struct GNUNET_CONTAINER_MultiHashMap *in_map; /** * Head of list of wire accounts we still need to look at. */ static struct WireAccount *wa_head; /** * Tail of list of wire accounts we still need to look at. */ static struct WireAccount *wa_tail; /** * Amount that is considered "tiny" */ static struct TALER_Amount tiny_amount; /** * Total amount that was transferred too much to the exchange. */ static TALER_ARL_DEF_AB (total_bad_amount_in_plus); /** * Total amount that was transferred too little to the exchange. */ static TALER_ARL_DEF_AB (total_bad_amount_in_minus); /** * Total amount where the exchange has the wrong sender account * for incoming funds and may thus wire funds to the wrong * destination when closing the reserve. */ static TALER_ARL_DEF_AB (total_misattribution_in); /** * Total amount credited to exchange accounts. */ static TALER_ARL_DEF_AB (total_wire_in); /** * Amount of zero in our currency. */ static struct TALER_Amount zero; /** * Handle to the context for interacting with the bank. */ static struct GNUNET_CURL_Context *ctx; /** * Scheduler context for running the @e ctx. */ static struct GNUNET_CURL_RescheduleContext *rc; /** * Should we run checks that only work for exchange-internal audits? */ static int internal_checks; /** * Should we ignore if the bank does not know our bank * account? */ static int ignore_account_404; /** * Database event handler to wake us up again. */ static struct GNUNET_DB_EventHandler *eh; /** * The auditors's configuration. */ static const struct GNUNET_CONFIGURATION_Handle *cfg; /* ***************************** Shutdown **************************** */ /** * Entry in map with wire information we expect to obtain from the * bank later. */ struct ReserveInInfo { /** * Hash of expected row offset. */ struct GNUNET_HashCode row_off_hash; /** * Expected details about the wire transfer. * The member "account_url" is to be allocated * at the end of this struct! */ struct TALER_BANK_CreditDetails credit_details; /** * RowID in reserves_in table. */ uint64_t rowid; }; /** * Free entry in #in_map. * * @param cls NULL * @param key unused key * @param value the `struct ReserveInInfo` to free * @return #GNUNET_OK */ static enum GNUNET_GenericReturnValue free_rii (void *cls, const struct GNUNET_HashCode *key, void *value) { struct ReserveInInfo *rii = value; (void) cls; GNUNET_assert (GNUNET_YES == GNUNET_CONTAINER_multihashmap_remove (in_map, key, rii)); GNUNET_free (rii); return GNUNET_OK; } /** * Task run on shutdown. * * @param cls NULL */ static void do_shutdown (void *cls) { struct WireAccount *wa; (void) cls; if (NULL != eh) { TALER_ARL_adb->event_listen_cancel (eh); eh = NULL; } TALER_ARL_done (); if (NULL != in_map) { GNUNET_CONTAINER_multihashmap_iterate (in_map, &free_rii, NULL); GNUNET_CONTAINER_multihashmap_destroy (in_map); in_map = NULL; } while (NULL != (wa = wa_head)) { if (NULL != wa->chh) { TALER_BANK_credit_history_cancel (wa->chh); wa->chh = NULL; } GNUNET_CONTAINER_DLL_remove (wa_head, wa_tail, wa); GNUNET_free (wa->label_reserve_in_serial_id); GNUNET_free (wa->label_wire_off_in); GNUNET_free (wa); } if (NULL != ctx) { GNUNET_CURL_fini (ctx); ctx = NULL; } if (NULL != rc) { GNUNET_CURL_gnunet_rc_destroy (rc); rc = NULL; } TALER_EXCHANGEDB_unload_accounts (); TALER_ARL_cfg = NULL; } /** * Start the database transactions and begin the audit. * * @return transaction status code */ static enum GNUNET_DB_QueryStatus begin_transaction (void); /** * Commit the transaction, checkpointing our progress in the auditor DB. * * @param qs transaction status so far */ static void commit (enum GNUNET_DB_QueryStatus qs) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Transaction logic ended with status %d\n", qs); TALER_ARL_edb->rollback (TALER_ARL_edb->cls); if (qs < 0) goto handle_db_error; qs = TALER_ARL_adb->update_balance ( TALER_ARL_adb->cls, TALER_ARL_SET_AB (total_wire_in), TALER_ARL_SET_AB (total_bad_amount_in_plus), TALER_ARL_SET_AB (total_bad_amount_in_minus), TALER_ARL_SET_AB (total_misattribution_in), NULL); if (0 > qs) goto handle_db_error; qs = TALER_ARL_adb->insert_balance ( TALER_ARL_adb->cls, TALER_ARL_SET_AB (total_wire_in), TALER_ARL_SET_AB (total_bad_amount_in_plus), TALER_ARL_SET_AB (total_bad_amount_in_minus), TALER_ARL_SET_AB (total_misattribution_in), NULL); if (0 > qs) goto handle_db_error; for (struct WireAccount *wa = wa_head; NULL != wa; wa = wa->next) { qs = TALER_ARL_adb->update_auditor_progress ( TALER_ARL_adb->cls, wa->label_reserve_in_serial_id, wa->last_reserve_in_serial_id, wa->label_wire_off_in, wa->wire_off_in, NULL); if (0 > qs) goto handle_db_error; qs = TALER_ARL_adb->insert_auditor_progress ( TALER_ARL_adb->cls, wa->label_reserve_in_serial_id, wa->last_reserve_in_serial_id, wa->label_wire_off_in, wa->wire_off_in, NULL); if (0 > qs) goto handle_db_error; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Transaction ends at %s=%llu for account `%s'\n", wa->label_reserve_in_serial_id, (unsigned long long) wa->last_reserve_in_serial_id, wa->ai->section_name); } qs = TALER_ARL_adb->commit (TALER_ARL_adb->cls); if (0 > qs) { GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); goto handle_db_error; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Transaction concluded!\n"); if (1 == test_mode) GNUNET_SCHEDULER_shutdown (); return; handle_db_error: TALER_ARL_adb->rollback (TALER_ARL_adb->cls); for (unsigned int max_retries = 3; max_retries>0; max_retries--) { if (GNUNET_DB_STATUS_HARD_ERROR == qs) break; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Serialization issue, trying again\n"); qs = begin_transaction (); } GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Hard database error, terminating\n"); GNUNET_SCHEDULER_shutdown (); } /** * Conclude the credit history check by logging entries that * were not found and freeing resources. Then move on to * processing debits. */ static void conclude_credit_history (void) { if (NULL != in_map) { GNUNET_assert (0 == GNUNET_CONTAINER_multihashmap_size (in_map)); GNUNET_CONTAINER_multihashmap_destroy (in_map); in_map = NULL; } commit (global_qs); } /** * Function called with details about incoming wire transfers * as claimed by the exchange DB. * * @param cls a `struct WireAccount` we are processing * @param rowid unique serial ID for the entry in our DB * @param reserve_pub public key of the reserve (also the WTID) * @param credit amount that was received * @param sender_account_details payto://-URL of the sender's bank account * @param wire_reference unique identifier for the wire transfer * @param execution_date when did we receive the funds * @return #GNUNET_OK to continue to iterate, #GNUNET_SYSERR to stop */ static enum GNUNET_GenericReturnValue reserve_in_cb (void *cls, uint64_t rowid, const struct TALER_ReservePublicKeyP *reserve_pub, const struct TALER_Amount *credit, const char *sender_account_details, uint64_t wire_reference, struct GNUNET_TIME_Timestamp execution_date) { struct WireAccount *wa = cls; struct ReserveInInfo *rii; size_t slen; char *snp; snp = TALER_payto_normalize (sender_account_details); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Analyzing exchange wire IN (%llu) at %s of %s with reserve_pub %s\n", (unsigned long long) rowid, GNUNET_TIME_timestamp2s (execution_date), TALER_amount2s (credit), TALER_B2S (reserve_pub)); TALER_ARL_amount_add (&TALER_ARL_USE_AB (total_wire_in), &TALER_ARL_USE_AB (total_wire_in), credit); slen = strlen (snp) + 1; rii = GNUNET_malloc (sizeof (struct ReserveInInfo) + slen); rii->rowid = rowid; rii->credit_details.type = TALER_BANK_CT_RESERVE; rii->credit_details.amount = *credit; rii->credit_details.execution_date = execution_date; rii->credit_details.details.reserve.reserve_pub = *reserve_pub; rii->credit_details.debit_account_uri = (const char *) &rii[1]; GNUNET_memcpy (&rii[1], snp, slen); GNUNET_free (snp); GNUNET_CRYPTO_hash (&wire_reference, sizeof (uint64_t), &rii->row_off_hash); if (GNUNET_OK != GNUNET_CONTAINER_multihashmap_put (in_map, &rii->row_off_hash, rii, GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)) { struct TALER_AUDITORDB_RowInconsistency ri = { .row_id = rowid, .row_table = "reserves_in", .diagnostic = "duplicate wire offset" }; enum GNUNET_DB_QueryStatus qs; GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Duplicate wire offset\n"); qs = TALER_ARL_adb->insert_row_inconsistency ( TALER_ARL_adb->cls, &ri); GNUNET_free (rii); if (qs < 0) { global_qs = qs; GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); return GNUNET_SYSERR; } return GNUNET_OK; } wa->last_reserve_in_serial_id = rowid + 1; return GNUNET_OK; } /** * Complain that we failed to match an entry from #in_map. * * @param cls a `struct WireAccount` * @param key unused key * @param value the `struct ReserveInInfo` to free * @return #GNUNET_OK */ static enum GNUNET_GenericReturnValue complain_in_not_found (void *cls, const struct GNUNET_HashCode *key, void *value) { struct WireAccount *wa = cls; struct ReserveInInfo *rii = value; enum GNUNET_DB_QueryStatus qs; struct TALER_AUDITORDB_ReserveInInconsistency riiDb = { .bank_row_id = rii->rowid, .diagnostic = "incoming wire transfer claimed by exchange not found", .account = (char *) wa->ai->section_name, .amount_exchange_expected = rii->credit_details.amount, .amount_wired = zero, .reserve_pub = rii->credit_details.details.reserve.reserve_pub, .timestamp = rii->credit_details.execution_date.abs_time }; (void) key; GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Incoming wire transfer #%llu claimed by exchange not found\n", (unsigned long long) rii->rowid); GNUNET_assert (TALER_BANK_CT_RESERVE == rii->credit_details.type); qs = TALER_ARL_adb->insert_reserve_in_inconsistency ( TALER_ARL_adb->cls, &riiDb); if (qs < 0) { GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); global_qs = qs; return GNUNET_SYSERR; } TALER_ARL_amount_add (&TALER_ARL_USE_AB (total_bad_amount_in_minus), &TALER_ARL_USE_AB (total_bad_amount_in_minus), &rii->credit_details.amount); return GNUNET_OK; } /** * Start processing the next wire account. * Shuts down if we are done. * * @param cls `struct WireAccount` with a wire account list to process */ static void process_credits (void *cls); /** * We got all of the incoming transactions for @a wa, * finish processing the account. * * @param[in,out] wa wire account to process */ static void conclude_account (struct WireAccount *wa) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Reconciling CREDIT processing of account `%s'\n", wa->ai->section_name); if (NULL != in_map) { GNUNET_CONTAINER_multihashmap_iterate (in_map, &complain_in_not_found, wa); /* clean up before 2nd phase */ GNUNET_CONTAINER_multihashmap_iterate (in_map, &free_rii, NULL); if (global_qs < 0) { commit (global_qs); return; } } process_credits (wa->next); } /** * Analyze credit transaction @a details into @a wa. * * @param[in,out] wa account that received the transfer * @param credit_details transfer details * @return true on success, false to stop loop at this point */ static bool analyze_credit ( struct WireAccount *wa, const struct TALER_BANK_CreditDetails *credit_details) { struct ReserveInInfo *rii; struct GNUNET_HashCode key; GNUNET_assert (TALER_BANK_CT_RESERVE == credit_details->type); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Analyzing bank CREDIT #%llu at %s of %s with Reserve-pub %s\n", (unsigned long long) credit_details->serial_id, GNUNET_TIME_timestamp2s (credit_details->execution_date), TALER_amount2s (&credit_details->amount), TALER_B2S (&credit_details->details.reserve.reserve_pub)); GNUNET_CRYPTO_hash (&credit_details->serial_id, sizeof (credit_details->serial_id), &key); rii = GNUNET_CONTAINER_multihashmap_get (in_map, &key); if (NULL == rii) { // FIXME: add to auditor DB and report missing! // (and modify balances!) GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Failed to find wire transfer at `%s' in exchange database.\n", GNUNET_TIME_timestamp2s (credit_details->execution_date)); return true; } /* Update offset */ wa->wire_off_in = credit_details->serial_id; /* compare records with expected data */ if (0 != GNUNET_memcmp (&credit_details->details.reserve.reserve_pub, &rii->credit_details.details.reserve.reserve_pub)) { struct TALER_AUDITORDB_ReserveInInconsistency riiDb = { .diagnostic = "wire subject does not match", .account = (char *) wa->ai->section_name, .bank_row_id = credit_details->serial_id, .amount_exchange_expected = rii->credit_details.amount, .amount_wired = zero, .reserve_pub = rii->credit_details.details.reserve.reserve_pub, .timestamp = rii->credit_details.execution_date.abs_time }; enum GNUNET_DB_QueryStatus qs; GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Reserve public key differs\n"); qs = TALER_ARL_adb->insert_reserve_in_inconsistency ( TALER_ARL_adb->cls, &riiDb); if (qs <= 0) { global_qs = qs; GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); return false; } TALER_ARL_amount_add (&TALER_ARL_USE_AB (total_bad_amount_in_minus), &TALER_ARL_USE_AB (total_bad_amount_in_minus), &rii->credit_details.amount); TALER_ARL_amount_add (&TALER_ARL_USE_AB (total_bad_amount_in_plus), &TALER_ARL_USE_AB (total_bad_amount_in_plus), &credit_details->amount); GNUNET_assert (GNUNET_OK == free_rii (NULL, &key, rii)); return true; } if (0 != TALER_amount_cmp (&rii->credit_details.amount, &credit_details->amount)) { struct TALER_AUDITORDB_ReserveInInconsistency riiDb = { .diagnostic = "wire amount does not match", .account = (char *) wa->ai->section_name, .bank_row_id = credit_details->serial_id, .amount_exchange_expected = rii->credit_details.amount, .amount_wired = credit_details->amount, .reserve_pub = rii->credit_details.details.reserve.reserve_pub, .timestamp = rii->credit_details.execution_date.abs_time }; enum GNUNET_DB_QueryStatus qs; GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Wire transfer amount differs\n"); qs = TALER_ARL_adb->insert_reserve_in_inconsistency ( TALER_ARL_adb->cls, &riiDb); if (qs <= 0) { global_qs = qs; GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); return false; } if (0 < TALER_amount_cmp (&credit_details->amount, &rii->credit_details.amount)) { /* details->amount > rii->details.amount: wire transfer was larger than it should have been */ struct TALER_Amount delta; TALER_ARL_amount_subtract (&delta, &credit_details->amount, &rii->credit_details.amount); TALER_ARL_amount_add (&TALER_ARL_USE_AB (total_bad_amount_in_plus), &TALER_ARL_USE_AB (total_bad_amount_in_plus), &delta); } else { /* rii->details.amount < details->amount: wire transfer was smaller than it should have been */ struct TALER_Amount delta; TALER_ARL_amount_subtract (&delta, &rii->credit_details.amount, &credit_details->amount); TALER_ARL_amount_add (&TALER_ARL_USE_AB (total_bad_amount_in_minus), &TALER_ARL_USE_AB (total_bad_amount_in_minus), &delta); } } { char *np; np = TALER_payto_normalize (credit_details->debit_account_uri); if (0 != strcasecmp (np, rii->credit_details.debit_account_uri)) { struct TALER_AUDITORDB_MisattributionInInconsistency mii = { .reserve_pub = rii->credit_details.details.reserve.reserve_pub, .amount = rii->credit_details.amount, .bank_row = credit_details->serial_id }; enum GNUNET_DB_QueryStatus qs; GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Origin bank account differs\n"); qs = TALER_ARL_adb->insert_misattribution_in_inconsistency ( TALER_ARL_adb->cls, &mii); if (qs <= 0) { global_qs = qs; GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); GNUNET_free (np); return false; } TALER_ARL_amount_add (&TALER_ARL_USE_AB (total_misattribution_in), &TALER_ARL_USE_AB (total_misattribution_in), &rii->credit_details.amount); } GNUNET_free (np); } if (GNUNET_TIME_timestamp_cmp (credit_details->execution_date, !=, rii->credit_details.execution_date)) { struct TALER_AUDITORDB_RowMinorInconsistencies rmi = { .problem_row = rii->rowid, .diagnostic = "execution date mismatch", .row_table = "reserves_in" }; enum GNUNET_DB_QueryStatus qs; GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Execution date differs\n"); qs = TALER_ARL_adb->insert_row_minor_inconsistencies ( TALER_ARL_adb->cls, &rmi); if (qs < 0) { /* FIXME: this error handling sucks... */ global_qs = qs; GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); return false; } } GNUNET_assert (GNUNET_OK == free_rii (NULL, &key, rii)); return true; } /** * This function is called for all transactions that * are credited to the exchange's account (incoming * transactions). * * @param cls `struct WireAccount` we are processing * @param chr HTTP response returned by the bank */ static void history_credit_cb (void *cls, const struct TALER_BANK_CreditHistoryResponse *chr) { struct WireAccount *wa = cls; wa->chh = NULL; switch (chr->http_status) { case MHD_HTTP_OK: for (unsigned int i = 0; i < chr->details.ok.details_length; i++) { const struct TALER_BANK_CreditDetails *cd = &chr->details.ok.details[i]; if (! analyze_credit (wa, cd)) { if (global_qs < 0) { /* FIXME: this error handling sucks, doesn't retry on SOFT errors, doesn't set global_ret, etc. */ GNUNET_SCHEDULER_shutdown (); return; } break; } } conclude_account (wa); return; case MHD_HTTP_NO_CONTENT: conclude_account (wa); return; case MHD_HTTP_NOT_FOUND: if (ignore_account_404) { conclude_account (wa); return; } break; default: break; } GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Error fetching credit history of account %s: %u (%s)\n", wa->ai->section_name, chr->http_status, TALER_ErrorCode_get_hint (chr->ec)); commit (GNUNET_DB_STATUS_HARD_ERROR); global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); } /* ***************************** Setup logic ************************ */ /** * Start processing the next wire account. * Shuts down if we are done. * * @param cls `struct WireAccount` with a wire account list to process */ static void process_credits (void *cls) { struct WireAccount *wa = cls; enum GNUNET_DB_QueryStatus qs; /* skip accounts where CREDIT is not enabled */ while ( (NULL != wa) && (GNUNET_NO == wa->ai->credit_enabled) ) wa = wa->next; if (NULL == wa) { /* done with all accounts, conclude check */ conclude_credit_history (); return; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Analyzing exchange's wire IN table for account `%s'\n", wa->ai->section_name); qs = TALER_ARL_edb->select_reserves_in_above_serial_id_by_account ( TALER_ARL_edb->cls, wa->ai->section_name, wa->last_reserve_in_serial_id, &reserve_in_cb, wa); if (0 > qs) { GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); return; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Starting bank CREDIT history of account `%s'\n", wa->ai->section_name); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "user `%s'\n", wa->ai->auth->details.basic.username); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "pass `%s'\n", wa->ai->auth->details.basic.password); wa->chh = TALER_BANK_credit_history (ctx, wa->ai->auth, wa->wire_off_in, MAX_PER_TRANSACTION, GNUNET_TIME_UNIT_ZERO, &history_credit_cb, wa); if (NULL == wa->chh) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to obtain bank transaction history\n"); commit (GNUNET_DB_STATUS_HARD_ERROR); global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); return; } } /** * Begin audit of CREDITs to the exchange. */ static void begin_credit_audit (void) { GNUNET_assert (NULL == in_map); in_map = GNUNET_CONTAINER_multihashmap_create (1024, GNUNET_YES); /* now go over all bank accounts and check delta with in_map */ process_credits (wa_head); } static enum GNUNET_DB_QueryStatus begin_transaction (void) { enum GNUNET_DB_QueryStatus qs; if (GNUNET_SYSERR == TALER_ARL_edb->preflight (TALER_ARL_edb->cls)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to initialize exchange database connection.\n"); return GNUNET_DB_STATUS_HARD_ERROR; } if (GNUNET_SYSERR == TALER_ARL_adb->preflight (TALER_ARL_adb->cls)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to initialize auditor database session.\n"); return GNUNET_DB_STATUS_HARD_ERROR; } global_qs = GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; if (GNUNET_OK != TALER_ARL_adb->start (TALER_ARL_adb->cls)) { GNUNET_break (0); return GNUNET_DB_STATUS_HARD_ERROR; } if (GNUNET_OK != TALER_ARL_edb->start_read_only (TALER_ARL_edb->cls, "wire credit auditor")) { GNUNET_break (0); return GNUNET_DB_STATUS_HARD_ERROR; } qs = TALER_ARL_adb->get_balance ( TALER_ARL_adb->cls, TALER_ARL_GET_AB (total_wire_in), TALER_ARL_GET_AB (total_bad_amount_in_plus), TALER_ARL_GET_AB (total_bad_amount_in_minus), TALER_ARL_GET_AB (total_misattribution_in), NULL); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); return qs; case GNUNET_DB_STATUS_SOFT_ERROR: GNUNET_break (0); return qs; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: break; } for (struct WireAccount *wa = wa_head; NULL != wa; wa = wa->next) { GNUNET_asprintf (&wa->label_reserve_in_serial_id, "wire-%s-%s", wa->ai->section_name, "reserve_in_serial_id"); GNUNET_asprintf (&wa->label_wire_off_in, "wire-%s-%s", wa->ai->section_name, "wire_off_in"); qs = TALER_ARL_adb->get_auditor_progress ( TALER_ARL_adb->cls, wa->label_reserve_in_serial_id, &wa->last_reserve_in_serial_id, wa->label_wire_off_in, &wa->wire_off_in, NULL); if (0 > qs) { GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); return qs; } wa->start_reserve_in_serial_id = wa->last_reserve_in_serial_id; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Starting from reserve_in at %s=%llu for account `%s'\n", wa->label_reserve_in_serial_id, (unsigned long long) wa->start_reserve_in_serial_id, wa->ai->section_name); } begin_credit_audit (); return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; } /** * Function called with information about a wire account. Adds the * account to our list for processing (if it is enabled and we can * load the plugin). * * @param cls closure, NULL * @param ai account information */ static void process_account_cb (void *cls, const struct TALER_EXCHANGEDB_AccountInfo *ai) { struct WireAccount *wa; (void) cls; if ((! ai->debit_enabled) && (! ai->credit_enabled)) return; /* not an active exchange account */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Found exchange account `%s'\n", ai->section_name); wa = GNUNET_new (struct WireAccount); wa->ai = ai; GNUNET_CONTAINER_DLL_insert (wa_head, wa_tail, wa); } /** * Function called on events received from Postgres. * * @param cls closure, NULL * @param extra additional event data provided * @param extra_size number of bytes in @a extra */ static void db_notify (void *cls, const void *extra, size_t extra_size) { (void) cls; (void) extra; (void) extra_size; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Received notification to wake wire helper\n"); if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != begin_transaction ()) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Audit failed\n"); GNUNET_break (0); global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); } } /** * Main function that will be run. * * @param cls closure * @param args remaining command-line arguments * @param cfgfile name of the configuration file used (for saving, can be NULL!) * @param c configuration */ static void run (void *cls, char *const *args, const char *cfgfile, const struct GNUNET_CONFIGURATION_Handle *c) { (void) cls; (void) args; (void) cfgfile; cfg = c; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Launching wire-credit auditor\n"); if (GNUNET_OK != TALER_ARL_init (c)) { global_ret = EXIT_FAILURE; return; } if (GNUNET_OK != TALER_config_get_amount (TALER_ARL_cfg, "auditor", "TINY_AMOUNT", &tiny_amount)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, "auditor", "TINY_AMOUNT"); global_ret = EXIT_NOTCONFIGURED; return; } GNUNET_assert (GNUNET_OK == TALER_amount_set_zero (TALER_ARL_currency, &zero)); GNUNET_SCHEDULER_add_shutdown (&do_shutdown, NULL); ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, &rc); rc = GNUNET_CURL_gnunet_rc_create (ctx); if (NULL == ctx) { GNUNET_break (0); global_ret = EXIT_FAILURE; return; } if (GNUNET_OK != TALER_EXCHANGEDB_load_accounts (TALER_ARL_cfg, TALER_EXCHANGEDB_ALO_CREDIT | TALER_EXCHANGEDB_ALO_AUTHDATA)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "No bank accounts configured\n"); global_ret = EXIT_NOTCONFIGURED; GNUNET_SCHEDULER_shutdown (); return; } TALER_EXCHANGEDB_find_accounts (&process_account_cb, NULL); if (0 == test_mode) { struct GNUNET_DB_EventHeaderP es = { .size = htons (sizeof (es)), .type = htons (TALER_DBEVENT_EXCHANGE_AUDITOR_WAKE_HELPER_WIRE) }; eh = TALER_ARL_adb->event_listen (TALER_ARL_adb->cls, &es, GNUNET_TIME_UNIT_FOREVER_REL, &db_notify, NULL); GNUNET_assert (NULL != eh); } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != begin_transaction ()) { GNUNET_break (0); global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); return; } } /** * The main function of the wire auditing tool. Checks that * the exchange's records of wire transfers match that of * the wire gateway. * * @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_flag ('i', "internal", "perform checks only applicable for exchange-internal audits", &internal_checks), GNUNET_GETOPT_option_flag ('I', "ignore-not-found", "continue, even if the bank account of the exchange was not found", &ignore_account_404), GNUNET_GETOPT_option_flag ('t', "test", "run in test mode and exit when idle", &test_mode), GNUNET_GETOPT_option_timetravel ('T', "timetravel"), GNUNET_GETOPT_OPTION_END }; enum GNUNET_GenericReturnValue ret; /* 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 (); if (GNUNET_OK != GNUNET_STRINGS_get_utf8_args (argc, argv, &argc, &argv)) return EXIT_INVALIDARGUMENT; ret = GNUNET_PROGRAM_run ( argc, argv, "taler-helper-auditor-wire-credit", gettext_noop ( "Audit exchange database for consistency with the bank's wire transfers"), options, &run, NULL); GNUNET_free_nz ((void *) argv); if (GNUNET_SYSERR == ret) return EXIT_INVALIDARGUMENT; if (GNUNET_NO == ret) return EXIT_SUCCESS; return global_ret; } /* end of taler-helper-auditor-wire-credit.c */