summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/auditor/taler-helper-auditor-aggregation.c8
-rw-r--r--src/auditor/taler-helper-auditor-coins.c6
-rw-r--r--src/exchange/taler-exchange-httpd_coins_get.c216
-rw-r--r--src/exchange/taler-exchange-httpd_reserves_history.c2
-rw-r--r--src/exchangedb/0002-purse_decision.sql25
-rw-r--r--src/exchangedb/0002-refresh_commitments.sql2
-rw-r--r--src/exchangedb/pg_get_coin_transactions.c417
-rw-r--r--src/exchangedb/pg_get_coin_transactions.h26
-rw-r--r--src/exchangedb/test_exchangedb.c11
-rw-r--r--src/include/taler_exchangedb_plugin.h24
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);