diff --git a/src/auditor/taler-auditor.c b/src/auditor/taler-auditor.c index 5db848982..fa8940f58 100644 --- a/src/auditor/taler-auditor.c +++ b/src/auditor/taler-auditor.c @@ -99,6 +99,11 @@ static struct TALER_AUDITORDB_Plugin *adb; */ static struct TALER_AUDITORDB_Session *asession; +/** + * After how long should idle reserves be closed? + */ +static struct GNUNET_TIME_Relative idle_reserve_expiration_time; + /** * Master public key of the exchange to audit. */ @@ -672,7 +677,7 @@ handle_reserve_in (void *cls, TALER_B2S (reserve_pub), TALER_amount2s (credit)); expiry = GNUNET_TIME_absolute_add (execution_date, - TALER_IDLE_RESERVE_EXPIRATION_TIME); + idle_reserve_expiration_time); rs->a_expiration_date = GNUNET_TIME_absolute_max (rs->a_expiration_date, expiry); return GNUNET_OK; @@ -973,7 +978,7 @@ handle_payback_by_reserve (void *cls, TALER_B2S (reserve_pub), TALER_amount2s (amount)); expiry = GNUNET_TIME_absolute_add (timestamp, - TALER_IDLE_RESERVE_EXPIRATION_TIME); + idle_reserve_expiration_time); rs->a_expiration_date = GNUNET_TIME_absolute_max (rs->a_expiration_date, expiry); return GNUNET_OK; @@ -1002,7 +1007,7 @@ handle_reserve_closed (void *cls, const struct TALER_Amount *closing_fee, const struct TALER_ReservePublicKeyP *reserve_pub, const json_t *receiver_account, - const json_t *transfer_details) + const struct TALER_WireTransferIdentifierRawP *transfer_details) { struct ReserveContext *rc = cls; struct GNUNET_HashCode key; @@ -3613,6 +3618,18 @@ run (void *cls, global_ret = 1; return; } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_time (cfg, + "exchangedb", + "IDLE_RESERVE_EXPIRATION_TIME", + &idle_reserve_expiration_time)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "exchangedb", + "IDLE_RESERVE_EXPIRATION_TIME"); + global_ret = 1; + return; + } if (NULL == (edb = TALER_EXCHANGEDB_plugin_load (cfg))) { diff --git a/src/exchange-lib/exchange_api_reserve.c b/src/exchange-lib/exchange_api_reserve.c index de243b009..c7f59c25d 100644 --- a/src/exchange-lib/exchange_api_reserve.c +++ b/src/exchange-lib/exchange_api_reserve.c @@ -335,8 +335,8 @@ parse_reserve_history (struct TALER_EXCHANGE_Handle *exchange, struct GNUNET_JSON_Specification closing_spec[] = { GNUNET_JSON_spec_json ("receiver_account_details", &rhistory[off].details.close_details.receiver_account_details), - GNUNET_JSON_spec_json ("wire_transfer", - &rhistory[off].details.close_details.transfer_details), + GNUNET_JSON_spec_fixed_auto ("wire_transfer", + &rhistory[off].details.close_details.wtid), GNUNET_JSON_spec_fixed_auto ("exchange_sig", &rhistory[off].details.close_details.exchange_sig), GNUNET_JSON_spec_fixed_auto ("exchange_pub", @@ -362,8 +362,7 @@ parse_reserve_history (struct TALER_EXCHANGE_Handle *exchange, &amount); TALER_JSON_hash (rhistory[off].details.close_details.receiver_account_details, &rcc.h_wire); - TALER_JSON_hash (rhistory[off].details.close_details.receiver_account_details, - &rcc.h_transfer); + rcc.wtid = rhistory[off].details.close_details.wtid; rcc.purpose.size = htonl (sizeof (rcc)); rcc.purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_RESERVE_CLOSED); rcc.reserve_pub = *reserve_pub; diff --git a/src/exchange/taler-exchange-aggregator.c b/src/exchange/taler-exchange-aggregator.c index e9587930a..54757d860 100644 --- a/src/exchange/taler-exchange-aggregator.c +++ b/src/exchange/taler-exchange-aggregator.c @@ -48,7 +48,7 @@ struct WirePlugin * Handle to the plugin. */ struct TALER_WIRE_Plugin *wire_plugin; - + /** * Name of the plugin. */ @@ -177,6 +177,34 @@ struct AggregationUnit }; +/** + * Context we use while closing a reserve. + */ +struct CloseTransferContext +{ + /** + * Handle for preparing the wire transfer. + */ + struct TALER_WIRE_PrepareHandle *ph; + + /** + * Our database session. + */ + struct TALER_EXCHANGEDB_Session *session; + + /** + * Wire transfer method. + */ + char *type; +}; + + +/** + * Active context while processing reserve closing, + * or NULL. + */ +static struct CloseTransferContext *ctc; + /** * Which currency is used by this exchange? */ @@ -235,6 +263,11 @@ static int global_ret; */ static int test_mode; +/** + * Did #run_reserve_closures() have any work during its last run? + */ +static int reserves_idle; + /** * Limit on the number of transactions we aggregate at once. Note * that the limit must be big enough to ensure that when transactions @@ -666,7 +699,8 @@ aggregate_cb (void *cls, /** - * Function to be called with the prepared transfer data. + * Function to be called with the prepared transfer data + * when running an aggregation on a merchant. * * @param cls closure with the `struct AggregationUnit` * @param buf transaction data to persist, NULL on error @@ -678,6 +712,319 @@ prepare_cb (void *cls, size_t buf_size); +/** + * Main work function that finds and triggers transfers for reserves + * closures. + * + * @param cls closure + */ +static void +run_reserve_closures (void *cls); + + +/** + * Main work function that queries the DB and aggregates transactions + * into larger wire transfers. + * + * @param cls NULL + */ +static void +run_aggregation (void *cls); + + +/** + * Function to be called with the prepared transfer data + * when closing a reserve. + * + * @param cls closure with a `struct CloseTransferContext` + * @param buf transaction data to persist, NULL on error + * @param buf_size number of bytes in @a buf, 0 on error + */ +static void +prepare_close_cb (void *cls, + const char *buf, + size_t buf_size) +{ + GNUNET_assert (cls == ctc); + ctc->ph = NULL; + if (NULL == buf) + { + GNUNET_break (0); /* why? how to best recover? */ + db_plugin->rollback (db_plugin->cls, + ctc->session); + /* start again */ + GNUNET_free (ctc->type); + GNUNET_free (ctc); + ctc = NULL; + task = GNUNET_SCHEDULER_add_now (&run_aggregation, + NULL); + return; + } + + /* Commit our intention to execute the wire transfer! */ + if (GNUNET_OK != + db_plugin->wire_prepare_data_insert (db_plugin->cls, + ctc->session, + ctc->type, + buf, + buf_size)) + { + GNUNET_break (0); /* why? how to best recover? */ + db_plugin->rollback (db_plugin->cls, + ctc->session); + /* start again */ + task = GNUNET_SCHEDULER_add_now (&run_aggregation, + NULL); + GNUNET_free (ctc->type); + GNUNET_free (ctc); + ctc = NULL; + return; + } + + /* finally commit */ + if (GNUNET_OK != + db_plugin->commit (db_plugin->cls, + ctc->session)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to commit database transaction!\n"); + } + GNUNET_free (ctc->type); + GNUNET_free (ctc); + ctc = NULL; + task = GNUNET_SCHEDULER_add_now (&run_aggregation, + NULL); +} + + +/** + * Function called with details about expired reserves. + * We trigger the reserve closure by inserting the respective + * closing record and prewire instructions into the respective + * tables. + * + * @param cls a `struct TALER_EXCHANGEDB_Session *` + * @param reserve_pub public key of the reserve + * @param left amount left in the reserve + * @param account_details information about the reserve's bank account + * @param expiration_date when did the reserve expire + * @return #GNUNET_OK to continue to iterate, #GNUNET_SYSERR to stop + */ +static int +expired_reserve_cb (void *cls, + const struct TALER_ReservePublicKeyP *reserve_pub, + const struct TALER_Amount *left, + const json_t *account_details, + struct GNUNET_TIME_Absolute expiration_date) +{ + struct TALER_EXCHANGEDB_Session *session = cls; + struct GNUNET_TIME_Absolute now; + struct TALER_WireTransferIdentifierRawP wtid; + struct TALER_Amount amount_without_fee; + const struct TALER_Amount *closing_fee; + int ret; + int iret; + const char *type; + struct WirePlugin *wp; + + GNUNET_assert (NULL == ctc); + now = GNUNET_TIME_absolute_get (); + + /* lookup wire plugin */ + type = extract_type (account_details); + if (NULL == type) + { + GNUNET_break (0); + db_plugin->rollback (db_plugin->cls, + session); + global_ret = GNUNET_SYSERR; + GNUNET_SCHEDULER_shutdown (); + return GNUNET_SYSERR; + } + wp = find_plugin (type); + if (NULL == wp) + { + GNUNET_break (0); + db_plugin->rollback (db_plugin->cls, + session); + global_ret = GNUNET_SYSERR; + GNUNET_SCHEDULER_shutdown (); + return GNUNET_SYSERR; + } + + /* lookup `closing_fee` */ + if (GNUNET_OK != + update_fees (wp, + now, + session)) + { + GNUNET_break (0); + db_plugin->rollback (db_plugin->cls, + session); + global_ret = GNUNET_SYSERR; + GNUNET_SCHEDULER_shutdown (); + return GNUNET_SYSERR; + } + closing_fee = &wp->af->closing_fee; + + /* calculate transfer amount */ + ret = TALER_amount_subtract (&amount_without_fee, + left, + closing_fee); + if ( (GNUNET_SYSERR == ret) || + (GNUNET_NO == ret) ) + { + /* Closing fee higher than remaining balance, close + without wire transfer. */ + closing_fee = left; + TALER_amount_get_zero (left->currency, + &amount_without_fee); + } + + /* NOTE: sizeof (*reserve_pub) == sizeof (wtid) right now, but to + be future-compatible, we use the memset + min construction */ + memset (&wtid, + 0, + sizeof (wtid)); + memcpy (&wtid, + reserve_pub, + GNUNET_MIN (sizeof (wtid), + sizeof (*reserve_pub))); + iret = db_plugin->insert_reserve_closed (db_plugin->cls, + session, + reserve_pub, + now, + account_details, + &wtid, + left, + closing_fee); + if ( (GNUNET_OK == ret) && + (GNUNET_OK == iret) ) + { + /* success, perform wire transfer */ + if (GNUNET_SYSERR == + wp->wire_plugin->amount_round (wp->wire_plugin->cls, + &amount_without_fee)) + { + GNUNET_break (0); + db_plugin->rollback (db_plugin->cls, + session); + global_ret = GNUNET_SYSERR; + GNUNET_SCHEDULER_shutdown (); + return GNUNET_SYSERR; + } + ctc = GNUNET_new (struct CloseTransferContext); + ctc->session = session; + ctc->type = GNUNET_strdup (type); + ctc->ph + = wp->wire_plugin->prepare_wire_transfer (wp->wire_plugin->cls, + au->wire, + &amount_without_fee, + exchange_base_url, + &wtid, + &prepare_close_cb, + ctc); + if (NULL == ctc->ph) + { + GNUNET_break (0); + db_plugin->rollback (db_plugin->cls, + session); + global_ret = GNUNET_SYSERR; + GNUNET_SCHEDULER_shutdown (); + GNUNET_free (ctc->type); + GNUNET_free (ctc); + ctc = NULL; + } + return GNUNET_SYSERR; + } + /* Check for hard failure */ + if (GNUNET_SYSERR == iret) + { + GNUNET_break (0); + db_plugin->rollback (db_plugin->cls, + session); + global_ret = GNUNET_SYSERR; + GNUNET_SCHEDULER_shutdown (); + return GNUNET_SYSERR; + } + /* Reserve balance was almost zero; just commit */ + if (GNUNET_OK != + db_plugin->commit (db_plugin->cls, + session)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to commit database transaction!\n"); + } + task = GNUNET_SCHEDULER_add_now (&run_reserve_closures, + NULL); + return GNUNET_SYSERR; +} + + +/** + * Main work function that finds and triggers transfers for reserves + * closures. + * + * @param cls closure + */ +static void +run_reserve_closures (void *cls) +{ + struct TALER_EXCHANGEDB_Session *session; + int ret; + const struct GNUNET_SCHEDULER_TaskContext *tc; + + task = NULL; + reserves_idle = GNUNET_NO; + tc = GNUNET_SCHEDULER_get_task_context (); + if (0 != (tc->reason & GNUNET_SCHEDULER_REASON_SHUTDOWN)) + return; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Checking for reserves to close\n"); + if (NULL == (session = db_plugin->get_session (db_plugin->cls))) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to obtain database session!\n"); + global_ret = GNUNET_SYSERR; + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + db_plugin->start (db_plugin->cls, + session)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to start database transaction!\n"); + global_ret = GNUNET_SYSERR; + GNUNET_SCHEDULER_shutdown (); + return; + } + ret = db_plugin->get_expired_reserves (db_plugin->cls, + session, + GNUNET_TIME_absolute_get (), + &expired_reserve_cb, + session); + if (GNUNET_SYSERR == ret) + { + GNUNET_break (0); + db_plugin->rollback (db_plugin->cls, + session); + global_ret = GNUNET_SYSERR; + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_NO == ret) + { + reserves_idle = GNUNET_YES; + db_plugin->rollback (db_plugin->cls, + session); + task = GNUNET_SCHEDULER_add_now (&run_aggregation, + NULL); + return; + } +} + + /** * Main work function that queries the DB and aggregates transactions * into larger wire transfers. @@ -687,6 +1034,7 @@ prepare_cb (void *cls, static void run_aggregation (void *cls) { + static int swap; struct TALER_EXCHANGEDB_Session *session; unsigned int i; int ret; @@ -696,6 +1044,12 @@ run_aggregation (void *cls) tc = GNUNET_SCHEDULER_get_task_context (); if (0 != (tc->reason & GNUNET_SCHEDULER_REASON_SHUTDOWN)) return; + if (0 == (++swap % 2)) + { + task = GNUNET_SCHEDULER_add_now (&run_reserve_closures, + NULL); + return; + } GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Checking for ready deposits to aggregate\n"); if (NULL == (session = db_plugin->get_session (db_plugin->cls))) @@ -748,9 +1102,13 @@ run_aggregation (void *cls) else { /* nothing to do, sleep for a minute and try again */ - task = GNUNET_SCHEDULER_add_delayed (GNUNET_TIME_UNIT_MINUTES, - &run_aggregation, - NULL); + if (GNUNET_NO == reserves_idle) + task = GNUNET_SCHEDULER_add_now (&run_reserve_closures, + NULL); + else + task = GNUNET_SCHEDULER_add_delayed (GNUNET_TIME_UNIT_MINUTES, + &run_aggregation, + NULL); } return; } diff --git a/src/exchange/taler-exchange-httpd_responses.c b/src/exchange/taler-exchange-httpd_responses.c index 1de383e37..0d7406451 100644 --- a/src/exchange/taler-exchange-httpd_responses.c +++ b/src/exchange/taler-exchange-httpd_responses.c @@ -886,17 +886,16 @@ compile_reserve_history (const struct TALER_EXCHANGEDB_ReserveHistory *rh, rcc.reserve_pub = pos->details.closing->reserve_pub; TALER_JSON_hash (pos->details.closing->receiver_account_details, &rcc.h_wire); - TALER_JSON_hash (pos->details.closing->transfer_details, - &rcc.h_transfer); + rcc.wtid = pos->details.closing->transfer_details; TEH_KS_sign (&rcc.purpose, &pub, &sig); GNUNET_assert (0 == json_array_append_new (json_history, - json_pack ("{s:s, s:O, s:O, s:o, s:o, s:o, s:o, s:o}", + json_pack ("{s:s, s:O, s:o, s:o, s:o, s:o, s:o, s:o}", "type", "CLOSING", "receiver_account_details", pos->details.closing->receiver_account_details, - "transfer_details", pos->details.closing->transfer_details, + "transfer_details", GNUNET_JSON_from_data_auto (&pos->details.closing->transfer_details), "exchange_pub", GNUNET_JSON_from_data_auto (&pub), "exchange_sig", GNUNET_JSON_from_data_auto (&sig), "timestamp", GNUNET_JSON_from_time_abs (pos->details.closing->execution_date), diff --git a/src/exchange/test-taler-exchange-aggregator-postgres.conf b/src/exchange/test-taler-exchange-aggregator-postgres.conf index a5ee91aa9..00736e44d 100644 --- a/src/exchange/test-taler-exchange-aggregator-postgres.conf +++ b/src/exchange/test-taler-exchange-aggregator-postgres.conf @@ -19,6 +19,15 @@ MASTER_PUBLIC_KEY = 98NJW3CQHZQGQXTY3K85K531XKPAPAVV4Q5V8PYYRR00NJGZWNVG # Expected base URL of the exchange. BASE_URL = "https://exchange.taler.net/" +[exchangedb] +# After how long do we close idle reserves? The exchange +# and the auditor must agree on this value. We currently +# expect it to be globally defined for the whole system, +# as there is no way for wallets to query this value. Thus, +# it is only configurable for testing, and should be treated +# as constant in production. +IDLE_RESERVE_EXPIRATION_TIME = 4 weeks + [exchangedb-postgres] #The connection string the plugin has to use for connecting to the database diff --git a/src/exchange/test_taler_exchange_httpd.conf b/src/exchange/test_taler_exchange_httpd.conf index 945031dd1..5f282713b 100644 --- a/src/exchange/test_taler_exchange_httpd.conf +++ b/src/exchange/test_taler_exchange_httpd.conf @@ -19,6 +19,16 @@ MASTER_PUBLIC_KEY = 98NJW3CQHZQGQXTY3K85K531XKPAPAVV4Q5V8PYYRR00NJGZWNVG DB = postgres +[exchangedb] +# After how long do we close idle reserves? The exchange +# and the auditor must agree on this value. We currently +# expect it to be globally defined for the whole system, +# as there is no way for wallets to query this value. Thus, +# it is only configurable for testing, and should be treated +# as constant in production. +IDLE_RESERVE_EXPIRATION_TIME = 4 weeks + + [exchangedb-postgres] DB_CONN_STR = "postgres:///talercheck" diff --git a/src/exchangedb/exchangedb.conf b/src/exchangedb/exchangedb.conf index 73e1603a9..7303025a9 100644 --- a/src/exchangedb/exchangedb.conf +++ b/src/exchangedb/exchangedb.conf @@ -12,3 +12,12 @@ AUDITOR_BASE_DIR = ${TALER_DATA_HOME}/auditors/ # contain files "$METHOD.fee" with the cost structure, where # $METHOD corresponds to a wire transfer method. WIREFEE_BASE_DIR = ${TALER_DATA_HOME}/exchange/wirefees/ + + +# After how long do we close idle reserves? The exchange +# and the auditor must agree on this value. We currently +# expect it to be globally defined for the whole system, +# as there is no way for wallets to query this value. Thus, +# it is only configurable for testing, and should be treated +# as constant in production. +IDLE_RESERVE_EXPIRATION_TIME = 4 weeks diff --git a/src/exchangedb/plugin_exchangedb_common.c b/src/exchangedb/plugin_exchangedb_common.c index ba182d425..fac911d68 100644 --- a/src/exchangedb/plugin_exchangedb_common.c +++ b/src/exchangedb/plugin_exchangedb_common.c @@ -64,8 +64,6 @@ common_free_reserve_history (void *cls, closing = rh->details.closing; if (NULL != closing->receiver_account_details) json_decref (closing->receiver_account_details); - if (NULL != closing->transfer_details) - json_decref (closing->transfer_details); GNUNET_free (closing); break; } diff --git a/src/exchangedb/plugin_exchangedb_postgres.c b/src/exchangedb/plugin_exchangedb_postgres.c index 7ef6cef97..35b24edb4 100644 --- a/src/exchangedb/plugin_exchangedb_postgres.c +++ b/src/exchangedb/plugin_exchangedb_postgres.c @@ -141,6 +141,11 @@ struct PostgresClosure * the configuration. */ char *connection_cfg_str; + + /** + * After how long should idle reserves be closed? + */ + struct GNUNET_TIME_Relative idle_reserve_expiration_time; }; @@ -316,9 +321,6 @@ postgres_create_tables (void *cls) ",fee_refund_curr VARCHAR("TALER_CURRENCY_LEN_STR") NOT NULL" ")"); /* denomination_revocations table is for remembering which denomination keys have been revoked */ - /* TODO (#4981): change denom_pub_hash to REFERENCE 'denominations', and - add denom_pub_hash column to denominations, changing other REFERENCEs - also to the hash!? */ SQLEXEC ("CREATE TABLE IF NOT EXISTS denomination_revocations" "(denom_revocations_serial_id BIGSERIAL" ",denom_pub_hash BYTEA PRIMARY KEY REFERENCES denominations (denom_pub_hash) ON DELETE CASCADE" @@ -332,6 +334,7 @@ postgres_create_tables (void *cls) grabbing the money, depending on the Exchange's terms of service) */ SQLEXEC ("CREATE TABLE IF NOT EXISTS reserves" "(reserve_pub BYTEA PRIMARY KEY CHECK(LENGTH(reserve_pub)=32)" + ",account_details TEXT NOT NULL " ",current_balance_val INT8 NOT NULL" ",current_balance_frac INT4 NOT NULL" ",current_balance_curr VARCHAR("TALER_CURRENCY_LEN_STR") NOT NULL" @@ -367,7 +370,7 @@ postgres_create_tables (void *cls) "(close_uuid BIGSERIAL PRIMARY KEY" ",reserve_pub BYTEA NOT NULL REFERENCES reserves (reserve_pub) ON DELETE CASCADE" ",execution_date INT8 NOT NULL" - ",transfer_details TEXT NOT NULL" + ",transfer_details BYTEA NOT NULL CHECK (LENGTH(transfer_details)=32)" ",receiver_account TEXT NOT NULL" ",amount_val INT8 NOT NULL" ",amount_frac INT4 NOT NULL" @@ -715,13 +718,14 @@ postgres_prepare (PGconn *db_conn) PREPARE ("reserve_create", "INSERT INTO reserves " "(reserve_pub" + ",account_details" ",current_balance_val" ",current_balance_frac" ",current_balance_curr" ",expiration_date" ") VALUES " - "($1, $2, $3, $4, $5);", - 5, NULL); + "($1, $2, $3, $4, $5, $6);", + 6, NULL); /* Used in #postgres_insert_reserve_closed() */ PREPARE ("reserves_close_insert", @@ -1581,7 +1585,22 @@ postgres_prepare (PGconn *db_conn) " FROM reserves_close" " WHERE reserve_pub=$1;", 1, NULL); - + + /* Used in #postgres_get_expired_reserves() */ + PREPARE ("get_expired_reserves", + "SELECT" + " expiration_date" + ",account_details" + ",reserve_pub" + ",current_balance_val" + ",current_balance_frac" + ",current_balance_curr" + " FROM reserves" + " WHERE expiration_date<=$1" + " AND (current_balance_val != 0 " + " OR current_balance_frac != 0);", + 1, NULL); + /* Used in #postgres_get_coin_transactions() to obtain payback transactions for a coin */ PREPARE ("payback_by_coin", @@ -2069,6 +2088,7 @@ postgres_reserves_in_insert (void *cls, const json_t *sender_account_details, const json_t *transfer_details) { + struct PostgresClosure *pg = cls; PGresult *result; int reserve_exists; struct TALER_EXCHANGEDB_Reserve reserve; @@ -2090,8 +2110,26 @@ postgres_reserves_in_insert (void *cls, GNUNET_break (0); goto rollback; } + if ( (0 == reserve.balance.value) && + (0 == reserve.balance.fraction) ) + { + /* TODO: reserve balance is empty, we might want to update + sender_account_details here. (So that IF a customer uses the + same reserve public key from a different account, we USUALLY + switch to the new account (but only if the old reserve was + drained).) This helps make sure that on reserve expiration the + funds go back to a valid account in cases where the customer + has closed the old bank account and some (buggy?) wallet keeps + using the same reserve key with the customer's new account. + + Note that for a non-drained reserve we should not switch, + as that opens an attack vector for an adversary who can see + the wire transfer subjects (i.e. when using Bitcoin). + */ + } + expiry = GNUNET_TIME_absolute_add (execution_time, - TALER_IDLE_RESERVE_EXPIRATION_TIME); + pg->idle_reserve_expiration_time); if (GNUNET_NO == reserve_exists) { /* New reserve, create balance for the first time; we do this @@ -2101,6 +2139,7 @@ postgres_reserves_in_insert (void *cls, as a foreign key. */ struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_auto_from_type (reserve_pub), + TALER_PQ_query_param_json (sender_account_details), TALER_PQ_query_param_amount (balance), GNUNET_PQ_query_param_absolute_time (&expiry), GNUNET_PQ_query_param_end @@ -2302,6 +2341,7 @@ postgres_insert_withdraw_info (void *cls, struct TALER_EXCHANGEDB_Session *session, const struct TALER_EXCHANGEDB_CollectableBlindcoin *collectable) { + struct PostgresClosure *pg = cls; PGresult *result; struct TALER_EXCHANGEDB_Reserve reserve; struct GNUNET_HashCode denom_pub_hash; @@ -2356,7 +2396,7 @@ postgres_insert_withdraw_info (void *cls, return GNUNET_NO; } expiry = GNUNET_TIME_absolute_add (now, - TALER_IDLE_RESERVE_EXPIRATION_TIME); + pg->idle_reserve_expiration_time); reserve.expiry = GNUNET_TIME_absolute_max (expiry, reserve.expiry); if (GNUNET_OK != reserves_update (cls, @@ -2618,8 +2658,8 @@ postgres_get_reserve_history (void *cls, &closing->execution_date), TALER_PQ_result_spec_json ("receiver_account", &closing->receiver_account_details), - TALER_PQ_result_spec_json ("transfer_details", - &closing->transfer_details), + GNUNET_PQ_result_spec_auto_from_type ("transfer_details", + &closing->transfer_details), GNUNET_PQ_result_spec_end }; if (GNUNET_OK != @@ -4934,6 +4974,93 @@ postgres_insert_wire_fee (void *cls, } +/** + * Obtain information about expired reserves and their + * remaining balances. + * + * @param cls closure of the plugin + * @param session database connection + * @param now timestamp based on which we decide expiration + * @param rec function to call on expired reserves + * @param rec_cls closure for @a rec + * @return #GNUNET_SYSERR on database error + * #GNUNET_NO if there are no expired non-empty reserves + * #GNUNET_OK on success + */ +static int +postgres_get_expired_reserves (void *cls, + struct TALER_EXCHANGEDB_Session *session, + struct GNUNET_TIME_Absolute now, + TALER_EXCHANGEDB_ReserveExpiredCallback rec, + void *rec_cls) +{ + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_absolute_time (&now), + GNUNET_PQ_query_param_end + }; + PGresult *result; + int nrows; + + result = GNUNET_PQ_exec_prepared (session->conn, + "get_expired_reserves", + params); + if (PGRES_TUPLES_OK != + PQresultStatus (result)) + { + BREAK_DB_ERR (result, session->conn); + PQclear (result); + return GNUNET_SYSERR; + } + nrows = PQntuples (result); + if (0 == nrows) + { + /* no matches found */ + PQclear (result); + return GNUNET_NO; + } + + for (int i=0;iconn, "reserves_close_insert", params); @@ -4985,6 +5114,38 @@ postgres_insert_reserve_closed (void *cls, return GNUNET_SYSERR; } PQclear (result); + + /* update reserve balance */ + reserve.pub = *reserve_pub; + if (GNUNET_OK != postgres_reserve_get (cls, + session, + &reserve)) + { + /* Should have been checked before we got here... */ + GNUNET_break (0); + return GNUNET_SYSERR; + } + ret = TALER_amount_subtract (&reserve.balance, + &reserve.balance, + amount_with_fee); + if (GNUNET_SYSERR == ret) + { + /* The reserve history was checked to make sure there is enough of a balance + left before we tried this; however, concurrent operations may have changed + the situation by now. We should re-try the transaction. */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Closing of reserve `%s' refused due to balance missmatch. Retrying.\n", + TALER_B2S (reserve_pub)); + return GNUNET_NO; + } + GNUNET_break (GNUNET_NO == ret); + if (GNUNET_OK != reserves_update (cls, + session, + &reserve)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } return GNUNET_OK; } @@ -6069,7 +6230,7 @@ postgres_select_reserve_closed_above_serial_id (void *cls, uint64_t rowid; struct TALER_ReservePublicKeyP reserve_pub; json_t *receiver_account; - json_t *transfer_details; + struct TALER_WireTransferIdentifierRawP wtid; struct TALER_Amount amount_with_fee; struct TALER_Amount closing_fee; struct GNUNET_TIME_Absolute execution_date; @@ -6080,8 +6241,8 @@ postgres_select_reserve_closed_above_serial_id (void *cls, &reserve_pub), GNUNET_PQ_result_spec_absolute_time ("execution_date", &execution_date), - TALER_PQ_result_spec_json ("transfer_details", - &transfer_details), + GNUNET_PQ_result_spec_auto_from_type ("transfer_details", + &wtid), TALER_PQ_result_spec_json ("receiver_account", &receiver_account), TALER_PQ_result_spec_amount ("amount", @@ -6107,7 +6268,7 @@ postgres_select_reserve_closed_above_serial_id (void *cls, &closing_fee, &reserve_pub, receiver_account, - transfer_details); + &wtid); GNUNET_PQ_cleanup_result (rs); if (GNUNET_OK != ret) break; @@ -6147,6 +6308,7 @@ postgres_insert_payback_request (void *cls, const struct GNUNET_HashCode *h_blind_ev, struct GNUNET_TIME_Absolute timestamp) { + struct PostgresClosure *pg = cls; PGresult *result; struct GNUNET_TIME_Absolute expiry; struct TALER_EXCHANGEDB_Reserve reserve; @@ -6215,7 +6377,7 @@ postgres_insert_payback_request (void *cls, return GNUNET_SYSERR; } expiry = GNUNET_TIME_absolute_add (timestamp, - TALER_IDLE_RESERVE_EXPIRATION_TIME); + pg->idle_reserve_expiration_time); reserve.expiry = GNUNET_TIME_absolute_max (expiry, reserve.expiry); if (GNUNET_OK != reserves_update (cls, @@ -6464,6 +6626,18 @@ libtaler_plugin_exchangedb_postgres_init (void *cls) return NULL; } } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_time (cfg, + "exchangedb", + "IDLE_RESERVE_EXPIRATION_TIME", + &pg->idle_reserve_expiration_time)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "exchangedb", + "IDLE_RESERVE_EXPIRATION_TIME"); + GNUNET_free (pg); + return NULL; + } plugin = GNUNET_new (struct TALER_EXCHANGEDB_Plugin); plugin->cls = pg; plugin->get_session = &postgres_get_session; @@ -6509,6 +6683,7 @@ libtaler_plugin_exchangedb_postgres_init (void *cls) plugin->insert_aggregation_tracking = &postgres_insert_aggregation_tracking; plugin->insert_wire_fee = &postgres_insert_wire_fee; plugin->get_wire_fee = &postgres_get_wire_fee; + plugin->get_expired_reserves = &postgres_get_expired_reserves; plugin->insert_reserve_closed = &postgres_insert_reserve_closed; plugin->wire_prepare_data_insert = &postgres_wire_prepare_data_insert; plugin->wire_prepare_data_mark_finished = &postgres_wire_prepare_data_mark_finished; diff --git a/src/exchangedb/test-exchange-db-postgres.conf b/src/exchangedb/test-exchange-db-postgres.conf index 0822bab44..926e2997e 100644 --- a/src/exchangedb/test-exchange-db-postgres.conf +++ b/src/exchangedb/test-exchange-db-postgres.conf @@ -6,3 +6,14 @@ DB = postgres #The connection string the plugin has to use for connecting to the database DB_CONN_STR = postgres:///talercheck + + +[exchangedb] + +# After how long do we close idle reserves? The exchange +# and the auditor must agree on this value. We currently +# expect it to be globally defined for the whole system, +# as there is no way for wallets to query this value. Thus, +# it is only configurable for testing, and should be treated +# as constant in production. +IDLE_RESERVE_EXPIRATION_TIME = 4 weeks diff --git a/src/exchangedb/test_exchangedb.c b/src/exchangedb/test_exchangedb.c index 83949d855..341d31f13 100644 --- a/src/exchangedb/test_exchangedb.c +++ b/src/exchangedb/test_exchangedb.c @@ -1612,24 +1612,37 @@ run (void *cls) &value, &cbc.h_coin_envelope, deadline)); + FAILIF (GNUNET_OK != + plugin->select_payback_above_serial_id (plugin->cls, + session, + 0, + &payback_cb, + &coin_blind)); + + GNUNET_assert (GNUNET_OK == + TALER_amount_add (&amount_with_fee, + &value, + &value)); sndr = json_loads ("{ \"account\":\"1\" }", 0, NULL); - just = json_loads ("{ \"trans-details\":\"2\" }", 0, NULL); GNUNET_assert (GNUNET_OK == TALER_string_to_amount (CURRENCY ":0.000010", &fee_closing)); - GNUNET_assert (GNUNET_OK == - TALER_string_to_amount (CURRENCY ":1.000010", - &amount_with_fee)); FAILIF (GNUNET_OK != plugin->insert_reserve_closed (plugin->cls, session, &reserve_pub, GNUNET_TIME_absolute_get (), - sndr /* receiver_account */, - just /* transfer_details */, + sndr, + &wire_out_wtid, &amount_with_fee, &fee_closing)); - json_decref (just); + FAILIF (GNUNET_OK != + check_reserve (session, + &reserve_pub, + 0, + 0, + value.currency)); + json_decref (sndr); result = 7; rh = plugin->get_reserve_history (plugin->cls, @@ -1880,13 +1893,6 @@ run (void *cls) &cbc.h_coin_envelope, deadline)); - FAILIF (GNUNET_OK != - plugin->select_payback_above_serial_id (plugin->cls, - session, - 0, - &payback_cb, - &coin_blind)); - auditor_row_cnt = 0; FAILIF (GNUNET_OK != plugin->select_refunds_above_serial_id (plugin->cls, diff --git a/src/include/taler_exchange_service.h b/src/include/taler_exchange_service.h index 15e51bc2a..d1d6f3bda 100644 --- a/src/include/taler_exchange_service.h +++ b/src/include/taler_exchange_service.h @@ -765,7 +765,7 @@ struct TALER_EXCHANGE_ReserveHistory /** * Wire transfer details for the outgoing wire transfer. */ - json_t *transfer_details; + struct TALER_WireTransferIdentifierRawP wtid; /** * Signature of the coin of type diff --git a/src/include/taler_exchangedb_lib.h b/src/include/taler_exchangedb_lib.h index 561738c22..e4284c27f 100644 --- a/src/include/taler_exchangedb_lib.h +++ b/src/include/taler_exchangedb_lib.h @@ -23,6 +23,7 @@ #ifndef TALER_EXCHANGEDB_LIB_H #define TALER_EXCHANGEDB_LIB_H + #include "taler_signatures.h" diff --git a/src/include/taler_exchangedb_plugin.h b/src/include/taler_exchangedb_plugin.h index 5daa9d2f8..b040077e8 100644 --- a/src/include/taler_exchangedb_plugin.h +++ b/src/include/taler_exchangedb_plugin.h @@ -100,8 +100,8 @@ struct TALER_EXCHANGEDB_ClosingTransfer * Detailed wire transfer information that uniquely identifies the * wire transfer. */ - json_t *transfer_details; - + struct TALER_WireTransferIdentifierRawP transfer_details; + }; @@ -991,7 +991,25 @@ typedef int const struct TALER_Amount *closing_fee, const struct TALER_ReservePublicKeyP *reserve_pub, const json_t *receiver_account, - const json_t *transfer_details); + const struct TALER_WireTransferIdentifierRawP *transfer_details); + + +/** + * Function called with details about expired reserves. + * + * @param cls closure + * @param reserve_pub public key of the reserve + * @param left amount left in the reserve + * @param account_details information about the reserve's bank account + * @param expiration_date when did the reserve expire + * @return #GNUNET_OK to continue to iterate, #GNUNET_SYSERR to stop + */ +typedef int +(*TALER_EXCHANGEDB_ReserveExpiredCallback)(void *cls, + const struct TALER_ReservePublicKeyP *reserve_pub, + const struct TALER_Amount *left, + const json_t *account_details, + struct GNUNET_TIME_Absolute expiration_date); /** @@ -1783,6 +1801,27 @@ struct TALER_EXCHANGEDB_Plugin struct TALER_MasterSignatureP *master_sig); + /** + * Obtain information about expired reserves and their + * remaining balances. + * + * @param cls closure of the plugin + * @param session database connection + * @param now timestamp based on which we decide expiration + * @param rec function to call on expired reserves + * @param rec_cls closure for @a rec + * @return #GNUNET_SYSERR on database error + * #GNUNET_NO if there are no expired non-empty reserves + * #GNUNET_OK on success + */ + int + (*get_expired_reserves)(void *cls, + struct TALER_EXCHANGEDB_Session *session, + struct GNUNET_TIME_Absolute now, + TALER_EXCHANGEDB_ReserveExpiredCallback rec, + void *rec_cls); + + /** * Insert reserve close operation into database. * @@ -1800,10 +1839,10 @@ struct TALER_EXCHANGEDB_Plugin int (*insert_reserve_closed)(void *cls, struct TALER_EXCHANGEDB_Session *session, - struct TALER_ReservePublicKeyP *reserve_pub, + const struct TALER_ReservePublicKeyP *reserve_pub, struct GNUNET_TIME_Absolute execution_date, const json_t *receiver_account, - const json_t *transfer_details, + const struct TALER_WireTransferIdentifierRawP *transfer_details, const struct TALER_Amount *amount_with_fee, const struct TALER_Amount *closing_fee); diff --git a/src/include/taler_signatures.h b/src/include/taler_signatures.h index de9a2f7c7..f46013090 100644 --- a/src/include/taler_signatures.h +++ b/src/include/taler_signatures.h @@ -46,11 +46,6 @@ */ #define TALER_CNC_KAPPA 3 -/** - * After what time do idle reserves "expire"? We might want to make - * this a configuration option (eventually). - */ -#define TALER_IDLE_RESERVE_EXPIRATION_TIME GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_YEARS, 5) /*********************************************/ /* Exchange offline signatures (with master key) */ @@ -1269,9 +1264,9 @@ struct TALER_ReserveCloseConfirmationPS struct GNUNET_HashCode h_wire; /** - * Hash of the transfer details. + * Wire transfer subject. */ - struct GNUNET_HashCode h_transfer; + struct TALER_WireTransferIdentifierRawP wtid; };