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/taler-exchange-httpd_melt.c | 254 +++++++++++++------------------ 1 file changed, 105 insertions(+), 149 deletions(-) (limited to 'src/exchange/taler-exchange-httpd_melt.c') 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; + } } -- cgit v1.2.3