diff --git a/src/include/taler_mintdb_plugin.h b/src/include/taler_mintdb_plugin.h index 851ed5265..c8013acc3 100644 --- a/src/include/taler_mintdb_plugin.h +++ b/src/include/taler_mintdb_plugin.h @@ -629,7 +629,8 @@ struct TALER_MINTDB_Plugin * * @param cls the @e cls of this struct with the plugin-specific state * @param session connection to use - * @return #GNUNET_OK on success + * @return #GNUNET_OK on success, #GNUNET_NO if the transaction + * can be retried, #GNUNET_SYSERR on hard failures */ int (*commit) (void *cls, diff --git a/src/mint/taler-mint-httpd_db.c b/src/mint/taler-mint-httpd_db.c index 021a3155d..da656abc2 100644 --- a/src/mint/taler-mint-httpd_db.c +++ b/src/mint/taler-mint-httpd_db.c @@ -24,6 +24,67 @@ #include "taler-mint-httpd_responses.h" #include "taler-mint-httpd_keystate.h" +/** + * How often should we retry a transaction before giving up + * (for transactions resulting in serialization/dead locks only). + */ +#define MAX_TRANSACTION_COMMIT_RETRIES 3 + +/** + * Code to begin a transaction, must be inline as we define a block + * that ends with #COMMIT_TRANSACTION() within which we perform a number + * of retries. Note that this code may call "return" internally, so + * it must be called within a function where any cleanup will be done + * by the caller. Furthermore, the function's return value must + * match that of a #TMH_RESPONSE_reply_internal_db_error() status code. + * + * @param session session handle + * @param connection connection handle + */ +#define START_TRANSACTION(session,connection) \ +{ /* start new scope, will be ended by COMMIT_TRANSACTION() */\ + unsigned int transaction_retries = 0; \ + int transaction_commit_result; \ +transaction_start_label: /* we will use goto for retries */ \ + if (GNUNET_OK != \ + TMH_plugin->start (TMH_plugin->cls, \ + session)) \ + { \ + GNUNET_break (0); \ + return TMH_RESPONSE_reply_internal_db_error (connection); \ + } + +/** + * Code to conclude a transaction, dual to #START_TRANSACTION(). Note + * that this code may call "return" internally, so it must be called + * within a function where any cleanup will be done by the caller. + * Furthermore, the function's return value must match that of a + * #TMH_RESPONSE_reply_internal_db_error() status code. + * + * @param session session handle + * @param connection connection handle + */ +#define COMMIT_TRANSACTION(session,connection) \ + transaction_commit_result = \ + TMH_plugin->commit (TMH_plugin->cls, \ + session); \ + if (GNUNET_SYSERR == transaction_commit_result) \ + { \ + TALER_LOG_WARNING ("Transaction commit failed in %s\n", __FUNCTION__); \ + return TMH_RESPONSE_reply_commit_error (connection); \ + } \ + if (GNUNET_NO == transaction_commit_result) \ + { \ + TALER_LOG_WARNING ("Transaction commit failed in %s\n", __FUNCTION__); \ + if (transaction_retries++ <= MAX_TRANSACTION_COMMIT_RETRIES) \ + goto transaction_start_label; \ + TALER_LOG_WARNING ("Transaction commit failed %u times in %s\n", \ + transaction_retries, \ + __FUNCTION__); \ + return TMH_RESPONSE_reply_commit_error (connection); \ + } \ +} /* end of scope opened by BEGIN_TRANSACTION */ + /** * Calculate the total value of all transactions performed. @@ -130,13 +191,8 @@ TMH_DB_execute_deposit (struct MHD_Connection *connection, &dki->issue.properties.value); TMH_KS_release (mks); - if (GNUNET_OK != - TMH_plugin->start (TMH_plugin->cls, - session)) - { - GNUNET_break (0); - return TMH_RESPONSE_reply_internal_db_error (connection); - } + START_TRANSACTION (session, connection); + /* fee for THIS transaction */ spent = deposit->amount_with_fee; /* add cost of all previous transactions */ @@ -179,13 +235,7 @@ TMH_DB_execute_deposit (struct MHD_Connection *connection, return TMH_RESPONSE_reply_internal_db_error (connection); } - if (GNUNET_OK != - TMH_plugin->commit (TMH_plugin->cls, - session)) - { - TALER_LOG_WARNING ("/deposit transaction commit failed\n"); - return TMH_RESPONSE_reply_commit_error (connection); - } + COMMIT_TRANSACTION(session, connection); GNUNET_assert (GNUNET_OK == TALER_amount_subtract (&amount_without_fee, &deposit->amount_with_fee, @@ -241,6 +291,187 @@ TMH_DB_execute_reserve_status (struct MHD_Connection *connection, } +/** + * Try to execute /reserve/withdraw transaction. + * + * @param connection request we are handling + * @param session database session we are using + * @param reserve reserve to withdraw from + * @param denomination_pub public key of the denomination requested + * @param dki denomination to withdraw + * @param blinded_msg blinded message to be signed + * @param blinded_msg_len number of bytes in @a blinded_msg + * @param signature signature over the withdraw request, to be stored in DB + * @param denom_sig[out] where to write the resulting signature + * (used to release memory in case of transaction failure + * @return MHD result code + */ +static int +execute_reserve_withdraw_transaction (struct MHD_Connection *connection, + struct TALER_MINTDB_Session *session, + struct TMH_KS_StateHandle *key_state, + const struct TALER_ReservePublicKeyP *reserve, + const struct TALER_DenominationPublicKey *denomination_pub, + const struct TALER_MINTDB_DenominationKeyIssueInformation *dki, + const char *blinded_msg, + size_t blinded_msg_len, + const struct TALER_ReserveSignatureP *signature, + struct TALER_DenominationSignature *denom_sig) +{ + struct TALER_MINTDB_ReserveHistory *rh; + const struct TALER_MINTDB_ReserveHistory *pos; + struct TALER_MINTDB_DenominationKeyIssueInformation *tdki; + struct TALER_MINTDB_CollectableBlindcoin collectable; + struct TALER_Amount amount_required; + struct TALER_Amount deposit_total; + struct TALER_Amount withdraw_total; + struct TALER_Amount balance; + struct TALER_Amount value; + struct TALER_Amount fee_withdraw; + struct GNUNET_HashCode h_blind; + int res; + + /* Check if balance is sufficient */ + START_TRANSACTION (session, connection); + rh = TMH_plugin->get_reserve_history (TMH_plugin->cls, + session, + reserve); + if (NULL == rh) + { + TMH_plugin->rollback (TMH_plugin->cls, + session); + return TMH_RESPONSE_reply_arg_unknown (connection, + "reserve_pub"); + } + + /* calculate amount required including fees */ + TALER_amount_ntoh (&value, + &dki->issue.properties.value); + TALER_amount_ntoh (&fee_withdraw, + &dki->issue.properties.fee_withdraw); + + if (GNUNET_OK != + TALER_amount_add (&amount_required, + &value, + &fee_withdraw)) + { + TMH_plugin->rollback (TMH_plugin->cls, + session); + return TMH_RESPONSE_reply_internal_db_error (connection); + } + + /* calculate balance of the reserve */ + res = 0; + for (pos = rh; NULL != pos; pos = pos->next) + { + switch (pos->type) + { + case TALER_MINTDB_RO_BANK_TO_MINT: + if (0 == (res & 1)) + deposit_total = pos->details.bank->amount; + else + if (GNUNET_OK != + TALER_amount_add (&deposit_total, + &deposit_total, + &pos->details.bank->amount)) + { + TMH_plugin->rollback (TMH_plugin->cls, + session); + return TMH_RESPONSE_reply_internal_db_error (connection); + } + res |= 1; + break; + case TALER_MINTDB_RO_WITHDRAW_COIN: + tdki = TMH_KS_denomination_key_lookup (key_state, + &pos->details.withdraw->denom_pub, + TMH_KS_DKU_WITHDRAW); + TALER_amount_ntoh (&value, + &tdki->issue.properties.value); + if (0 == (res & 2)) + withdraw_total = value; + else + if (GNUNET_OK != + TALER_amount_add (&withdraw_total, + &withdraw_total, + &value)) + { + TMH_plugin->rollback (TMH_plugin->cls, + session); + return TMH_RESPONSE_reply_internal_db_error (connection); + } + res |= 2; + break; + } + } + if (0 == (res & 1)) + { + /* did not encounter any deposit operations, how can we have a reserve? */ + GNUNET_break (0); + return TMH_RESPONSE_reply_internal_db_error (connection); + } + if (0 == (res & 2)) + { + /* did not encounter any withdraw operations, set to zero */ + TALER_amount_get_zero (deposit_total.currency, + &withdraw_total); + } + /* All reserve balances should be non-negative */ + GNUNET_assert (GNUNET_SYSERR != + TALER_amount_subtract (&balance, + &deposit_total, + &withdraw_total)); + if (0 < TALER_amount_cmp (&amount_required, + &balance)) + { + TMH_plugin->rollback (TMH_plugin->cls, + session); + res = TMH_RESPONSE_reply_reserve_withdraw_insufficient_funds (connection, + rh); + TMH_plugin->free_reserve_history (TMH_plugin->cls, + rh); + return res; + } + TMH_plugin->free_reserve_history (TMH_plugin->cls, + rh); + + /* Balance is good, sign the coin! */ + denom_sig->rsa_signature + = GNUNET_CRYPTO_rsa_sign (dki->denom_priv.rsa_private_key, + blinded_msg, + blinded_msg_len); + if (NULL == denom_sig->rsa_signature) + { + GNUNET_break (0); + TMH_plugin->rollback (TMH_plugin->cls, + session); + return TMH_RESPONSE_reply_internal_error (connection, + "Internal error"); + } + collectable.sig = *denom_sig; + collectable.denom_pub = *denomination_pub; + collectable.amount_with_fee = amount_required; + collectable.withdraw_fee = fee_withdraw; + collectable.reserve_pub = *reserve; + collectable.h_coin_envelope = h_blind; + collectable.reserve_sig = *signature; + if (GNUNET_OK != + TMH_plugin->insert_withdraw_info (TMH_plugin->cls, + session, + &collectable)) + { + GNUNET_break (0); + TMH_plugin->rollback (TMH_plugin->cls, + session); + return TMH_RESPONSE_reply_internal_db_error (connection); + } + COMMIT_TRANSACTION (session, connection); + + return TMH_RESPONSE_reply_reserve_withdraw_success (connection, + &collectable); +} + + + /** * Execute a "/reserve/withdraw". Given a reserve and a properly signed * request to withdraw a coin, check the balance of the reserve and @@ -264,19 +495,10 @@ TMH_DB_execute_reserve_withdraw (struct MHD_Connection *connection, const struct TALER_ReserveSignatureP *signature) { struct TALER_MINTDB_Session *session; - struct TALER_MINTDB_ReserveHistory *rh; - const struct TALER_MINTDB_ReserveHistory *pos; struct TMH_KS_StateHandle *key_state; - struct TALER_MINTDB_CollectableBlindcoin collectable; struct TALER_MINTDB_DenominationKeyIssueInformation *dki; - struct TALER_MINTDB_DenominationKeyIssueInformation *tdki; - struct GNUNET_CRYPTO_rsa_Signature *sig; - struct TALER_Amount amount_required; - struct TALER_Amount deposit_total; - struct TALER_Amount withdraw_total; - struct TALER_Amount balance; - struct TALER_Amount value; - struct TALER_Amount fee_withdraw; + struct TALER_MINTDB_CollectableBlindcoin collectable; + struct TALER_DenominationSignature denom_sig; struct GNUNET_HashCode h_blind; int res; @@ -304,14 +526,13 @@ TMH_DB_execute_reserve_withdraw (struct MHD_Connection *connection, if (GNUNET_YES == res) { res = TMH_RESPONSE_reply_reserve_withdraw_success (connection, - &collectable); + &collectable); GNUNET_CRYPTO_rsa_signature_free (collectable.sig.rsa_signature); GNUNET_CRYPTO_rsa_public_key_free (collectable.denom_pub.rsa_public_key); return res; } GNUNET_assert (GNUNET_NO == res); - /* Check if balance is sufficient */ key_state = TMH_KS_acquire (); dki = TMH_KS_denomination_key_lookup (key_state, denomination_pub, @@ -325,162 +546,19 @@ TMH_DB_execute_reserve_withdraw (struct MHD_Connection *connection, "error", "Denomination not found"); } - if (GNUNET_OK != - TMH_plugin->start (TMH_plugin->cls, - session)) - { - GNUNET_break (0); - TMH_KS_release (key_state); - return TMH_RESPONSE_reply_internal_db_error (connection); - } - - rh = TMH_plugin->get_reserve_history (TMH_plugin->cls, - session, - reserve); - if (NULL == rh) - { - TMH_plugin->rollback (TMH_plugin->cls, - session); - TMH_KS_release (key_state); - return TMH_RESPONSE_reply_arg_unknown (connection, - "reserve_pub"); - } - - /* calculate amount required including fees */ - TALER_amount_ntoh (&value, - &dki->issue.properties.value); - TALER_amount_ntoh (&fee_withdraw, - &dki->issue.properties.fee_withdraw); - - if (GNUNET_OK != - TALER_amount_add (&amount_required, - &value, - &fee_withdraw)) - { - TMH_plugin->rollback (TMH_plugin->cls, - session); - TMH_KS_release (key_state); - return TMH_RESPONSE_reply_internal_db_error (connection); - } - - /* calculate balance of the reserve */ - res = 0; - for (pos = rh; NULL != pos; pos = pos->next) - { - switch (pos->type) - { - case TALER_MINTDB_RO_BANK_TO_MINT: - if (0 == (res & 1)) - deposit_total = pos->details.bank->amount; - else - if (GNUNET_OK != - TALER_amount_add (&deposit_total, - &deposit_total, - &pos->details.bank->amount)) - { - TMH_plugin->rollback (TMH_plugin->cls, - session); - TMH_KS_release (key_state); - return TMH_RESPONSE_reply_internal_db_error (connection); - } - res |= 1; - break; - case TALER_MINTDB_RO_WITHDRAW_COIN: - tdki = TMH_KS_denomination_key_lookup (key_state, - &pos->details.withdraw->denom_pub, - TMH_KS_DKU_WITHDRAW); - TALER_amount_ntoh (&value, - &tdki->issue.properties.value); - if (0 == (res & 2)) - withdraw_total = value; - else - if (GNUNET_OK != - TALER_amount_add (&withdraw_total, - &withdraw_total, - &value)) - { - TMH_plugin->rollback (TMH_plugin->cls, - session); - TMH_KS_release (key_state); - return TMH_RESPONSE_reply_internal_db_error (connection); - } - res |= 2; - break; - } - } - if (0 == (res & 1)) - { - /* did not encounter any deposit operations, how can we have a reserve? */ - GNUNET_break (0); - return TMH_RESPONSE_reply_internal_db_error (connection); - } - if (0 == (res & 2)) - { - /* did not encounter any withdraw operations, set to zero */ - TALER_amount_get_zero (deposit_total.currency, - &withdraw_total); - } - /* All reserve balances should be non-negative */ - GNUNET_assert (GNUNET_SYSERR != - TALER_amount_subtract (&balance, - &deposit_total, - &withdraw_total)); - if (0 < TALER_amount_cmp (&amount_required, - &balance)) - { - TMH_KS_release (key_state); - TMH_plugin->rollback (TMH_plugin->cls, - session); - res = TMH_RESPONSE_reply_reserve_withdraw_insufficient_funds (connection, - rh); - TMH_plugin->free_reserve_history (TMH_plugin->cls, - rh); - return res; - } - TMH_plugin->free_reserve_history (TMH_plugin->cls, - rh); - - /* Balance is good, sign the coin! */ - sig = GNUNET_CRYPTO_rsa_sign (dki->denom_priv.rsa_private_key, - blinded_msg, - blinded_msg_len); + denom_sig.rsa_signature = NULL; + res = execute_reserve_withdraw_transaction (connection, + session, + key_state, + reserve, + denomination_pub, + dki, + blinded_msg, + blinded_msg_len, + signature, + &denom_sig); + GNUNET_CRYPTO_rsa_signature_free (denom_sig.rsa_signature); TMH_KS_release (key_state); - if (NULL == sig) - { - GNUNET_break (0); - TMH_plugin->rollback (TMH_plugin->cls, - session); - return TMH_RESPONSE_reply_internal_error (connection, - "Internal error"); - } - collectable.sig.rsa_signature = sig; - collectable.denom_pub = *denomination_pub; - collectable.amount_with_fee = amount_required; - collectable.withdraw_fee = fee_withdraw; - collectable.reserve_pub = *reserve; - collectable.h_coin_envelope = h_blind; - collectable.reserve_sig = *signature; - if (GNUNET_OK != - TMH_plugin->insert_withdraw_info (TMH_plugin->cls, - session, - &collectable)) - { - GNUNET_break (0); - GNUNET_CRYPTO_rsa_signature_free (sig); - TMH_plugin->rollback (TMH_plugin->cls, - session); - return TMH_RESPONSE_reply_internal_db_error (connection); - } - if (GNUNET_OK != - TMH_plugin->commit (TMH_plugin->cls, - session)) - { - TALER_LOG_WARNING ("/reserve/withdraw transaction commit failed\n"); - return TMH_RESPONSE_reply_commit_error (connection); - } - res = TMH_RESPONSE_reply_reserve_withdraw_success (connection, - &collectable); - GNUNET_CRYPTO_rsa_signature_free (sig); return res; } @@ -633,13 +711,7 @@ TMH_DB_execute_refresh_melt (struct MHD_Connection *connection, GNUNET_break (0); return TMH_RESPONSE_reply_internal_db_error (connection); } - if (GNUNET_OK != - TMH_plugin->start (TMH_plugin->cls, - session)) - { - GNUNET_break (0); - return TMH_RESPONSE_reply_internal_db_error (connection); - } + START_TRANSACTION (session, connection); res = TMH_plugin->get_refresh_session (TMH_plugin->cls, session, session_hash, @@ -741,13 +813,7 @@ TMH_DB_execute_refresh_melt (struct MHD_Connection *connection, } } - if (GNUNET_OK != - TMH_plugin->commit (TMH_plugin->cls, - session)) - { - TALER_LOG_WARNING ("/refresh/melt transaction commit failed\n"); - return TMH_RESPONSE_reply_commit_error (connection); - } + COMMIT_TRANSACTION (session, connection); return TMH_RESPONSE_reply_refresh_melt_success (connection, session_hash, refresh_session.noreveal_index); @@ -1038,10 +1104,10 @@ refresh_mint_coin (struct MHD_Connection *connection, } if (GNUNET_OK != TMH_plugin->insert_refresh_out (TMH_plugin->cls, - session, - session_hash, - coin_off, - &ev_sig)) + session, + session_hash, + coin_off, + &ev_sig)) { GNUNET_break (0); GNUNET_CRYPTO_rsa_signature_free (ev_sig.rsa_signature); @@ -1051,6 +1117,74 @@ refresh_mint_coin (struct MHD_Connection *connection, } +/** + * The client request was well-formed, now execute the DB transaction + * of a "/refresh/reveal" operation. We use the @a ev_sigs and + * @a commit_coins to clean up resources after this function returns + * as we might experience retries of the database transaction. + * + * @param connection the MHD connection to handle + * @param session database session + * @param session_hash hash identifying the refresh session + * @param refresh_session information about the refresh operation we are doing + * @param denom_pubs array of "num_newcoins" denomination keys for the new coins + * @param ev_sigs[out] where to store generated signatures for the new coins, + * array of length "num_newcoins", memory released by the + * caller + * @param commit_coins[out] array of length "num_newcoins" to be used for + * information about the new coins from the commitment. + * @return MHD result code + */ +static int +execute_refresh_reveal_transaction (struct MHD_Connection *connection, + struct TALER_MINTDB_Session *session, + const struct GNUNET_HashCode *session_hash, + struct TALER_MINTDB_RefreshSession *refresh_session, + struct TALER_MINTDB_RefreshMelt *melts, + struct TALER_DenominationPublicKey *denom_pubs, + struct TALER_DenominationSignature *ev_sigs, + struct TALER_MINTDB_RefreshCommitCoin *commit_coins) +{ + unsigned int j; + struct TMH_KS_StateHandle *key_state; + + START_TRANSACTION (session, connection); + if (GNUNET_OK != + TMH_plugin->get_refresh_commit_coins (TMH_plugin->cls, + session, + session_hash, + refresh_session->noreveal_index, + refresh_session->num_newcoins, + commit_coins)) + { + GNUNET_break (0); + return TMH_RESPONSE_reply_internal_db_error (connection); + } + key_state = TMH_KS_acquire (); + for (j=0;jnum_newcoins;j++) + { + if (NULL == ev_sigs[j].rsa_signature) /* could be non-NULL during retries */ + ev_sigs[j] = refresh_mint_coin (connection, + session, + session_hash, + key_state, + &denom_pubs[j], + &commit_coins[j], + j); + if (NULL == ev_sigs[j].rsa_signature) + { + TMH_KS_release (key_state); + return TMH_RESPONSE_reply_internal_db_error (connection); + } + } + TMH_KS_release (key_state); + COMMIT_TRANSACTION (session, connection); + return TMH_RESPONSE_reply_refresh_reveal_success (connection, + refresh_session->num_newcoins, + ev_sigs); +} + + /** * Execute a "/refresh/reveal". The client is revealing to us the * transfer keys for @a #TALER_CNC_KAPPA-1 sets of coins. Verify that the @@ -1074,7 +1208,6 @@ TMH_DB_execute_refresh_reveal (struct MHD_Connection *connection, int res; struct TALER_MINTDB_Session *session; struct TALER_MINTDB_RefreshSession refresh_session; - struct TMH_KS_StateHandle *key_state; struct TALER_MINTDB_RefreshMelt *melts; struct TALER_DenominationPublicKey *denom_pubs; struct TALER_DenominationSignature *ev_sigs; @@ -1164,82 +1297,27 @@ TMH_DB_execute_refresh_reveal (struct MHD_Connection *connection, GNUNET_free (melts); /* Client request OK, start transaction */ - if (GNUNET_OK != - TMH_plugin->start (TMH_plugin->cls, - session)) - { - GNUNET_break (0); - for (j=0;jget_refresh_commit_coins (TMH_plugin->cls, - session, - session_hash, - refresh_session.noreveal_index, - refresh_session.num_newcoins, - commit_coins)) - { - GNUNET_break (0); - GNUNET_free (commit_coins); - for (j=0;jcommit (TMH_plugin->cls, - session)) - { - TALER_LOG_WARNING ("/refresh/reveal transaction commit failed\n"); - for (i=0;i +# +# +# This script uses 'curl' to POST various ill-formed requests to the +# taler-mint-httpd. Basically, the goal is to make sure that the +# HTTP server survives (and produces the 'correct' error code). +# +# We read the JSON snippets from afl-tests/ +# +# Setup keys. +taler-mint-keyup -d test-mint-home -m test-mint-home/master.priv +# Run test... +for n in afl-tests/*.req +do + echo -n "Test $n" + taler-mint-httpd -d test-mint-home/ -t 1 -f $n -C > /dev/null || { echo "FAIL!"; exit 1; } + echo "OK" +done +exit 0 diff --git a/src/mintdb/plugin_mintdb_postgres.c b/src/mintdb/plugin_mintdb_postgres.c index 42aac19f1..621108026 100644 --- a/src/mintdb/plugin_mintdb_postgres.c +++ b/src/mintdb/plugin_mintdb_postgres.c @@ -1095,7 +1095,31 @@ postgres_commit (void *cls, if (PGRES_COMMAND_OK != PQresultStatus (result)) { - GNUNET_break (0); + const char *sqlstate; + + sqlstate = PQresultErrorField (result, + PG_DIAG_SQLSTATE); + if (NULL == sqlstate) + { + /* very unexpected... */ + GNUNET_break (0); + PQclear (result); + return GNUNET_SYSERR; + } + /* 40P01: deadlock, 40001: serialization failure */ + if ( (0 == strcmp (sqlstate, + "40P01")) || + (0 == strcmp (sqlstate, + "40001")) ) + { + /* These two can be retried and have a fair chance of working + the next time */ + PQclear (result); + return GNUNET_NO; + } + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Database commit failure: %s\n", + sqlstate); PQclear (result); return GNUNET_SYSERR; }