diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/auditor/taler-helper-auditor-aggregation.c | 8 | ||||
-rw-r--r-- | src/auditor/taler-helper-auditor-coins.c | 6 | ||||
-rw-r--r-- | src/exchange/taler-exchange-httpd_coins_get.c | 216 | ||||
-rw-r--r-- | src/exchange/taler-exchange-httpd_reserves_history.c | 2 | ||||
-rw-r--r-- | src/exchangedb/0002-purse_decision.sql | 25 | ||||
-rw-r--r-- | src/exchangedb/0002-refresh_commitments.sql | 2 | ||||
-rw-r--r-- | src/exchangedb/pg_get_coin_transactions.c | 417 | ||||
-rw-r--r-- | src/exchangedb/pg_get_coin_transactions.h | 26 | ||||
-rw-r--r-- | src/exchangedb/test_exchangedb.c | 11 | ||||
-rw-r--r-- | src/include/taler_exchangedb_plugin.h | 24 |
10 files changed, 502 insertions, 235 deletions
diff --git a/src/auditor/taler-helper-auditor-aggregation.c b/src/auditor/taler-helper-auditor-aggregation.c index 81892c16c..8075e100b 100644 --- a/src/auditor/taler-helper-auditor-aggregation.c +++ b/src/auditor/taler-helper-auditor-aggregation.c @@ -767,7 +767,7 @@ wire_transfer_information_cb ( struct TALER_CoinPublicInfo coin; enum GNUNET_DB_QueryStatus qs; struct TALER_PaytoHashP hpt; - uint64_t etag = 0; + uint64_t etag_out; TALER_payto_hash (account_pay_uri, &hpt); @@ -780,12 +780,14 @@ wire_transfer_information_cb ( "h-payto does not match payto URI"); } /* Obtain coin's transaction history */ - /* TODO: could use 'etag' mechanism to only fetch transactions + /* TODO: could use 'start' mechanism to only fetch transactions we did not yet process, instead of going over them again and again.*/ qs = TALER_ARL_edb->get_coin_transactions (TALER_ARL_edb->cls, coin_pub, - &etag, + 0, + 0, + &etag_out, &tl); if ( (qs < 0) || (NULL == tl) ) diff --git a/src/auditor/taler-helper-auditor-coins.c b/src/auditor/taler-helper-auditor-coins.c index 8c3d66b98..f873fa3cb 100644 --- a/src/auditor/taler-helper-auditor-coins.c +++ b/src/auditor/taler-helper-auditor-coins.c @@ -435,14 +435,16 @@ check_coin_history (const struct TALER_CoinSpendPublicKeyP *coin_pub, struct TALER_Amount refunded; struct TALER_Amount deposit_fee; bool have_refund; - uint64_t etag = 0; + uint64_t etag_out; /* TODO: could use 'etag' mechanism to only fetch transactions we did not yet process, instead of going over them again and again. */ qs = TALER_ARL_edb->get_coin_transactions (TALER_ARL_edb->cls, coin_pub, - &etag, + 0, + 0, + &etag_out, &tl); if (0 >= qs) return qs; diff --git a/src/exchange/taler-exchange-httpd_coins_get.c b/src/exchange/taler-exchange-httpd_coins_get.c index c5cf6ba56..7553bf199 100644 --- a/src/exchange/taler-exchange-httpd_coins_get.c +++ b/src/exchange/taler-exchange-httpd_coins_get.c @@ -15,7 +15,7 @@ */ /** * @file taler-exchange-httpd_coins_get.c - * @brief Handle GET /coins/$COIN_PUB requests + * @brief Handle GET /coins/$COIN_PUB/history requests * @author Christian Grothoff */ #include "platform.h" @@ -538,102 +538,162 @@ MHD_RESULT TEH_handler_coins_get (struct TEH_RequestContext *rc, const struct TALER_CoinSpendPublicKeyP *coin_pub) { - enum GNUNET_DB_QueryStatus qs; - struct TALER_EXCHANGEDB_TransactionList *tl; - const char *etags; - uint64_t etag = 0; - - etags = MHD_lookup_connection_value (rc->connection, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_IF_NONE_MATCH); - if (NULL != etags) + struct TALER_EXCHANGEDB_TransactionList *tl = NULL; + uint64_t start_off = 0; + uint64_t etag_in = 0; + uint64_t etag_out; + char etagp[24]; + struct MHD_Response *resp; + unsigned int http_status; + + TALER_MHD_parse_request_number (rc->connection, + "start", + &start_off); + /* Check signature */ { - char dummy; - unsigned long long ev; + struct TALER_CoinSpendSignatureP coin_sig; + bool required = true; - if (1 != sscanf (etags, - "\"%llu\"%c", - &ev, - &dummy)) + TALER_MHD_parse_request_header_auto (rc->connection, + TALER_COIN_HISTORY_SIGNATURE_HEADER, + &coin_sig, + required); + if (GNUNET_OK != + TALER_wallet_coin_history_verify (start_off, + coin_pub, + &coin_sig)) { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Client send malformed `If-None-Match' header `%s'\n", - etags); + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_COIN_HISTORY_BAD_SIGNATURE, + NULL); + } + } + + /* Get etag */ + { + const char *etags; + + etags = MHD_lookup_connection_value (rc->connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_IF_NONE_MATCH); + if (NULL != etags) + { + char dummy; + unsigned long long ev; + + if (1 != sscanf (etags, + "\"%llu\"%c", + &ev, + &dummy)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Client send malformed `If-None-Match' header `%s'\n", + etags); + } + else + { + etag_in = (uint64_t) ev; + } } else { - etag = (uint64_t) ev; + etag_in = start_off; } } - qs = TEH_plugin->get_coin_transactions (TEH_plugin->cls, - coin_pub, - &etag, - &tl); - switch (qs) + + /* Get history from DB between etag and now */ { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_coin_history"); - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); /* single-shot query should never have soft-errors */ - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - "get_coin_history"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - if (0 == etag) + enum GNUNET_DB_QueryStatus qs; + + qs = TEH_plugin->get_coin_transactions (TEH_plugin->cls, + coin_pub, + start_off, + etag_in, + &etag_out, + &tl); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_coin_history"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); /* single-shot query should never have soft-errors */ + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + "get_coin_history"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_NOT_FOUND, TALER_EC_EXCHANGE_GENERIC_COIN_UNKNOWN, NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* Handled below */ + break; + } + } + + GNUNET_snprintf (etagp, + sizeof (etagp), + "\"%llu\"", + (unsigned long long) etag_out); + if (etag_in == etag_out) + { return TEH_RESPONSE_reply_not_modified (rc->connection, - etags, + etagp, &add_response_headers, NULL); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + } + if (NULL == tl) + { + /* 204: empty history */ + resp = MHD_create_response_from_buffer (0, + "", + MHD_RESPMEM_PERSISTENT); + http_status = MHD_HTTP_NO_CONTENT; + } + else + { + /* 200: regular history */ + json_t *history; + + history = compile_transaction_history (coin_pub, + tl); + TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, + tl); + tl = NULL; + if (NULL == history) { - json_t *history; - char etagp[24]; - MHD_RESULT ret; - struct MHD_Response *resp; - - GNUNET_snprintf (etagp, - sizeof (etagp), - "\"%llu\"", - (unsigned long long) etag); - history = compile_transaction_history (coin_pub, - tl); - TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, - tl); - tl = NULL; - if (NULL == history) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_JSON_ALLOCATION_FAILURE, - "Failed to compile coin history"); - } - resp = TALER_MHD_MAKE_JSON_PACK ( - GNUNET_JSON_pack_array_steal ("history", - history)); - GNUNET_break (MHD_YES == - MHD_add_response_header (resp, - MHD_HTTP_HEADER_ETAG, - etagp)); - ret = MHD_queue_response (rc->connection, - MHD_HTTP_OK, - resp); - GNUNET_break (MHD_YES == ret); - MHD_destroy_response (resp); - return ret; + GNUNET_break (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_JSON_ALLOCATION_FAILURE, + "Failed to compile coin history"); } + resp = TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_array_steal ("history", + history)); + http_status = MHD_HTTP_OK; + } + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_ETAG, + etagp)); + { + MHD_RESULT ret; + + ret = MHD_queue_response (rc->connection, + http_status, + resp); + GNUNET_break (MHD_YES == ret); + MHD_destroy_response (resp); + return ret; } - GNUNET_break (0); - return MHD_NO; } diff --git a/src/exchange/taler-exchange-httpd_reserves_history.c b/src/exchange/taler-exchange-httpd_reserves_history.c index 0c692c8fd..a73b5ab69 100644 --- a/src/exchange/taler-exchange-httpd_reserves_history.c +++ b/src/exchange/taler-exchange-httpd_reserves_history.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2022 Taler Systems SA + Copyright (C) 2014-2023 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software diff --git a/src/exchangedb/0002-purse_decision.sql b/src/exchangedb/0002-purse_decision.sql index 3eeeea8a7..bd712ad25 100644 --- a/src/exchangedb/0002-purse_decision.sql +++ b/src/exchangedb/0002-purse_decision.sql @@ -1,6 +1,6 @@ -- -- This file is part of TALER --- Copyright (C) 2014--2022 Taler Systems SA +-- Copyright (C) 2014--2023 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 @@ -74,16 +74,19 @@ CREATE OR REPLACE FUNCTION purse_decision_insert_trigger() LANGUAGE plpgsql AS $$ BEGIN - INSERT INTO exchange.coin_history - (coin_pub - ,table_name - ,serial_id) - SELECT - pd.coin_pub - ,'purse_decision' - ,NEW.purse_decision_serial_id - FROM purse_deposits pd - WHERE purse_pub = NEW.purse_pub; + IF NEW.refunded + THEN + INSERT INTO exchange.coin_history + (coin_pub + ,table_name + ,serial_id) + SELECT + pd.coin_pub + ,'purse_decision' + ,NEW.purse_decision_serial_id + FROM purse_deposits pd + WHERE purse_pub = NEW.purse_pub; + END IF; RETURN NEW; END $$; COMMENT ON FUNCTION purse_decision_insert_trigger() diff --git a/src/exchangedb/0002-refresh_commitments.sql b/src/exchangedb/0002-refresh_commitments.sql index 50e298c2a..e577f1e1c 100644 --- a/src/exchangedb/0002-refresh_commitments.sql +++ b/src/exchangedb/0002-refresh_commitments.sql @@ -1,6 +1,6 @@ -- -- This file is part of TALER --- Copyright (C) 2014--2022 Taler Systems SA +-- Copyright (C) 2014--2023 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 diff --git a/src/exchangedb/pg_get_coin_transactions.c b/src/exchangedb/pg_get_coin_transactions.c index 704e7c5c5..e5d3b9b01 100644 --- a/src/exchangedb/pg_get_coin_transactions.c +++ b/src/exchangedb/pg_get_coin_transactions.c @@ -24,8 +24,17 @@ #include "taler_pq_lib.h" #include "pg_get_coin_transactions.h" #include "pg_helper.h" +#include "pg_start_read_committed.h" +#include "pg_commit.h" +#include "pg_rollback.h" #include "plugin_exchangedb_common.h" +/** + * How often do we re-try when encountering DB serialization issues? + * (We are read-only, so can only happen due to concurrent insert, + * which should be very rare.) + */ +#define RETRIES 3 /** * Closure for callbacks called from #postgres_get_coin_transactions() @@ -43,11 +52,6 @@ struct CoinHistoryContext const struct TALER_CoinSpendPublicKeyP *coin_pub; /** - * Closure for all callbacks of this database plugin. - */ - void *db_cls; - - /** * Plugin context. */ struct PostgresClosure *pg; @@ -57,10 +61,6 @@ struct CoinHistoryContext */ bool failed; - /** - * Set to 'true' if we found a deposit or melt (for invariant check). - */ - bool have_deposit_or_melt; }; @@ -86,7 +86,6 @@ add_coin_deposit (void *cls, struct TALER_EXCHANGEDB_TransactionList *tl; uint64_t serial_id; - chc->have_deposit_or_melt = true; deposit = GNUNET_new (struct TALER_EXCHANGEDB_DepositListEntry); { struct GNUNET_PQ_ResultSpec rs[] = { @@ -170,7 +169,6 @@ add_coin_purse_deposit (void *cls, struct TALER_EXCHANGEDB_TransactionList *tl; uint64_t serial_id; - chc->have_deposit_or_melt = true; deposit = GNUNET_new (struct TALER_EXCHANGEDB_PurseDepositListEntry); { bool not_finished; @@ -246,7 +244,6 @@ add_coin_melt (void *cls, struct TALER_EXCHANGEDB_TransactionList *tl; uint64_t serial_id; - chc->have_deposit_or_melt = true; melt = GNUNET_new (struct TALER_EXCHANGEDB_MeltListEntry); { struct GNUNET_PQ_ResultSpec rs[] = { @@ -669,6 +666,11 @@ add_coin_reserve_open (void *cls, struct Work { /** + * Name of the table. + */ + const char *table; + + /** * SQL prepared statement name. */ const char *statement; @@ -680,58 +682,174 @@ struct Work }; -enum GNUNET_DB_QueryStatus -TEH_PG_get_coin_transactions ( - void *cls, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - uint64_t *etag, - struct TALER_EXCHANGEDB_TransactionList **tlp) +/** + * We found a coin history entry. Lookup details + * from the respective table and store in @a cls. + * + * @param[in,out] cls a `struct CoinHistoryContext` + * @param result a coin history entry result set + * @param num_results total number of results in @a results + */ +static void +handle_history_entry (void *cls, + PGresult *result, + unsigned int num_results) { - struct PostgresClosure *pg = cls; + struct CoinHistoryContext *chc = cls; + struct PostgresClosure *pg = chc->pg; static const struct Work work[] = { /** #TALER_EXCHANGEDB_TT_DEPOSIT */ - { "get_deposit_with_coin_pub", + { "coin_deposits", + "get_deposit_with_coin_pub", &add_coin_deposit }, /** #TALER_EXCHANGEDB_TT_MELT */ - { "get_refresh_session_by_coin", + { "refresh_commitments", + "get_refresh_session_by_coin", &add_coin_melt }, /** #TALER_EXCHANGEDB_TT_PURSE_DEPOSIT */ - { "get_purse_deposit_by_coin_pub", + { "purse_deposits", + "get_purse_deposit_by_coin_pub", &add_coin_purse_deposit }, /** #TALER_EXCHANGEDB_TT_PURSE_REFUND */ - { "get_purse_decision_by_coin_pub", + { "purse_decision", + "get_purse_decision_by_coin_pub", &add_coin_purse_decision }, /** #TALER_EXCHANGEDB_TT_REFUND */ - { "get_refunds_by_coin", + { "refunds", + "get_refunds_by_coin", &add_coin_refund }, /** #TALER_EXCHANGEDB_TT_OLD_COIN_RECOUP */ - { "recoup_by_old_coin", + { "recoup_refresh::OLD", + "recoup_by_old_coin", &add_old_coin_recoup }, /** #TALER_EXCHANGEDB_TT_RECOUP */ - { "recoup_by_coin", + { "recoup", + "recoup_by_coin", &add_coin_recoup }, /** #TALER_EXCHANGEDB_TT_RECOUP_REFRESH */ - { "recoup_by_refreshed_coin", + { "recoup_refresh::NEW", + "recoup_by_refreshed_coin", &add_coin_recoup_refresh }, /** #TALER_EXCHANGEDB_TT_RESERVE_OPEN */ - { "reserve_open_by_coin", + { "reserves_open_deposits", + "reserve_open_by_coin", &add_coin_reserve_open }, - { NULL, NULL } + { NULL, NULL, NULL } + }; + char *table_name; + uint64_t serial_id; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_string ("table_name", + &table_name), + GNUNET_PQ_result_spec_uint64 ("serial_id", + &serial_id), + GNUNET_PQ_result_spec_end + }; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (chc->coin_pub), + GNUNET_PQ_query_param_uint64 (&serial_id), + GNUNET_PQ_query_param_end }; + + for (unsigned int i = 0; i<num_results; i++) + { + enum GNUNET_DB_QueryStatus qs; + bool found = false; + + if (GNUNET_OK != + GNUNET_PQ_extract_result (result, + rs, + i)) + { + GNUNET_break (0); + chc->failed = true; + return; + } + + for (unsigned int s = 0; + NULL != work[s].statement; + s++) + { + if (0 != strcmp (table_name, + work[s].table)) + continue; + found = true; + qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, + work[s].statement, + params, + work[s].cb, + chc); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Coin %s had %d transactions at %llu in table %s\n", + TALER_B2S (chc->coin_pub), + (int) qs, + (unsigned long long) serial_id, + table_name); + if (0 >= qs) + chc->failed = true; + break; + } + if (! found) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Coin history includes unsupported table `%s`\n", + table_name); + chc->failed = true; + } + GNUNET_PQ_cleanup_result (rs); + if (chc->failed) + break; + } +} + + +enum GNUNET_DB_QueryStatus +TEH_PG_get_coin_transactions ( + void *cls, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + uint64_t start_off, + uint64_t etag_in, + uint64_t *etag_out, + struct TALER_EXCHANGEDB_TransactionList **tlp) +{ + struct PostgresClosure *pg = cls; struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_auto_from_type (coin_pub), GNUNET_PQ_query_param_end }; - enum GNUNET_DB_QueryStatus qs; + struct GNUNET_PQ_QueryParam lparams[] = { + GNUNET_PQ_query_param_auto_from_type (coin_pub), + GNUNET_PQ_query_param_uint64 (&start_off), + GNUNET_PQ_query_param_end + }; struct CoinHistoryContext chc = { .head = NULL, .coin_pub = coin_pub, - .pg = pg, - .db_cls = cls + .pg = pg }; - *etag = 0; // FIXME: etag not yet implemented! - PREPARE (pg, // done! + *tlp = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Getting transactions for coin %s\n", + TALER_B2S (coin_pub)); + PREPARE (pg, + "get_coin_history_etag", + "SELECT" + " coin_history_serial_id" + " FROM coin_history" + " WHERE coin_pub=$1" + " ORDER BY coin_history_serial_id DESC" + " LIMIT 1;"); + PREPARE (pg, + "get_coin_history", + "SELECT" + " table_name" + ",serial_id" + " FROM coin_history" + " WHERE coin_pub=$1" + " AND coin_history_serial_id > $2" + " ORDER BY coin_history_serial_id DESC;"); + PREPARE (pg, "get_deposit_with_coin_pub", "SELECT" " cdep.amount_with_fee" @@ -758,8 +876,9 @@ TEH_PG_get_coin_transactions ( " ON (kc.coin_pub = cdep.coin_pub)" " JOIN denominations denoms" " USING (denominations_serial)" - " WHERE cdep.coin_pub=$1;"); - PREPARE (pg, // done! + " WHERE cdep.coin_pub=$1" + " AND cdep.coin_deposit_serial_id=$2;"); + PREPARE (pg, "get_refresh_session_by_coin", "SELECT" " rc" @@ -774,8 +893,9 @@ TEH_PG_get_coin_transactions ( " ON (refresh_commitments.old_coin_pub = kc.coin_pub)" " JOIN denominations denoms" " USING (denominations_serial)" - " WHERE old_coin_pub=$1;"); - PREPARE (pg, // done! + " WHERE old_coin_pub=$1" + " AND melt_serial_id=$2;"); + PREPARE (pg, "get_purse_deposit_by_coin_pub", "SELECT" " partner_base_url" @@ -797,10 +917,26 @@ TEH_PG_get_coin_transactions ( " ON (pd.coin_pub = kc.coin_pub)" " JOIN denominations denoms" " USING (denominations_serial)" - // FIXME: use to-be-created materialized index - // on coin_pub (query crosses partitions!) - " WHERE pd.coin_pub=$1;"); - PREPARE (pg, // done! + " WHERE pd.coin_pub=$1" + " AND pd.purse_deposit_serial_id=$2;"); + PREPARE (pg, + "get_purse_decision_by_coin_pub", + "SELECT" + " pdes.purse_pub" + ",pd.amount_with_fee" + ",denom.fee_refund" + ",pdes.purse_decision_serial_id" + " FROM purse_decision pdes" + " JOIN purse_deposits pd" + " USING (purse_pub)" + " JOIN known_coins kc" + " ON (pd.coin_pub = kc.coin_pub)" + " JOIN denominations denom" + " USING (denominations_serial)" + " WHERE pd.coin_pub=$1" + " AND pdes.purse_decision_serial_id=$2" + " AND pdes.refunded;"); + PREPARE (pg, "get_refunds_by_coin", "SELECT" " bdep.merchant_pub" @@ -819,83 +955,66 @@ TEH_PG_get_coin_transactions ( " ON (ref.coin_pub = kc.coin_pub)" " JOIN denominations denom" " USING (denominations_serial)" - " WHERE ref.coin_pub=$1;"); - PREPARE (pg, // done! - "get_purse_decision_by_coin_pub", - "SELECT" - " pdes.purse_pub" - ",pd.amount_with_fee" - ",denom.fee_refund" - ",pdes.purse_decision_serial_id" - " FROM purse_deposits pd" - " JOIN purse_decision pdes" - " USING (purse_pub)" - " JOIN known_coins kc" - " ON (pd.coin_pub = kc.coin_pub)" - " JOIN denominations denom" - " USING (denominations_serial)" - " WHERE pd.coin_pub=$1" - " AND pdes.refunded;"); - PREPARE (pg, // done! + " WHERE ref.coin_pub=$1" + " AND ref.refund_serial_id=$2;"); + PREPARE (pg, "recoup_by_old_coin", "SELECT" " coins.coin_pub" - ",coin_sig" - ",coin_blind" - ",amount" - ",recoup_timestamp" + ",rr.coin_sig" + ",rr.coin_blind" + ",rr.amount" + ",rr.recoup_timestamp" ",denoms.denom_pub_hash" ",coins.denom_sig" - ",recoup_refresh_uuid" - " FROM recoup_refresh" + ",rr.recoup_refresh_uuid" + " FROM recoup_refresh rr" " JOIN known_coins coins" " USING (coin_pub)" " JOIN denominations denoms" " USING (denominations_serial)" - " WHERE rrc_serial IN" + " WHERE recoup_refresh_uuid=$2" + " AND rrc_serial IN" " (SELECT rrc.rrc_serial" - " FROM refresh_commitments" - " JOIN refresh_revealed_coins rrc" - " USING (melt_serial_id)" - " WHERE old_coin_pub=$1);"); - PREPARE (pg, // done + " FROM refresh_commitments melt" + " JOIN refresh_revealed_coins rrc" + " USING (melt_serial_id)" + " WHERE melt.old_coin_pub=$1);"); + PREPARE (pg, "recoup_by_coin", "SELECT" - " reserves.reserve_pub" + " res.reserve_pub" ",denoms.denom_pub_hash" - ",coin_sig" - ",coin_blind" - ",amount" - ",recoup_timestamp" - ",recoup_uuid" + ",rcp.coin_sig" + ",rcp.coin_blind" + ",rcp.amount" + ",rcp.recoup_timestamp" + ",rcp.recoup_uuid" " FROM recoup rcp" - /* NOTE: suboptimal JOIN follows: crosses shards! - Could theoretically be improved via a materialized - index. But likely not worth it (query is rare and - number of reserve shards might be limited) */ " JOIN reserves_out ro" " USING (reserve_out_serial_id)" - " JOIN reserves" + " JOIN reserves res" " USING (reserve_uuid)" " JOIN known_coins coins" " USING (coin_pub)" " JOIN denominations denoms" " ON (denoms.denominations_serial = coins.denominations_serial)" - " WHERE coins.coin_pub=$1;"); + " WHERE rcp.recoup_uuid=$2" + " AND coins.coin_pub=$1;"); /* Used in #postgres_get_coin_transactions() to obtain recoup transactions for a refreshed coin */ - PREPARE (pg, // done! + PREPARE (pg, "recoup_by_refreshed_coin", "SELECT" " old_coins.coin_pub AS old_coin_pub" - ",coin_sig" - ",coin_blind" - ",amount" - ",recoup_timestamp" + ",rr.coin_sig" + ",rr.coin_blind" + ",rr.amount" + ",rr.recoup_timestamp" ",denoms.denom_pub_hash" ",coins.denom_sig" ",recoup_refresh_uuid" - " FROM recoup_refresh" + " FROM recoup_refresh rr" " JOIN refresh_revealed_coins rrc" " USING (rrc_serial)" " JOIN refresh_commitments rfc" @@ -903,11 +1022,12 @@ TEH_PG_get_coin_transactions ( " JOIN known_coins old_coins" " ON (rfc.old_coin_pub = old_coins.coin_pub)" " JOIN known_coins coins" - " ON (recoup_refresh.coin_pub = coins.coin_pub)" + " ON (rr.coin_pub = coins.coin_pub)" " JOIN denominations denoms" " ON (denoms.denominations_serial = coins.denominations_serial)" - " WHERE coins.coin_pub=$1;"); - PREPARE (pg, // done + " WHERE rr.recoup_refresh_uuid=$2" + " AND coins.coin_pub=$1;"); + PREPARE (pg, "reserve_open_by_coin", "SELECT" " reserve_open_deposit_uuid" @@ -915,36 +1035,97 @@ TEH_PG_get_coin_transactions ( ",reserve_sig" ",contribution" " FROM reserves_open_deposits" - " WHERE coin_pub=$1;"); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Getting transactions for coin %s\n", - TALER_B2S (coin_pub)); - for (unsigned int i = 0; NULL != work[i].statement; i++) + " WHERE coin_pub=$1" + " AND reserve_open_deposit_uuid=$2;"); + + for (unsigned int i = 0; i<RETRIES; i++) { - qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - work[i].statement, - params, - work[i].cb, - &chc); + enum GNUNET_DB_QueryStatus qs; + uint64_t end; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_uint64 ("coin_history_serial_id", + &end), + GNUNET_PQ_result_spec_end + }; + + if (GNUNET_OK != + TEH_PG_start_read_committed (pg, + "get-coin-transactions")) + { + GNUNET_break (0); + return GNUNET_DB_STATUS_HARD_ERROR; + } + /* First only check the last item, to see if + we even need to iterate */ + qs = GNUNET_PQ_eval_prepared_singleton_select ( + pg->conn, + "get_coin_history_etag", + params, + rs); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + TEH_PG_rollback (pg); + return qs; + case GNUNET_DB_STATUS_SOFT_ERROR: + TEH_PG_rollback (pg); + continue; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + TEH_PG_rollback (pg); + return qs; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + *etag_out = end; + if (end == etag_in) + return qs; + } + /* We indeed need to iterate over the history */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Coin %s yielded %d transactions of type %s\n", + "Current ETag for coin %s is %llu\n", TALER_B2S (coin_pub), - qs, - work[i].statement); - if ( (0 > qs) || - (chc.failed) ) + (unsigned long long) end); + + qs = GNUNET_PQ_eval_prepared_multi_select ( + pg->conn, + "get_coin_history", + lparams, + &handle_history_entry, + &chc); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + TEH_PG_rollback (pg); + return qs; + case GNUNET_DB_STATUS_SOFT_ERROR: + TEH_PG_rollback (pg); + continue; + default: + break; + } + if (chc.failed) + { + TEH_PG_rollback (pg); + TEH_COMMON_free_coin_transaction_list (pg, + chc.head); + return GNUNET_DB_STATUS_SOFT_ERROR; + } + qs = TEH_PG_commit (pg); + switch (qs) { - if (NULL != chc.head) - TEH_COMMON_free_coin_transaction_list (cls, - chc.head); - *tlp = NULL; - if (chc.failed) - qs = GNUNET_DB_STATUS_HARD_ERROR; + case GNUNET_DB_STATUS_HARD_ERROR: + TEH_COMMON_free_coin_transaction_list (pg, + chc.head); + chc.head = NULL; return qs; + case GNUNET_DB_STATUS_SOFT_ERROR: + TEH_COMMON_free_coin_transaction_list (pg, + chc.head); + chc.head = NULL; + continue; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + *tlp = chc.head; + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; } } - *tlp = chc.head; - if (NULL == chc.head) - return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; - return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; + return GNUNET_DB_STATUS_SOFT_ERROR; } diff --git a/src/exchangedb/pg_get_coin_transactions.h b/src/exchangedb/pg_get_coin_transactions.h index d49b97bc6..c19df3874 100644 --- a/src/exchangedb/pg_get_coin_transactions.h +++ b/src/exchangedb/pg_get_coin_transactions.h @@ -27,23 +27,31 @@ /** - * Compile a list of all (historic) transactions performed with the given coin - * (/refresh/melt, /deposit, /refund and /recoup operations). - * Should return 0 if @a etag is already current, otherwise - * return the full history and update @a etag. @a etag - * should be set to the last row ID of the given coin - * in the coin history table. + * Compile a list of (historic) transactions performed with the given coin + * (melt, refund, recoup and deposit operations). Should return 0 if the @a + * coin_pub is unknown, otherwise determine @a etag_out and if it is past @a + * etag_in return the history after @a start_off. @a etag_out should be set + * to the last row ID of the given @a coin_pub in the coin history table. * - * @param cls the `struct PostgresClosure` with the plugin-specific state + * @param cls the @e cls of this struct with the plugin-specific state * @param coin_pub coin to investigate - * @param[in,out] etag known etag, updated to current etag * @param[out] tlp set to list of transactions, NULL if coin is fresh + * @param start_off starting offset from which on to return entries + * @param etag_in up to this offset the client already has a response, do not + * return anything unless @a etag_out will be larger + * @param[out] etag_out set to the latest history offset known for this @a coin_pub + * @param[out] tlp set to list of transactions, set to NULL if coin has no + * transaction history past @a start_off or if @a etag_in is equal + * to the value written to @a etag_out. * @return database transaction status */ enum GNUNET_DB_QueryStatus TEH_PG_get_coin_transactions ( void *cls, const struct TALER_CoinSpendPublicKeyP *coin_pub, - uint64_t *etag, + uint64_t start_off, + uint64_t etag_in, + uint64_t *etag_out, struct TALER_EXCHANGEDB_TransactionList **tlp); + #endif diff --git a/src/exchangedb/test_exchangedb.c b/src/exchangedb/test_exchangedb.c index 9a30a1895..f2df1f382 100644 --- a/src/exchangedb/test_exchangedb.c +++ b/src/exchangedb/test_exchangedb.c @@ -1723,13 +1723,16 @@ run (void *cls) /* Just to test fetching a coin with melt history */ struct TALER_EXCHANGEDB_TransactionList *tl; enum GNUNET_DB_QueryStatus qs; - uint64_t etag = 0; + uint64_t etag; qs = plugin->get_coin_transactions (plugin->cls, &refresh.coin.coin_pub, + 0, + 0, &etag, &tl); - FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs); + FAILIF (0 >= qs); + FAILIF (NULL == tl); plugin->free_coin_transaction_list (plugin->cls, tl); } @@ -1980,6 +1983,8 @@ run (void *cls) qs = plugin->get_coin_transactions (plugin->cls, &refund.coin.coin_pub, + 0, + 0, &etag, &tl); } @@ -2437,7 +2442,7 @@ main (int argc, return -1; } GNUNET_log_setup (argv[0], - "WARNING", + "INFO", NULL); plugin_name++; (void) GNUNET_asprintf (&testname, diff --git a/src/include/taler_exchangedb_plugin.h b/src/include/taler_exchangedb_plugin.h index cc71f7770..fc11a292f 100644 --- a/src/include/taler_exchangedb_plugin.h +++ b/src/include/taler_exchangedb_plugin.h @@ -4652,23 +4652,29 @@ struct TALER_EXCHANGEDB_Plugin /** - * Compile a list of all (historic) transactions performed - * with the given coin (melt, refund, recoup and deposit operations). - * Should return 0 if @a etag is already current, otherwise - * return the full history and update @a etag. @a etag - * should be set to the last row ID of the given coin - * in the coin history table. + * Compile a list of (historic) transactions performed with the given coin + * (melt, refund, recoup and deposit operations). Should return 0 if the @a + * coin_pub is unknown, otherwise determine @a etag_out and if it is past @a + * etag_in return the history after @a start_off. @a etag_out should be set + * to the last row ID of the given @a coin_pub in the coin history table. * * @param cls the @e cls of this struct with the plugin-specific state * @param coin_pub coin to investigate - * @param[in,out] etag known etag, updated to current etag - * @param[out] tlp set to list of transactions, NULL if coin is fresh + * @param start_off starting offset from which on to return entries + * @param etag_in up to this offset the client already has a response, do not + * return anything unless @a etag_out will be larger + * @param[out] etag_out set to the latest history offset known for this @a coin_pub + * @param[out] tlp set to list of transactions, set to NULL if coin has no + * transaction history past @a start_off or if @a etag_in is equal + * to the value written to @a etag_out. * @return database transaction status */ enum GNUNET_DB_QueryStatus (*get_coin_transactions)(void *cls, const struct TALER_CoinSpendPublicKeyP *coin_pub, - uint64_t *etag, + uint64_t start_off, + uint64_t etag_in, + uint64_t *etag_out, struct TALER_EXCHANGEDB_TransactionList **tlp); |