/* This file is part of TALER Copyright (C) 2018, 2021 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 testing/testing_api_cmd_auditor_deposit_confirmation.c * @brief command for testing /deposit_confirmation. * @author Christian Grothoff */ #include "platform.h" #include "taler_json_lib.h" #include #include "taler_auditor_service.h" #include "taler_testing_lib.h" #include "taler_signatures.h" #include "backoff.h" /** * How long do we wait AT MOST when retrying? */ #define MAX_BACKOFF GNUNET_TIME_relative_multiply ( \ GNUNET_TIME_UNIT_MILLISECONDS, 100) /** * How often do we retry before giving up? */ #define NUM_RETRIES 5 /** * State for a "deposit confirmation" CMD. */ struct DepositConfirmationState { /** * Reference to any command that is able to provide a deposit. */ const char *deposit_reference; /** * What is the deposited amount without the fee (i.e. the * amount we expect in the deposit confirmation)? */ const char *amount_without_fee; /** * Which coin of the @e deposit_reference should we confirm. */ unsigned int coin_index; /** * DepositConfirmation handle while operation is running. */ struct TALER_AUDITOR_DepositConfirmationHandle *dc; /** * Auditor connection. */ struct TALER_AUDITOR_Handle *auditor; /** * Interpreter state. */ struct TALER_TESTING_Interpreter *is; /** * Task scheduled to try later. */ struct GNUNET_SCHEDULER_Task *retry_task; /** * How long do we wait until we retry? */ struct GNUNET_TIME_Relative backoff; /** * Expected HTTP response code. */ unsigned int expected_response_code; /** * How often should we retry on (transient) failures? */ unsigned int do_retry; }; /** * Run the command. * * @param cls closure. * @param cmd the command to execute. * @param is the interpreter state. */ static void deposit_confirmation_run (void *cls, const struct TALER_TESTING_Command *cmd, struct TALER_TESTING_Interpreter *is); /** * Task scheduled to re-try #deposit_confirmation_run. * * @param cls a `struct DepositConfirmationState` */ static void do_retry (void *cls) { struct DepositConfirmationState *dcs = cls; dcs->retry_task = NULL; dcs->is->commands[dcs->is->ip].last_req_time = GNUNET_TIME_absolute_get (); deposit_confirmation_run (dcs, NULL, dcs->is); } /** * Callback to analyze the /deposit-confirmation response, just used * to check if the response code is acceptable. * * @param cls closure. * @param hr HTTP response details */ static void deposit_confirmation_cb (void *cls, const struct TALER_AUDITOR_HttpResponse *hr) { struct DepositConfirmationState *dcs = cls; dcs->dc = NULL; if (dcs->expected_response_code != hr->http_status) { if (0 != dcs->do_retry) { dcs->do_retry--; if ( (0 == hr->http_status) || (TALER_EC_GENERIC_DB_SOFT_FAILURE == hr->ec) || (MHD_HTTP_INTERNAL_SERVER_ERROR == hr->http_status) ) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Retrying deposit confirmation failed with %u/%d\n", hr->http_status, (int) hr->ec); /* on DB conflicts, do not use backoff */ if (TALER_EC_GENERIC_DB_SOFT_FAILURE == hr->ec) dcs->backoff = GNUNET_TIME_UNIT_ZERO; else dcs->backoff = GNUNET_TIME_randomized_backoff (dcs->backoff, MAX_BACKOFF); dcs->is->commands[dcs->is->ip].num_tries++; dcs->retry_task = GNUNET_SCHEDULER_add_delayed (dcs->backoff, &do_retry, dcs); return; } } GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u to command %s in %s:%u\n", hr->http_status, dcs->is->commands[dcs->is->ip].label, __FILE__, __LINE__); json_dumpf (hr->reply, stderr, 0); TALER_TESTING_interpreter_fail (dcs->is); return; } TALER_TESTING_interpreter_next (dcs->is); } /** * Run the command. * * @param cls closure. * @param cmd the command to execute. * @param is the interpreter state. */ static void deposit_confirmation_run (void *cls, const struct TALER_TESTING_Command *cmd, struct TALER_TESTING_Interpreter *is) { struct DepositConfirmationState *dcs = cls; const struct TALER_TESTING_Command *deposit_cmd; struct TALER_MerchantWireHash h_wire; struct TALER_PrivateContractHash h_contract_terms; const struct GNUNET_TIME_Absolute *exchange_timestamp = NULL; struct GNUNET_TIME_Absolute timestamp; const struct GNUNET_TIME_Absolute *wire_deadline; struct GNUNET_TIME_Absolute refund_deadline; struct TALER_Amount amount_without_fee; struct TALER_CoinSpendPublicKeyP coin_pub; const struct TALER_MerchantPrivateKeyP *merchant_priv; struct TALER_MerchantPublicKeyP merchant_pub; const struct TALER_ExchangePublicKeyP *exchange_pub; const struct TALER_ExchangeSignatureP *exchange_sig; const json_t *wire_details; const json_t *contract_terms; const struct TALER_CoinSpendPrivateKeyP *coin_priv; const struct TALER_EXCHANGE_Keys *keys; const struct TALER_EXCHANGE_SigningPublicKey *spk; (void) cmd; dcs->is = is; GNUNET_assert (NULL != dcs->deposit_reference); deposit_cmd = TALER_TESTING_interpreter_lookup_command (is, dcs->deposit_reference); if (NULL == deposit_cmd) { GNUNET_break (0); TALER_TESTING_interpreter_fail (is); return; } GNUNET_assert (GNUNET_OK == TALER_TESTING_get_trait_exchange_pub (deposit_cmd, dcs->coin_index, &exchange_pub)); GNUNET_assert (GNUNET_OK == TALER_TESTING_get_trait_exchange_sig (deposit_cmd, dcs->coin_index, &exchange_sig)); GNUNET_assert (GNUNET_OK == TALER_TESTING_get_trait_absolute_time (deposit_cmd, dcs->coin_index, &exchange_timestamp)); GNUNET_assert (GNUNET_OK == TALER_TESTING_get_trait_wire_deadline (deposit_cmd, dcs->coin_index, &wire_deadline)); GNUNET_assert (NULL != exchange_timestamp); keys = TALER_EXCHANGE_get_keys (dcs->is->exchange); GNUNET_assert (NULL != keys); spk = TALER_EXCHANGE_get_signing_key_info (keys, exchange_pub); GNUNET_assert (GNUNET_OK == TALER_TESTING_get_trait_contract_terms (deposit_cmd, &contract_terms)); /* Very unlikely to fail */ GNUNET_assert (NULL != contract_terms); GNUNET_assert (GNUNET_OK == TALER_JSON_contract_hash (contract_terms, &h_contract_terms)); GNUNET_assert (GNUNET_OK == TALER_TESTING_get_trait_wire_details (deposit_cmd, &wire_details)); GNUNET_assert (GNUNET_OK == TALER_JSON_merchant_wire_signature_hash (wire_details, &h_wire)); GNUNET_assert (GNUNET_OK == TALER_TESTING_get_trait_coin_priv (deposit_cmd, dcs->coin_index, &coin_priv)); GNUNET_CRYPTO_eddsa_key_get_public (&coin_priv->eddsa_priv, &coin_pub.eddsa_pub); GNUNET_assert (GNUNET_OK == TALER_TESTING_get_trait_merchant_priv (deposit_cmd, &merchant_priv)); GNUNET_CRYPTO_eddsa_key_get_public (&merchant_priv->eddsa_priv, &merchant_pub.eddsa_pub); GNUNET_assert (GNUNET_OK == TALER_string_to_amount (dcs->amount_without_fee, &amount_without_fee)); /* timestamp is mandatory */ { struct GNUNET_JSON_Specification spec[] = { TALER_JSON_spec_absolute_time ("timestamp", ×tamp), GNUNET_JSON_spec_end () }; if (GNUNET_OK != GNUNET_JSON_parse (contract_terms, spec, NULL, NULL)) { GNUNET_break (0); TALER_TESTING_interpreter_fail (is); return; } } /* refund deadline is optional, defaults to zero */ { struct GNUNET_JSON_Specification spec[] = { TALER_JSON_spec_absolute_time ("refund_deadline", &refund_deadline), GNUNET_JSON_spec_end () }; if (GNUNET_OK != GNUNET_JSON_parse (contract_terms, spec, NULL, NULL)) { refund_deadline = timestamp; } } dcs->dc = TALER_AUDITOR_deposit_confirmation (dcs->auditor, &h_wire, &h_contract_terms, *exchange_timestamp, *wire_deadline, refund_deadline, &amount_without_fee, &coin_pub, &merchant_pub, exchange_pub, exchange_sig, &keys->master_pub, spk->valid_from, spk->valid_until, spk->valid_legal, &spk->master_sig, &deposit_confirmation_cb, dcs); if (NULL == dcs->dc) { GNUNET_break (0); TALER_TESTING_interpreter_fail (is); return; } return; } /** * Free the state of a "deposit_confirmation" CMD, and possibly cancel a * pending operation thereof. * * @param cls closure, a `struct DepositConfirmationState` * @param cmd the command which is being cleaned up. */ static void deposit_confirmation_cleanup (void *cls, const struct TALER_TESTING_Command *cmd) { struct DepositConfirmationState *dcs = cls; if (NULL != dcs->dc) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Command %u (%s) did not complete\n", dcs->is->ip, cmd->label); TALER_AUDITOR_deposit_confirmation_cancel (dcs->dc); dcs->dc = NULL; } if (NULL != dcs->retry_task) { GNUNET_SCHEDULER_cancel (dcs->retry_task); dcs->retry_task = NULL; } GNUNET_free (dcs); } struct TALER_TESTING_Command TALER_TESTING_cmd_deposit_confirmation (const char *label, struct TALER_AUDITOR_Handle *auditor, const char *deposit_reference, unsigned int coin_index, const char *amount_without_fee, unsigned int expected_response_code) { struct DepositConfirmationState *dcs; dcs = GNUNET_new (struct DepositConfirmationState); dcs->auditor = auditor; dcs->deposit_reference = deposit_reference; dcs->coin_index = coin_index; dcs->amount_without_fee = amount_without_fee; dcs->expected_response_code = expected_response_code; { struct TALER_TESTING_Command cmd = { .cls = dcs, .label = label, .run = &deposit_confirmation_run, .cleanup = &deposit_confirmation_cleanup }; return cmd; } } struct TALER_TESTING_Command TALER_TESTING_cmd_deposit_confirmation_with_retry ( struct TALER_TESTING_Command cmd) { struct DepositConfirmationState *dcs; GNUNET_assert (&deposit_confirmation_run == cmd.run); dcs = cmd.cls; dcs->do_retry = NUM_RETRIES; return cmd; } /* end of testing_auditor_api_cmd_deposit_confirmation.c */