From 87376e02eba3f5c2cf83a493446dee0c300565a4 Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Sat, 25 Dec 2021 13:56:33 +0100 Subject: protocol v12 changes (/recoup split, signature changes) plus database sharding plus O(n^2)=>O(n) worst-case complexity reduction on coin balance checks --- src/exchange/Makefile.am | 1 + src/exchange/taler-exchange-httpd.c | 5 + src/exchange/taler-exchange-httpd_db.c | 313 +--------------- src/exchange/taler-exchange-httpd_db.h | 31 +- src/exchange/taler-exchange-httpd_deposit.c | 149 +++----- src/exchange/taler-exchange-httpd_keys.c | 2 +- src/exchange/taler-exchange-httpd_melt.c | 254 ++++++------- src/exchange/taler-exchange-httpd_metrics.c | 20 +- src/exchange/taler-exchange-httpd_metrics.h | 5 +- src/exchange/taler-exchange-httpd_recoup-refresh.c | 411 +++++++++++++++++++++ src/exchange/taler-exchange-httpd_recoup-refresh.h | 46 +++ src/exchange/taler-exchange-httpd_recoup.c | 320 +++++----------- .../taler-exchange-httpd_refreshes_reveal.c | 383 +++++-------------- src/exchange/taler-exchange-httpd_refund.c | 296 ++++----------- src/exchange/taler-exchange-httpd_responses.c | 50 ++- src/exchange/taler-exchange-httpd_responses.h | 6 +- src/exchange/taler-exchange-httpd_withdraw.c | 42 +-- 17 files changed, 934 insertions(+), 1400 deletions(-) create mode 100644 src/exchange/taler-exchange-httpd_recoup-refresh.c create mode 100644 src/exchange/taler-exchange-httpd_recoup-refresh.h (limited to 'src/exchange') diff --git a/src/exchange/Makefile.am b/src/exchange/Makefile.am index c20378c22..e7688f735 100644 --- a/src/exchange/Makefile.am +++ b/src/exchange/Makefile.am @@ -102,6 +102,7 @@ taler_exchange_httpd_SOURCES = \ taler-exchange-httpd_metrics.c taler-exchange-httpd_metrics.h \ taler-exchange-httpd_mhd.c taler-exchange-httpd_mhd.h \ taler-exchange-httpd_recoup.c taler-exchange-httpd_recoup.h \ + taler-exchange-httpd_recoup-refresh.c taler-exchange-httpd_recoup-refresh.h \ taler-exchange-httpd_refreshes_reveal.c taler-exchange-httpd_refreshes_reveal.h \ taler-exchange-httpd_refund.c taler-exchange-httpd_refund.h \ taler-exchange-httpd_reserves_get.c taler-exchange-httpd_reserves_get.h \ diff --git a/src/exchange/taler-exchange-httpd.c b/src/exchange/taler-exchange-httpd.c index 0535a54ea..526c93588 100644 --- a/src/exchange/taler-exchange-httpd.c +++ b/src/exchange/taler-exchange-httpd.c @@ -42,6 +42,7 @@ #include "taler-exchange-httpd_metrics.h" #include "taler-exchange-httpd_mhd.h" #include "taler-exchange-httpd_recoup.h" +#include "taler-exchange-httpd_recoup-refresh.h" #include "taler-exchange-httpd_refreshes_reveal.h" #include "taler-exchange-httpd_refund.h" #include "taler-exchange-httpd_reserves_get.h" @@ -256,6 +257,10 @@ handle_post_coins (struct TEH_RequestContext *rc, .op = "recoup", .handler = &TEH_handler_recoup }, + { + .op = "recoup-refresh", + .handler = &TEH_handler_recoup_refresh + }, { .op = "refund", .handler = &TEH_handler_refund diff --git a/src/exchange/taler-exchange-httpd_db.c b/src/exchange/taler-exchange-httpd_db.c index 388679c38..3600d7931 100644 --- a/src/exchange/taler-exchange-httpd_db.c +++ b/src/exchange/taler-exchange-httpd_db.c @@ -29,55 +29,6 @@ #include "taler-exchange-httpd_responses.h" -/** - * Send a response for a failed request. The transaction history of the given - * coin demonstrates that the @a residual value of the coin is below the @a - * requested contribution of the coin for the operation. Thus, the exchange - * refuses the operation. - * - * @param connection the connection to send the response to - * @param coin_pub public key of the coin - * @param coin_value original value of the coin - * @param tl transaction history for the coin - * @param requested how much this coin was supposed to contribute, including fee - * @param residual remaining value of the coin (after subtracting @a tl) - * @return a MHD result code - */ -static MHD_RESULT -reply_insufficient_funds ( - struct MHD_Connection *connection, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_Amount *coin_value, - struct TALER_EXCHANGEDB_TransactionList *tl, - const struct TALER_Amount *requested, - const struct TALER_Amount *residual) -{ - json_t *history; - - history = TEH_RESPONSE_compile_transaction_history (coin_pub, - tl); - if (NULL == history) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_GENERIC_HISTORY_DB_ERROR_INSUFFICIENT_FUNDS, - NULL); - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_CONFLICT, - TALER_JSON_pack_ec (TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS), - GNUNET_JSON_pack_data_auto ("coin_pub", - coin_pub), - TALER_JSON_pack_amount ("original_value", - coin_value), - TALER_JSON_pack_amount ("residual_value", - residual), - TALER_JSON_pack_amount ("requested_value", - requested), - GNUNET_JSON_pack_array_steal ("history", - history)); -} - - /** * How often should we retry a transaction before giving up * (for transactions resulting in serialization/dead locks only). @@ -91,24 +42,22 @@ reply_insufficient_funds ( #define MAX_TRANSACTION_COMMIT_RETRIES 100 -/** - * Ensure coin is known in the database, and handle conflicts and errors. - * - * @param coin the coin to make known - * @param connection MHD request context - * @param[out] mhd_ret set to MHD status on error - * @return transaction status, negative on error (@a mhd_ret will be set in this case) - */ enum GNUNET_DB_QueryStatus TEH_make_coin_known (const struct TALER_CoinPublicInfo *coin, struct MHD_Connection *connection, + uint64_t *known_coin_id, MHD_RESULT *mhd_ret) { enum TALER_EXCHANGEDB_CoinKnownStatus cks; + struct TALER_DenominationHash h_denom_pub; + struct TALER_AgeHash age_hash; /* make sure coin is 'known' in database */ cks = TEH_plugin->ensure_coin_known (TEH_plugin->cls, - coin); + coin, + known_coin_id, + &h_denom_pub, + &age_hash); switch (cks) { case TALER_EXCHANGEDB_CKS_ADDED: @@ -124,250 +73,20 @@ TEH_make_coin_known (const struct TALER_CoinPublicInfo *coin, TALER_EC_GENERIC_DB_STORE_FAILED, NULL); return GNUNET_DB_STATUS_HARD_ERROR; - case TALER_EXCHANGEDB_CKS_CONFLICT: - break; - } - - { - struct TALER_EXCHANGEDB_TransactionList *tl; - enum GNUNET_DB_QueryStatus qs; - - qs = TEH_plugin->get_coin_transactions (TEH_plugin->cls, - &coin->coin_pub, - GNUNET_NO, - &tl); - if (0 > qs) - { - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - return qs; - } - // FIXME: why do we even return the transaction - // history here!? This is a coin with multiple - // associated denominations, after all... - // => this is probably the wrong call, as this - // is NOT about insufficient funds! - *mhd_ret - = TEH_RESPONSE_reply_coin_insufficient_funds ( - connection, - TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY, - &coin->coin_pub, - tl); - TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, - tl); - return GNUNET_DB_STATUS_HARD_ERROR; - } -} - - -/** - * Called when we actually know that the balance (was) insufficient. - * Re-does the check (slowly) to compute the full error message for - * the client. - * - * @param connection HTTP connection to report hard errors on - * @param coin_pub coin to analyze - * @param coin_value total value of the original coin (by denomination) - * @param op_cost cost of the current operation (for error reporting) - * @param check_recoup should we include recoup transactions in the check - * @param zombie_required additional requirement that the coin must - * be a zombie coin, or also hard failure - * @param[out] mhd_ret set to response status code, on hard error only - * @return transaction status - */ -static enum GNUNET_DB_QueryStatus -check_coin_balance (struct MHD_Connection *connection, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_Amount *coin_value, - const struct TALER_Amount *op_cost, - bool check_recoup, - bool zombie_required, - MHD_RESULT *mhd_ret) -{ - struct TALER_EXCHANGEDB_TransactionList *tl; - struct TALER_Amount spent; - enum GNUNET_DB_QueryStatus qs; - - /* Start with zero cost, as we already added this melt transaction - to the DB, so we will see it again during the queries below. */ - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (TEH_currency, - &spent)); - - /* get historic transaction costs of this coin, including recoups as - we might be a zombie coin */ - qs = TEH_plugin->get_coin_transactions (TEH_plugin->cls, - coin_pub, - check_recoup, - &tl); - if (0 > qs) - { - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "coin transaction history"); - return qs; - } - if (zombie_required) - { - /* The denomination key is only usable for a melt if this is a true - zombie coin, i.e. it was refreshed and the resulting fresh coin was - then recouped. Check that this is truly the case. */ - for (struct TALER_EXCHANGEDB_TransactionList *tp = tl; - NULL != tp; - tp = tp->next) - { - if (TALER_EXCHANGEDB_TT_OLD_COIN_RECOUP == tp->type) - { - zombie_required = false; /* clear flag: was satisfied! */ - break; - } - } - if (zombie_required) - { - /* zombie status not satisfied */ - GNUNET_break_op (0); - TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, - tl); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_MELT_COIN_EXPIRED_NO_ZOMBIE, - NULL); - return GNUNET_DB_STATUS_HARD_ERROR; - } - } - if (GNUNET_OK != - TALER_EXCHANGEDB_calculate_transaction_list_totals (tl, - &spent, - &spent)) - { - GNUNET_break (0); - TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, - tl); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_GENERIC_COIN_HISTORY_COMPUTATION_FAILED, - NULL); - return GNUNET_DB_STATUS_HARD_ERROR; - } - - /* Refuse to refresh when the coin's value is insufficient - for the cost of all transactions. */ - if (0 > TALER_amount_cmp (coin_value, - &spent)) - { - struct TALER_Amount coin_residual; - struct TALER_Amount spent_already; - - /* First subtract the melt cost from 'spent' to - compute the total amount already spent of the coin */ - GNUNET_assert (0 <= - TALER_amount_subtract (&spent_already, - &spent, - op_cost)); - /* The residual coin value is the original coin value minus - what we have spent (before the melt) */ - GNUNET_assert (0 <= - TALER_amount_subtract (&coin_residual, - coin_value, - &spent_already)); - *mhd_ret = reply_insufficient_funds ( - connection, - coin_pub, - coin_value, - tl, - op_cost, - &coin_residual); - TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, - tl); - return GNUNET_DB_STATUS_HARD_ERROR; - } - - /* This should not happen: The coin has sufficient funds - after all!?!? */ - TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, - tl); - return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; -} - - -enum GNUNET_DB_QueryStatus -TEH_check_coin_balance (struct MHD_Connection *connection, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_Amount *coin_value, - const struct TALER_Amount *op_cost, - bool check_recoup, - bool zombie_required, - MHD_RESULT *mhd_ret) -{ - bool balance_ok = false; - bool zombie_ok = false; - enum GNUNET_DB_QueryStatus qs; - - qs = TEH_plugin->do_check_coin_balance (TEH_plugin->cls, - coin_pub, - coin_value, - check_recoup, - zombie_required, - &balance_ok, - &zombie_ok); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - *mhd_ret = TALER_MHD_reply_with_error ( + case TALER_EXCHANGEDB_CKS_DENOM_CONFLICT: + *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "check_coin_balance"); - return qs; - case GNUNET_DB_STATUS_SOFT_ERROR: - return qs; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_break (0); - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "check_coin_balance"); + TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY, + &coin->coin_pub); return GNUNET_DB_STATUS_HARD_ERROR; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* handled below */ - break; - } - if (! zombie_ok) - { - GNUNET_break_op (0); - *mhd_ret = TALER_MHD_reply_with_error ( + case TALER_EXCHANGEDB_CKS_AGE_CONFLICT: + *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_MELT_COIN_EXPIRED_NO_ZOMBIE, - NULL); + TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH, + &coin->coin_pub); return GNUNET_DB_STATUS_HARD_ERROR; } - if (balance_ok) - return qs; - /* balance is not OK, do expensive call to compute full error message */ - qs = check_coin_balance (connection, - coin_pub, - coin_value, - op_cost, - check_recoup, - zombie_required, - mhd_ret); - if (qs < 0) - return qs; /* we expected to fail (same check as before!) */ - GNUNET_break (0); /* stored procedure and individual statements - disagree, should be impossible! */ - *mhd_ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - "stored procedure disagrees with full coin transaction history fetch"); + GNUNET_assert (0); return GNUNET_DB_STATUS_HARD_ERROR; } diff --git a/src/exchange/taler-exchange-httpd_db.h b/src/exchange/taler-exchange-httpd_db.h index 5ee3b41d5..7c954ffe1 100644 --- a/src/exchange/taler-exchange-httpd_db.h +++ b/src/exchange/taler-exchange-httpd_db.h @@ -32,44 +32,17 @@ * * @param coin the coin to make known * @param connection MHD request context + * @param[out] known_coin_id set to the unique ID for the coin in the DB * @param[out] mhd_ret set to MHD status on error * @return transaction status, negative on error (@a mhd_ret will be set in this case) */ enum GNUNET_DB_QueryStatus TEH_make_coin_known (const struct TALER_CoinPublicInfo *coin, struct MHD_Connection *connection, + uint64_t *known_coin_id, MHD_RESULT *mhd_ret); -/** - * Check that a coin has an adequate balance so that we can - * commit the current transaction. If the balance is - * insufficient for all transactions associated with the - * coin, return a hard error. - * - * We first do a "fast" check using a stored procedure, and - * only obtain the "full" data on failure (for performance). - * - * @param connection HTTP connection to report hard errors on - * @param coin_pub coin to analyze - * @param coin_value total value of the original coin (by denomination) - * @param op_cost cost of the current operation (for error reporting) - * @param check_recoup should we include recoup transactions in the check - * @param zombie_required additional requirement that the coin must - * be a zombie coin, or also hard failure - * @param[out] mhd_ret set to response status code, on hard error only - * @return transaction status - */ -enum GNUNET_DB_QueryStatus -TEH_check_coin_balance (struct MHD_Connection *connection, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_Amount *coin_value, - const struct TALER_Amount *op_cost, - bool check_recoup, - bool zombie_required, - MHD_RESULT *mhd_ret); - - /** * Function implementing a database transaction. Runs the transaction * logic; IF it returns a non-error code, the transaction logic MUST diff --git a/src/exchange/taler-exchange-httpd_deposit.c b/src/exchange/taler-exchange-httpd_deposit.c index 11094d11c..84741b5c3 100644 --- a/src/exchange/taler-exchange-httpd_deposit.c +++ b/src/exchange/taler-exchange-httpd_deposit.c @@ -47,6 +47,7 @@ * @param connection connection to the client * @param coin_pub public key of the coin * @param h_wire hash of wire details + * @param h_extensions hash of applicable extensions * @param h_contract_terms hash of contract details * @param exchange_timestamp exchange's timestamp * @param refund_deadline until when this deposit be refunded @@ -118,23 +119,21 @@ struct DepositContext /** * Our timestamp (when we received the request). + * Possibly updated by the transaction if the + * request is idempotent (was repeated). */ struct GNUNET_TIME_Timestamp exchange_timestamp; /** - * Calculated hash over the wire details. + * Hash of the payto URI. */ - struct TALER_MerchantWireHash h_wire; + struct TALER_PaytoHash h_payto; /** - * Value of the coin. + * Row of of the coin in the known_coins table. */ - struct TALER_Amount value; + uint64_t known_coin_id; - /** - * payto:// URI of the credited account. - */ - const char *payto_uri; }; @@ -157,15 +156,18 @@ deposit_transaction (void *cls, MHD_RESULT *mhd_ret) { struct DepositContext *dc = cls; - const struct TALER_EXCHANGEDB_Deposit *deposit = dc->deposit; - struct TALER_Amount spent; enum GNUNET_DB_QueryStatus qs; - struct TALER_Amount deposit_fee; - - /* begin optimistically: assume this is a new deposit */ - qs = TEH_plugin->insert_deposit (TEH_plugin->cls, - dc->exchange_timestamp, - deposit); + bool balance_ok; + bool in_conflict; + + qs = TEH_plugin->do_deposit (TEH_plugin->cls, + dc->deposit, + dc->known_coin_id, + &dc->h_payto, + false, /* FIXME-OEC: extension blocked */ + &dc->exchange_timestamp, + &balance_ok, + &in_conflict); if (qs < 0) { if (GNUNET_DB_STATUS_SOFT_ERROR == qs) @@ -174,73 +176,30 @@ deposit_transaction (void *cls, *mhd_ret = TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_STORE_FAILED, - NULL); + "deposit"); return qs; } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + if (in_conflict) { - /* Check for idempotency: did we get this request before? */ - qs = TEH_plugin->have_deposit (TEH_plugin->cls, - deposit, - &deposit_fee, - &dc->exchange_timestamp); - if (qs < 0) - { - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - return qs; - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "have_deposit"); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - /* Conflict on insert, but record does not exist? - That makes no sense. */ - GNUNET_break (0); - return GNUNET_DB_STATUS_HARD_ERROR; - } - - { - struct TALER_Amount amount_without_fee; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "/deposit replay, accepting again!\n"); - GNUNET_assert (0 <= - TALER_amount_subtract (&amount_without_fee, - &deposit->amount_with_fee, - &deposit_fee)); - *mhd_ret = reply_deposit_success (connection, - &deposit->coin.coin_pub, - &dc->h_wire, - NULL /* h_extensions! */, - &deposit->h_contract_terms, - dc->exchange_timestamp, - deposit->refund_deadline, - deposit->wire_deadline, - &deposit->merchant_pub, - &amount_without_fee); - /* Note: we return "hard error" to ensure the wrapper - does not retry the transaction, and to also not generate - a "fresh" response (as we would on "success") */ - return GNUNET_DB_STATUS_HARD_ERROR; - } + TEH_plugin->rollback (TEH_plugin->cls); + *mhd_ret + = TEH_RESPONSE_reply_coin_insufficient_funds ( + connection, + TALER_EC_EXCHANGE_DEPOSIT_CONFLICTING_CONTRACT, + &dc->deposit->coin.coin_pub); + return GNUNET_DB_STATUS_HARD_ERROR; } - - /* Start with zero cost, as we already added this melt transaction - to the DB, so we will see it again during the queries below. */ - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (TEH_currency, - &spent)); - - return TEH_check_coin_balance (connection, - &deposit->coin.coin_pub, - &dc->value, - &deposit->amount_with_fee, - false, /* no need for recoup */ - false, /* no need for zombie */ - mhd_ret); + if (! balance_ok) + { + TEH_plugin->rollback (TEH_plugin->cls); + *mhd_ret + = TEH_RESPONSE_reply_coin_insufficient_funds ( + connection, + TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, + &dc->deposit->coin.coin_pub); + return GNUNET_DB_STATUS_HARD_ERROR; + } + return qs; } @@ -263,9 +222,10 @@ TEH_handler_deposit (struct MHD_Connection *connection, { struct DepositContext dc; struct TALER_EXCHANGEDB_Deposit deposit; + const char *payto_uri; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_string ("merchant_payto_uri", - &dc.payto_uri), + &payto_uri), GNUNET_JSON_spec_fixed_auto ("wire_salt", &deposit.wire_salt), TALER_JSON_spec_amount ("contribution", @@ -290,6 +250,7 @@ TEH_handler_deposit (struct MHD_Connection *connection, &deposit.wire_deadline), GNUNET_JSON_spec_end () }; + struct TALER_MerchantWireHash h_wire; memset (&deposit, 0, @@ -316,7 +277,7 @@ TEH_handler_deposit (struct MHD_Connection *connection, { char *emsg; - emsg = TALER_payto_validate (dc.payto_uri); + emsg = TALER_payto_validate (payto_uri); if (NULL != emsg) { MHD_RESULT ret; @@ -331,7 +292,6 @@ TEH_handler_deposit (struct MHD_Connection *connection, return ret; } } - deposit.receiver_wire_account = (char *) dc.payto_uri; if (GNUNET_TIME_timestamp_cmp (deposit.refund_deadline, >, deposit.wire_deadline)) @@ -343,9 +303,12 @@ TEH_handler_deposit (struct MHD_Connection *connection, TALER_EC_EXCHANGE_DEPOSIT_REFUND_DEADLINE_AFTER_WIRE_DEADLINE, NULL); } - TALER_merchant_wire_signature_hash (dc.payto_uri, + deposit.receiver_wire_account = (char *) payto_uri; + TALER_payto_hash (payto_uri, + &dc.h_payto); + TALER_merchant_wire_signature_hash (payto_uri, &deposit.wire_salt, - &dc.h_wire); + &h_wire); dc.deposit = &deposit; /* new deposit */ @@ -366,42 +329,30 @@ TEH_handler_deposit (struct MHD_Connection *connection, if (GNUNET_TIME_absolute_is_past (dk->meta.expire_deposit.abs_time)) { /* This denomination is past the expiration time for deposits */ - struct GNUNET_TIME_Timestamp now; - - now = GNUNET_TIME_timestamp_get (); GNUNET_JSON_parse_free (spec); return TEH_RESPONSE_reply_expired_denom_pub_hash ( connection, &deposit.coin.denom_pub_hash, - now, TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, "DEPOSIT"); } if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time)) { /* This denomination is not yet valid */ - struct GNUNET_TIME_Timestamp now; - - now = GNUNET_TIME_timestamp_get (); GNUNET_JSON_parse_free (spec); return TEH_RESPONSE_reply_expired_denom_pub_hash ( connection, &deposit.coin.denom_pub_hash, - now, TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, "DEPOSIT"); } if (dk->recoup_possible) { - struct GNUNET_TIME_Timestamp now; - - now = GNUNET_TIME_timestamp_get (); /* This denomination has been revoked */ GNUNET_JSON_parse_free (spec); return TEH_RESPONSE_reply_expired_denom_pub_hash ( connection, &deposit.coin.denom_pub_hash, - now, TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, "DEPOSIT"); } @@ -419,7 +370,6 @@ TEH_handler_deposit (struct MHD_Connection *connection, TALER_EC_EXCHANGE_DENOMINATION_SIGNATURE_INVALID, NULL); } - dc.value = dk->meta.value; } if (0 < TALER_amount_cmp (&deposit.deposit_fee, &deposit.amount_with_fee)) @@ -435,7 +385,7 @@ TEH_handler_deposit (struct MHD_Connection *connection, if (GNUNET_OK != TALER_wallet_deposit_verify (&deposit.amount_with_fee, &deposit.deposit_fee, - &dc.h_wire, + &h_wire, &deposit.h_contract_terms, NULL /* h_extensions! */, &deposit.coin.denom_pub_hash, @@ -470,6 +420,7 @@ TEH_handler_deposit (struct MHD_Connection *connection, /* make sure coin is 'known' in database */ qs = TEH_make_coin_known (&deposit.coin, connection, + &dc.known_coin_id, &mhd_ret); /* no transaction => no serialization failures should be possible */ GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); @@ -506,7 +457,7 @@ TEH_handler_deposit (struct MHD_Connection *connection, &deposit.deposit_fee)); res = reply_deposit_success (connection, &deposit.coin.coin_pub, - &dc.h_wire, + &h_wire, NULL /* h_extensions! */, &deposit.h_contract_terms, dc.exchange_timestamp, diff --git a/src/exchange/taler-exchange-httpd_keys.c b/src/exchange/taler-exchange-httpd_keys.c index a9cd864aa..5d7476771 100644 --- a/src/exchange/taler-exchange-httpd_keys.c +++ b/src/exchange/taler-exchange-httpd_keys.c @@ -57,7 +57,7 @@ * #TALER_PROTOCOL_CURRENT and #TALER_PROTOCOL_AGE in * exchange_api_handle.c! */ -#define EXCHANGE_PROTOCOL_VERSION "11:0:1" +#define EXCHANGE_PROTOCOL_VERSION "12:0:0" /** diff --git a/src/exchange/taler-exchange-httpd_melt.c b/src/exchange/taler-exchange-httpd_melt.c index e4b2af290..54f1385d7 100644 --- a/src/exchange/taler-exchange-httpd_melt.c +++ b/src/exchange/taler-exchange-httpd_melt.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2020 Taler Systems SA + Copyright (C) 2014-2021 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 @@ -89,6 +89,11 @@ struct MeltContext */ struct TALER_EXCHANGEDB_Refresh refresh_session; + /** + * UUID of the coin in the known_coins table. + */ + uint64_t known_coin_id; + /** * Information about the @e coin's value. */ @@ -141,15 +146,19 @@ melt_transaction (void *cls, { struct MeltContext *rmc = cls; enum GNUNET_DB_QueryStatus qs; - uint32_t noreveal_index; + bool balance_ok; /* pick challenge and persist it */ rmc->refresh_session.noreveal_index = GNUNET_CRYPTO_random_u32 (GNUNET_CRYPTO_QUALITY_STRONG, TALER_CNC_KAPPA); + if (0 > - (qs = TEH_plugin->insert_melt (TEH_plugin->cls, - &rmc->refresh_session))) + (qs = TEH_plugin->do_melt (TEH_plugin->cls, + &rmc->refresh_session, + rmc->known_coin_id, + &rmc->zombie_required, + &balance_ok))) { if (GNUNET_DB_STATUS_SOFT_ERROR != qs) { @@ -161,64 +170,43 @@ melt_transaction (void *cls, } return qs; } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs); + if (rmc->zombie_required) { - /* Check if we already created a matching refresh_session */ - qs = TEH_plugin->get_melt_index (TEH_plugin->cls, - &rmc->refresh_session.rc, - &noreveal_index); - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) - { - TALER_LOG_DEBUG ("Coin was previously melted, returning old reply\n"); - *mhd_ret = reply_melt_success (connection, - &rmc->refresh_session.rc, - noreveal_index); - /* Note: we return "hard error" to ensure the wrapper - does not retry the transaction, and to also not generate - a "fresh" response (as we would on "success") */ - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (0 > qs) - { - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "melt index"); - return qs; - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - /* Conflict on insert, but record does not exist? - That makes no sense. */ - GNUNET_break (0); - return GNUNET_DB_STATUS_HARD_ERROR; - } + GNUNET_break_op (0); + TEH_plugin->rollback (TEH_plugin->cls); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_MELT_COIN_EXPIRED_NO_ZOMBIE, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (! balance_ok) + { + TEH_plugin->rollback (TEH_plugin->cls); + *mhd_ret + = TEH_RESPONSE_reply_coin_insufficient_funds ( + connection, + TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, + &rmc->refresh_session.coin.coin_pub); + return GNUNET_DB_STATUS_HARD_ERROR; } - return TEH_check_coin_balance (connection, - &rmc->refresh_session.coin.coin_pub, - &rmc->coin_value, - &rmc->refresh_session.amount_with_fee, - true, - rmc->zombie_required, - mhd_ret); + /* All good, commit, final response will be generated by caller */ + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; } /** * Handle a "melt" request after the first parsing has - * happened. We now need to validate the coins being melted and the - * session signature and then hand things of to execute the melt - * operation. This function parses the JSON arrays and then passes - * processing on to #melt_transaction(). + * happened. Performs the database transactions. * * @param connection the MHD connection to handle * @param[in,out] rmc details about the melt request * @return MHD result code */ static MHD_RESULT -handle_melt (struct MHD_Connection *connection, - struct MeltContext *rmc) +database_melt (struct MHD_Connection *connection, + struct MeltContext *rmc) { if (GNUNET_SYSERR == TEH_plugin->preflight (TEH_plugin->cls)) @@ -230,36 +218,6 @@ handle_melt (struct MHD_Connection *connection, "preflight failure"); } - /* verify signature of coin for melt operation */ - { - struct TALER_RefreshMeltCoinAffirmationPS body = { - .purpose.size = htonl (sizeof (body)), - .purpose.purpose = htonl (TALER_SIGNATURE_WALLET_COIN_MELT), - .rc = rmc->refresh_session.rc, - .h_denom_pub = rmc->refresh_session.coin.denom_pub_hash, - .coin_pub = rmc->refresh_session.coin.coin_pub - }; - - TALER_amount_hton (&body.amount_with_fee, - &rmc->refresh_session.amount_with_fee); - TALER_amount_hton (&body.melt_fee, - &rmc->coin_refresh_fee); - - if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify ( - TALER_SIGNATURE_WALLET_COIN_MELT, - &body, - &rmc->refresh_session.coin_sig.eddsa_signature, - &rmc->refresh_session.coin.coin_pub.eddsa_pub)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_MELT_COIN_SIGNATURE_INVALID, - NULL); - } - } - /* first, make sure coin is known */ if (! rmc->coin_is_dirty) { @@ -268,6 +226,7 @@ handle_melt (struct MHD_Connection *connection, qs = TEH_make_coin_known (&rmc->refresh_session.coin, connection, + &rmc->known_coin_id, &mhd_ret); /* no transaction => no serialization failures should be possible */ GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); @@ -305,8 +264,8 @@ handle_melt (struct MHD_Connection *connection, * @return MHD status code */ static MHD_RESULT -check_for_denomination_key (struct MHD_Connection *connection, - struct MeltContext *rmc) +check_melt_valid (struct MHD_Connection *connection, + struct MeltContext *rmc) { /* Baseline: check if deposits/refreshs are generally simply still allowed for this denomination */ @@ -321,30 +280,64 @@ check_for_denomination_key (struct MHD_Connection *connection, return mret; if (GNUNET_TIME_absolute_is_past (dk->meta.expire_legal.abs_time)) { - struct GNUNET_TIME_Timestamp now; - - now = GNUNET_TIME_timestamp_get (); /* Way too late now, even zombies have expired */ return TEH_RESPONSE_reply_expired_denom_pub_hash ( connection, &rmc->refresh_session.coin.denom_pub_hash, - now, TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, "MELT"); } if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time)) { - struct GNUNET_TIME_Timestamp now; - - now = GNUNET_TIME_timestamp_get (); /* This denomination is not yet valid */ return TEH_RESPONSE_reply_expired_denom_pub_hash ( connection, &rmc->refresh_session.coin.denom_pub_hash, - now, TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, "MELT"); } + + rmc->coin_refresh_fee = dk->meta.fee_refresh; + rmc->coin_value = dk->meta.value; + /* sanity-check that "total melt amount > melt fee" */ + if (0 < + TALER_amount_cmp (&rmc->coin_refresh_fee, + &rmc->refresh_session.amount_with_fee)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_EXCHANGE_MELT_FEES_EXCEED_CONTRIBUTION, + NULL); + } + + if (GNUNET_OK != + TALER_test_coin_valid (&rmc->refresh_session.coin, + &dk->denom_pub)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_DENOMINATION_SIGNATURE_INVALID, + NULL); + } + + /* verify signature of coin for melt operation */ + if (GNUNET_OK != + TALER_wallet_melt_verify (&rmc->refresh_session.amount_with_fee, + &rmc->coin_refresh_fee, + &rmc->refresh_session.rc, + &rmc->refresh_session.coin.denom_pub_hash, + &rmc->refresh_session.coin.coin_pub, + &rmc->refresh_session.coin_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_MELT_COIN_SIGNATURE_INVALID, + NULL); + } + if (GNUNET_TIME_absolute_is_past (dk->meta.expire_deposit.abs_time)) { /* We are past deposit expiration time, but maybe this is a zombie? */ @@ -357,6 +350,7 @@ check_for_denomination_key (struct MHD_Connection *connection, qs = TEH_plugin->get_coin_denomination ( TEH_plugin->cls, &rmc->refresh_session.coin.coin_pub, + &rmc->known_coin_id, &denom_hash); if (0 > qs) { @@ -369,14 +363,10 @@ check_for_denomination_key (struct MHD_Connection *connection, } if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) { - struct GNUNET_TIME_Timestamp now; - - now = GNUNET_TIME_timestamp_get (); /* We never saw this coin before, so _this_ justification is not OK */ return TEH_RESPONSE_reply_expired_denom_pub_hash ( connection, &rmc->refresh_session.coin.denom_pub_hash, - now, TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, "MELT"); } @@ -389,67 +379,25 @@ check_for_denomination_key (struct MHD_Connection *connection, &rmc->refresh_session.coin.denom_pub_hash)) { GNUNET_break_op (0); - // => this is probably the wrong call, as this - // is NOT about insufficient funds! - // (see also taler-exchange-httpd_db.c for an equivalent issue) - return TEH_RESPONSE_reply_coin_insufficient_funds ( + return TALER_MHD_reply_with_ec ( connection, TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY, - &rmc->refresh_session.coin.coin_pub, - NULL); + TALER_B2S (&denom_hash)); } rmc->zombie_required = true; /* check later that zombie is satisfied */ } - rmc->coin_refresh_fee = dk->meta.fee_refresh; - rmc->coin_value = dk->meta.value; - /* check coin is actually properly signed */ - if (GNUNET_OK != - TALER_test_coin_valid (&rmc->refresh_session.coin, - &dk->denom_pub)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_DENOMINATION_SIGNATURE_INVALID, - NULL); - } - - /* sanity-check that "total melt amount > melt fee" */ - if (0 < - TALER_amount_cmp (&rmc->coin_refresh_fee, - &rmc->refresh_session.amount_with_fee)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_EXCHANGE_MELT_FEES_EXCEED_CONTRIBUTION, - NULL); - } - return handle_melt (connection, - rmc); + return database_melt (connection, + rmc); } -/** - * Handle a "/coins/$COIN_PUB/melt" request. Parses the request into the JSON - * components and then hands things of to #check_for_denomination_key() to - * validate the melted coins, the signature and execute the melt using - * handle_melt(). - - * @param connection the MHD connection to handle - * @param coin_pub public key of the coin - * @param root uploaded JSON data - * @return MHD result code - */ MHD_RESULT TEH_handler_melt (struct MHD_Connection *connection, const struct TALER_CoinSpendPublicKeyP *coin_pub, const json_t *root) { struct MeltContext rmc; - enum GNUNET_GenericReturnValue ret; - MHD_RESULT res; struct GNUNET_JSON_Specification spec[] = { TALER_JSON_spec_denom_sig ("denom_sig", &rmc.refresh_session.coin.denom_sig), @@ -469,16 +417,24 @@ TEH_handler_melt (struct MHD_Connection *connection, 0, sizeof (rmc)); rmc.refresh_session.coin.coin_pub = *coin_pub; - ret = TALER_MHD_parse_json_data (connection, - root, - spec); - if (GNUNET_OK != ret) - return (GNUNET_SYSERR == ret) ? MHD_NO : MHD_YES; - - res = check_for_denomination_key (connection, - &rmc); - GNUNET_JSON_parse_free (spec); - return res; + + { + enum GNUNET_GenericReturnValue ret; + ret = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_OK != ret) + return (GNUNET_SYSERR == ret) ? MHD_NO : MHD_YES; + } + + { + MHD_RESULT res; + + res = check_melt_valid (connection, + &rmc); + GNUNET_JSON_parse_free (spec); + return res; + } } diff --git a/src/exchange/taler-exchange-httpd_metrics.c b/src/exchange/taler-exchange-httpd_metrics.c index 2ea889ff0..8c8cd343a 100644 --- a/src/exchange/taler-exchange-httpd_metrics.c +++ b/src/exchange/taler-exchange-httpd_metrics.c @@ -51,18 +51,12 @@ TEH_handler_metrics (struct TEH_RequestContext *rc, "taler_exchange_serialization_failures{type=\"%s\"} %llu\n" "taler_exchange_serialization_failures{type=\"%s\"} %llu\n" "taler_exchange_serialization_failures{type=\"%s\"} %llu\n" - "taler_exchange_serialization_failures{type=\"%s\"} %llu\n" - "taler_exchange_serialization_failures{type=\"%s\"} %llu\n" - "taler_exchange_serialization_failures{type=\"%s\"} %llu\n" "# HELP taler_exchange_received_requests " " number of received requests by type\n" "# TYPE taler_exchange_received_requests counter\n" "taler_exchange_received_requests{type=\"%s\"} %llu\n" "taler_exchange_received_requests{type=\"%s\"} %llu\n" "taler_exchange_received_requests{type=\"%s\"} %llu\n" - "taler_exchange_received_requests{type=\"%s\"} %llu\n" - "taler_exchange_received_requests{type=\"%s\"} %llu\n" - "taler_exchange_received_requests{type=\"%s\"} %llu\n" "taler_exchange_received_requests{type=\"%s\"} %llu\n", "other", TEH_METRICS_num_conflict[TEH_MT_OTHER], @@ -72,12 +66,6 @@ TEH_handler_metrics (struct TEH_RequestContext *rc, TEH_METRICS_num_conflict[TEH_MT_WITHDRAW], "melt", TEH_METRICS_num_conflict[TEH_MT_MELT], - "reveal-precheck", - TEH_METRICS_num_conflict[TEH_MT_REVEAL_PRECHECK], - "reveal", - TEH_METRICS_num_conflict[TEH_MT_REVEAL], - "reveal-persist", - TEH_METRICS_num_conflict[TEH_MT_REVEAL_PERSIST], "other", TEH_METRICS_num_requests[TEH_MT_OTHER], "deposit", @@ -85,13 +73,7 @@ TEH_handler_metrics (struct TEH_RequestContext *rc, "withdraw", TEH_METRICS_num_requests[TEH_MT_WITHDRAW], "melt", - TEH_METRICS_num_requests[TEH_MT_MELT], - "reveal-precheck", - TEH_METRICS_num_requests[TEH_MT_REVEAL_PRECHECK], - "reveal", - TEH_METRICS_num_requests[TEH_MT_REVEAL], - "reveal-persist", - TEH_METRICS_num_requests[TEH_MT_REVEAL_PERSIST]); + TEH_METRICS_num_requests[TEH_MT_MELT]); resp = MHD_create_response_from_buffer (strlen (reply), reply, MHD_RESPMEM_MUST_FREE); diff --git a/src/exchange/taler-exchange-httpd_metrics.h b/src/exchange/taler-exchange-httpd_metrics.h index 39e463169..55e5372a7 100644 --- a/src/exchange/taler-exchange-httpd_metrics.h +++ b/src/exchange/taler-exchange-httpd_metrics.h @@ -35,10 +35,7 @@ enum TEH_MetricType TEH_MT_DEPOSIT = 1, TEH_MT_WITHDRAW = 2, TEH_MT_MELT = 3, - TEH_MT_REVEAL_PRECHECK = 4, - TEH_MT_REVEAL = 5, - TEH_MT_REVEAL_PERSIST = 6, - TEH_MT_COUNT = 7 /* MUST BE LAST! */ + TEH_MT_COUNT = 4 /* MUST BE LAST! */ }; diff --git a/src/exchange/taler-exchange-httpd_recoup-refresh.c b/src/exchange/taler-exchange-httpd_recoup-refresh.c new file mode 100644 index 000000000..acadc999e --- /dev/null +++ b/src/exchange/taler-exchange-httpd_recoup-refresh.c @@ -0,0 +1,411 @@ +/* + This file is part of TALER + Copyright (C) 2017-2021 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 + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file taler-exchange-httpd_recoup-refresh.c + * @brief Handle /recoup-refresh requests; parses the POST and JSON and + * verifies the coin signature before handing things off + * to the database. + * @author Christian Grothoff + */ +#include "platform.h" +#include +#include +#include +#include +#include +#include "taler_json_lib.h" +#include "taler_mhd_lib.h" +#include "taler-exchange-httpd_db.h" +#include "taler-exchange-httpd_recoup-refresh.h" +#include "taler-exchange-httpd_responses.h" +#include "taler-exchange-httpd_keys.h" +#include "taler_exchangedb_lib.h" + + +/** + * Closure for #recoup_refresh_transaction(). + */ +struct RecoupContext +{ + + /** + * Set by #recoup_transaction() to the old coin that will + * receive the recoup. + */ + struct TALER_CoinSpendPublicKeyP old_coin_pub; + + /** + * Details about the coin. + */ + const struct TALER_CoinPublicInfo *coin; + + /** + * Key used to blind the coin. + */ + const union TALER_DenominationBlindingKeyP *coin_bks; + + /** + * Signature of the coin requesting recoup. + */ + const struct TALER_CoinSpendSignatureP *coin_sig; + + /** + * The amount requested to be recouped. + */ + const struct TALER_Amount *requested_amount; + + /** + * Unique ID of the coin in the known_coins table. + */ + uint64_t known_coin_id; + + /** + * Unique ID of the refresh reveal context of the melt for the new coin. + */ + uint64_t rrc_serial; + + /** + * Set by #recoup_transaction to the timestamp when the recoup + * was accepted. + */ + struct GNUNET_TIME_Timestamp now; + +}; + + +/** + * Execute a "recoup-refresh". The validity of the coin and signature have + * already been checked. The database must now check that the coin is not + * (double) spent, and execute the transaction. + * + * IF it returns a non-error code, the transaction logic MUST + * NOT queue a MHD response. IF it returns an hard error, the + * transaction logic MUST queue a MHD response and set @a mhd_ret. IF + * it returns the soft error code, the function MAY be called again to + * retry and MUST not queue a MHD response. + * + * @param cls the `struct RecoupContext *` + * @param connection MHD request which triggered the transaction + * @param[out] mhd_ret set to MHD response status for @a connection, + * if transaction failed (!) + * @return transaction status code + */ +static enum GNUNET_DB_QueryStatus +recoup_refresh_transaction (void *cls, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) +{ + struct RecoupContext *pc = cls; + enum GNUNET_DB_QueryStatus qs; + bool recoup_ok; + bool internal_failure; + + /* Finally, store new refund data */ + pc->now = GNUNET_TIME_timestamp_get (); + qs = TEH_plugin->do_recoup_refresh (TEH_plugin->cls, + &pc->old_coin_pub, + pc->rrc_serial, + pc->requested_amount, + pc->coin_bks, + &pc->coin->coin_pub, + pc->known_coin_id, + pc->coin_sig, + &pc->now, + &recoup_ok, + &internal_failure); + if (0 > qs) + { + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "do_recoup_refresh"); + return qs; + } + + if (internal_failure) + { + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + "coin transaction history"); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (! recoup_ok) + { + *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( + connection, + TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, + &pc->coin->coin_pub); + return GNUNET_DB_STATUS_HARD_ERROR; + } + return qs; +} + + +/** + * We have parsed the JSON information about the recoup request. Do + * some basic sanity checks (especially that the signature on the + * request and coin is valid) and then execute the recoup operation. + * Note that we need the DB to check the fee structure, so this is not + * done here but during the recoup_transaction(). + * + * @param connection the MHD connection to handle + * @param coin information about the coin + * @param coin_bks blinding data of the coin (to be checked) + * @param coin_sig signature of the coin + * @param requested_amount requested amount to be recouped + * @return MHD result code + */ +static MHD_RESULT +verify_and_execute_recoup_refresh ( + struct MHD_Connection *connection, + const struct TALER_CoinPublicInfo *coin, + const union TALER_DenominationBlindingKeyP *coin_bks, + const struct TALER_CoinSpendSignatureP *coin_sig, + const struct TALER_Amount *requested_amount) +{ + struct RecoupContext pc; + const struct TEH_DenominationKey *dk; + MHD_RESULT mret; + struct TALER_BlindedCoinHash h_blind; + + /* check denomination exists and is in recoup mode */ + dk = TEH_keys_denomination_by_hash (&coin->denom_pub_hash, + connection, + &mret); + if (NULL == dk) + return mret; + if (GNUNET_TIME_absolute_is_past (dk->meta.expire_deposit.abs_time)) + { + /* This denomination is past the expiration time for recoup */ + return TEH_RESPONSE_reply_expired_denom_pub_hash ( + connection, + &coin->denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, + "RECOUP-REFRESH"); + } + if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time)) + { + /* This denomination is not yet valid */ + return TEH_RESPONSE_reply_expired_denom_pub_hash ( + connection, + &coin->denom_pub_hash, + TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, + "RECOUP-REFRESH"); + } + if (! dk->recoup_possible) + { + /* This denomination is not eligible for recoup */ + return TEH_RESPONSE_reply_expired_denom_pub_hash ( + connection, + &coin->denom_pub_hash, + TALER_EC_EXCHANGE_RECOUP_REFRESH_NOT_ELIGIBLE, + "RECOUP-REFRESH"); + } + + /* check denomination signature */ + if (GNUNET_YES != + TALER_test_coin_valid (coin, + &dk->denom_pub)) + { + TALER_LOG_WARNING ("Invalid coin passed for recoup\n"); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_DENOMINATION_SIGNATURE_INVALID, + NULL); + } + + /* check recoup request signature */ + if (GNUNET_OK != + TALER_wallet_recoup_refresh_verify (&coin->denom_pub_hash, + coin_bks, + requested_amount, + &coin->coin_pub, + coin_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_RECOUP_REFRESH_SIGNATURE_INVALID, + NULL); + } + + { + void *coin_ev; + size_t coin_ev_size; + struct TALER_CoinPubHash c_hash; + + if (GNUNET_OK != + TALER_denom_blind (&dk->denom_pub, + coin_bks, + NULL, /* FIXME-Oec: TALER_AgeHash * */ + &coin->coin_pub, + &c_hash, + &coin_ev, + &coin_ev_size)) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_RECOUP_REFRESH_BLINDING_FAILED, + NULL); + } + TALER_coin_ev_hash (coin_ev, + coin_ev_size, + &h_blind); + GNUNET_free (coin_ev); + } + + pc.coin_sig = coin_sig; + pc.coin_bks = coin_bks; + pc.coin = coin; + pc.requested_amount = requested_amount; + + { + MHD_RESULT mhd_ret = MHD_NO; + enum GNUNET_DB_QueryStatus qs; + + /* make sure coin is 'known' in database */ + qs = TEH_make_coin_known (coin, + connection, + &pc.known_coin_id, + &mhd_ret); + /* no transaction => no serialization failures should be possible */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + if (qs < 0) + return mhd_ret; + } + + { + enum GNUNET_DB_QueryStatus qs; + + qs = TEH_plugin->get_old_coin_by_h_blind (TEH_plugin->cls, + &h_blind, + &pc.old_coin_pub, + &pc.rrc_serial); + if (0 > qs) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_old_coin_by_h_blind"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Recoup-refresh requested for unknown envelope %s\n", + GNUNET_h2s (&h_blind.hash)); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_EXCHANGE_RECOUP_REFRESH_MELT_NOT_FOUND, + NULL); + } + } + + /* Perform actual recoup transaction */ + { + MHD_RESULT mhd_ret; + + if (GNUNET_OK != + TEH_DB_run_transaction (connection, + "run recoup-refresh", + TEH_MT_OTHER, + &mhd_ret, + &recoup_refresh_transaction, + &pc)) + return mhd_ret; + } + /* Recoup succeeded, return result */ + return TALER_MHD_REPLY_JSON_PACK (connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_data_auto ( + "old_coin_pub", + &pc.old_coin_pub)); +} + + +/** + * Handle a "/coins/$COIN_PUB/recoup-refresh" request. Parses the JSON, and, if + * successful, passes the JSON data to #verify_and_execute_recoup_refresh() to further + * check the details of the operation specified. If everything checks out, + * this will ultimately lead to the refund being executed, or rejected. + * + * @param connection the MHD connection to handle + * @param coin_pub public key of the coin + * @param root uploaded JSON data + * @return MHD result code + */ +MHD_RESULT +TEH_handler_recoup_refresh (struct MHD_Connection *connection, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const json_t *root) +{ + enum GNUNET_GenericReturnValue ret; + struct TALER_CoinPublicInfo coin; + union TALER_DenominationBlindingKeyP coin_bks; + struct TALER_CoinSpendSignatureP coin_sig; + struct TALER_Amount amount; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", + &coin.denom_pub_hash), + TALER_JSON_spec_denom_sig ("denom_sig", + &coin.denom_sig), + GNUNET_JSON_spec_fixed_auto ("coin_blind_key_secret", + &coin_bks), + GNUNET_JSON_spec_fixed_auto ("coin_sig", + &coin_sig), + TALER_JSON_spec_amount ("amount", + TEH_currency, + &amount), + GNUNET_JSON_spec_end () + }; + + memset (&coin, + 0, + sizeof (coin)); + coin.coin_pub = *coin_pub; + ret = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_SYSERR == ret) + return MHD_NO; /* hard failure */ + if (GNUNET_NO == ret) + return MHD_YES; /* failure */ + { + MHD_RESULT res; + + res = verify_and_execute_recoup_refresh (connection, + &coin, + &coin_bks, + &coin_sig, + &amount); + GNUNET_JSON_parse_free (spec); + return res; + } +} + + +/* end of taler-exchange-httpd_recoup-refresh.c */ diff --git a/src/exchange/taler-exchange-httpd_recoup-refresh.h b/src/exchange/taler-exchange-httpd_recoup-refresh.h new file mode 100644 index 000000000..25c12fac3 --- /dev/null +++ b/src/exchange/taler-exchange-httpd_recoup-refresh.h @@ -0,0 +1,46 @@ +/* + This file is part of TALER + Copyright (C) 2017, 2021 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 + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see +*/ +/** + * @file taler-exchange-httpd_recoup_refresh.h + * @brief Handle /recoup-refresh requests + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_RECOUP_REFRESH_H +#define TALER_EXCHANGE_HTTPD_RECOUP_REFRESH_H + +#include +#include +#include "taler-exchange-httpd.h" + + +/** + * Handle a "/coins/$COIN_PUB/recoup-refresh" request. Parses the JSON, and, if + * successful, passes the JSON data to #verify_and_execute_recoup_refresh() to further + * check the details of the operation specified. If everything checks out, + * this will ultimately lead to the refund being executed, or rejected. + * + * @param connection the MHD connection to handle + * @param coin_pub public key of the coin + * @param root uploaded JSON data + * @return MHD result code + */ +MHD_RESULT +TEH_handler_recoup_refresh (struct MHD_Connection *connection, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const json_t *root); + + +#endif diff --git a/src/exchange/taler-exchange-httpd_recoup.c b/src/exchange/taler-exchange-httpd_recoup.c index 58495e530..28e81f9ec 100644 --- a/src/exchange/taler-exchange-httpd_recoup.c +++ b/src/exchange/taler-exchange-httpd_recoup.c @@ -45,9 +45,10 @@ struct RecoupContext struct TALER_BlindedCoinHash h_blind; /** - * Full value of the coin. + * Set by #recoup_transaction() to the reserve that will + * receive the recoup, if #refreshed is #GNUNET_NO. */ - struct TALER_Amount value; + struct TALER_ReservePublicKeyP reserve_pub; /** * Details about the coin. @@ -65,45 +66,29 @@ struct RecoupContext const struct TALER_CoinSpendSignatureP *coin_sig; /** - * Where does the value of the recouped coin go? Which member - * of the union is valid depends on @e refreshed. + * The amount requested to be recouped. */ - union - { - /** - * Set by #recoup_transaction() to the reserve that will - * receive the recoup, if #refreshed is #GNUNET_NO. - */ - struct TALER_ReservePublicKeyP reserve_pub; - - /** - * Set by #recoup_transaction() to the old coin that will - * receive the recoup, if #refreshed is #GNUNET_YES. - */ - struct TALER_CoinSpendPublicKeyP old_coin_pub; - } target; + const struct TALER_Amount *requested_amount; /** - * Set by #recoup_transaction() to the amount that will be paid back + * Unique ID of the withdraw operation in the reserves_out table. */ - struct TALER_Amount amount; - const struct TALER_Amount *requested_amount; + uint64_t reserve_out_serial_id; /** - * Set by #recoup_transaction to the timestamp when the recoup - * was accepted. + * Unique ID of the coin in the known_coins table. */ - struct GNUNET_TIME_Timestamp now; + uint64_t known_coin_id; /** - * true if the client claims the coin originated from a refresh. + * Set by #recoup_transaction to the timestamp when the recoup + * was accepted. */ - bool refreshed; + struct GNUNET_TIME_Timestamp now; }; -// FIXME: this code should be simplified by using TEH_check_coin_balance() /** * Execute a "recoup". The validity of the coin and signature have * already been checked. The database must now check that the coin is @@ -127,159 +112,53 @@ recoup_transaction (void *cls, MHD_RESULT *mhd_ret) { struct RecoupContext *pc = cls; - struct TALER_EXCHANGEDB_TransactionList *tl; - struct TALER_Amount spent; - struct TALER_Amount recouped; enum GNUNET_DB_QueryStatus qs; - bool existing_recoup_found; + bool recoup_ok; + bool internal_failure; - /* Check whether a recoup is allowed, and if so, to which - reserve / account the money should go */ - - /* Calculate remaining balance, including recoups already applied. */ - qs = TEH_plugin->get_coin_transactions (TEH_plugin->cls, - &pc->coin->coin_pub, - GNUNET_YES, - &tl); + /* Finally, store new refund data */ + pc->now = GNUNET_TIME_timestamp_get (); + qs = TEH_plugin->do_recoup (TEH_plugin->cls, + &pc->reserve_pub, + pc->reserve_out_serial_id, + pc->requested_amount, + pc->coin_bks, + &pc->coin->coin_pub, + pc->known_coin_id, + pc->coin_sig, + &pc->now, + &recoup_ok, + &internal_failure); if (0 > qs) { if (GNUNET_DB_STATUS_HARD_ERROR == qs) - { - GNUNET_break (0); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "coin transaction list"); - } + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "do_recoup"); return qs; } - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (pc->value.currency, - &spent)); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (pc->value.currency, - &recouped)); - /* Check if this coin has been recouped already at least once */ - existing_recoup_found = false; - for (struct TALER_EXCHANGEDB_TransactionList *pos = tl; - NULL != pos; - pos = pos->next) - { - if ( (TALER_EXCHANGEDB_TT_RECOUP == pos->type) || - (TALER_EXCHANGEDB_TT_RECOUP_REFRESH == pos->type) ) - { - existing_recoup_found = true; - break; - } - } - if (GNUNET_OK != - TALER_EXCHANGEDB_calculate_transaction_list_totals (tl, - &spent, - &spent)) + if (internal_failure) { GNUNET_break (0); - TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, - tl); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - "coin transaction history"); - return GNUNET_DB_STATUS_HARD_ERROR; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Recoup: calculated spent %s\n", - TALER_amount2s (&spent)); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Recoup: coin value %s\n", - TALER_amount2s (&pc->value)); - if (0 > - TALER_amount_subtract (&pc->amount, - &pc->value, - &spent)) - { - GNUNET_break (0); - TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, - tl); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_RECOUP_COIN_BALANCE_NEGATIVE, - NULL); + *mhd_ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + "do_recoup"); return GNUNET_DB_STATUS_HARD_ERROR; } - if (TALER_amount_is_zero (&pc->amount)) + if (! recoup_ok) { - /* Recoup has no effect: coin fully spent! */ - enum GNUNET_DB_QueryStatus ret; - - TEH_plugin->rollback (TEH_plugin->cls); - if (GNUNET_NO == existing_recoup_found) - { - /* Refuse: insufficient funds for recoup */ - *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds (connection, - TALER_EC_EXCHANGE_RECOUP_COIN_BALANCE_ZERO, - &pc->coin->coin_pub, - tl); - ret = GNUNET_DB_STATUS_HARD_ERROR; - } - else - { - /* We didn't add any new recoup transaction, but there was at least - one recoup before, so we give a success response (idempotency!) */ - ret = GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; - } - TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, - tl); - return ret; - } - TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, - tl); - pc->now = GNUNET_TIME_timestamp_get (); - if (0 != TALER_amount_cmp (&pc->amount, - pc->requested_amount)) - { - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, - TALER_amount2s (&pc->amount)); + *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( + connection, + TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, + &pc->coin->coin_pub); return GNUNET_DB_STATUS_HARD_ERROR; } - - /* add coin to list of wire transfers for recoup */ - if (pc->refreshed) - { - qs = TEH_plugin->insert_recoup_refresh_request (TEH_plugin->cls, - pc->coin, - pc->coin_sig, - pc->coin_bks, - &pc->amount, - &pc->h_blind, - pc->now); - } - else - { - qs = TEH_plugin->insert_recoup_request (TEH_plugin->cls, - &pc->target.reserve_pub, - pc->coin, - pc->coin_sig, - pc->coin_bks, - &pc->amount, - &pc->h_blind, - pc->now); - } - if (0 > qs) - { - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - { - TALER_LOG_WARNING ("Failed to store recoup information in database\n"); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "recoup request"); - } - return qs; - } - return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; + return qs; } @@ -295,7 +174,6 @@ recoup_transaction (void *cls, * @param coin_bks blinding data of the coin (to be checked) * @param coin_sig signature of the coin * @param requested_amount requested amount to be recouped - * @param refreshed true if the coin was refreshed * @return MHD result code */ static MHD_RESULT @@ -304,12 +182,10 @@ verify_and_execute_recoup ( const struct TALER_CoinPublicInfo *coin, const union TALER_DenominationBlindingKeyP *coin_bks, const struct TALER_CoinSpendSignatureP *coin_sig, - const struct TALER_Amount *requested_amount, - bool refreshed) + const struct TALER_Amount *requested_amount) { struct RecoupContext pc; const struct TEH_DenominationKey *dk; - struct TALER_CoinPubHash c_hash; MHD_RESULT mret; /* check denomination exists and is in recoup mode */ @@ -324,7 +200,6 @@ verify_and_execute_recoup ( return TEH_RESPONSE_reply_expired_denom_pub_hash ( connection, &coin->denom_pub_hash, - GNUNET_TIME_timestamp_get (), TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, "RECOUP"); } @@ -334,7 +209,6 @@ verify_and_execute_recoup ( return TEH_RESPONSE_reply_expired_denom_pub_hash ( connection, &coin->denom_pub_hash, - GNUNET_TIME_timestamp_get (), TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, "RECOUP"); } @@ -344,23 +218,21 @@ verify_and_execute_recoup ( return TEH_RESPONSE_reply_expired_denom_pub_hash ( connection, &coin->denom_pub_hash, - GNUNET_TIME_timestamp_get (), TALER_EC_EXCHANGE_RECOUP_NOT_ELIGIBLE, "RECOUP"); } - pc.value = dk->meta.value; - /* check denomination signature */ if (GNUNET_YES != TALER_test_coin_valid (coin, &dk->denom_pub)) { - TALER_LOG_WARNING ("Invalid coin passed for recoup\n"); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_DENOMINATION_SIGNATURE_INVALID, - NULL); + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_DENOMINATION_SIGNATURE_INVALID, + NULL); } /* check recoup request signature */ @@ -372,15 +244,17 @@ verify_and_execute_recoup ( coin_sig)) { GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_EXCHANGE_RECOUP_SIGNATURE_INVALID, - NULL); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_EXCHANGE_RECOUP_SIGNATURE_INVALID, + NULL); } { void *coin_ev; size_t coin_ev_size; + struct TALER_CoinPubHash c_hash; if (GNUNET_OK != TALER_denom_blind (&dk->denom_pub, @@ -392,10 +266,11 @@ verify_and_execute_recoup ( &coin_ev_size)) { GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_RECOUP_BLINDING_FAILED, - NULL); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_RECOUP_BLINDING_FAILED, + NULL); } TALER_coin_ev_hash (coin_ev, coin_ev_size, @@ -406,7 +281,6 @@ verify_and_execute_recoup ( pc.coin_sig = coin_sig; pc.coin_bks = coin_bks; pc.coin = coin; - pc.refreshed = refreshed; pc.requested_amount = requested_amount; { @@ -416,6 +290,7 @@ verify_and_execute_recoup ( /* make sure coin is 'known' in database */ qs = TEH_make_coin_known (coin, connection, + &pc.known_coin_id, &mhd_ret); /* no transaction => no serialization failures should be possible */ GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); @@ -426,35 +301,18 @@ verify_and_execute_recoup ( { enum GNUNET_DB_QueryStatus qs; - if (pc.refreshed) - { - qs = TEH_plugin->get_old_coin_by_h_blind (TEH_plugin->cls, - &pc.h_blind, - &pc.target.old_coin_pub); - if (0 > qs) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "old coin by h_blind"); - } - } - else + qs = TEH_plugin->get_reserve_by_h_blind (TEH_plugin->cls, + &pc.h_blind, + &pc.reserve_pub, + &pc.reserve_out_serial_id); + if (0 > qs) { - qs = TEH_plugin->get_reserve_by_h_blind (TEH_plugin->cls, - &pc.h_blind, - &pc.target.reserve_pub); - if (0 > qs) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "reserve by h_blind"); - } + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_reserve_by_h_blind"); } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { @@ -483,21 +341,11 @@ verify_and_execute_recoup ( return mhd_ret; } /* Recoup succeeded, return result */ - return (refreshed) - ? TALER_MHD_REPLY_JSON_PACK (connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_data_auto ( - "old_coin_pub", - &pc.target.old_coin_pub), - GNUNET_JSON_pack_bool ("refreshed", - true)) - : TALER_MHD_REPLY_JSON_PACK (connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_data_auto ( - "reserve_pub", - &pc.target.reserve_pub), - GNUNET_JSON_pack_bool ("refreshed", - false)); + return TALER_MHD_REPLY_JSON_PACK (connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_data_auto ( + "reserve_pub", + &pc.reserve_pub)); } @@ -522,7 +370,6 @@ TEH_handler_recoup (struct MHD_Connection *connection, union TALER_DenominationBlindingKeyP coin_bks; struct TALER_CoinSpendSignatureP coin_sig; struct TALER_Amount amount; - bool refreshed = false; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("denom_pub_hash", &coin.denom_pub_hash), @@ -535,12 +382,12 @@ TEH_handler_recoup (struct MHD_Connection *connection, TALER_JSON_spec_amount ("amount", TEH_currency, &amount), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_bool ("refreshed", - &refreshed)), GNUNET_JSON_spec_end () }; + memset (&coin, + 0, + sizeof (coin)); coin.coin_pub = *coin_pub; ret = TALER_MHD_parse_json_data (connection, root, @@ -556,8 +403,7 @@ TEH_handler_recoup (struct MHD_Connection *connection, &coin, &coin_bks, &coin_sig, - &amount, - refreshed); + &amount); GNUNET_JSON_parse_free (spec); return res; } diff --git a/src/exchange/taler-exchange-httpd_refreshes_reveal.c b/src/exchange/taler-exchange-httpd_refreshes_reveal.c index 36362a234..1cd28048b 100644 --- a/src/exchange/taler-exchange-httpd_refreshes_reveal.c +++ b/src/exchange/taler-exchange-httpd_refreshes_reveal.c @@ -36,13 +36,6 @@ */ #define MAX_FRESH_COINS 256 -/** - * How often do we at most retry the reveal transaction sequence? - * Twice should really suffice in all cases (as the possible conflict - * cannot happen more than once). - */ -#define MAX_REVEAL_RETRIES 2 - /** * Send a response for "/refreshes/$RCH/reveal". @@ -53,10 +46,10 @@ * @return a MHD result code */ static MHD_RESULT -reply_refreshes_reveal_success (struct MHD_Connection *connection, - unsigned int num_freshcoins, - const struct - TALER_BlindedDenominationSignature *sigs) +reply_refreshes_reveal_success ( + struct MHD_Connection *connection, + unsigned int num_freshcoins, + const struct TALER_BlindedDenominationSignature *sigs) { json_t *list; @@ -120,158 +113,35 @@ struct RevealContext */ const struct TALER_RefreshCoinData *rcds; - /** - * Signatures over the link data (of type - * #TALER_SIGNATURE_WALLET_COIN_LINK) - */ - const struct TALER_CoinSpendSignatureP *link_sigs; - - /** - * Envelopes with the signatures to be returned. Initially NULL. - */ - struct TALER_BlindedDenominationSignature *ev_sigs; - /** * Size of the @e dks, @e rcds and @e ev_sigs arrays (if non-NULL). */ unsigned int num_fresh_coins; - /** - * Result from preflight checks. #GNUNET_NO for no result, - * #GNUNET_YES if preflight found previous successful operation, - * #GNUNET_SYSERR if prefight check failed hard (and generated - * an MHD response already). - */ - int preflight_ok; - }; /** - * Function called with information about a refresh order we already - * persisted. Stores the result in @a cls so we don't do the calculation - * again. - * - * @param cls closure with a `struct RevealContext` - * @param num_freshcoins size of the @a rrcs array - * @param rrcs array of @a num_freshcoins information about coins to be created - * @param num_tprivs number of entries in @a tprivs, should be #TALER_CNC_KAPPA - 1 - * @param tprivs array of @e num_tprivs transfer private keys - * @param tp transfer public key information - */ -static void -check_exists_cb (void *cls, - uint32_t num_freshcoins, - const struct TALER_EXCHANGEDB_RefreshRevealedCoin *rrcs, - unsigned int num_tprivs, - const struct TALER_TransferPrivateKeyP *tprivs, - const struct TALER_TransferPublicKeyP *tp) -{ - struct RevealContext *rctx = cls; - - if (0 == num_freshcoins) - { - GNUNET_break (0); - return; - } - /* This should be a database invariant for us */ - GNUNET_break (TALER_CNC_KAPPA - 1 == num_tprivs); - /* Given that the $RCH value matched, we don't actually need to check these - values (we checked before). However, if a client repeats a request with - invalid values the 2nd time, that's a protocol violation we should at least - log (but it's safe to ignore it). */ - GNUNET_break_op (0 == - GNUNET_memcmp (tp, - &rctx->gamma_tp)); - GNUNET_break_op (0 == - memcmp (tprivs, - &rctx->transfer_privs, - sizeof (struct TALER_TransferPrivateKeyP) - * num_tprivs)); - /* We usually sign early (optimistic!), but in case we change that *and* - we do find the operation in the database, we could use this: */ - if (NULL == rctx->ev_sigs) - { - rctx->ev_sigs = GNUNET_new_array (num_freshcoins, - struct TALER_BlindedDenominationSignature); - for (unsigned int i = 0; iev_sigs[i], - &rrcs[i].coin_sig); - } -} - - -/** - * Check if the "/refreshes/$RCH/reveal" request was already successful - * before. If so, just return the old result. - * - * @param cls closure of type `struct RevealContext` - * @param connection MHD request which triggered the transaction - * @param[out] mhd_ret set to MHD response status for @a connection, - * if transaction failed (!) - * @return transaction status - */ -static enum GNUNET_DB_QueryStatus -refreshes_reveal_preflight (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct RevealContext *rctx = cls; - enum GNUNET_DB_QueryStatus qs; - - /* Try to see if we already have given an answer before. */ - qs = TEH_plugin->get_refresh_reveal (TEH_plugin->cls, - &rctx->rc, - &check_exists_cb, - rctx); - switch (qs) - { - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return qs; /* continue normal execution */ - case GNUNET_DB_STATUS_SOFT_ERROR: - return qs; - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (qs); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "refresh reveal"); - rctx->preflight_ok = GNUNET_SYSERR; - return GNUNET_DB_STATUS_HARD_ERROR; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - default: - /* Hossa, already found our reply! */ - GNUNET_assert (NULL != rctx->ev_sigs); - rctx->preflight_ok = GNUNET_YES; - return qs; - } -} - - -/** - * Execute a "/refreshes/$RCH/reveal". The client is revealing to us the + * Check client's revelation against the original commitment. + * The client is revealing to us the * transfer keys for @a #TALER_CNC_KAPPA-1 sets of coins. Verify that the * revealed transfer keys would allow linkage to the blinded coins. * - * IF it returns a non-error code, the transaction logic MUST - * NOT queue a MHD response. IF it returns an hard error, the - * transaction logic MUST queue a MHD response and set @a mhd_ret. IF - * it returns the soft error code, the function MAY be called again to - * retry and MUST not queue a MHD response. + * IF it returns #GNUNET_OK, the transaction logic MUST + * NOT queue a MHD response. IF it returns an error, the + * transaction logic MUST queue a MHD response and set @a mhd_ret. * - * @param cls closure of type `struct RevealContext` + * @param rctx our operation context * @param connection MHD request which triggered the transaction * @param[out] mhd_ret set to MHD response status for @a connection, * if transaction failed (!) - * @return transaction status + * @return #GNUNET_OK if commitment was OK */ -static enum GNUNET_DB_QueryStatus -refreshes_reveal_transaction (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) +static enum GNUNET_GenericReturnValue +check_commitment (struct RevealContext *rctx, + struct MHD_Connection *connection, + MHD_RESULT *mhd_ret) { - struct RevealContext *rctx = cls; - /* Verify commitment */ { /* Note that the contents of rcs[melt.session.noreveal_index] @@ -363,7 +233,7 @@ refreshes_reveal_transaction (void *cls, TALER_EC_EXCHANGE_REFRESHES_REVEAL_COMMITMENT_VIOLATION), GNUNET_JSON_pack_data_auto ("rc_expected", &rc_expected)); - return GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_SYSERR; } } /* end of checking "rc_expected" */ @@ -390,7 +260,7 @@ refreshes_reveal_transaction (void *cls, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_EXCHANGE_REFRESHES_REVEAL_COST_CALCULATION_OVERFLOW, NULL); - return GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_SYSERR; } } if (0 < TALER_amount_cmp (&refresh_cost, @@ -401,60 +271,10 @@ refreshes_reveal_transaction (void *cls, MHD_HTTP_BAD_REQUEST, TALER_EC_EXCHANGE_REFRESHES_REVEAL_AMOUNT_INSUFFICIENT, NULL); - return GNUNET_DB_STATUS_HARD_ERROR; - } - } - return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; -} - - -/** - * Persist result of a "/refreshes/$RCH/reveal" operation. - * - * @param cls closure of type `struct RevealContext` - * @param connection MHD request which triggered the transaction - * @param[out] mhd_ret set to MHD response status for @a connection, - * if transaction failed (!) - * @return transaction status - */ -static enum GNUNET_DB_QueryStatus -refreshes_reveal_persist (void *cls, - struct MHD_Connection *connection, - MHD_RESULT *mhd_ret) -{ - struct RevealContext *rctx = cls; - enum GNUNET_DB_QueryStatus qs; - - /* Persist operation result in DB */ - { - struct TALER_EXCHANGEDB_RefreshRevealedCoin rrcs[rctx->num_fresh_coins]; - - for (unsigned int i = 0; inum_fresh_coins; i++) - { - struct TALER_EXCHANGEDB_RefreshRevealedCoin *rrc = &rrcs[i]; - - rrc->denom_pub = rctx->dks[i]->denom_pub; - rrc->orig_coin_link_sig = rctx->link_sigs[i]; - rrc->coin_ev = rctx->rcds[i].coin_ev; - rrc->coin_ev_size = rctx->rcds[i].coin_ev_size; - rrc->coin_sig = rctx->ev_sigs[i]; + return GNUNET_SYSERR; } - qs = TEH_plugin->insert_refresh_reveal (TEH_plugin->cls, - &rctx->rc, - rctx->num_fresh_coins, - rrcs, - TALER_CNC_KAPPA - 1, - rctx->transfer_privs, - &rctx->gamma_tp); } - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - { - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "refresh_reveal"); - } - return qs; + return GNUNET_OK; } @@ -481,9 +301,18 @@ resolve_refreshes_reveal_denominations (struct MHD_Connection *connection, struct TALER_DenominationHash dk_h[num_fresh_coins]; struct TALER_RefreshCoinData rcds[num_fresh_coins]; struct TALER_CoinSpendSignatureP link_sigs[num_fresh_coins]; - enum GNUNET_GenericReturnValue res; + struct TALER_BlindedDenominationSignature ev_sigs[num_fresh_coins]; MHD_RESULT ret; struct TEH_KeyStateHandle *ksh; + uint64_t melt_serial_id; + + rctx->num_fresh_coins = num_fresh_coins; + memset (dks, 0, sizeof (dks)); + memset (rcds, 0, sizeof (rcds)); + memset (link_sigs, 0, sizeof (link_sigs)); + memset (ev_sigs, 0, sizeof (ev_sigs)); + rctx->dks = dks; + rctx->rcds = rcds; ksh = TEH_keys_get_state (); if (NULL == ksh) @@ -501,7 +330,7 @@ resolve_refreshes_reveal_denominations (struct MHD_Connection *connection, &dk_h[i]), GNUNET_JSON_spec_end () }; - MHD_RESULT mret; + enum GNUNET_GenericReturnValue res; res = TALER_MHD_parse_json_array (connection, new_denoms_h_json, @@ -509,15 +338,13 @@ resolve_refreshes_reveal_denominations (struct MHD_Connection *connection, i, -1); if (GNUNET_OK != res) - { return (GNUNET_NO == res) ? MHD_YES : MHD_NO; - } dks[i] = TEH_keys_denomination_by_hash2 (ksh, &dk_h[i], connection, - &mret); + &ret); if (NULL == dks[i]) - return mret; + return ret; if (GNUNET_TIME_absolute_is_past (dks[i]->meta.expire_withdraw.abs_time)) { @@ -525,7 +352,6 @@ resolve_refreshes_reveal_denominations (struct MHD_Connection *connection, return TEH_RESPONSE_reply_expired_denom_pub_hash ( connection, &dk_h[i], - GNUNET_TIME_timestamp_get (), TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, "REVEAL"); } @@ -535,7 +361,6 @@ resolve_refreshes_reveal_denominations (struct MHD_Connection *connection, return TEH_RESPONSE_reply_expired_denom_pub_hash ( connection, &dk_h[i], - GNUNET_TIME_timestamp_get (), TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, "REVEAL"); } @@ -560,6 +385,7 @@ resolve_refreshes_reveal_denominations (struct MHD_Connection *connection, &rcd->coin_ev_size), GNUNET_JSON_spec_end () }; + enum GNUNET_GenericReturnValue res; res = TALER_MHD_parse_json_array (connection, coin_evs, @@ -582,7 +408,8 @@ resolve_refreshes_reveal_denominations (struct MHD_Connection *connection, if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != (qs = TEH_plugin->get_melt (TEH_plugin->cls, &rctx->rc, - &rctx->melt))) + &rctx->melt, + &melt_serial_id))) { switch (qs) { @@ -609,8 +436,6 @@ resolve_refreshes_reveal_denominations (struct MHD_Connection *connection, } goto cleanup; } - /* Obtain basic information about the refresh operation and what - gamma we committed to. */ if (rctx->melt.session.noreveal_index >= TALER_CNC_KAPPA) { GNUNET_break (0); @@ -625,9 +450,11 @@ resolve_refreshes_reveal_denominations (struct MHD_Connection *connection, for (unsigned int i = 0; inum_fresh_coins = num_fresh_coins; - rctx->rcds = rcds; - rctx->dks = dks; - rctx->link_sigs = link_sigs; + if (GNUNET_OK != + check_commitment (rctx, + connection, + &ret)) + goto cleanup; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Optimistically creating %u signatures\n", + "Creating %u signatures\n", (unsigned int) rctx->num_fresh_coins); /* sign _early_ (optimistic!) to keep out of transaction scope! */ - rctx->ev_sigs = GNUNET_new_array (rctx->num_fresh_coins, - struct TALER_BlindedDenominationSignature); - // FIXME: this is sequential, modify logic to enable parallel signing! for (unsigned int i = 0; inum_fresh_coins; i++) { enum TALER_ErrorCode ec = TALER_EC_NONE; - rctx->ev_sigs[i] + ev_sigs[i] = TEH_keys_denomination_sign ( &dk_h[i], - rctx->rcds[i].coin_ev, - rctx->rcds[i].coin_ev_size, + rcds[i].coin_ev, + rcds[i].coin_ev_size, &ec); if (TALER_EC_NONE != ec) { @@ -687,81 +513,50 @@ resolve_refreshes_reveal_denominations (struct MHD_Connection *connection, goto cleanup; } } - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Signatures ready, starting DB interaction\n"); - /* We try the three transactions a few times, as theoretically - the pre-check might be satisfied by a concurrent transaction - voiding our final commit due to uniqueness violation; naturally, - on hard errors we exit immediately */ - for (unsigned int retries = 0; retries < MAX_REVEAL_RETRIES; retries++) + /* Persist operation result in DB */ { - /* do transactional work */ - rctx->preflight_ok = GNUNET_NO; - if ( (GNUNET_OK == - TEH_DB_run_transaction (connection, - "reveal pre-check", - TEH_MT_REVEAL_PRECHECK, - &ret, - &refreshes_reveal_preflight, - rctx)) && - (GNUNET_YES == rctx->preflight_ok) ) + struct TALER_EXCHANGEDB_RefreshRevealedCoin rrcs[rctx->num_fresh_coins]; + enum GNUNET_DB_QueryStatus qs; + + for (unsigned int i = 0; inum_fresh_coins; i++) { - /* Generate final (positive) response */ - GNUNET_assert (NULL != rctx->ev_sigs); - ret = reply_refreshes_reveal_success (connection, - num_fresh_coins, - rctx->ev_sigs); - GNUNET_break (MHD_NO != ret); - goto cleanup; /* aka 'break' */ + struct TALER_EXCHANGEDB_RefreshRevealedCoin *rrc = &rrcs[i]; + + rrc->h_denom_pub = dk_h[i]; + rrc->orig_coin_link_sig = link_sigs[i]; + rrc->coin_ev = rcds[i].coin_ev; + rrc->coin_ev_size = rcds[i].coin_ev_size; + rrc->coin_sig = ev_sigs[i]; } - if (GNUNET_SYSERR == rctx->preflight_ok) + qs = TEH_plugin->insert_refresh_reveal (TEH_plugin->cls, + melt_serial_id, + num_fresh_coins, + rrcs, + TALER_CNC_KAPPA - 1, + rctx->transfer_privs, + &rctx->gamma_tp); + if (0 > qs) { GNUNET_break (0); - goto cleanup; /* aka 'break' */ - } - if (GNUNET_OK != - TEH_DB_run_transaction (connection, - "run reveal", - TEH_MT_REVEAL, - &ret, - &refreshes_reveal_transaction, - rctx)) - { - /* reveal failed, too bad */ - GNUNET_break_op (0); - goto cleanup; /* aka 'break' */ - } - if (GNUNET_OK == - TEH_DB_run_transaction (connection, - "persist reveal", - TEH_MT_REVEAL_PERSIST, - &ret, - &refreshes_reveal_persist, - rctx)) - { - /* Generate final (positive) response */ - GNUNET_assert (NULL != rctx->ev_sigs); - ret = reply_refreshes_reveal_success (connection, - num_fresh_coins, - rctx->ev_sigs); - break; + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_refresh_reveal"); + goto cleanup; } - /* If we get here, the final transaction failed, possibly - due to a conflict between the pre-flight and us persisting - the result, so we go again. */ - } /* end for (retries...) */ + } + /* Generate final (positive) response */ + ret = reply_refreshes_reveal_success (connection, + num_fresh_coins, + ev_sigs); cleanup: GNUNET_break (MHD_NO != ret); /* free resources */ - if (NULL != rctx->ev_sigs) - { - for (unsigned int i = 0; iev_sigs[i]); - GNUNET_free (rctx->ev_sigs); - rctx->ev_sigs = NULL; /* just to be safe... */ - } + for (unsigned int i = 0; itransfer_privs[i]), + GNUNET_JSON_spec_fixed_auto (NULL, + &rctx->transfer_privs[i]), GNUNET_JSON_spec_end () }; enum GNUNET_GenericReturnValue res; @@ -861,11 +657,16 @@ TEH_handler_reveal (struct TEH_RequestContext *rc, json_t *new_denoms_h; struct RevealContext rctx; struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("transfer_pub", &rctx.gamma_tp), - GNUNET_JSON_spec_json ("transfer_privs", &transfer_privs), - GNUNET_JSON_spec_json ("link_sigs", &link_sigs), - GNUNET_JSON_spec_json ("coin_evs", &coin_evs), - GNUNET_JSON_spec_json ("new_denoms_h", &new_denoms_h), + GNUNET_JSON_spec_fixed_auto ("transfer_pub", + &rctx.gamma_tp), + GNUNET_JSON_spec_json ("transfer_privs", + &transfer_privs), + GNUNET_JSON_spec_json ("link_sigs", + &link_sigs), + GNUNET_JSON_spec_json ("coin_evs", + &coin_evs), + GNUNET_JSON_spec_json ("new_denoms_h", + &new_denoms_h), GNUNET_JSON_spec_end () }; diff --git a/src/exchange/taler-exchange-httpd_refund.c b/src/exchange/taler-exchange-httpd_refund.c index a1ef50a32..a3b066280 100644 --- a/src/exchange/taler-exchange-httpd_refund.c +++ b/src/exchange/taler-exchange-httpd_refund.c @@ -81,6 +81,28 @@ reply_refund_success (struct MHD_Connection *connection, } +/** + * Closure for refund_transaction(). + */ +struct RefundContext +{ + /** + * Details about the deposit operation. + */ + const struct TALER_EXCHANGEDB_Refund *refund; + + /** + * Deposit fee of the coin. + */ + struct TALER_Amount deposit_fee; + + /** + * Unique ID of the coin in known_coins. + */ + uint64_t known_coin_id; +}; + + /** * Execute a "/refund" transaction. Returns a confirmation that the * refund was successful, or a failure if we are not aware of a @@ -103,255 +125,67 @@ refund_transaction (void *cls, struct MHD_Connection *connection, MHD_RESULT *mhd_ret) { - const struct TALER_EXCHANGEDB_Refund *refund = cls; - struct TALER_EXCHANGEDB_TransactionList *tl; /* head of original list */ - struct TALER_EXCHANGEDB_TransactionList *tlx; /* head of sublist that applies to merchant and contract */ - struct TALER_EXCHANGEDB_TransactionList *tln; /* next element, during iteration */ - struct TALER_EXCHANGEDB_TransactionList *tlp; /* previous element in 'tl' list, during iteration */ + struct RefundContext *rctx = cls; + const struct TALER_EXCHANGEDB_Refund *refund = rctx->refund; enum GNUNET_DB_QueryStatus qs; - bool deposit_found; /* deposit_total initialized? */ - bool refund_found; /* refund_total initialized? */ - struct TALER_Amount deposit_total; - struct TALER_Amount refund_total; - - tl = NULL; - qs = TEH_plugin->get_coin_transactions (TEH_plugin->cls, - &refund->coin.coin_pub, - GNUNET_NO, - &tl); + bool not_found; + bool refund_ok; + bool conflict; + bool gone; + + /* Finally, store new refund data */ + qs = TEH_plugin->do_refund (TEH_plugin->cls, + refund, + &rctx->deposit_fee, + rctx->known_coin_id, + ¬_found, + &refund_ok, + &gone, + &conflict); if (0 > qs) { if (GNUNET_DB_STATUS_HARD_ERROR == qs) *mhd_ret = TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, - "coin transactions"); + "do refund"); return qs; } - deposit_found = false; - refund_found = false; - tlx = NULL; /* relevant subset of transactions */ - tln = NULL; - tlp = NULL; - for (struct TALER_EXCHANGEDB_TransactionList *tli = tl; - NULL != tli; - tli = tln) + + if (gone) { - tln = tli->next; - switch (tli->type) - { - case TALER_EXCHANGEDB_TT_DEPOSIT: - { - const struct TALER_EXCHANGEDB_DepositListEntry *dep; - - dep = tli->details.deposit; - if ( (0 == GNUNET_memcmp (&dep->merchant_pub, - &refund->details.merchant_pub)) && - (0 == GNUNET_memcmp (&dep->h_contract_terms, - &refund->details.h_contract_terms)) ) - { - /* check if we already send the money for this /deposit */ - if (dep->done) - { - TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, - tlx); - TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, - tln); - /* money was already transferred to merchant, can no longer refund */ - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_GONE, - TALER_EC_EXCHANGE_REFUND_MERCHANT_ALREADY_PAID, - NULL); - return GNUNET_DB_STATUS_HARD_ERROR; - } - - /* deposit applies and was not yet wired; add to total (it is NOT - the case that multiple deposits of the same coin for the same - contract are really allowed (see UNIQUE constraint on 'deposits' - table), but in case this changes we tolerate it with this code - anyway). */// - if (deposit_found) - { - GNUNET_assert (0 <= - TALER_amount_add (&deposit_total, - &deposit_total, - &dep->amount_with_fee)); - } - else - { - deposit_total = dep->amount_with_fee; - deposit_found = true; - } - /* move 'tli' from 'tl' to 'tlx' list */ - if (NULL == tlp) - tl = tln; - else - tlp->next = tln; - tli->next = tlx; - tlx = tli; - break; - } - else - { - tlp = tli; - } - break; - } - case TALER_EXCHANGEDB_TT_MELT: - /* Melts cannot be refunded, ignore here */ - break; - case TALER_EXCHANGEDB_TT_REFUND: - { - const struct TALER_EXCHANGEDB_RefundListEntry *ref; - - ref = tli->details.refund; - if ( (0 != GNUNET_memcmp (&ref->merchant_pub, - &refund->details.merchant_pub)) || - (0 != GNUNET_memcmp (&ref->h_contract_terms, - &refund->details.h_contract_terms)) ) - { - tlp = tli; - break; /* refund does not apply to our transaction */ - } - /* Check if existing refund request matches in everything but the amount */ - if ( (ref->rtransaction_id == - refund->details.rtransaction_id) && - (0 != TALER_amount_cmp (&ref->refund_amount, - &refund->details.refund_amount)) ) - { - /* Generate precondition failed response, with ONLY the conflicting entry */ - TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, - tlx); - TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, - tln); - tli->next = NULL; - *mhd_ret = TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_PRECONDITION_FAILED, - TALER_JSON_pack_amount ("detail", - &ref->refund_amount), - TALER_JSON_pack_ec (TALER_EC_EXCHANGE_REFUND_INCONSISTENT_AMOUNT), - GNUNET_JSON_pack_array_steal ("history", - TEH_RESPONSE_compile_transaction_history ( - &refund->coin.coin_pub, - tli))); - TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, - tli); - return GNUNET_DB_STATUS_HARD_ERROR; - } - /* Check if existing refund request matches in everything including the amount */ - if ( (ref->rtransaction_id == - refund->details.rtransaction_id) && - (0 == TALER_amount_cmp (&ref->refund_amount, - &refund->details.refund_amount)) ) - { - /* we can blanketly approve, as this request is identical to one - we saw before */ - *mhd_ret = reply_refund_success (connection, - &refund->coin.coin_pub, - ref); - TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, - tlx); - TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, - tl); - /* we still abort the transaction, as there is nothing to be - committed! */ - return GNUNET_DB_STATUS_HARD_ERROR; - } - - /* We have another refund, that relates, add to total */ - if (refund_found) - { - GNUNET_assert (0 <= - TALER_amount_add (&refund_total, - &refund_total, - &ref->refund_amount)); - } - else - { - refund_total = ref->refund_amount; - refund_found = true; - } - /* move 'tli' from 'tl' to 'tlx' list */ - if (NULL == tlp) - tl = tln; - else - tlp->next = tln; - tli->next = tlx; - tlx = tli; - break; - } - case TALER_EXCHANGEDB_TT_OLD_COIN_RECOUP: - /* Recoups cannot be refunded, ignore here */ - break; - case TALER_EXCHANGEDB_TT_RECOUP: - /* Recoups cannot be refunded, ignore here */ - break; - case TALER_EXCHANGEDB_TT_RECOUP_REFRESH: - /* Recoups cannot be refunded, ignore here */ - break; - } + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_GONE, + TALER_EC_EXCHANGE_REFUND_MERCHANT_ALREADY_PAID, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; } - /* no need for 'tl' anymore, everything we may still care about is in tlx now */ - TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, - tl); - /* handle if deposit was NOT found */ - if (! deposit_found) + if (conflict) + { + TEH_plugin->rollback (TEH_plugin->cls); + *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( + connection, + TALER_EC_EXCHANGE_REFUND_INCONSISTENT_AMOUNT, + &refund->coin.coin_pub); + return GNUNET_DB_STATUS_HARD_ERROR; + } + if (not_found) { - TALER_LOG_WARNING ("Deposit to /refund was not found\n"); - TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, - tlx); *mhd_ret = TALER_MHD_reply_with_error (connection, MHD_HTTP_NOT_FOUND, TALER_EC_EXCHANGE_REFUND_DEPOSIT_NOT_FOUND, NULL); return GNUNET_DB_STATUS_HARD_ERROR; } - - /* check total refund amount is sufficiently low */ - if (refund_found) - GNUNET_break (0 <= - TALER_amount_add (&refund_total, - &refund_total, - &refund->details.refund_amount)); - else - refund_total = refund->details.refund_amount; - - if (1 == TALER_amount_cmp (&refund_total, - &deposit_total) ) + if (! refund_ok) { - *mhd_ret = TALER_MHD_REPLY_JSON_PACK ( + TEH_plugin->rollback (TEH_plugin->cls); + *mhd_ret = TEH_RESPONSE_reply_coin_insufficient_funds ( connection, - MHD_HTTP_CONFLICT, - GNUNET_JSON_pack_string ("detail", - "total amount refunded exceeds total amount deposited for this coin"), - TALER_JSON_pack_ec ( - TALER_EC_EXCHANGE_REFUND_CONFLICT_DEPOSIT_INSUFFICIENT), - GNUNET_JSON_pack_array_steal ("history", - TEH_RESPONSE_compile_transaction_history ( - &refund->coin.coin_pub, - tlx))); - TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, - tlx); + TALER_EC_EXCHANGE_REFUND_CONFLICT_DEPOSIT_INSUFFICIENT, + &refund->coin.coin_pub); return GNUNET_DB_STATUS_HARD_ERROR; } - TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, - tlx); - - - /* Finally, store new refund data */ - qs = TEH_plugin->insert_refund (TEH_plugin->cls, - refund); - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - { - TALER_LOG_WARNING ("Failed to store /refund information in database\n"); - *mhd_ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "refund"); - return qs; - } - /* Success or soft failure */ return qs; } @@ -371,7 +205,11 @@ verify_and_execute_refund (struct MHD_Connection *connection, struct TALER_EXCHANGEDB_Refund *refund) { struct TALER_DenominationHash denom_hash; + struct RefundContext rctx = { + .refund = refund + }; + // FIXME: move to libtalerutil! { struct TALER_RefundRequestPS rr = { .purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_REFUND), @@ -404,6 +242,7 @@ verify_and_execute_refund (struct MHD_Connection *connection, qs = TEH_plugin->get_coin_denomination (TEH_plugin->cls, &refund->coin.coin_pub, + &rctx.known_coin_id, &denom_hash); if (0 > qs) { @@ -438,6 +277,7 @@ verify_and_execute_refund (struct MHD_Connection *connection, return mret; } refund->details.refund_fee = dk->meta.fee_refund; + rctx.deposit_fee = dk->meta.fee_deposit; } /* Finally run the actual transaction logic */ @@ -450,7 +290,7 @@ verify_and_execute_refund (struct MHD_Connection *connection, TEH_MT_OTHER, &mhd_ret, &refund_transaction, - (void *) refund)) + &rctx)) { return mhd_ret; } diff --git a/src/exchange/taler-exchange-httpd_responses.c b/src/exchange/taler-exchange-httpd_responses.c index 8c24efc9d..5739e6709 100644 --- a/src/exchange/taler-exchange-httpd_responses.c +++ b/src/exchange/taler-exchange-httpd_responses.c @@ -122,25 +122,15 @@ TEH_RESPONSE_compile_transaction_history ( { const struct TALER_EXCHANGEDB_MeltListEntry *melt = pos->details.melt; - struct TALER_RefreshMeltCoinAffirmationPS ms = { - .purpose.purpose = htonl (TALER_SIGNATURE_WALLET_COIN_MELT), - .purpose.size = htonl (sizeof (ms)), - .rc = melt->rc, - .h_denom_pub = melt->h_denom_pub, - .coin_pub = *coin_pub - }; - TALER_amount_hton (&ms.amount_with_fee, - &melt->amount_with_fee); - TALER_amount_hton (&ms.melt_fee, - &melt->melt_fee); #if ENABLE_SANITY_CHECKS - /* internal sanity check before we hand out a bogus sig... */ if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_WALLET_COIN_MELT, - &ms, - &melt->coin_sig.eddsa_signature, - &coin_pub->eddsa_pub)) + TALER_wallet_melt_verify (&melt->amount_with_fee, + &melt->melt_fee, + &melt->rc, + &melt->h_denom_pub, + coin_pub, + &melt->coin_sig)) { GNUNET_break (0); json_decref (history); @@ -175,6 +165,7 @@ TEH_RESPONSE_compile_transaction_history ( const struct TALER_EXCHANGEDB_RefundListEntry *refund = pos->details.refund; struct TALER_Amount value; + // FIXME: move to libtalerutil! struct TALER_RefundRequestPS rr = { .purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_REFUND), .purpose.size = htonl (sizeof (rr)), @@ -461,13 +452,14 @@ MHD_RESULT TEH_RESPONSE_reply_expired_denom_pub_hash ( struct MHD_Connection *connection, const struct TALER_DenominationHash *dph, - struct GNUNET_TIME_Timestamp now, enum TALER_ErrorCode ec, const char *oper) { struct TALER_ExchangePublicKeyP epub; struct TALER_ExchangeSignatureP esig; enum TALER_ErrorCode ecr; + struct GNUNET_TIME_Timestamp now + = GNUNET_TIME_timestamp_get (); struct TALER_DenominationExpiredAffirmationPS dua = { .purpose.size = htonl (sizeof (dua)), .purpose.purpose = htonl ( @@ -525,13 +517,31 @@ MHD_RESULT TEH_RESPONSE_reply_coin_insufficient_funds ( struct MHD_Connection *connection, enum TALER_ErrorCode ec, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_EXCHANGEDB_TransactionList *tl) + const struct TALER_CoinSpendPublicKeyP *coin_pub) { + struct TALER_EXCHANGEDB_TransactionList *tl; + enum GNUNET_DB_QueryStatus qs; json_t *history; + // FIXME: maybe start read-committed transaction here? + // => check all callers (that they aborted already!) + qs = TEH_plugin->get_coin_transactions (TEH_plugin->cls, + coin_pub, + GNUNET_NO, + &tl); + if (0 > qs) + { + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + } + history = TEH_RESPONSE_compile_transaction_history (coin_pub, tl); + TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, + tl); if (NULL == history) { GNUNET_break (0); @@ -542,7 +552,7 @@ TEH_RESPONSE_reply_coin_insufficient_funds ( } return TALER_MHD_REPLY_JSON_PACK ( connection, - MHD_HTTP_CONFLICT, + TALER_ErrorCode_get_http_status_safe (ec), TALER_JSON_pack_ec (ec), GNUNET_JSON_pack_array_steal ("history", history)); diff --git a/src/exchange/taler-exchange-httpd_responses.h b/src/exchange/taler-exchange-httpd_responses.h index e15901120..db2286ffa 100644 --- a/src/exchange/taler-exchange-httpd_responses.h +++ b/src/exchange/taler-exchange-httpd_responses.h @@ -67,7 +67,6 @@ TEH_RESPONSE_reply_unknown_denom_pub_hash ( * * @param connection connection to the client * @param dph denomination public key hash - * @param now timestamp to use * @param ec error code to use * @param oper name of the operation that is not allowed at this time * @return MHD result code @@ -76,7 +75,6 @@ MHD_RESULT TEH_RESPONSE_reply_expired_denom_pub_hash ( struct MHD_Connection *connection, const struct TALER_DenominationHash *dph, - struct GNUNET_TIME_Timestamp now, enum TALER_ErrorCode ec, const char *oper); @@ -90,15 +88,13 @@ TEH_RESPONSE_reply_expired_denom_pub_hash ( * @param connection connection to the client * @param ec error code to return * @param coin_pub public key of the coin - * @param tl transaction list to use to build reply * @return MHD result code */ MHD_RESULT TEH_RESPONSE_reply_coin_insufficient_funds ( struct MHD_Connection *connection, enum TALER_ErrorCode ec, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_EXCHANGEDB_TransactionList *tl); + const struct TALER_CoinSpendPublicKeyP *coin_pub); /** diff --git a/src/exchange/taler-exchange-httpd_withdraw.c b/src/exchange/taler-exchange-httpd_withdraw.c index dfd10b5e4..2e53803a9 100644 --- a/src/exchange/taler-exchange-httpd_withdraw.c +++ b/src/exchange/taler-exchange-httpd_withdraw.c @@ -94,7 +94,7 @@ struct WithdrawContext /** * Blinded planchet. */ - char *blinded_msg; + void *blinded_msg; /** * Number of bytes in @e blinded_msg. @@ -141,6 +141,7 @@ withdraw_transaction (void *cls, bool found = false; bool balance_ok = false; struct GNUNET_TIME_Timestamp now; + uint64_t ruuid; now = GNUNET_TIME_timestamp_get (); wc->collectable.reserve_pub = wc->wsrd.reserve_pub; @@ -150,7 +151,8 @@ withdraw_transaction (void *cls, now, &found, &balance_ok, - &wc->kyc); + &wc->kyc, + &ruuid); if (0 > qs) { if (GNUNET_DB_STATUS_HARD_ERROR == qs) @@ -174,6 +176,7 @@ withdraw_transaction (void *cls, struct TALER_Amount balance; TEH_plugin->rollback (TEH_plugin->cls); + // FIXME: maybe start read-committed here? if (GNUNET_OK != TEH_plugin->start (TEH_plugin->cls, "get_reserve_history on insufficient balance")) @@ -232,7 +235,7 @@ withdraw_transaction (void *cls, qs2 = TEH_plugin->do_withdraw_limit_check ( TEH_plugin->cls, - &wc->collectable.reserve_pub, + ruuid, GNUNET_TIME_absolute_subtract (now.abs_time, TEH_kyc_config.withdraw_period), &TEH_kyc_config.withdraw_limit, @@ -249,6 +252,7 @@ withdraw_transaction (void *cls, } if (! below_limit) { + TEH_plugin->rollback (TEH_plugin->cls); *mhd_ret = TALER_MHD_REPLY_JSON_PACK ( connection, MHD_HTTP_ACCEPTED, @@ -313,7 +317,7 @@ TEH_handler_withdraw (struct TEH_RequestContext *rc, struct WithdrawContext wc; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_varsize ("coin_ev", - (void **) &wc.blinded_msg, + &wc.blinded_msg, &wc.blinded_msg_len), GNUNET_JSON_spec_fixed_auto ("reserve_sig", &wc.collectable.reserve_sig), @@ -398,7 +402,6 @@ TEH_handler_withdraw (struct TEH_RequestContext *rc, return TEH_RESPONSE_reply_expired_denom_pub_hash ( rc->connection, &wc.collectable.denom_pub_hash, - GNUNET_TIME_timestamp_get (), TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED, "WITHDRAW"); } @@ -413,7 +416,6 @@ TEH_handler_withdraw (struct TEH_RequestContext *rc, return TEH_RESPONSE_reply_expired_denom_pub_hash ( rc->connection, &wc.collectable.denom_pub_hash, - GNUNET_TIME_timestamp_get (), TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE, "WITHDRAW"); } @@ -428,7 +430,6 @@ TEH_handler_withdraw (struct TEH_RequestContext *rc, return TEH_RESPONSE_reply_expired_denom_pub_hash ( rc->connection, &wc.collectable.denom_pub_hash, - GNUNET_TIME_timestamp_get (), TALER_EC_EXCHANGE_GENERIC_DENOMINATION_REVOKED, "WITHDRAW"); } @@ -437,22 +438,21 @@ TEH_handler_withdraw (struct TEH_RequestContext *rc, } } + if (0 > + TALER_amount_add (&wc.collectable.amount_with_fee, + &dk->meta.value, + &dk->meta.fee_withdraw)) { - if (0 > - TALER_amount_add (&wc.collectable.amount_with_fee, - &dk->meta.value, - &dk->meta.fee_withdraw)) - { - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW, - NULL); - } - TALER_amount_hton (&wc.wsrd.amount_with_fee, - &wc.collectable.amount_with_fee); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW, + NULL); } + TALER_amount_hton (&wc.wsrd.amount_with_fee, + &wc.collectable.amount_with_fee); + // FIXME: move this logic into libtalerutil! /* verify signature! */ wc.wsrd.purpose.size = htonl (sizeof (wc.wsrd)); @@ -495,7 +495,7 @@ TEH_handler_withdraw (struct TEH_RequestContext *rc, NULL); } - /* run transaction and sign (if not optimistically signed before) */ + /* run transaction */ { MHD_RESULT mhd_ret; -- cgit v1.2.3