From d803d86bf9c69947f6b6e37caf8800abbd24135f Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Mon, 16 May 2022 14:01:04 +0200 Subject: -work purse_deposit conflict handling --- src/exchange/taler-exchange-httpd_purses_merge.c | 2 +- src/exchange/taler-exchange-httpd_responses.c | 2 + src/exchangedb/plugin_exchangedb_postgres.c | 2 +- src/lib/exchange_api_purse_deposit.c | 332 ++++++++++++++++++----- src/lib/exchange_api_purse_merge.c | 2 +- src/testing/test_exchange_p2p.c | 32 +++ 6 files changed, 302 insertions(+), 70 deletions(-) (limited to 'src') diff --git a/src/exchange/taler-exchange-httpd_purses_merge.c b/src/exchange/taler-exchange-httpd_purses_merge.c index 9ca973d16..397492d0e 100644 --- a/src/exchange/taler-exchange-httpd_purses_merge.c +++ b/src/exchange/taler-exchange-httpd_purses_merge.c @@ -275,7 +275,7 @@ merge_transaction (void *cls, &merge_timestamp, &partner_url, &reserve_pub); - if (qs < 0) + if (qs <= 0) { if (GNUNET_DB_STATUS_SOFT_ERROR == qs) return qs; diff --git a/src/exchange/taler-exchange-httpd_responses.c b/src/exchange/taler-exchange-httpd_responses.c index 725e08d96..e2b33c79a 100644 --- a/src/exchange/taler-exchange-httpd_responses.c +++ b/src/exchange/taler-exchange-httpd_responses.c @@ -566,6 +566,8 @@ TEH_RESPONSE_reply_coin_insufficient_funds ( connection, TALER_ErrorCode_get_http_status_safe (ec), TALER_JSON_pack_ec (ec), + GNUNET_JSON_pack_data_auto ("coin_pub", + coin_pub), GNUNET_JSON_pack_array_steal ("history", history)); } diff --git a/src/exchangedb/plugin_exchangedb_postgres.c b/src/exchangedb/plugin_exchangedb_postgres.c index e6b86813f..4d5efb9c8 100644 --- a/src/exchangedb/plugin_exchangedb_postgres.c +++ b/src/exchangedb/plugin_exchangedb_postgres.c @@ -3620,7 +3620,7 @@ prepare_statements (struct PostgresClosure *pg) ",merge_timestamp" ",partner_base_url" " FROM purse_merges" - " JOIN partners USING (partner_serial_id)" + " LEFT JOIN partners USING (partner_serial_id)" " WHERE purse_pub=$1;", 1), /* Used in #postgres_do_account_merge() */ diff --git a/src/lib/exchange_api_purse_deposit.c b/src/lib/exchange_api_purse_deposit.c index f71bb3895..67f5355d9 100644 --- a/src/lib/exchange_api_purse_deposit.c +++ b/src/lib/exchange_api_purse_deposit.c @@ -33,6 +33,28 @@ #include "exchange_api_curl_defaults.h" +/** + * Information we track per coin. + */ +struct Coin +{ + /** + * Coin's public key. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; + + /** + * Coin's denomination. + */ + struct TALER_DenominationHashP h_denom_pub; + + /** + * How much did we say the coin contributed. + */ + struct TALER_Amount contribution; +}; + + /** * @brief A purse create with deposit handle */ @@ -83,7 +105,7 @@ struct TALER_EXCHANGE_PurseDepositHandle /** * Array of @e num_deposits coins we are depositing. */ - struct TALER_CoinSpendPublicKeyP *coins; + struct Coin *coins; /** * Number of coins we are depositing. @@ -122,7 +144,6 @@ handle_purse_deposit_finished (void *cls, break; case MHD_HTTP_OK: { - const struct TALER_EXCHANGE_Keys *key_state; struct GNUNET_TIME_Timestamp etime; struct TALER_ExchangeSignatureP exchange_sig; struct TALER_ExchangePublicKeyP exchange_pub; @@ -158,9 +179,8 @@ handle_purse_deposit_finished (void *cls, dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; break; } - key_state = TALER_EXCHANGE_get_keys (pch->exchange); if (GNUNET_OK != - TALER_EXCHANGE_test_signing_key (key_state, + TALER_EXCHANGE_test_signing_key (keys, &exchange_pub)) { GNUNET_break_op (0); @@ -191,85 +211,260 @@ handle_purse_deposit_finished (void *cls, /* This should never happen, either us or the exchange is buggy (or API version conflict); just pass JSON reply to the application */ dr.hr.ec = TALER_JSON_get_error_code (j); - dr.hr.hint = TALER_JSON_get_error_hint (j); break; case MHD_HTTP_FORBIDDEN: dr.hr.ec = TALER_JSON_get_error_code (j); - dr.hr.hint = TALER_JSON_get_error_hint (j); /* Nothing really to verify, exchange says one of the signatures is invalid; as we checked them, this should never happen, we should pass the JSON reply to the application */ break; case MHD_HTTP_NOT_FOUND: dr.hr.ec = TALER_JSON_get_error_code (j); - dr.hr.hint = TALER_JSON_get_error_hint (j); /* Nothing really to verify, this should never happen, we should pass the JSON reply to the application */ break; case MHD_HTTP_CONFLICT: + dr.hr.ec = TALER_JSON_get_error_code (j); + switch (dr.hr.ec) { - const char *partner_url = NULL; - struct TALER_CoinSpendPublicKeyP coin_pub; - struct TALER_CoinSpendSignatureP coin_sig; - struct TALER_Amount amount; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("coin_sig", - &coin_sig), - GNUNET_JSON_spec_fixed_auto ("coin_pub", - &coin_pub), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("partner_url", - &partner_url), - NULL), - TALER_JSON_spec_amount ("amount", - keys->currency, - &amount), - GNUNET_JSON_spec_end () - }; - bool found = false; - - if (GNUNET_OK != - GNUNET_JSON_parse (j, - spec, - NULL, NULL)) + case TALER_EC_EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA: { - GNUNET_break_op (0); - dr.hr.http_status = 0; - dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + const char *partner_url = NULL; + struct TALER_CoinSpendPublicKeyP coin_pub; + struct TALER_CoinSpendSignatureP coin_sig; + struct TALER_Amount amount; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("coin_sig", + &coin_sig), + GNUNET_JSON_spec_fixed_auto ("coin_pub", + &coin_pub), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("partner_url", + &partner_url), + NULL), + TALER_JSON_spec_amount ("amount", + keys->currency, + &amount), + GNUNET_JSON_spec_end () + }; + bool found = false; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + for (unsigned int i = 0; inum_deposits; i++) + if (0 == GNUNET_memcmp (&coin_pub, + &pch->coins[i].coin_pub)) + { + found = true; + break; + } + if (! found) + { + /* proof is about a coin we did not even deposit */ + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + if (NULL == partner_url) + partner_url = pch->base_url; + if (GNUNET_OK != + TALER_wallet_purse_deposit_verify ( + partner_url, + &pch->purse_pub, + &amount, + &coin_pub, + &coin_sig)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + /* meta data conflict is real! */ break; } - for (unsigned int i = 0; inum_deposits; i++) - if (0 == GNUNET_memcmp (&coin_pub, - &pch->coins[i])) + case TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS: + { + json_t *history; + struct TALER_Amount total; + struct TALER_DenominationHashP h_denom_pub; + const struct TALER_EXCHANGE_DenomPublicKey *dki; + struct TALER_CoinSpendPublicKeyP coin_pub; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("coin_pub", + &coin_pub), + GNUNET_JSON_spec_json ("history", + &history), + GNUNET_JSON_spec_end () + }; + bool found = false; + const struct Coin *my_coin; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, NULL)) { - found = true; + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; break; } - if (! found) - { - /* proof is about a coin we did not even deposit */ - GNUNET_break_op (0); - dr.hr.http_status = 0; - dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - break; + for (unsigned int i = 0; inum_deposits; i++) + { + if (0 == GNUNET_memcmp (&coin_pub, + &pch->coins[i].coin_pub)) + { + found = true; + my_coin = &pch->coins[i]; + break; + } + } + if (! found) + { + /* proof is about a coin we did not even deposit */ + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + dki = TALER_EXCHANGE_get_denomination_key_by_hash ( + keys, + &my_coin->h_denom_pub); + if (NULL == dki) + { + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN; + GNUNET_break_op (0); + break; + } + if (GNUNET_OK != + TALER_EXCHANGE_verify_coin_history (dki, + dki->value.currency, + &coin_pub, + history, + &h_denom_pub, + &total)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + json_decref (history); + break; + } + json_decref (history); + if (0 > + TALER_amount_add (&total, + &total, + &my_coin->contribution)) + { + /* clearly not OK if our transaction would have caused + the overflow... */ + break; + } + if (0 >= TALER_amount_cmp (&total, + &dki->value)) + { + /* transaction should have still fit */ + GNUNET_break (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + /* everything OK, proof of double-spending was provided */ } - if (NULL == partner_url) - partner_url = pch->base_url; - if (GNUNET_OK != - TALER_wallet_purse_deposit_verify ( - partner_url, - &pch->purse_pub, - &amount, - &coin_pub, - &coin_sig)) + case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY: { - GNUNET_break_op (0); - dr.hr.http_status = 0; - dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - break; + json_t *history; + struct TALER_Amount total; + struct TALER_DenominationHashP h_denom_pub; + const struct Coin *my_coin; + const struct TALER_EXCHANGE_DenomPublicKey *dki; + struct TALER_CoinSpendPublicKeyP coin_pub; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("coin_pub", + &coin_pub), + GNUNET_JSON_spec_json ("history", + &history), + GNUNET_JSON_spec_end () + }; + bool found = false; + + if (GNUNET_OK != + GNUNET_JSON_parse (j, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + for (unsigned int i = 0; inum_deposits; i++) + { + if (0 == GNUNET_memcmp (&coin_pub, + &pch->coins[i].coin_pub)) + { + found = true; + my_coin = &pch->coins[i]; + break; + } + } + if (! found) + { + /* proof is about a coin we did not even deposit */ + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + dki = TALER_EXCHANGE_get_denomination_key_by_hash ( + keys, + &my_coin->h_denom_pub); + memset (&h_denom_pub, + 0, + sizeof (h_denom_pub)); + if (GNUNET_OK != + TALER_EXCHANGE_verify_coin_history (dki, + dki->value.currency, + &coin_pub, + history, + &h_denom_pub, + &total)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + json_decref (history); + break; + } + json_decref (history); + if (0 == GNUNET_memcmp (&dki->h_key, + &h_denom_pub)) + { + /* sorry, this proves nothing */ + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } + /* everything OK, proof of conflicting denomination was provided */ } - /* conflict is real! */ - } + default: + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + break; + } /* ec switch */ break; case MHD_HTTP_GONE: /* could happen if denomination was revoked */ @@ -277,18 +472,15 @@ handle_purse_deposit_finished (void *cls, signature here, alas tricky in case our /keys is outdated => left to clients */ dr.hr.ec = TALER_JSON_get_error_code (j); - dr.hr.hint = TALER_JSON_get_error_hint (j); break; case MHD_HTTP_INTERNAL_SERVER_ERROR: dr.hr.ec = TALER_JSON_get_error_code (j); - dr.hr.hint = TALER_JSON_get_error_hint (j); /* Server had an internal issue; we should retry, but this API leaves this to the application */ break; default: /* unexpected response code */ dr.hr.ec = TALER_JSON_get_error_code (j); - dr.hr.hint = TALER_JSON_get_error_hint (j); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u/%d for exchange deposit\n", (unsigned int) response_code, @@ -296,6 +488,10 @@ handle_purse_deposit_finished (void *cls, GNUNET_break_op (0); break; } + if (TALER_EC_NONE == dr.hr.ec) + dr.hr.hint = NULL; + else + dr.hr.hint = TALER_ErrorCode_get_hint (dr.hr.ec); pch->cb (pch->cb_cls, &dr); TALER_EXCHANGE_purse_deposit_cancel (pch); @@ -361,11 +557,11 @@ TALER_EXCHANGE_purse_deposit ( "/"); pch->num_deposits = num_deposits; pch->coins = GNUNET_new_array (num_deposits, - struct TALER_CoinSpendPublicKeyP); + struct Coin); for (unsigned int i = 0; icoins[i]; + struct Coin *coin = &pch->coins[i]; json_t *jdeposit; struct TALER_CoinSpendSignatureP coin_sig; #if FIXME_OEC @@ -390,7 +586,9 @@ TALER_EXCHANGE_purse_deposit ( } #endif GNUNET_CRYPTO_eddsa_key_get_public (&deposit->coin_priv.eddsa_priv, - &coin_pub->eddsa_pub); + &coin->coin_pub.eddsa_pub); + coin->h_denom_pub = deposit->h_denom_pub; + coin->contribution = deposit->amount; TALER_wallet_purse_deposit_sign ( pch->base_url, &pch->purse_pub, @@ -413,7 +611,7 @@ TALER_EXCHANGE_purse_deposit ( TALER_JSON_pack_denom_sig ("ub_sig", &deposit->denom_sig), GNUNET_JSON_pack_data_auto ("coin_pub", - coin_pub), + &coin->coin_pub), GNUNET_JSON_pack_data_auto ("coin_sig", &coin_sig)); GNUNET_assert (0 == diff --git a/src/lib/exchange_api_purse_merge.c b/src/lib/exchange_api_purse_merge.c index 2becd946f..a32b44d48 100644 --- a/src/lib/exchange_api_purse_merge.c +++ b/src/lib/exchange_api_purse_merge.c @@ -118,7 +118,7 @@ make_payto (const char *exchange_url, end = GNUNET_STRINGS_data_to_string ( reserve_pub, - sizeof (reserve_pub), + sizeof (*reserve_pub), pub_str, sizeof (pub_str)); *end = '\0'; diff --git a/src/testing/test_exchange_p2p.c b/src/testing/test_exchange_p2p.c index e4d6f5b2f..b3e98daa4 100644 --- a/src/testing/test_exchange_p2p.c +++ b/src/testing/test_exchange_p2p.c @@ -112,6 +112,8 @@ run (void *cls, */ CMD_TRANSFER_TO_EXCHANGE ("create-reserve-1", "EUR:5.01"), + CMD_TRANSFER_TO_EXCHANGE ("create-reserve-2", + "EUR:5.01"), TALER_TESTING_cmd_reserve_poll ("poll-reserve-1", "create-reserve-1", "EUR:5.01", @@ -122,6 +124,11 @@ run (void *cls, bc.user42_payto, bc.exchange_payto, "create-reserve-1"), + TALER_TESTING_cmd_check_bank_admin_transfer ("check-create-reserve-2", + "EUR:5.01", + bc.user42_payto, + bc.exchange_payto, + "create-reserve-2"), /** * Make a reserve exist, according to the previous * transfer. @@ -193,6 +200,13 @@ run (void *cls, "EUR:1", MHD_HTTP_OK), #endif + /* Test conflicting merge */ + TALER_TESTING_cmd_purse_merge ( + "purse-merge-into-reserve", + MHD_HTTP_CONFLICT, + "push-get-contract", + "create-reserve-2"), + TALER_TESTING_cmd_end () }; struct TALER_TESTING_Command pull[] = { @@ -241,6 +255,24 @@ run (void *cls, "create-reserve-1", "EUR:2", MHD_HTTP_OK), +#endif + /* create 2nd purse for a deposit conflict */ + TALER_TESTING_cmd_purse_create_with_reserve ( + "purse-create-with-reserve-2", + MHD_HTTP_OK, + "{\"amount\":\"EUR:4\",\"summary\":\"beer\"}", + true /* upload contract */, + GNUNET_TIME_UNIT_MINUTES, /* expiration */ + "create-reserve-1"), +#if FIXME_RESERVE_HISTORY + TALER_TESTING_cmd_purse_deposit_coins ( + "purse-deposit-coins-conflict", + MHD_HTTP_CONFLICT, + 0 /* min age */, + "purse-create-with-reserve-2", + "withdraw-coin-1", + "EUR:4.01", + NULL), #endif TALER_TESTING_cmd_end () }; -- cgit v1.2.3