exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

commit 01373461c56e6561ed066c9d4b492cd9091929c1
parent 39e51a95be1539467efc146bf72e88631414ef9d
Author: Christian Grothoff <christian@grothoff.org>
Date:   Sun,  1 Sep 2024 16:50:30 +0200

add idempotency check for batch-deposit on kyc failure

Diffstat:
Msrc/exchange/taler-exchange-httpd_batch-deposit.c | 52++++++++++++++++++++--------------------------------
Msrc/exchangedb/Makefile.am | 1+
Asrc/exchangedb/exchange_do_check_deposit_idempotent.sql | 123+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/exchangedb/perf_deposits_get_ready.c | 3+--
Msrc/exchangedb/perf_select_refunds_by_coin.c | 2+-
Asrc/exchangedb/pg_do_check_deposit_idempotent.c | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/exchangedb/pg_do_check_deposit_idempotent.h | 45+++++++++++++++++++++++++++++++++++++++++++++
Msrc/exchangedb/plugin_exchangedb_postgres.c | 3+++
Msrc/exchangedb/procedures.sql.in | 1+
Msrc/include/taler_exchangedb_plugin.h | 17+++++++++++++++++
10 files changed, 324 insertions(+), 35 deletions(-)

diff --git a/src/exchange/taler-exchange-httpd_batch-deposit.c b/src/exchange/taler-exchange-httpd_batch-deposit.c @@ -451,43 +451,31 @@ static bool check_request_idempotent ( struct BatchDepositContext *bdc) { -#if FIXME_PLACEHOLDER const struct TEH_RequestContext *rc = bdc->rc; + enum GNUNET_DB_QueryStatus qs; + bool is_idempotent; - for (unsigned int i = 0; i<bwc->planchets_length; i++) + qs = TEH_plugin->do_check_deposit_idempotent ( + TEH_plugin->cls, + &bdc->bd, + &bdc->exchange_timestamp, + &is_idempotent); + if (0 > qs) { - struct PlanchetContext *pc = &bwc->planchets[i]; - enum GNUNET_DB_QueryStatus qs; - struct TALER_EXCHANGEDB_CollectableBlindcoin collectable; - - qs = TEH_plugin->get_withdraw_info ( - TEH_plugin->cls, - &pc->collectable.h_coin_envelope, - &collectable); - if (0 > qs) - { - /* FIXME: soft error not handled correctly! */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); - finish_loop (bwc, - TALER_MHD_reply_with_error ( - rc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_withdraw_info")); - return true; - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - return false; - pc->collectable = collectable; + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + finish_loop (bdc, + TALER_MHD_reply_with_error ( + rc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_withdraw_info")); + return true; } - /* generate idempotent reply */ - TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_BATCH_WITHDRAW]++; - bwc->phase = BDC_PHASE_GENERATE_REPLY_SUCCESS; + if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) || + (! is_idempotent) ) + return false; + bdc->phase = BDC_PHASE_REPLY_SUCCESS; return true; -#else - GNUNET_break (0); // NOT IMPLEMENTED - return false; -#endif } diff --git a/src/exchangedb/Makefile.am b/src/exchangedb/Makefile.am @@ -130,6 +130,7 @@ libtaler_plugin_exchangedb_postgres_la_SOURCES = \ pg_profit_drains_get_pending.h pg_profit_drains_get_pending.c \ pg_get_drain_profit.h pg_get_drain_profit.c \ pg_get_purse_deposit.h pg_get_purse_deposit.c \ + pg_do_check_deposit_idempotent.h pg_do_check_deposit_idempotent.c \ pg_insert_contract.h pg_insert_contract.c \ pg_select_contract.h pg_select_contract.c \ pg_select_purse_merge.h pg_select_purse_merge.c \ diff --git a/src/exchangedb/exchange_do_check_deposit_idempotent.sql b/src/exchangedb/exchange_do_check_deposit_idempotent.sql @@ -0,0 +1,123 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2024 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 <http://www.gnu.org/licenses/> +-- +CREATE OR REPLACE FUNCTION exchange_do_check_deposit_idempotent( + -- For batch_deposits + IN in_shard INT8, + IN in_merchant_pub BYTEA, + IN in_wallet_timestamp INT8, + IN in_exchange_timestamp INT8, + IN in_refund_deadline INT8, + IN in_wire_deadline INT8, + IN in_h_contract_terms BYTEA, + IN in_wallet_data_hash BYTEA, -- can be NULL + IN in_wire_salt BYTEA, + IN in_wire_target_h_payto BYTEA, + IN in_policy_details_serial_id INT8, -- can be NULL + IN in_policy_blocked BOOLEAN, + -- For wire_targets + IN in_receiver_wire_account TEXT, + -- For coin_deposits + IN ina_coin_pub BYTEA[], + IN ina_coin_sig BYTEA[], + IN ina_amount_with_fee taler_amount[], + OUT out_exchange_timestamp INT8, + OUT out_is_idempotent BOOL + ) +LANGUAGE plpgsql +AS $$ +DECLARE + wtsi INT8; -- wire target serial id + bdsi INT8; -- batch_deposits serial id + i INT4; + ini_amount_with_fee taler_amount; + ini_coin_pub BYTEA; + ini_coin_sig BYTEA; +BEGIN +-- Shards: +-- SELECT wire_targets (by h_payto); +-- INSERT batch_deposits (by shard, merchant_pub), ON CONFLICT idempotency check; +-- PERFORM[] coin_deposits (by coin_pub), ON CONFLICT idempotency check; + +out_exchange_timestamp = in_exchange_timestamp; + +-- First, get the 'wtsi' +SELECT wire_target_serial_id + INTO wtsi + FROM wire_targets + WHERE wire_target_h_payto=in_wire_target_h_payto + AND payto_uri=in_receiver_wire_account; + +IF NOT FOUND +THEN + out_is_idempotent = FALSE; + RETURN; +END IF; + + +-- Idempotency check: see if an identical record exists. +-- We do select over merchant_pub, h_contract_terms and wire_target_h_payto +-- first to maximally increase the chance of using the existing index. +SELECT + exchange_timestamp + ,batch_deposit_serial_id + INTO + out_exchange_timestamp + ,bdsi + FROM batch_deposits + WHERE shard=in_shard + AND merchant_pub=in_merchant_pub + AND h_contract_terms=in_h_contract_terms + AND wire_target_h_payto=in_wire_target_h_payto + -- now check the rest, too + AND ( (wallet_data_hash=in_wallet_data_hash) OR + (wallet_data_hash IS NULL AND in_wallet_data_hash IS NULL) ) + AND wire_salt=in_wire_salt + AND wallet_timestamp=in_wallet_timestamp + AND refund_deadline=in_refund_deadline + AND wire_deadline=in_wire_deadline + AND ( (policy_details_serial_id=in_policy_details_serial_id) OR + (policy_details_serial_id IS NULL AND in_policy_details_serial_id IS NULL) ); + +IF NOT FOUND +THEN + out_is_idempotent=FALSE; + RETURN; +END IF; + + +-- Check each coin + +FOR i IN 1..array_length(ina_coin_pub,1) +LOOP + ini_coin_pub = ina_coin_pub[i]; + ini_coin_sig = ina_coin_sig[i]; + ini_amount_with_fee = ina_amount_with_fee[i]; + + PERFORM FROM coin_deposits + WHERE batch_deposit_serial_id=bdsi + AND coin_pub=ini_coin_pub + AND coin_sig=ini_coin_sig + AND amount_with_fee=ini_amount_with_fee; + IF NOT FOUND + THEN + out_is_idempotent=FALSE; + RETURN; + END IF; +END LOOP; -- end FOR all coins + +out_is_idempotent=TRUE; + +END $$; diff --git a/src/exchangedb/perf_deposits_get_ready.c b/src/exchangedb/perf_deposits_get_ready.c @@ -68,8 +68,6 @@ static int result; */ static struct TALER_EXCHANGEDB_Plugin *plugin; -static struct TALER_DenomFeeSet fees; - static struct TALER_MerchantWireHashP h_wire_wt; /** @@ -203,6 +201,7 @@ run (void *cls) unsigned int *perm; unsigned long long duration_sq; struct TALER_EXCHANGEDB_RefreshRevealedCoin *ccoin; + struct TALER_DenomFeeSet fees; struct GNUNET_CRYPTO_BlindingInputValues bi = { .cipher = GNUNET_CRYPTO_BSA_RSA, .rc = 0 diff --git a/src/exchangedb/perf_select_refunds_by_coin.c b/src/exchangedb/perf_select_refunds_by_coin.c @@ -67,7 +67,6 @@ static int result; */ static struct TALER_EXCHANGEDB_Plugin *plugin; -static struct TALER_DenomFeeSet fees; static struct TALER_MerchantWireHashP h_wire_wt; @@ -231,6 +230,7 @@ run (void *cls) unsigned long long duration_sq; struct TALER_EXCHANGEDB_RefreshRevealedCoin *ccoin; struct TALER_DenominationPublicKey *new_denom_pubs = NULL; + struct TALER_DenomFeeSet fees; unsigned int count = 0; ref = GNUNET_new_array (ROUNDS + 1, diff --git a/src/exchangedb/pg_do_check_deposit_idempotent.c b/src/exchangedb/pg_do_check_deposit_idempotent.c @@ -0,0 +1,112 @@ +/* + This file is part of TALER + Copyright (C) 2022-2023 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 <http://www.gnu.org/licenses/> + */ +/** + * @file exchangedb/pg_do_deposit.c + * @brief Implementation of the do_deposit function for Postgres + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler_error_codes.h" +#include "taler_dbevents.h" +#include "taler_pq_lib.h" +#include "pg_do_check_deposit_idempotent.h" +#include "pg_helper.h" +#include "pg_compute_shard.h" + + +enum GNUNET_DB_QueryStatus +TEH_PG_do_check_deposit_idempotent ( + void *cls, + const struct TALER_EXCHANGEDB_BatchDeposit *bd, + struct GNUNET_TIME_Timestamp *exchange_timestamp, + bool *is_idempotent) +{ + struct PostgresClosure *pg = cls; + uint64_t deposit_shard = TEH_PG_compute_shard (&bd->merchant_pub); + const struct TALER_CoinSpendPublicKeyP *coin_pubs[GNUNET_NZL (bd->num_cdis)]; + const struct TALER_CoinSpendSignatureP *coin_sigs[GNUNET_NZL (bd->num_cdis)]; + struct TALER_Amount amounts_with_fee[GNUNET_NZL (bd->num_cdis)]; + struct GNUNET_PQ_QueryParam params[] = { + /* data for batch_deposits */ + GNUNET_PQ_query_param_uint64 (&deposit_shard), + GNUNET_PQ_query_param_auto_from_type (&bd->merchant_pub), + GNUNET_PQ_query_param_timestamp (&bd->wallet_timestamp), + GNUNET_PQ_query_param_timestamp (exchange_timestamp), + GNUNET_PQ_query_param_timestamp (&bd->refund_deadline), + GNUNET_PQ_query_param_timestamp (&bd->wire_deadline), + GNUNET_PQ_query_param_auto_from_type (&bd->h_contract_terms), + (bd->no_wallet_data_hash) + ? GNUNET_PQ_query_param_null () + : GNUNET_PQ_query_param_auto_from_type (&bd->wallet_data_hash), + GNUNET_PQ_query_param_auto_from_type (&bd->wire_salt), + GNUNET_PQ_query_param_auto_from_type (&bd->wire_target_h_payto), + (0 == bd->policy_details_serial_id) + ? GNUNET_PQ_query_param_null () + : GNUNET_PQ_query_param_uint64 (&bd->policy_details_serial_id), + GNUNET_PQ_query_param_bool (bd->policy_blocked), + /* to create entry in wire_targets */ + GNUNET_PQ_query_param_string (bd->receiver_wire_account), + /* arrays for coin_deposits */ + GNUNET_PQ_query_param_array_ptrs_auto_from_type (bd->num_cdis, + coin_pubs, + pg->conn), + GNUNET_PQ_query_param_array_ptrs_auto_from_type (bd->num_cdis, + coin_sigs, + pg->conn), + TALER_PQ_query_param_array_amount (bd->num_cdis, + amounts_with_fee, + pg->conn), + GNUNET_PQ_query_param_end + }; + bool no_time; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_timestamp ("exchange_timestamp", + exchange_timestamp), + &no_time), + GNUNET_PQ_result_spec_bool ("is_idempotent", + is_idempotent), + GNUNET_PQ_result_spec_end + }; + enum GNUNET_DB_QueryStatus qs; + + for (unsigned int i = 0; i < bd->num_cdis; i++) + { + const struct TALER_EXCHANGEDB_CoinDepositInformation *cdi + = &bd->cdis[i]; + + amounts_with_fee[i] = cdi->amount_with_fee; + coin_pubs[i] = &cdi->coin.coin_pub; + coin_sigs[i] = &cdi->csig; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Do deposit %u = %s\n", + i, + TALER_B2S (&cdi->coin.coin_pub)); + } + PREPARE (pg, + "call_check_deposit_idempotent", + "SELECT " + " out_exchange_timestamp AS exchange_timestamp" + ",out_is_idempotent AS is_idempotent" + " FROM exchange_do_check_deposit_idempotent" + " ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16);"); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "call_check_deposit_idempotent", + params, + rs); + GNUNET_PQ_cleanup_query_params_closures (params); + return qs; +} diff --git a/src/exchangedb/pg_do_check_deposit_idempotent.h b/src/exchangedb/pg_do_check_deposit_idempotent.h @@ -0,0 +1,45 @@ +/* + This file is part of TALER + Copyright (C) 2024 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 <http://www.gnu.org/licenses/> + */ +/** + * @file exchangedb/pg_do_check_deposit_idempotent.h + * @brief implementation of the do_check_deposit_idempotent function for Postgres + * @author Christian Grothoff + */ +#ifndef PG_DO_CHECK_DEPOSIT_IDEMPOTENT_H +#define PG_DO_CHECK_DEPOSIT_IDEMPOTENT_H + +#include "taler_util.h" +#include "taler_json_lib.h" +#include "taler_exchangedb_plugin.h" + + +/** + * Check ifdeposit operation is idempotent to existing one. + * + * @param cls the `struct PostgresClosure` with the plugin-specific state + * @param bd batch deposit operation details + * @param[in,out] exchange_timestamp time to use for the deposit (possibly updated) + * @param[out] is_idempotent set to true if the request is idempotent + * @return query execution status + */ +enum GNUNET_DB_QueryStatus +TEH_PG_do_check_deposit_idempotent ( + void *cls, + const struct TALER_EXCHANGEDB_BatchDeposit *bd, + struct GNUNET_TIME_Timestamp *exchange_timestamp, + bool *is_idempotent); + +#endif diff --git a/src/exchangedb/plugin_exchangedb_postgres.c b/src/exchangedb/plugin_exchangedb_postgres.c @@ -38,6 +38,7 @@ #include "pg_delete_aggregation_transient.h" #include "pg_get_link_data.h" #include "pg_helper.h" +#include "pg_do_check_deposit_idempotent.h" #include "pg_do_reserve_open.h" #include "pg_get_coin_transactions.h" #include "pg_get_expired_reserves.h" @@ -744,6 +745,8 @@ libtaler_plugin_exchangedb_postgres_init (void *cls) = &TEH_PG_select_aml_decisions; plugin->select_deposit_amounts_for_kyc_check = &TEH_PG_select_deposit_amounts_for_kyc_check; + plugin->do_check_deposit_idempotent + = &TEH_PG_do_check_deposit_idempotent; plugin->insert_signkey_revocation = &TEH_PG_insert_signkey_revocation; plugin->select_aml_attributes diff --git a/src/exchangedb/procedures.sql.in b/src/exchangedb/procedures.sql.in @@ -27,6 +27,7 @@ SET search_path TO exchange; #include "exchange_do_batch_withdraw_insert.sql" #include "exchange_do_age_withdraw.sql" #include "exchange_do_deposit.sql" +#include "exchange_do_check_deposit_idempotent.sql" #include "exchange_do_melt.sql" #include "exchange_do_select_deposits_missing_wire.sql" #include "exchange_do_select_justification_for_missing_wire.sql" diff --git a/src/include/taler_exchangedb_plugin.h b/src/include/taler_exchangedb_plugin.h @@ -4218,6 +4218,23 @@ struct TALER_EXCHANGEDB_Plugin /** + * Check ifdeposit operation is idempotent to existing one. + * + * @param cls the `struct PostgresClosure` with the plugin-specific state + * @param bd batch deposit operation details + * @param[in,out] exchange_timestamp time to use for the deposit (possibly updated) + * @param[out] is_idempotent set to true if the request is idempotent + * @return query execution status + */ + enum GNUNET_DB_QueryStatus + (*do_check_deposit_idempotent)( + void *cls, + const struct TALER_EXCHANGEDB_BatchDeposit *bd, + struct GNUNET_TIME_Timestamp *exchange_timestamp, + bool *is_idempotent); + + + /** * Perform melt operation, checking for sufficient balance * of the coin and possibly persisting the melt details. *