-work purse_deposit conflict handling

This commit is contained in:
Christian Grothoff 2022-05-16 14:01:04 +02:00
parent 3db8f0f22d
commit d803d86bf9
No known key found for this signature in database
GPG Key ID: 939E6BE1E29FC3CC
6 changed files with 303 additions and 71 deletions

View File

@ -275,7 +275,7 @@ merge_transaction (void *cls,
&merge_timestamp, &merge_timestamp,
&partner_url, &partner_url,
&reserve_pub); &reserve_pub);
if (qs < 0) if (qs <= 0)
{ {
if (GNUNET_DB_STATUS_SOFT_ERROR == qs) if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
return qs; return qs;

View File

@ -566,6 +566,8 @@ TEH_RESPONSE_reply_coin_insufficient_funds (
connection, connection,
TALER_ErrorCode_get_http_status_safe (ec), TALER_ErrorCode_get_http_status_safe (ec),
TALER_JSON_pack_ec (ec), TALER_JSON_pack_ec (ec),
GNUNET_JSON_pack_data_auto ("coin_pub",
coin_pub),
GNUNET_JSON_pack_array_steal ("history", GNUNET_JSON_pack_array_steal ("history",
history)); history));
} }

View File

@ -3620,7 +3620,7 @@ prepare_statements (struct PostgresClosure *pg)
",merge_timestamp" ",merge_timestamp"
",partner_base_url" ",partner_base_url"
" FROM purse_merges" " FROM purse_merges"
" JOIN partners USING (partner_serial_id)" " LEFT JOIN partners USING (partner_serial_id)"
" WHERE purse_pub=$1;", " WHERE purse_pub=$1;",
1), 1),
/* Used in #postgres_do_account_merge() */ /* Used in #postgres_do_account_merge() */

View File

