/* This file is part of TALER Copyright (C) 2022 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. TALER is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with TALER; see the file COPYING. If not, see */ /** * @file backenddb/pg_authorize_reward.c * @brief Implementation of the authorize_reward function for Postgres * @author Christian Grothoff */ #include "platform.h" #include #include #include #include "pg_authorize_reward.h" #include "pg_helper.h" /** * How often do we re-try if we run into a DB serialization error? */ #define MAX_RETRIES 3 /** * Closure for #lookup_reserve_for_reward_cb(). */ struct LookupReserveForRewardContext { /** * Postgres context. */ struct PostgresClosure *pg; /** * Public key of the reserve we found. */ struct TALER_ReservePublicKeyP reserve_pub; /** * How much money must be left in the reserve. */ struct TALER_Amount required_amount; /** * Set to the expiration time of the reserve we found. * #GNUNET_TIME_UNIT_FOREVER_ABS if we found none. */ struct GNUNET_TIME_Timestamp expiration; /** * Error status. */ enum TALER_ErrorCode ec; /** * Did we find a good reserve? */ bool ok; }; /** * How long must a reserve be at least still valid before we use * it for a reward? */ #define MIN_EXPIRATION GNUNET_TIME_UNIT_HOURS /** * Function to be called with the results of a SELECT statement * that has returned @a num_results results about accounts. * * @param[in,out] cls of type `struct LookupReserveForRewardContext *` * @param result the postgres result * @param num_results the number of results in @a result */ static void lookup_reserve_for_reward_cb (void *cls, PGresult *result, unsigned int num_results) { struct LookupReserveForRewardContext *lac = cls; for (unsigned int i = 0; i < num_results; i++) { struct TALER_ReservePublicKeyP reserve_pub; struct TALER_Amount committed_amount; struct TALER_Amount remaining; struct TALER_Amount initial_balance; struct GNUNET_TIME_Timestamp expiration; struct GNUNET_PQ_ResultSpec rs[] = { GNUNET_PQ_result_spec_auto_from_type ("reserve_pub", &reserve_pub), TALER_PQ_result_spec_amount_with_currency ("exchange_initial_balance", &initial_balance), TALER_PQ_result_spec_amount_with_currency ("rewards_committed", &committed_amount), GNUNET_PQ_result_spec_timestamp ("expiration", &expiration), GNUNET_PQ_result_spec_end }; if (GNUNET_OK != GNUNET_PQ_extract_result (result, rs, i)) { GNUNET_break (0); lac->ec = TALER_EC_GENERIC_DB_FETCH_FAILED; return; } if (0 > TALER_amount_subtract (&remaining, &initial_balance, &committed_amount)) { GNUNET_break (0); continue; } if (0 > TALER_amount_cmp (&remaining, &lac->required_amount)) { /* insufficient balance */ if (lac->ok) continue; /* got another reserve */ lac->ec = TALER_EC_MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_INSUFFICIENT_FUNDS; continue; } if ( (! GNUNET_TIME_absolute_is_never (lac->expiration.abs_time)) && GNUNET_TIME_timestamp_cmp (expiration, >, lac->expiration) && GNUNET_TIME_relative_cmp ( GNUNET_TIME_absolute_get_remaining (lac->expiration.abs_time), >, MIN_EXPIRATION) ) { /* reserve expired */ if (lac->ok) continue; /* got another reserve */ lac->ec = TALER_EC_MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_RESERVE_EXPIRED; continue; } lac->ok = true; lac->ec = TALER_EC_NONE; lac->expiration = expiration; lac->reserve_pub = reserve_pub; } } enum TALER_ErrorCode TMH_PG_authorize_reward (void *cls, const char *instance_id, const struct TALER_ReservePublicKeyP *reserve_pub, const struct TALER_Amount *amount, const char *justification, const char *next_url, struct TALER_RewardIdentifierP *reward_id, struct GNUNET_TIME_Timestamp *expiration) { struct PostgresClosure *pg = cls; unsigned int retries = 0; enum GNUNET_DB_QueryStatus qs; struct TALER_Amount rewards_committed; struct TALER_Amount exchange_initial_balance; const struct TALER_ReservePublicKeyP *reserve_pubp; struct LookupReserveForRewardContext lac = { .pg = pg, .required_amount = *amount, .expiration = GNUNET_TIME_UNIT_FOREVER_TS }; check_connection (pg); PREPARE (pg, "lookup_reserve_for_reward", "SELECT" " reserve_pub" ",expiration" ",exchange_initial_balance" ",rewards_committed" " FROM merchant_reward_reserves" " WHERE" " merchant_serial =" " (SELECT merchant_serial" " FROM merchant_instances" " WHERE merchant_id=$1)"); PREPARE (pg, "lookup_reserve_status", "SELECT" " expiration" ",exchange_initial_balance" ",rewards_committed" " FROM merchant_reward_reserves" " WHERE reserve_pub = $2" " AND merchant_serial =" " (SELECT merchant_serial" " FROM merchant_instances" " WHERE merchant_id=$1)"); PREPARE (pg, "update_reserve_rewards_committed", "UPDATE merchant_reward_reserves SET" " rewards_committed=$3" " WHERE reserve_pub=$2" " AND merchant_serial =" " (SELECT merchant_serial" " FROM merchant_instances" " WHERE merchant_id=$1)"); PREPARE (pg, "insert_reward", "INSERT INTO merchant_rewards" "(reserve_serial" ",reward_id" ",justification" ",next_url" ",expiration" ",amount" ",picked_up" ") " "SELECT" " reserve_serial, $3, $4, $5, $6, $7, $8" " FROM merchant_reward_reserves" " WHERE reserve_pub=$2" " AND merchant_serial = " " (SELECT merchant_serial" " FROM merchant_instances" " WHERE merchant_id=$1)"); RETRY: reserve_pubp = reserve_pub; if (MAX_RETRIES < ++retries) { GNUNET_break (0); return TALER_EC_GENERIC_DB_SOFT_FAILURE; } if (GNUNET_OK != TMH_PG_start (pg, "authorize reward")) { GNUNET_break (0); return TALER_EC_GENERIC_DB_START_FAILED; } if (NULL == reserve_pubp) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_end }; qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, "lookup_reserve_for_reward", params, &lookup_reserve_for_reward_cb, &lac); switch (qs) { case GNUNET_DB_STATUS_SOFT_ERROR: TMH_PG_rollback (pg); goto RETRY; case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); TMH_PG_rollback (pg); return TALER_EC_GENERIC_DB_FETCH_FAILED; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: TMH_PG_rollback (pg); return TALER_EC_MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_RESERVE_NOT_FOUND; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: default: break; } if (TALER_EC_NONE != lac.ec) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Enabling reward reserved failed with status %d\n", lac.ec); TMH_PG_rollback (pg); return lac.ec; } GNUNET_assert (lac.ok); reserve_pubp = &lac.reserve_pub; } { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_auto_from_type (reserve_pubp), GNUNET_PQ_query_param_end }; struct GNUNET_PQ_ResultSpec rs[] = { GNUNET_PQ_result_spec_timestamp ("expiration", expiration), TALER_PQ_result_spec_amount_with_currency ("rewards_committed", &rewards_committed), TALER_PQ_result_spec_amount_with_currency ("exchange_initial_balance", &exchange_initial_balance), GNUNET_PQ_result_spec_end }; qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, "lookup_reserve_status", params, rs); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { TMH_PG_rollback (pg); goto RETRY; } if (qs < 0) { GNUNET_break (0); TMH_PG_rollback (pg); return TALER_EC_GENERIC_DB_FETCH_FAILED; } if (0 == qs) { TMH_PG_rollback (pg); return TALER_EC_MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_RESERVE_NOT_FOUND; } } { struct TALER_Amount remaining; if (0 > TALER_amount_subtract (&remaining, &exchange_initial_balance, &rewards_committed)) { GNUNET_break (0); TMH_PG_rollback (pg); return TALER_EC_GENERIC_DB_INVARIANT_FAILURE; } if (0 > TALER_amount_cmp (&remaining, amount)) { TMH_PG_rollback (pg); return TALER_EC_MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_INSUFFICIENT_FUNDS; } } GNUNET_assert (0 <= TALER_amount_add (&rewards_committed, &rewards_committed, amount)); { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_auto_from_type (reserve_pubp), TALER_PQ_query_param_amount_with_currency (pg->conn, &rewards_committed), GNUNET_PQ_query_param_end }; qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, "update_reserve_rewards_committed", params); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { TMH_PG_rollback (pg); goto RETRY; } if (qs < 0) { GNUNET_break (0); TMH_PG_rollback (pg); return TALER_EC_GENERIC_DB_STORE_FAILED; } } GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, reward_id, sizeof (*reward_id)); { struct TALER_Amount zero; struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_auto_from_type (reserve_pubp), GNUNET_PQ_query_param_auto_from_type (reward_id), GNUNET_PQ_query_param_string (justification), GNUNET_PQ_query_param_string (next_url), GNUNET_PQ_query_param_timestamp (expiration), TALER_PQ_query_param_amount_with_currency (pg->conn, amount), TALER_PQ_query_param_amount_with_currency (pg->conn, &zero), GNUNET_PQ_query_param_end }; GNUNET_assert (GNUNET_OK == TALER_amount_set_zero (amount->currency, &zero)); qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, "insert_reward", params); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { TMH_PG_rollback (pg); goto RETRY; } if (qs < 0) { GNUNET_break (0); TMH_PG_rollback (pg); return TALER_EC_GENERIC_DB_STORE_FAILED; } } qs = TMH_PG_commit (pg); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) goto RETRY; if (qs < 0) { GNUNET_break (0); TMH_PG_rollback (pg); return TALER_EC_GENERIC_DB_COMMIT_FAILED; } return TALER_EC_NONE; }