taler-exchange-httpd_post-purses-PURSE_PUB-deposit.c (19036B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2022 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU Affero General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 12 13 You should have received a copy of the GNU Affero General Public License along with 14 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 /** 17 * @file taler-exchange-httpd_post-purses-PURSE_PUB-deposit.c 18 * @brief Handle /purses/$PID/deposit requests; parses the POST and JSON and 19 * verifies the coin signature before handing things off 20 * to the database. 21 * @author Christian Grothoff 22 */ 23 #include "platform.h" /* UNNECESSARY? */ 24 #include <gnunet/gnunet_util_lib.h> 25 #include <gnunet/gnunet_json_lib.h> 26 #include <jansson.h> 27 #include <microhttpd.h> 28 #include "taler/taler_dbevents.h" /* UNNECESSARY? */ 29 #include "taler/taler_json_lib.h" 30 #include "taler/taler_mhd_lib.h" 31 #include "taler-exchange-httpd_common_deposit.h" 32 #include "taler-exchange-httpd_post-purses-PURSE_PUB-deposit.h" 33 #include "taler-exchange-httpd_responses.h" 34 #include "exchangedb_lib.h" 35 #include "taler-exchange-httpd_get-keys.h" 36 #include "exchange-database/get_purse.h" 37 #include "exchange-database/get_purse_request.h" 38 #include "exchange-database/do_purse_deposit.h" 39 #include "exchange-database/event_notify.h" 40 #include "exchange-database/get_purse_deposit.h" 41 #include "exchange-database/preflight.h" 42 #include "exchange-database/rollback.h" 43 44 45 /** 46 * Closure for #deposit_transaction. 47 */ 48 struct PurseDepositContext 49 { 50 /** 51 * Public key of the purse we are creating. 52 */ 53 const struct TALER_PurseContractPublicKeyP *purse_pub; 54 55 /** 56 * Total amount to be put into the purse. 57 */ 58 struct TALER_Amount amount; 59 60 /** 61 * Total actually deposited by all the coins. 62 */ 63 struct TALER_Amount deposit_total; 64 65 /** 66 * When should the purse expire. 67 */ 68 struct GNUNET_TIME_Timestamp purse_expiration; 69 70 /** 71 * Hash of the contract (needed for signing). 72 */ 73 struct TALER_PrivateContractHashP h_contract_terms; 74 75 /** 76 * Our current time. 77 */ 78 struct GNUNET_TIME_Timestamp exchange_timestamp; 79 80 /** 81 * Array of coins being deposited. 82 */ 83 struct TEH_PurseDepositedCoin *coins; 84 85 /** 86 * Length of the @e coins array. 87 */ 88 unsigned int num_coins; 89 90 /** 91 * Minimum age for deposits into this purse. 92 */ 93 uint32_t min_age; 94 }; 95 96 97 /** 98 * Send confirmation of purse creation success to client. 99 * 100 * @param connection connection to the client 101 * @param pcc details about the request that succeeded 102 * @return MHD result code 103 */ 104 static enum MHD_Result 105 reply_deposit_success (struct MHD_Connection *connection, 106 const struct PurseDepositContext *pcc) 107 { 108 struct TALER_ExchangePublicKeyP pub; 109 struct TALER_ExchangeSignatureP sig; 110 enum TALER_ErrorCode ec; 111 112 if (TALER_EC_NONE != 113 (ec = TALER_exchange_online_purse_created_sign ( 114 &TEH_keys_exchange_sign_, 115 pcc->exchange_timestamp, 116 pcc->purse_expiration, 117 &pcc->amount, 118 &pcc->deposit_total, 119 pcc->purse_pub, 120 &pcc->h_contract_terms, 121 &pub, 122 &sig))) 123 { 124 GNUNET_break (0); 125 return TALER_MHD_reply_with_ec (connection, 126 ec, 127 NULL); 128 } 129 return TALER_MHD_REPLY_JSON_PACK ( 130 connection, 131 MHD_HTTP_OK, 132 TALER_JSON_pack_amount ("total_deposited", 133 &pcc->deposit_total), 134 TALER_JSON_pack_amount ("purse_value_after_fees", 135 &pcc->amount), 136 GNUNET_JSON_pack_timestamp ("exchange_timestamp", 137 pcc->exchange_timestamp), 138 GNUNET_JSON_pack_timestamp ("purse_expiration", 139 pcc->purse_expiration), 140 GNUNET_JSON_pack_data_auto ("h_contract_terms", 141 &pcc->h_contract_terms), 142 GNUNET_JSON_pack_data_auto ("exchange_sig", 143 &sig), 144 GNUNET_JSON_pack_data_auto ("exchange_pub", 145 &pub)); 146 } 147 148 149 /** 150 * Execute database transaction for /purses/$PID/deposit. Runs the transaction 151 * logic; IF it returns a non-error code, the transaction logic MUST NOT queue 152 * a MHD response. IF it returns an hard error, the transaction logic MUST 153 * queue a MHD response and set @a mhd_ret. IF it returns the soft error 154 * code, the function MAY be called again to retry and MUST not queue a MHD 155 * response. 156 * 157 * @param cls a `struct PurseDepositContext` 158 * @param connection MHD request context 159 * @param[out] mhd_ret set to MHD status on error 160 * @return transaction status 161 */ 162 static enum GNUNET_DB_QueryStatus 163 deposit_transaction (void *cls, 164 struct MHD_Connection *connection, 165 enum MHD_Result *mhd_ret) 166 { 167 struct PurseDepositContext *pcc = cls; 168 enum GNUNET_DB_QueryStatus qs; 169 170 qs = GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; 171 for (unsigned int i = 0; i<pcc->num_coins; i++) 172 { 173 struct TEH_PurseDepositedCoin *coin = &pcc->coins[i]; 174 bool balance_ok = false; 175 bool conflict = true; 176 bool too_late = true; 177 178 qs = TEH_make_coin_known (&coin->cpi, 179 connection, 180 &coin->known_coin_id, 181 mhd_ret); 182 if (qs < 0) 183 return qs; 184 qs = TALER_EXCHANGEDB_do_purse_deposit (TEH_pg, 185 pcc->purse_pub, 186 &coin->cpi.coin_pub, 187 &coin->amount, 188 &coin->coin_sig, 189 &coin->amount_minus_fee, 190 &balance_ok, 191 &too_late, 192 &conflict); 193 if (qs <= 0) 194 { 195 if (GNUNET_DB_STATUS_SOFT_ERROR == qs) 196 return qs; 197 GNUNET_break (0 != qs); 198 TALER_LOG_WARNING ( 199 "Failed to store purse deposit information in database\n"); 200 *mhd_ret = TALER_MHD_reply_with_error (connection, 201 MHD_HTTP_INTERNAL_SERVER_ERROR, 202 TALER_EC_GENERIC_DB_STORE_FAILED, 203 "do purse deposit"); 204 return GNUNET_DB_STATUS_HARD_ERROR; 205 } 206 if (! balance_ok) 207 { 208 *mhd_ret 209 = TEH_RESPONSE_reply_coin_insufficient_funds ( 210 connection, 211 TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS, 212 &coin->cpi.denom_pub_hash, 213 &coin->cpi.coin_pub); 214 return GNUNET_DB_STATUS_HARD_ERROR; 215 } 216 if (too_late) 217 { 218 TALER_EXCHANGEDB_rollback (TEH_pg); 219 *mhd_ret 220 = TALER_MHD_reply_with_ec ( 221 connection, 222 TALER_EC_EXCHANGE_PURSE_DEPOSIT_DECIDED_ALREADY, 223 NULL); 224 return GNUNET_DB_STATUS_HARD_ERROR; 225 } 226 if (conflict) 227 { 228 struct TALER_Amount amount; 229 struct TALER_CoinSpendSignatureP coin_sig; 230 struct TALER_DenominationHashP h_denom_pub; 231 struct TALER_AgeCommitmentHashP hac; 232 const struct TALER_AgeCommitmentHashP *phac = NULL; 233 bool no_age_commitment; 234 char *partner_url = NULL; 235 236 TALER_EXCHANGEDB_rollback (TEH_pg); 237 qs = TALER_EXCHANGEDB_get_purse_deposit (TEH_pg, 238 pcc->purse_pub, 239 &coin->cpi.coin_pub, 240 &amount, 241 &h_denom_pub, 242 &hac, 243 &no_age_commitment, 244 &coin_sig, 245 &partner_url); 246 if (qs < 0) 247 { 248 GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); 249 TALER_LOG_WARNING ( 250 "Failed to fetch purse deposit information from database\n"); 251 *mhd_ret = TALER_MHD_reply_with_error (connection, 252 MHD_HTTP_INTERNAL_SERVER_ERROR, 253 TALER_EC_GENERIC_DB_FETCH_FAILED, 254 "get purse deposit"); 255 return GNUNET_DB_STATUS_HARD_ERROR; 256 } 257 if (! no_age_commitment) 258 phac = &hac; 259 260 *mhd_ret 261 = TALER_MHD_REPLY_JSON_PACK ( 262 connection, 263 MHD_HTTP_CONFLICT, 264 TALER_JSON_pack_ec ( 265 TALER_EC_EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA), 266 GNUNET_JSON_pack_data_auto ("coin_pub", 267 &coin->cpi.coin_pub), 268 GNUNET_JSON_pack_data_auto ("h_denom_pub", 269 &h_denom_pub), 270 GNUNET_JSON_pack_allow_null ( 271 GNUNET_JSON_pack_data_auto ("h_age_commitment", 272 phac)), 273 GNUNET_JSON_pack_data_auto ("coin_sig", 274 &coin_sig), 275 GNUNET_JSON_pack_allow_null ( 276 GNUNET_JSON_pack_string ("partner_url", 277 partner_url)), 278 TALER_JSON_pack_amount ("amount", 279 &amount)); 280 GNUNET_free (partner_url); 281 return GNUNET_DB_STATUS_HARD_ERROR; 282 } 283 } 284 return qs; 285 } 286 287 288 /** 289 * Parse a coin and check signature of the coin and the denomination 290 * signature over the coin. 291 * 292 * @param[in,out] connection our HTTP connection 293 * @param[in,out] pcc request context 294 * @param[out] coin coin to initialize 295 * @param jcoin coin to parse 296 * @return #GNUNET_OK on success, #GNUNET_NO if an error was returned, 297 * #GNUNET_SYSERR on failure and no error could be returned 298 */ 299 static enum GNUNET_GenericReturnValue 300 parse_coin (struct MHD_Connection *connection, 301 struct PurseDepositContext *pcc, 302 struct TEH_PurseDepositedCoin *coin, 303 const json_t *jcoin) 304 { 305 enum GNUNET_GenericReturnValue iret; 306 307 if (GNUNET_OK != 308 (iret = TEH_common_purse_deposit_parse_coin (connection, 309 coin, 310 jcoin))) 311 return iret; 312 if (GNUNET_OK != 313 (iret = TEH_common_deposit_check_purse_deposit ( 314 connection, 315 coin, 316 pcc->purse_pub, 317 pcc->min_age))) 318 return iret; 319 if (0 > 320 TALER_amount_add (&pcc->deposit_total, 321 &pcc->deposit_total, 322 &coin->amount_minus_fee)) 323 { 324 GNUNET_break (0); 325 return (MHD_YES == 326 TALER_MHD_reply_with_error (connection, 327 MHD_HTTP_INTERNAL_SERVER_ERROR, 328 TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT, 329 "total deposit contribution")) 330 ? GNUNET_NO 331 : GNUNET_SYSERR; 332 } 333 return GNUNET_OK; 334 } 335 336 337 enum MHD_Result 338 TEH_handler_purses_deposit ( 339 struct TEH_RequestContext *rc, 340 const struct TALER_PurseContractPublicKeyP *purse_pub, 341 const json_t *root) 342 { 343 struct MHD_Connection *connection = rc->connection; 344 struct PurseDepositContext pcc = { 345 .purse_pub = purse_pub, 346 .exchange_timestamp = GNUNET_TIME_timestamp_get () 347 }; 348 const json_t *deposits; 349 json_t *deposit; 350 unsigned int idx; 351 struct GNUNET_JSON_Specification spec[] = { 352 GNUNET_JSON_spec_array_const ("deposits", 353 &deposits), 354 GNUNET_JSON_spec_end () 355 }; 356 357 { 358 enum GNUNET_GenericReturnValue res; 359 360 res = TALER_MHD_parse_json_data (connection, 361 root, 362 spec); 363 if (GNUNET_SYSERR == res) 364 { 365 GNUNET_break (0); 366 return MHD_NO; /* hard failure */ 367 } 368 if (GNUNET_NO == res) 369 { 370 GNUNET_break_op (0); 371 return MHD_YES; /* failure */ 372 } 373 } 374 GNUNET_assert (GNUNET_OK == 375 TALER_amount_set_zero (TEH_currency, 376 &pcc.deposit_total)); 377 pcc.num_coins = (unsigned int) json_array_size (deposits); 378 if ( (0 == pcc.num_coins) || 379 (((size_t) pcc.num_coins) != json_array_size (deposits)) || 380 (pcc.num_coins > TALER_MAX_COINS) ) 381 { 382 GNUNET_break_op (0); 383 return TALER_MHD_reply_with_error (connection, 384 MHD_HTTP_BAD_REQUEST, 385 TALER_EC_GENERIC_PARAMETER_MALFORMED, 386 "deposits"); 387 } 388 389 { 390 enum GNUNET_DB_QueryStatus qs; 391 struct GNUNET_TIME_Timestamp create_timestamp; 392 struct GNUNET_TIME_Timestamp merge_timestamp; 393 bool was_deleted; 394 bool was_refunded; 395 396 qs = TALER_EXCHANGEDB_get_purse ( 397 TEH_pg, 398 pcc.purse_pub, 399 &create_timestamp, 400 &pcc.purse_expiration, 401 &pcc.amount, 402 &pcc.deposit_total, 403 &pcc.h_contract_terms, 404 &merge_timestamp, 405 &was_deleted, 406 &was_refunded); 407 switch (qs) 408 { 409 case GNUNET_DB_STATUS_HARD_ERROR: 410 GNUNET_break (0); 411 return TALER_MHD_reply_with_error (connection, 412 MHD_HTTP_INTERNAL_SERVER_ERROR, 413 TALER_EC_GENERIC_DB_FETCH_FAILED, 414 "select purse"); 415 case GNUNET_DB_STATUS_SOFT_ERROR: 416 GNUNET_break (0); 417 return TALER_MHD_reply_with_error (connection, 418 MHD_HTTP_INTERNAL_SERVER_ERROR, 419 TALER_EC_GENERIC_DB_FETCH_FAILED, 420 "select purse"); 421 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 422 return TALER_MHD_reply_with_error (connection, 423 MHD_HTTP_NOT_FOUND, 424 TALER_EC_EXCHANGE_GENERIC_PURSE_UNKNOWN, 425 NULL); 426 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 427 break; /* handled below */ 428 } 429 if (was_refunded || 430 was_deleted) 431 { 432 return TALER_MHD_reply_with_error ( 433 connection, 434 MHD_HTTP_GONE, 435 was_deleted 436 ? TALER_EC_EXCHANGE_GENERIC_PURSE_DELETED 437 : TALER_EC_EXCHANGE_GENERIC_PURSE_EXPIRED, 438 GNUNET_TIME_timestamp2s (pcc.purse_expiration)); 439 } 440 } 441 442 /* Obtain the minimum age required for deposits into this purse; 443 #TALER_EXCHANGEDB_get_purse() does not return it, but we must 444 have it before we check the coins below. */ 445 { 446 enum GNUNET_DB_QueryStatus qs; 447 struct TALER_PurseMergePublicKeyP merge_pub; 448 struct GNUNET_TIME_Timestamp purse_expiration; 449 struct TALER_PrivateContractHashP h_contract_terms; 450 struct TALER_Amount target_amount; 451 struct TALER_Amount balance; 452 struct TALER_PurseContractSignatureP purse_sig; 453 454 qs = TALER_EXCHANGEDB_get_purse_request ( 455 TEH_pg, 456 pcc.purse_pub, 457 &merge_pub, 458 &purse_expiration, 459 &h_contract_terms, 460 &pcc.min_age, 461 &target_amount, 462 &balance, 463 &purse_sig); 464 switch (qs) 465 { 466 case GNUNET_DB_STATUS_HARD_ERROR: 467 case GNUNET_DB_STATUS_SOFT_ERROR: 468 GNUNET_break (0); 469 return TALER_MHD_reply_with_error (connection, 470 MHD_HTTP_INTERNAL_SERVER_ERROR, 471 TALER_EC_GENERIC_DB_FETCH_FAILED, 472 "select purse request"); 473 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 474 return TALER_MHD_reply_with_error (connection, 475 MHD_HTTP_NOT_FOUND, 476 TALER_EC_EXCHANGE_GENERIC_PURSE_UNKNOWN, 477 NULL); 478 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 479 break; 480 } 481 } 482 483 /* parse deposits */ 484 pcc.coins = GNUNET_new_array (pcc.num_coins, 485 struct TEH_PurseDepositedCoin); 486 json_array_foreach (deposits, idx, deposit) 487 { 488 enum GNUNET_GenericReturnValue res; 489 struct TEH_PurseDepositedCoin *coin = &pcc.coins[idx]; 490 491 res = parse_coin (connection, 492 &pcc, 493 coin, 494 deposit); 495 if (GNUNET_OK != res) 496 { 497 /* Note: coins[idx] may have been fully parsed before the 498 check failed, so it must be released as well. */ 499 for (unsigned int i = 0; i<=idx; i++) 500 TEH_common_purse_deposit_free_coin (&pcc.coins[i]); 501 GNUNET_free (pcc.coins); 502 return (GNUNET_NO == res) ? MHD_YES : MHD_NO; 503 } 504 } 505 506 if (GNUNET_SYSERR == 507 TALER_EXCHANGEDB_preflight (TEH_pg)) 508 { 509 GNUNET_break (0); 510 for (unsigned int i = 0; i<pcc.num_coins; i++) 511 TEH_common_purse_deposit_free_coin (&pcc.coins[i]); 512 GNUNET_free (pcc.coins); 513 return TALER_MHD_reply_with_error (connection, 514 MHD_HTTP_INTERNAL_SERVER_ERROR, 515 TALER_EC_GENERIC_DB_START_FAILED, 516 "preflight failure"); 517 } 518 519 /* execute transaction */ 520 { 521 enum MHD_Result mhd_ret; 522 523 if (GNUNET_OK != 524 TEH_DB_run_transaction (connection, 525 "execute purse deposit", 526 TEH_MT_REQUEST_PURSE_DEPOSIT, 527 &mhd_ret, 528 &deposit_transaction, 529 &pcc)) 530 { 531 for (unsigned int i = 0; i<pcc.num_coins; i++) 532 TEH_common_purse_deposit_free_coin (&pcc.coins[i]); 533 GNUNET_free (pcc.coins); 534 return mhd_ret; 535 } 536 } 537 { 538 struct TALER_EXCHANGEDB_PurseEventP rep = { 539 .header.size = htons (sizeof (rep)), 540 .header.type = htons (TALER_DBEVENT_EXCHANGE_PURSE_DEPOSITED), 541 .purse_pub = *pcc.purse_pub 542 }; 543 544 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 545 "Notifying about purse deposit %s\n", 546 TALER_B2S (pcc.purse_pub)); 547 TALER_EXCHANGEDB_event_notify (TEH_pg, 548 &rep.header, 549 NULL, 550 0); 551 } 552 553 /* generate regular response */ 554 { 555 enum MHD_Result res; 556 557 res = reply_deposit_success (connection, 558 &pcc); 559 for (unsigned int i = 0; i<pcc.num_coins; i++) 560 TEH_common_purse_deposit_free_coin (&pcc.coins[i]); 561 GNUNET_free (pcc.coins); 562 return res; 563 } 564 } 565 566 567 /* end of taler-exchange-httpd_purses_deposit.c */