@ -33,6 +33,28 @@
#include "exchange_api_curl_defaults.h" #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 * @brief A purse create with deposit handle
*/ */
@ -83,7 +105,7 @@ struct TALER_EXCHANGE_PurseDepositHandle
/** /**
* Array of @e num_deposits coins we are depositing. * Array of @e num_deposits coins we are depositing.
*/ */
struct TALER_CoinSpendPublicKeyP *coins; struct Coin *coins;
/** /**
* Number of coins we are depositing. * Number of coins we are depositing.
@ -122,7 +144,6 @@ handle_purse_deposit_finished (void *cls,
break; break;
case MHD_HTTP_OK: case MHD_HTTP_OK:
{ {
const struct TALER_EXCHANGE_Keys *key_state;
struct GNUNET_TIME_Timestamp etime; struct GNUNET_TIME_Timestamp etime;
struct TALER_ExchangeSignatureP exchange_sig; struct TALER_ExchangeSignatureP exchange_sig;
struct TALER_ExchangePublicKeyP exchange_pub; struct TALER_ExchangePublicKeyP exchange_pub;
@ -158,9 +179,8 @@ handle_purse_deposit_finished (void *cls,
dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
break; break;
} }
key_state = TALER_EXCHANGE_get_keys (pch->exchange);
if (GNUNET_OK != if (GNUNET_OK !=
TALER_EXCHANGE_test_signing_key (key_state, TALER_EXCHANGE_test_signing_key (keys,
&exchange_pub)) &exchange_pub))
{ {
GNUNET_break_op (0); GNUNET_break_op (0);
@ -191,22 +211,23 @@ handle_purse_deposit_finished (void *cls,
/* This should never happen, either us or the exchange is buggy /* This should never happen, either us or the exchange is buggy
(or API version conflict); just pass JSON reply to the application */ (or API version conflict); just pass JSON reply to the application */
dr.hr.ec = TALER_JSON_get_error_code (j); dr.hr.ec = TALER_JSON_get_error_code (j);
dr.hr.hint = TALER_JSON_get_error_hint (j);
break; break;
case MHD_HTTP_FORBIDDEN: case MHD_HTTP_FORBIDDEN:
dr.hr.ec = TALER_JSON_get_error_code (j); 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 /* Nothing really to verify, exchange says one of the signatures is
invalid; as we checked them, this should never happen, we invalid; as we checked them, this should never happen, we
should pass the JSON reply to the application */ should pass the JSON reply to the application */
break; break;
case MHD_HTTP_NOT_FOUND: case MHD_HTTP_NOT_FOUND:
dr.hr.ec = TALER_JSON_get_error_code (j); 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 /* Nothing really to verify, this should never
happen, we should pass the JSON reply to the application */ happen, we should pass the JSON reply to the application */
break; break;
case MHD_HTTP_CONFLICT: case MHD_HTTP_CONFLICT:
dr.hr.ec = TALER_JSON_get_error_code (j);
switch (dr.hr.ec)
{
case TALER_EC_EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA:
{ {
const char *partner_url = NULL; const char *partner_url = NULL;
struct TALER_CoinSpendPublicKeyP coin_pub; struct TALER_CoinSpendPublicKeyP coin_pub;
@ -240,7 +261,7 @@ handle_purse_deposit_finished (void *cls,
} }
for (unsigned int i = 0; i<pch->num_deposits; i++) for (unsigned int i = 0; i<pch->num_deposits; i++)
if (0 == GNUNET_memcmp (&coin_pub, if (0 == GNUNET_memcmp (&coin_pub,
&pch->coins[i])) &pch->coins[i].coin_pub))
{ {
found = true; found = true;
break; break;
@ -268,8 +289,182 @@ handle_purse_deposit_finished (void *cls,
dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
break; break;
} }
/* conflict is real! */ /* meta data conflict is real! */
break;
} }
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))
{
GNUNET_break_op (0);
dr.hr.http_status = 0;
dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
break;
}
for (unsigned int i = 0; i<pch->num_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 */
}
case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY:
{
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; i<pch->num_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 */
}
default:
GNUNET_break_op (0);
dr.hr.http_status = 0;
dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
break;
} /* ec switch */
break; break;
case MHD_HTTP_GONE: case MHD_HTTP_GONE:
/* could happen if denomination was revoked */ /* could happen if denomination was revoked */
@ -277,18 +472,15 @@ handle_purse_deposit_finished (void *cls,
signature here, alas tricky in case our /keys signature here, alas tricky in case our /keys
is outdated => left to clients */ is outdated => left to clients */
dr.hr.ec = TALER_JSON_get_error_code (j); dr.hr.ec = TALER_JSON_get_error_code (j);
dr.hr.hint = TALER_JSON_get_error_hint (j);
break; break;
case MHD_HTTP_INTERNAL_SERVER_ERROR: case MHD_HTTP_INTERNAL_SERVER_ERROR:
dr.hr.ec = TALER_JSON_get_error_code (j); 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 /* Server had an internal issue; we should retry, but this API
leaves this to the application */ leaves this to the application */
break; break;
default: default:
/* unexpected response code */ /* unexpected response code */
dr.hr.ec = TALER_JSON_get_error_code (j); dr.hr.ec = TALER_JSON_get_error_code (j);
dr.hr.hint = TALER_JSON_get_error_hint (j);
GNUNET_log (GNUNET_ERROR_TYPE_ERROR, GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Unexpected response code %u/%d for exchange deposit\n", "Unexpected response code %u/%d for exchange deposit\n",
(unsigned int) response_code, (unsigned int) response_code,
@ -296,6 +488,10 @@ handle_purse_deposit_finished (void *cls,
GNUNET_break_op (0); GNUNET_break_op (0);
break; 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, pch->cb (pch->cb_cls,
&dr); &dr);
TALER_EXCHANGE_purse_deposit_cancel (pch); TALER_EXCHANGE_purse_deposit_cancel (pch);
@ -361,11 +557,11 @@ TALER_EXCHANGE_purse_deposit (
"/"); "/");
pch->num_deposits = num_deposits; pch->num_deposits = num_deposits;
pch->coins = GNUNET_new_array (num_deposits, pch->coins = GNUNET_new_array (num_deposits,
struct TALER_CoinSpendPublicKeyP); struct Coin);
for (unsigned int i = 0; i<num_deposits; i++) for (unsigned int i = 0; i<num_deposits; i++)
{ {
const struct TALER_EXCHANGE_PurseDeposit *deposit = &deposits[i]; const struct TALER_EXCHANGE_PurseDeposit *deposit = &deposits[i];
struct TALER_CoinSpendPublicKeyP *coin_pub = &pch->coins[i]; struct Coin *coin = &pch->coins[i];
json_t *jdeposit; json_t *jdeposit;
struct TALER_CoinSpendSignatureP coin_sig; struct TALER_CoinSpendSignatureP coin_sig;
#if FIXME_OEC #if FIXME_OEC
@ -390,7 +586,9 @@ TALER_EXCHANGE_purse_deposit (
} }
#endif #endif
GNUNET_CRYPTO_eddsa_key_get_public (&deposit->coin_priv.eddsa_priv, 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 ( TALER_wallet_purse_deposit_sign (
pch->base_url, pch->base_url,
&pch->purse_pub, &pch->purse_pub,
@ -413,7 +611,7 @@ TALER_EXCHANGE_purse_deposit (
TALER_JSON_pack_denom_sig ("ub_sig", TALER_JSON_pack_denom_sig ("ub_sig",
&deposit->denom_sig), &deposit->denom_sig),
GNUNET_JSON_pack_data_auto ("coin_pub", GNUNET_JSON_pack_data_auto ("coin_pub",
coin_pub), &coin->coin_pub),
GNUNET_JSON_pack_data_auto ("coin_sig", GNUNET_JSON_pack_data_auto ("coin_sig",
&coin_sig)); &coin_sig));
GNUNET_assert (0 == GNUNET_assert (0 ==

View File

@ -118,7 +118,7 @@ make_payto (const char *exchange_url,
end = GNUNET_STRINGS_data_to_string ( end = GNUNET_STRINGS_data_to_string (
reserve_pub, reserve_pub,
sizeof (reserve_pub), sizeof (*reserve_pub),
pub_str, pub_str,
sizeof (pub_str)); sizeof (pub_str));
*end = '\0'; *end = '\0';

View File

@ -112,6 +112,8 @@ run (void *cls,
*/ */
CMD_TRANSFER_TO_EXCHANGE ("create-reserve-1", CMD_TRANSFER_TO_EXCHANGE ("create-reserve-1",
"EUR:5.01"), "EUR:5.01"),
CMD_TRANSFER_TO_EXCHANGE ("create-reserve-2",
"EUR:5.01"),
TALER_TESTING_cmd_reserve_poll ("poll-reserve-1", TALER_TESTING_cmd_reserve_poll ("poll-reserve-1",
"create-reserve-1", "create-reserve-1",
"EUR:5.01", "EUR:5.01",
@ -122,6 +124,11 @@ run (void *cls,
bc.user42_payto, bc.user42_payto,
bc.exchange_payto, bc.exchange_payto,
"create-reserve-1"), "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 * Make a reserve exist, according to the previous
* transfer. * transfer.
@ -193,6 +200,13 @@ run (void *cls,
"EUR:1", "EUR:1",
MHD_HTTP_OK), MHD_HTTP_OK),
#endif #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 () TALER_TESTING_cmd_end ()
}; };
struct TALER_TESTING_Command pull[] = { struct TALER_TESTING_Command pull[] = {
@ -241,6 +255,24 @@ run (void *cls,
"create-reserve-1", "create-reserve-1",
"EUR:2", "EUR:2",
MHD_HTTP_OK), 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 #endif
TALER_TESTING_cmd_end () TALER_TESTING_cmd_end ()
}; };