From 0c959e75d109fc914ab8891518f7963ade74cff8 Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Thu, 5 May 2016 22:57:55 +0200 Subject: [PATCH] working on #3641: more on /refund implementation --- src/exchange/taler-exchange-httpd_db.c | 280 ++++++++++++++++++ src/exchange/taler-exchange-httpd_db.h | 14 + src/exchange/taler-exchange-httpd_refund.c | 20 +- src/exchange/taler-exchange-httpd_responses.c | 80 +++++ src/exchange/taler-exchange-httpd_responses.h | 38 +++ src/exchangedb/plugin_exchangedb_common.c | 5 +- src/exchangedb/plugin_exchangedb_postgres.c | 40 +++ src/include/taler_exchangedb_plugin.h | 42 ++- 8 files changed, 512 insertions(+), 7 deletions(-) diff --git a/src/exchange/taler-exchange-httpd_db.c b/src/exchange/taler-exchange-httpd_db.c index 237a1aa7c..499623e40 100644 --- a/src/exchange/taler-exchange-httpd_db.c +++ b/src/exchange/taler-exchange-httpd_db.c @@ -105,12 +105,16 @@ calculate_transaction_list_totals (struct TALER_EXCHANGEDB_TransactionList *tl, { struct TALER_Amount spent = *off; struct TALER_EXCHANGEDB_TransactionList *pos; + struct TALER_Amount refunded; + TALER_amount_get_zero (spent.currency, + &refunded); for (pos = tl; NULL != pos; pos = pos->next) { switch (pos->type) { case TALER_EXCHANGEDB_TT_DEPOSIT: + /* spent += pos->amount_with_fee */ if (GNUNET_OK != TALER_amount_add (&spent, &spent, @@ -121,6 +125,7 @@ calculate_transaction_list_totals (struct TALER_EXCHANGEDB_TransactionList *tl, } break; case TALER_EXCHANGEDB_TT_REFRESH_MELT: + /* spent += pos->amount_with_fee */ if (GNUNET_OK != TALER_amount_add (&spent, &spent, @@ -130,8 +135,37 @@ calculate_transaction_list_totals (struct TALER_EXCHANGEDB_TransactionList *tl, return GNUNET_SYSERR; } break; + case TALER_EXCHANGEDB_TT_REFUND: + /* refunded += pos->refund_amount - pos->refund_fee */ + if (GNUNET_OK != + TALER_amount_add (&refunded, + &refunded, + &pos->details.refund->refund_amount)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_amount_subtract (&refunded, + &refunded, + &pos->details.refund->refund_fee)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + break; } } + /* spent = spent - refunded */ + if (GNUNET_OK != + TALER_amount_subtract (&spent, + &spent, + &refunded)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + *ret = spent; return GNUNET_OK; } @@ -261,6 +295,252 @@ TMH_DB_execute_deposit (struct MHD_Connection *connection, } +/** + * Execute a "/refund". Returns a confirmation that the refund + * was successful, or a failure if we are not aware of a matching + * /deposit or if it is too late to do the refund. + * + * @param connection the MHD connection to handle + * @param refund refund details + * @return MHD result code + */ +int +TMH_DB_execute_refund (struct MHD_Connection *connection, + const struct TALER_EXCHANGEDB_Refund *refund) +{ + struct TALER_EXCHANGEDB_Session *session; + struct TALER_EXCHANGEDB_TransactionList *tl; + struct TALER_EXCHANGEDB_TransactionList *tlp; + const struct TALER_EXCHANGEDB_Deposit *dep; + const struct TALER_EXCHANGEDB_Refund *ref; + struct TMH_KS_StateHandle *mks; + struct TALER_EXCHANGEDB_DenominationKeyIssueInformation *dki; + struct TALER_Amount expect_fee; + int ret; + int deposit_found; + int refund_found; + int done; + int fee_cmp; + + if (NULL == (session = TMH_plugin->get_session (TMH_plugin->cls))) + { + GNUNET_break (0); + return TMH_RESPONSE_reply_internal_db_error (connection); + } + dep = NULL; + ref = NULL; + START_TRANSACTION (session, connection); + tl = TMH_plugin->get_coin_transactions (TMH_plugin->cls, + session, + &refund->coin.coin_pub); + if (NULL == tl) + { + TMH_plugin->rollback (TMH_plugin->cls, + session); + return TMH_RESPONSE_reply_refund_failure (connection, + MHD_HTTP_NOT_FOUND); + } + deposit_found = GNUNET_NO; + for (tlp = tl; NULL != tlp; tlp = tlp->next) + { + switch (tlp->type) + { + case TALER_EXCHANGEDB_TT_DEPOSIT: + { + dep = tlp->details.deposit; + if ( (0 == memcmp (&dep->merchant_pub, + &refund->merchant_pub, + sizeof (struct TALER_MerchantPublicKeyP))) && + (0 == memcmp (&dep->h_contract, + &refund->h_contract, + sizeof (struct GNUNET_HashCode))) && + (dep->transaction_id == refund->transaction_id) ) + { + deposit_found = GNUNET_YES; + break; + } + } + break; + case TALER_EXCHANGEDB_TT_REFRESH_MELT: + /* Melts cannot be refunded, ignore here */ + break; + case TALER_EXCHANGEDB_TT_REFUND: + { + ref = tlp->details.refund; + /* First, check if existing refund request is identical */ + if ( (0 == memcmp (&ref->merchant_pub, + &refund->merchant_pub, + sizeof (struct TALER_MerchantPublicKeyP))) && + (0 == memcmp (&ref->h_contract, + &refund->h_contract, + sizeof (struct GNUNET_HashCode))) && + (ref->transaction_id == refund->transaction_id) && + (ref->rtransaction_id == refund->rtransaction_id) ) + { + refund_found = GNUNET_YES; + break; + } + /* Second, check if existing refund request conflicts */ + if ( (0 == memcmp (&ref->merchant_pub, + &refund->merchant_pub, + sizeof (struct TALER_MerchantPublicKeyP))) && + (0 == memcmp (&ref->h_contract, + &refund->h_contract, + sizeof (struct GNUNET_HashCode))) && + (ref->transaction_id == refund->transaction_id) && + (ref->rtransaction_id != refund->rtransaction_id) ) + { + GNUNET_break_op (0); /* conflicting refound found */ + refund_found = GNUNET_SYSERR; + /* NOTE: Alternatively we could total up all existing + refunds and check if the sum still permits the + refund requested (thus allowing multiple, partial + refunds). Fow now, we keep it simple. */ + break; + } + } + break; + } + } + /* handle if deposit was NOT found */ + if (GNUNET_NO == deposit_found) + { + TMH_plugin->rollback (TMH_plugin->cls, + session); + TMH_plugin->free_coin_transaction_list (TMH_plugin->cls, + tl); + return TMH_RESPONSE_reply_deposit_unknown (connection); + } + /* handle if conflicting refund found */ + if (GNUNET_SYSERR == refund_found) + { + TMH_plugin->rollback (TMH_plugin->cls, + session); + ret = TMH_RESPONSE_reply_refund_conflict (connection, + tl); + TMH_plugin->free_coin_transaction_list (TMH_plugin->cls, + tl); + return ret; + } + /* handle if identical refund found */ + if (GNUNET_YES == refund_found) + { + /* /refund already done, simply re-transmit confirmation */ + TMH_plugin->rollback (TMH_plugin->cls, + session); + ret = TMH_RESPONSE_reply_refund_success (connection, + ref); + TMH_plugin->free_coin_transaction_list (TMH_plugin->cls, + tl); + return ret; + } + + /* check currency is compatible */ + if ( (GNUNET_YES != + TALER_amount_cmp_currency (&refund->refund_amount, + &dep->amount_with_fee)) || + (GNUNET_YES != + TALER_amount_cmp_currency (&refund->refund_fee, + &dep->deposit_fee)) ) + { + GNUNET_break_op (0); /* currency missmatch */ + TMH_plugin->rollback (TMH_plugin->cls, + session); + return TMH_RESPONSE_reply_refund_failure (connection, + MHD_HTTP_PRECONDITION_FAILED); + } + + /* check if we already send the money for the /deposit */ + done = TMH_plugin->test_deposit_done (TMH_plugin->cls, + session, + dep); + if (GNUNET_SYSERR == done) + { + /* Internal error, we first had the deposit in the history, + but now it is gone? */ + GNUNET_break (0); + TMH_plugin->free_coin_transaction_list (TMH_plugin->cls, + tl); + TMH_plugin->rollback (TMH_plugin->cls, + session); + return TMH_RESPONSE_reply_internal_error (connection, + "database inconsistent"); + } + if (GNUNET_YES == done) + { + /* money was already transferred to merchant, can no longer refund */ + TMH_plugin->rollback (TMH_plugin->cls, + session); + TMH_plugin->free_coin_transaction_list (TMH_plugin->cls, + tl); + return TMH_RESPONSE_reply_refund_failure (connection, + MHD_HTTP_GONE); + } + + /* We no longer need 'tl' or 'dep' or 'ref' */ + TMH_plugin->free_coin_transaction_list (TMH_plugin->cls, + tl); + + /* check refund amount is sufficiently low */ + if (1 == TALER_amount_cmp (&refund->refund_amount, + &dep->amount_with_fee) ) + { + GNUNET_break_op (0); /* cannot refund more than original value */ + return TMH_RESPONSE_reply_refund_failure (connection, + MHD_HTTP_PRECONDITION_FAILED); + } + + /* Check refund fee matches fee of denomination key! */ + mks = TMH_KS_acquire (); + dki = TMH_KS_denomination_key_lookup (mks, + &dep->coin.denom_pub, + TMH_KS_DKU_DEPOSIT); + if (NULL == dki) + { + /* DKI not found, but we do have a coin with this DK in our database; + not good... */ + GNUNET_break (0); + TMH_KS_release (mks); + return TMH_RESPONSE_reply_internal_error (connection, + "denomination key not found"); + } + TALER_amount_ntoh (&expect_fee, + &dki->issue.properties.fee_refund); + fee_cmp = TALER_amount_cmp (&refund->refund_fee, + &expect_fee); + TMH_KS_release (mks); + + if (-1 == fee_cmp) + { + TMH_plugin->rollback (TMH_plugin->cls, + session); + return TMH_RESPONSE_reply_arg_invalid (connection, + "refund_fee"); + } + if (1 == fee_cmp) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Refund fee proposed by merchant is higher than necessary.\n"); + } + + /* Finally, store new refund data */ + if (GNUNET_OK != + TMH_plugin->insert_refund (TMH_plugin->cls, + session, + refund)) + { + TALER_LOG_WARNING ("Failed to store /refund information in database\n"); + TMH_plugin->rollback (TMH_plugin->cls, + session); + return TMH_RESPONSE_reply_internal_db_error (connection); + } + COMMIT_TRANSACTION(session, connection); + + return TMH_RESPONSE_reply_refund_success (connection, + refund); +} + + /** * Execute a /reserve/status. Given the public key of a reserve, * return the associated transaction history. diff --git a/src/exchange/taler-exchange-httpd_db.h b/src/exchange/taler-exchange-httpd_db.h index d6245a702..7cf498285 100644 --- a/src/exchange/taler-exchange-httpd_db.h +++ b/src/exchange/taler-exchange-httpd_db.h @@ -40,6 +40,20 @@ TMH_DB_execute_deposit (struct MHD_Connection *connection, const struct TALER_EXCHANGEDB_Deposit *deposit); +/** + * Execute a "/refund". Returns a confirmation that the refund + * was successful, or a failure if we are not aware of a matching + * /deposit or if it is too late to do the refund. + * + * @param connection the MHD connection to handle + * @param refund refund details + * @return MHD result code + */ +int +TMH_DB_execute_refund (struct MHD_Connection *connection, + const struct TALER_EXCHANGEDB_Refund *refund); + + /** * Execute a "/reserve/status". Given the public key of a reserve, * return the associated transaction history. diff --git a/src/exchange/taler-exchange-httpd_refund.c b/src/exchange/taler-exchange-httpd_refund.c index c5f24000d..b6dc203c0 100644 --- a/src/exchange/taler-exchange-httpd_refund.c +++ b/src/exchange/taler-exchange-httpd_refund.c @@ -63,6 +63,21 @@ verify_and_execute_refund (struct MHD_Connection *connection, &refund->refund_fee); dr.merchant = refund->merchant_pub; dr.coin_pub = refund->coin.coin_pub; + if (GNUNET_YES != + TALER_amount_cmp_currency (&refund->refund_amount, + &refund->refund_fee) ) + { + GNUNET_break_op (0); + return TMH_RESPONSE_reply_arg_invalid (connection, + "refund_fee"); + } + if (-1 == TALER_amount_cmp (&refund->refund_amount, + &refund->refund_fee) ) + { + GNUNET_break_op (0); + return TMH_RESPONSE_reply_signature_invalid (connection, + "refund_amount"); + } if (GNUNET_OK != GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_MERCHANT_REFUND, &dr.purpose, @@ -73,13 +88,8 @@ verify_and_execute_refund (struct MHD_Connection *connection, return TMH_RESPONSE_reply_signature_invalid (connection, "merchant_sig"); } -#if 1 - GNUNET_break (0); // FIXME: not implemented - return MHD_NO; -#else return TMH_DB_execute_refund (connection, refund); -#endif } diff --git a/src/exchange/taler-exchange-httpd_responses.c b/src/exchange/taler-exchange-httpd_responses.c index c8a72f499..2011a5e49 100644 --- a/src/exchange/taler-exchange-httpd_responses.c +++ b/src/exchange/taler-exchange-httpd_responses.c @@ -642,6 +642,86 @@ compile_reserve_history (const struct TALER_EXCHANGEDB_ReserveHistory *rh, } +/** + * Generate refund conflict failure message. Returns the + * transaction list @a tl with the details about the conflict. + * + * @param connection connection to the client + * @param tl transaction list showing the conflict + * @return MHD result code + */ +int +TMH_RESPONSE_reply_refund_conflict (struct MHD_Connection *connection, + const struct TALER_EXCHANGEDB_TransactionList *tl) +{ + return TMH_RESPONSE_reply_json_pack (connection, + MHD_HTTP_CONFLICT, + "{s:s, s:o}", + "status", "conflicting refund", + "history", compile_transaction_history (tl)); +} + + +/** + * Generate generic refund failure message. All the details + * are in the @a response_code. The body can be empty. + * + * @param connection connection to the client + * @param response_code response code to generate + * @return MHD result code + */ +int +TMH_RESPONSE_reply_refund_failure (struct MHD_Connection *connection, + unsigned int response_code) +{ + return TMH_RESPONSE_reply_json_pack (connection, + response_code, + "{s:s}", + "error", + "no details"); +} + + +/** + * Generate successful refund confirmation message. + * + * @param connection connection to the client + * @param refund details about the successful refund + * @return MHD result code + */ +int +TMH_RESPONSE_reply_refund_success (struct MHD_Connection *connection, + const struct TALER_EXCHANGEDB_Refund *refund) +{ + struct TALER_RefundConfirmationPS rc; + struct TALER_ExchangePublicKeyP pub; + struct TALER_ExchangeSignatureP sig; + + rc.purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND); + rc.purpose.size = htonl (sizeof (struct TALER_RefundConfirmationPS)); + rc.h_contract = refund->h_contract; + rc.transaction_id = GNUNET_htonll (refund->transaction_id); + rc.coin_pub = refund->coin.coin_pub; + rc.merchant = refund->merchant_pub; + rc.rtransaction_id = GNUNET_htonll (refund->rtransaction_id); + TALER_amount_hton (&rc.refund_amount, + &refund->refund_amount); + TALER_amount_hton (&rc.refund_fee, + &refund->refund_fee); + TMH_KS_sign (&rc.purpose, + &pub, + &sig); + return TMH_RESPONSE_reply_json_pack (connection, + MHD_HTTP_OK, + "{s:s, s:o, s:o}", + "status", "REFUND_OK", + "sig", GNUNET_JSON_from_data (&sig, + sizeof (sig)), + "pub", GNUNET_JSON_from_data (&pub, + sizeof (pub))); +} + + /** * Send reserve status information to client. * diff --git a/src/exchange/taler-exchange-httpd_responses.h b/src/exchange/taler-exchange-httpd_responses.h index c7139bf20..85c2e1f32 100644 --- a/src/exchange/taler-exchange-httpd_responses.h +++ b/src/exchange/taler-exchange-httpd_responses.h @@ -247,6 +247,44 @@ TMH_RESPONSE_reply_deposit_insufficient_funds (struct MHD_Connection *connection const struct TALER_EXCHANGEDB_TransactionList *tl); +/** + * Generate refund conflict failure message. Returns the + * transaction list @a tl with the details about the conflict. + * + * @param connection connection to the client + * @param tl transaction list showing the conflict + * @return MHD result code + */ +int +TMH_RESPONSE_reply_refund_conflict (struct MHD_Connection *connection, + const struct TALER_EXCHANGEDB_TransactionList *tl); + + +/** + * Generate generic refund failure message. All the details + * are in the @a response_code. The body can be empty. + * + * @param connection connection to the client + * @param response_code response code to generate + * @return MHD result code + */ +int +TMH_RESPONSE_reply_refund_failure (struct MHD_Connection *connection, + unsigned int response_code); + + +/** + * Generate successful refund confirmation message. + * + * @param connection connection to the client + * @param refund details about the successful refund + * @return MHD result code + */ +int +TMH_RESPONSE_reply_refund_success (struct MHD_Connection *connection, + const struct TALER_EXCHANGEDB_Refund *refund); + + /** * A merchant asked for details about a deposit, but * we do not know anything about the deposit. Generate the diff --git a/src/exchangedb/plugin_exchangedb_common.c b/src/exchangedb/plugin_exchangedb_common.c index c8e689cfd..c18b958cf 100644 --- a/src/exchangedb/plugin_exchangedb_common.c +++ b/src/exchangedb/plugin_exchangedb_common.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2015 GNUnet e.V. + Copyright (C) 2015, 2016 Inria & GNUnet e.V. TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -107,6 +107,9 @@ common_free_coin_transaction_list (void *cls, case TALER_EXCHANGEDB_TT_REFRESH_MELT: GNUNET_free (list->details.melt); break; + case TALER_EXCHANGEDB_TT_REFUND: + GNUNET_free (list->details.refund); + break; } GNUNET_free (list); list = next; diff --git a/src/exchangedb/plugin_exchangedb_postgres.c b/src/exchangedb/plugin_exchangedb_postgres.c index 612dabb79..b189b7a17 100644 --- a/src/exchangedb/plugin_exchangedb_postgres.c +++ b/src/exchangedb/plugin_exchangedb_postgres.c @@ -2176,6 +2176,26 @@ postgres_mark_deposit_tiny (void *cls, } +/** + * Test if a deposit was marked as done, thereby declaring that it cannot be + * refunded anymore. + * + * @param cls the @e cls of this struct with the plugin-specific state + * @param session connection to the database + * @param deposit the deposit to check + * @return #GNUNET_YES if is is marked done done, #GNUNET_NO if not, + * #GNUNET_SYSERR on error (deposit unknown) + */ +static int +postgres_test_deposit_done (void *cls, + struct TALER_EXCHANGEDB_Session *session, + const struct TALER_EXCHANGEDB_Deposit *deposit) +{ + GNUNET_break (0); // not implemented + return GNUNET_SYSERR; +} + + /** * Mark a deposit as done, thereby declaring that it cannot be * executed at all anymore, and should no longer be returned by @@ -2466,6 +2486,24 @@ postgres_insert_deposit (void *cls, } +/** + * Insert information about refunded coin into the database. + * + * @param cls the @e cls of this struct with the plugin-specific state + * @param session connection to the database + * @param refund refund information to store + * @return #GNUNET_OK on success, #GNUNET_SYSERR on error + */ +static int +postgres_insert_refund (void *cls, + struct TALER_EXCHANGEDB_Session *session, + const struct TALER_EXCHANGEDB_Refund *refund) +{ + GNUNET_break (0); // not implemented + return GNUNET_SYSERR; +} + + /** * Lookup refresh session data under the given @a session_hash. * @@ -4242,10 +4280,12 @@ libtaler_plugin_exchangedb_postgres_init (void *cls) plugin->free_reserve_history = &common_free_reserve_history; plugin->have_deposit = &postgres_have_deposit; plugin->mark_deposit_tiny = &postgres_mark_deposit_tiny; + plugin->test_deposit_done = &postgres_test_deposit_done; plugin->mark_deposit_done = &postgres_mark_deposit_done; plugin->get_ready_deposit = &postgres_get_ready_deposit; plugin->iterate_matching_deposits = &postgres_iterate_matching_deposits; plugin->insert_deposit = &postgres_insert_deposit; + plugin->insert_refund = &postgres_insert_refund; plugin->get_refresh_session = &postgres_get_refresh_session; plugin->create_refresh_session = &postgres_create_refresh_session; plugin->insert_refresh_melt = &postgres_insert_refresh_melt; diff --git a/src/include/taler_exchangedb_plugin.h b/src/include/taler_exchangedb_plugin.h index 8e00be751..7dcdb698f 100644 --- a/src/include/taler_exchangedb_plugin.h +++ b/src/include/taler_exchangedb_plugin.h @@ -499,7 +499,12 @@ enum TALER_EXCHANGEDB_TransactionType /** * /refresh/melt operation. */ - TALER_EXCHANGEDB_TT_REFRESH_MELT = 1 + TALER_EXCHANGEDB_TT_REFRESH_MELT = 1, + + /** + * /refund operation. + */ + TALER_EXCHANGEDB_TT_REFUND = 2 }; @@ -536,6 +541,11 @@ struct TALER_EXCHANGEDB_TransactionList */ struct TALER_EXCHANGEDB_RefreshMelt *melt; + /** + * Details if transaction was a /refund operation. + */ + struct TALER_EXCHANGEDB_Refund *refund; + } details; }; @@ -953,6 +963,20 @@ struct TALER_EXCHANGEDB_Plugin const struct TALER_EXCHANGEDB_Deposit *deposit); + /** + * Insert information about refunded coin into the database. + * + * @param cls the @e cls of this struct with the plugin-specific state + * @param session connection to the database + * @param refund refund information to store + * @return #GNUNET_OK on success, #GNUNET_SYSERR on error + */ + int + (*insert_refund) (void *cls, + struct TALER_EXCHANGEDB_Session *session, + const struct TALER_EXCHANGEDB_Refund *refund); + + /** * Mark a deposit as tiny, thereby declaring that it cannot be * executed by itself and should no longer be returned by @@ -969,6 +993,22 @@ struct TALER_EXCHANGEDB_Plugin unsigned long long rowid); + /** + * Test if a deposit was marked as done, thereby declaring that it cannot be + * refunded anymore. + * + * @param cls the @e cls of this struct with the plugin-specific state + * @param session connection to the database + * @param deposit the deposit to check + * @return #GNUNET_YES if is is marked done done, #GNUNET_NO if not, + * #GNUNET_SYSERR on error (deposit unknown) + */ + int + (*test_deposit_done) (void *cls, + struct TALER_EXCHANGEDB_Session *session, + const struct TALER_EXCHANGEDB_Deposit *deposit); + + /** * Mark a deposit as done, thereby declaring that it cannot be * executed at all anymore, and should no longer be returned by