/* This file is part of TALER Copyright (C) 2018-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 testing/testing_api_cmd_refresh.c * @brief commands for testing all "refresh" features. * @author Marcello Stanisci */ #include "platform.h" #include "taler_json_lib.h" #include #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 /** * How long do we wait AT MOST when retrying? */ #define MAX_BACKOFF GNUNET_TIME_relative_multiply ( \ GNUNET_TIME_UNIT_MILLISECONDS, 100) /** * Information about a fresh coin generated by the refresh * operation. */ struct TALER_TESTING_FreshCoinData { /** * If @e amount is NULL, this specifies the denomination key to * use. Otherwise, this will be set (by the interpreter) to the * denomination PK matching @e amount. */ const struct TALER_EXCHANGE_DenomPublicKey *pk; /** * Set (by the interpreter) to the exchange's signature over the * coin's public key. */ struct TALER_DenominationSignature sig; /** * Set (by the interpreter) to the coin's private key. */ struct TALER_CoinSpendPrivateKeyP coin_priv; /* * Fresh age commitment for the coin with proof and its hash, NULL if not * applicable. */ struct TALER_AgeCommitmentProof *age_commitment_proof; struct TALER_AgeCommitmentHash h_age_commitment; /** * The blinding key (needed for recoup operations). */ union GNUNET_CRYPTO_BlindingSecretP blinding_key; }; /** * State for a "refresh melt" command. */ struct RefreshMeltState { /** * Reference to reserve_withdraw operations for coin to * be used for the /refresh/melt operation. */ const char *coin_reference; /** * Data used in the refresh operation. */ struct TALER_EXCHANGE_RefreshData refresh_data; /** * Our command. */ const struct TALER_TESTING_Command *cmd; /** * Reference to a previous melt command. */ const char *melt_reference; /** * Melt handle while operation is running. */ struct TALER_EXCHANGE_MeltHandle *rmh; /** * Expected entry in the coin history created by this * operation. */ struct TALER_EXCHANGE_CoinHistoryEntry che; /** * Interpreter state. */ struct TALER_TESTING_Interpreter *is; /** * Array of the denomination public keys * corresponding to the @e num_fresh_coins; */ struct TALER_EXCHANGE_DenomPublicKey *fresh_pks; /** * Array of @e num_fresh_coins of results from * the melt operation. */ struct TALER_EXCHANGE_MeltBlindingDetail *mbds; /** * Entropy seed for the refresh-melt operation. */ struct TALER_RefreshMasterSecretP rms; /** * Private key of the dirty coin being melted. */ const struct TALER_CoinSpendPrivateKeyP *melt_priv; /** * Public key of the dirty coin being melted. */ struct TALER_CoinSpendPublicKeyP melt_pub; /** * Task scheduled to try later. */ struct GNUNET_SCHEDULER_Task *retry_task; /** * How long do we wait until we retry? */ struct GNUNET_TIME_Relative backoff; /** * How long did we wait in total for retries? */ struct GNUNET_TIME_Relative total_backoff; /** * Amounts to be generated during melt. */ const char **melt_fresh_amounts; /** * Number of fresh coins generated by the melt. */ unsigned int num_fresh_coins; /** * Expected HTTP response code. */ unsigned int expected_response_code; /** * if set to #GNUNET_YES, then two /refresh/melt operations * will be performed. This is needed to trigger the logic * that manages those already-made requests. Note: it * is not possible to just copy-and-paste a test refresh melt * CMD to have the same effect, because every data preparation * generates new planchets that (in turn) make the whole "hash" * different from any previous one, therefore NOT allowing the * exchange to pick any previous /rerfesh/melt operation from * the database. */ bool double_melt; /** * How often should we retry on (transient) failures? */ unsigned int do_retry; /** * Set by the melt callback as it comes from the exchange. */ uint16_t noreveal_index; }; /** * State for a "refresh reveal" CMD. */ struct RefreshRevealState { /** * Link to a "refresh melt" command. */ const char *melt_reference; /** * Reveal handle while operation is running. */ struct TALER_EXCHANGE_RefreshesRevealHandle *rrh; /** * Our command. */ const struct TALER_TESTING_Command *cmd; /** * Convenience struct to keep in one place all the * data related to one fresh coin, set by the reveal callback * as it comes from the exchange. */ struct TALER_TESTING_FreshCoinData *fresh_coins; /** * Array of @e num_fresh_coins planchet secrets derived * from the transfer secret per fresh coin. */ struct TALER_PlanchetMasterSecretP *psa; /** * 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; /** * How long did we wait in total for retries? */ struct GNUNET_TIME_Relative total_backoff; /** * Number of fresh coins withdrawn, set by the * reveal callback as it comes from the exchange, * it is the length of the @e fresh_coins array. */ unsigned int num_fresh_coins; /** * Expected HTTP response code. */ unsigned int expected_response_code; /** * How often should we retry on (transient) failures? */ unsigned int do_retry; }; /** * State for a "refresh link" CMD. */ struct RefreshLinkState { /** * Link to a "refresh reveal" command. */ const char *reveal_reference; /** * Our command. */ const struct TALER_TESTING_Command *cmd; /** * Handle to the ongoing operation. */ struct TALER_EXCHANGE_LinkHandle *rlh; /** * 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; /** * How long did we wait in total for retries? */ struct GNUNET_TIME_Relative total_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 refresh_reveal_run (void *cls, const struct TALER_TESTING_Command *cmd, struct TALER_TESTING_Interpreter *is); /** * Task scheduled to re-try #refresh_reveal_run. * * @param cls a `struct RefreshRevealState` */ static void do_reveal_retry (void *cls) { struct RefreshRevealState *rrs = cls; rrs->retry_task = NULL; TALER_TESTING_touch_cmd (rrs->is); refresh_reveal_run (rrs, NULL, rrs->is); } /** * "refresh reveal" request callback; it checks that the response * code is expected and copies into its command's state the data * coming from the exchange, namely the fresh coins. * * @param cls closure, a `struct RefreshRevealState` * @param rr HTTP response details */ static void reveal_cb (void *cls, const struct TALER_EXCHANGE_RevealResult *rr) { struct RefreshRevealState *rrs = cls; const struct TALER_EXCHANGE_HttpResponse *hr = &rr->hr; const struct TALER_TESTING_Command *melt_cmd; rrs->rrh = NULL; if (rrs->expected_response_code != hr->http_status) { if (0 != rrs->do_retry) { rrs->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 refresh reveal 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) rrs->backoff = GNUNET_TIME_UNIT_ZERO; else rrs->backoff = GNUNET_TIME_randomized_backoff (rrs->backoff, MAX_BACKOFF); rrs->total_backoff = GNUNET_TIME_relative_add (rrs->total_backoff, rrs->backoff); TALER_TESTING_inc_tries (rrs->is); rrs->retry_task = GNUNET_SCHEDULER_add_delayed (rrs->backoff, &do_reveal_retry, rrs); return; } } TALER_TESTING_unexpected_status (rrs->is, hr->http_status, rrs->expected_response_code); return; } melt_cmd = TALER_TESTING_interpreter_lookup_command (rrs->is, rrs->melt_reference); if (NULL == melt_cmd) { GNUNET_break (0); TALER_TESTING_interpreter_fail (rrs->is); return; } switch (hr->http_status) { case MHD_HTTP_OK: rrs->num_fresh_coins = rr->details.ok.num_coins; rrs->psa = GNUNET_new_array (rrs->num_fresh_coins, struct TALER_PlanchetMasterSecretP); rrs->fresh_coins = GNUNET_new_array (rrs->num_fresh_coins, struct TALER_TESTING_FreshCoinData); for (unsigned int i = 0; inum_fresh_coins; i++) { const struct TALER_EXCHANGE_RevealedCoinInfo *coin = &rr->details.ok.coins[i]; struct TALER_TESTING_FreshCoinData *fc = &rrs->fresh_coins[i]; rrs->psa[i] = coin->ps; fc->blinding_key = coin->bks; if (GNUNET_OK != TALER_TESTING_get_trait_denom_pub (melt_cmd, i, &fc->pk)) { GNUNET_break (0); TALER_TESTING_interpreter_fail (rrs->is); return; } fc->coin_priv = coin->coin_priv; if (NULL != coin->age_commitment_proof) { fc->age_commitment_proof = TALER_age_commitment_proof_duplicate (coin->age_commitment_proof); fc->h_age_commitment = coin->h_age_commitment; } TALER_denom_sig_copy (&fc->sig, &coin->sig); } if (0 != rrs->total_backoff.rel_value_us) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Total reveal backoff for %s was %s\n", rrs->cmd->label, GNUNET_STRINGS_relative_time_to_string (rrs->total_backoff, true)); } break; default: GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Unknown HTTP status %u/%d\n", hr->http_status, (int) hr->ec); } TALER_TESTING_interpreter_next (rrs->is); } /** * Run the command. * * @param cls closure. * @param cmd the command to execute. * @param is the interpreter state. */ static void melt_run (void *cls, const struct TALER_TESTING_Command *cmd, struct TALER_TESTING_Interpreter *is); /** * Run the command. * * @param cls closure. * @param cmd the command to execute. * @param is the interpreter state. */ static void refresh_reveal_run (void *cls, const struct TALER_TESTING_Command *cmd, struct TALER_TESTING_Interpreter *is) { struct RefreshRevealState *rrs = cls; struct RefreshMeltState *rms; const struct TALER_TESTING_Command *melt_cmd; rrs->cmd = cmd; rrs->is = is; melt_cmd = TALER_TESTING_interpreter_lookup_command (is, rrs->melt_reference); if (NULL == melt_cmd) { GNUNET_break (0); TALER_TESTING_interpreter_fail (rrs->is); return; } GNUNET_assert (melt_cmd->run == &melt_run); rms = melt_cmd->cls; { struct TALER_ExchangeWithdrawValues alg_values[rms->num_fresh_coins]; for (unsigned int i = 0; inum_fresh_coins; i++) alg_values[i] = rms->mbds[i].alg_value; rrs->rrh = TALER_EXCHANGE_refreshes_reveal ( TALER_TESTING_interpreter_get_context (is), TALER_TESTING_get_exchange_url (is), &rms->rms, &rms->refresh_data, rms->num_fresh_coins, alg_values, rms->noreveal_index, &reveal_cb, rrs); } if (NULL == rrs->rrh) { GNUNET_break (0); TALER_TESTING_interpreter_fail (is); return; } } /** * Free the state from a "refresh reveal" CMD, and possibly * cancel a pending operation thereof. * * @param cls closure. * @param cmd the command which is being cleaned up. */ static void refresh_reveal_cleanup (void *cls, const struct TALER_TESTING_Command *cmd) { struct RefreshRevealState *rrs = cls; (void) cmd; if (NULL != rrs->rrh) { TALER_TESTING_command_incomplete (rrs->is, cmd->label); TALER_EXCHANGE_refreshes_reveal_cancel (rrs->rrh); rrs->rrh = NULL; } if (NULL != rrs->retry_task) { GNUNET_SCHEDULER_cancel (rrs->retry_task); rrs->retry_task = NULL; } for (unsigned int j = 0; j < rrs->num_fresh_coins; j++) { TALER_denom_sig_free (&rrs->fresh_coins[j].sig); TALER_age_commitment_proof_free (rrs->fresh_coins[j].age_commitment_proof); GNUNET_free (rrs->fresh_coins[j].age_commitment_proof); } GNUNET_free (rrs->fresh_coins); GNUNET_free (rrs->psa); rrs->num_fresh_coins = 0; GNUNET_free (rrs); } /** * Run the command. * * @param cls closure. * @param cmd the command to execute. * @param is the interpreter state. */ static void refresh_link_run (void *cls, const struct TALER_TESTING_Command *cmd, struct TALER_TESTING_Interpreter *is); /** * Task scheduled to re-try #refresh_link_run. * * @param cls a `struct RefreshLinkState` */ static void do_link_retry (void *cls) { struct RefreshLinkState *rls = cls; rls->retry_task = NULL; TALER_TESTING_touch_cmd (rls->is); refresh_link_run (rls, NULL, rls->is); } /** * "refresh link" operation callback, checks that HTTP response * code is expected _and_ that all the linked coins were actually * withdrawn by the "refresh reveal" CMD. * * @param cls closure. * @param lr HTTP response details */ static void link_cb (void *cls, const struct TALER_EXCHANGE_LinkResult *lr) { struct RefreshLinkState *rls = cls; const struct TALER_EXCHANGE_HttpResponse *hr = &lr->hr; const struct TALER_TESTING_Command *reveal_cmd; unsigned int found; const unsigned int *num_fresh_coins; rls->rlh = NULL; if (rls->expected_response_code != hr->http_status) { if (0 != rls->do_retry) { rls->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 refresh link 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) rls->backoff = GNUNET_TIME_UNIT_ZERO; else rls->backoff = GNUNET_TIME_randomized_backoff (rls->backoff, MAX_BACKOFF); rls->total_backoff = GNUNET_TIME_relative_add (rls->total_backoff, rls->backoff); TALER_TESTING_inc_tries (rls->is); rls->retry_task = GNUNET_SCHEDULER_add_delayed (rls->backoff, &do_link_retry, rls); return; } } TALER_TESTING_unexpected_status (rls->is, hr->http_status, rls->expected_response_code); return; } reveal_cmd = TALER_TESTING_interpreter_lookup_command (rls->is, rls->reveal_reference); if (NULL == reveal_cmd) { GNUNET_break (0); TALER_TESTING_interpreter_fail (rls->is); return; } switch (hr->http_status) { case MHD_HTTP_OK: /* check that number of coins returned matches */ if (GNUNET_OK != TALER_TESTING_get_trait_array_length (reveal_cmd, &num_fresh_coins)) { GNUNET_break (0); TALER_TESTING_interpreter_fail (rls->is); return; } if (lr->details.ok.num_coins != *num_fresh_coins) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected number of fresh coins: %d vs %d in %s:%u\n", lr->details.ok.num_coins, *num_fresh_coins, __FILE__, __LINE__); TALER_TESTING_interpreter_fail (rls->is); return; } /* check that the coins match */ for (unsigned int i = 0; idetails.ok.num_coins; i++) for (unsigned int j = i + 1; jdetails.ok.num_coins; j++) if (0 == GNUNET_memcmp (&lr->details.ok.coins[i].coin_priv, &lr->details.ok.coins[j].coin_priv)) GNUNET_break (0); /* Note: coins might be legitimately permutated in here... */ found = 0; /* Will point to the pointer inside the cmd state. */ { const struct TALER_TESTING_FreshCoinData **fc = NULL; if (GNUNET_OK != TALER_TESTING_get_trait_fresh_coins (reveal_cmd, &fc)) { GNUNET_break (0); TALER_TESTING_interpreter_fail (rls->is); return; } for (unsigned int i = 0; idetails.ok.num_coins; i++) { const struct TALER_EXCHANGE_LinkedCoinInfo *lci_i = &lr->details.ok.coins[i]; for (unsigned int j = 0; jdetails.ok.num_coins; j++) { const struct TALER_TESTING_FreshCoinData *fcj = &(*fc)[j]; if ( (0 == GNUNET_memcmp (&fcj->coin_priv, &lci_i->coin_priv)) && (0 == TALER_denom_sig_cmp (&fcj->sig, &lci_i->sig)) && (0 == TALER_denom_pub_cmp (&fcj->pk->key, &lci_i->pub)) ) { found++; break; } } /* for j*/ } /* for i */ } if (found != lr->details.ok.num_coins) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Only %u/%u coins match expectations\n", found, lr->details.ok.num_coins); GNUNET_break (0); TALER_TESTING_interpreter_fail (rls->is); return; } if (0 != rls->total_backoff.rel_value_us) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Total link backoff for %s was %s\n", rls->cmd->label, GNUNET_STRINGS_relative_time_to_string (rls->total_backoff, true)); } break; default: GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unknown HTTP response code %u/%d.\n", hr->http_status, hr->ec); } TALER_TESTING_interpreter_next (rls->is); } /** * Run the command. * * @param cls closure. * @param cmd the command to execute. * @param is the interpreter state. */ static void refresh_link_run (void *cls, const struct TALER_TESTING_Command *cmd, struct TALER_TESTING_Interpreter *is) { struct RefreshLinkState *rls = cls; struct RefreshRevealState *rrs; struct RefreshMeltState *rms; const struct TALER_TESTING_Command *reveal_cmd; const struct TALER_TESTING_Command *melt_cmd; const struct TALER_TESTING_Command *coin_cmd; const char *exchange_url; rls->cmd = cmd; rls->is = is; exchange_url = TALER_TESTING_get_exchange_url (is); if (NULL == exchange_url) { GNUNET_break (0); return; } reveal_cmd = TALER_TESTING_interpreter_lookup_command (rls->is, rls->reveal_reference); if (NULL == reveal_cmd) { GNUNET_break (0); TALER_TESTING_interpreter_fail (rls->is); return; } rrs = reveal_cmd->cls; melt_cmd = TALER_TESTING_interpreter_lookup_command (rls->is, rrs->melt_reference); if (NULL == melt_cmd) { GNUNET_break (0); TALER_TESTING_interpreter_fail (rls->is); return; } /* find reserve_withdraw command */ GNUNET_assert (melt_cmd->run == &melt_run); rms = melt_cmd->cls; coin_cmd = TALER_TESTING_interpreter_lookup_command (rls->is, rms->coin_reference); if (NULL == coin_cmd) { GNUNET_break (0); TALER_TESTING_interpreter_fail (rls->is); return; } const struct TALER_CoinSpendPrivateKeyP *coin_priv; if (GNUNET_OK != TALER_TESTING_get_trait_coin_priv (coin_cmd, 0, &coin_priv)) { GNUNET_break (0); TALER_TESTING_interpreter_fail (rls->is); return; } /* finally, use private key from withdraw sign command */ rls->rlh = TALER_EXCHANGE_link ( TALER_TESTING_interpreter_get_context (is), exchange_url, coin_priv, rms->refresh_data.melt_age_commitment_proof, &link_cb, rls); if (NULL == rls->rlh) { GNUNET_break (0); TALER_TESTING_interpreter_fail (rls->is); return; } } /** * Free the state of the "refresh link" CMD, and possibly * cancel a operation thereof. * * @param cls closure * @param cmd the command which is being cleaned up. */ static void refresh_link_cleanup (void *cls, const struct TALER_TESTING_Command *cmd) { struct RefreshLinkState *rls = cls; if (NULL != rls->rlh) { TALER_TESTING_command_incomplete (rls->is, cmd->label); TALER_EXCHANGE_link_cancel (rls->rlh); rls->rlh = NULL; } if (NULL != rls->retry_task) { GNUNET_SCHEDULER_cancel (rls->retry_task); rls->retry_task = NULL; } GNUNET_free (rls); } /** * Task scheduled to re-try #melt_run. * * @param cls a `struct RefreshMeltState` */ static void do_melt_retry (void *cls) { struct RefreshMeltState *rms = cls; rms->retry_task = NULL; TALER_TESTING_touch_cmd (rms->is); melt_run (rms, NULL, rms->is); } /** * Callback for a "refresh melt" operation; checks if the HTTP * response code is okay and re-run the melt operation if the * CMD was set to do so. * * @param cls closure. * @param mr melt response details */ static void melt_cb (void *cls, const struct TALER_EXCHANGE_MeltResponse *mr) { struct RefreshMeltState *rms = cls; const struct TALER_EXCHANGE_HttpResponse *hr = &mr->hr; rms->rmh = NULL; if (rms->expected_response_code != hr->http_status) { if (0 != rms->do_retry) { rms->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 refresh melt 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) rms->backoff = GNUNET_TIME_UNIT_ZERO; else rms->backoff = GNUNET_TIME_randomized_backoff (rms->backoff, MAX_BACKOFF); rms->total_backoff = GNUNET_TIME_relative_add (rms->total_backoff, rms->backoff); TALER_TESTING_inc_tries (rms->is); rms->retry_task = GNUNET_SCHEDULER_add_delayed (rms->backoff, &do_melt_retry, rms); return; } } TALER_TESTING_unexpected_status_with_body (rms->is, hr->http_status, rms->expected_response_code, hr->reply); return; } if (MHD_HTTP_OK == hr->http_status) { rms->noreveal_index = mr->details.ok.noreveal_index; if (mr->details.ok.num_mbds != rms->num_fresh_coins) { GNUNET_break (0); TALER_TESTING_interpreter_fail (rms->is); return; } GNUNET_free (rms->mbds); rms->mbds = GNUNET_new_array ( mr->details.ok.num_mbds, struct TALER_EXCHANGE_MeltBlindingDetail); for (unsigned int i = 0; idetails.ok.num_mbds; i++) TALER_denom_ewv_copy (&rms->mbds[i].alg_value, &mr->details.ok.mbds[i].alg_value); } if (0 != rms->total_backoff.rel_value_us) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Total melt backoff for %s was %s\n", rms->cmd->label, GNUNET_STRINGS_relative_time_to_string (rms->total_backoff, true)); } if (rms->double_melt) { TALER_LOG_DEBUG ("Doubling the melt (%s)\n", rms->cmd->label); rms->rmh = TALER_EXCHANGE_melt ( TALER_TESTING_interpreter_get_context (rms->is), TALER_TESTING_get_exchange_url (rms->is), TALER_TESTING_get_keys (rms->is), &rms->rms, &rms->refresh_data, &melt_cb, rms); rms->double_melt = false; return; } TALER_TESTING_interpreter_next (rms->is); } /** * Run the command. * * @param cls closure. * @param cmd the command to execute. * @param is the interpreter state. */ static void melt_run (void *cls, const struct TALER_TESTING_Command *cmd, struct TALER_TESTING_Interpreter *is) { struct RefreshMeltState *rms = cls; unsigned int num_fresh_coins; const char *default_melt_fresh_amounts[] = { "EUR:1", "EUR:1", "EUR:1", "EUR:0.1", NULL }; const char **melt_fresh_amounts; rms->cmd = cmd; if (NULL == (melt_fresh_amounts = rms->melt_fresh_amounts)) melt_fresh_amounts = default_melt_fresh_amounts; rms->is = is; rms->noreveal_index = UINT16_MAX; TALER_refresh_master_setup_random (&rms->rms); for (num_fresh_coins = 0; NULL != melt_fresh_amounts[num_fresh_coins]; num_fresh_coins++) ; rms->num_fresh_coins = num_fresh_coins; rms->fresh_pks = GNUNET_new_array ( num_fresh_coins, struct TALER_EXCHANGE_DenomPublicKey); { struct TALER_Amount melt_amount; struct TALER_Amount fresh_amount; const struct TALER_AgeCommitmentProof *age_commitment_proof = NULL; const struct TALER_AgeCommitmentHash *h_age_commitment = NULL; const struct TALER_DenominationSignature *melt_sig; const struct TALER_EXCHANGE_DenomPublicKey *melt_denom_pub; const struct TALER_TESTING_Command *coin_command; bool age_restricted_denom; if (NULL == (coin_command = TALER_TESTING_interpreter_lookup_command ( is, rms->coin_reference))) { GNUNET_break (0); TALER_TESTING_interpreter_fail (rms->is); return; } if (GNUNET_OK != TALER_TESTING_get_trait_coin_priv (coin_command, 0, &rms->melt_priv)) { GNUNET_break (0); TALER_TESTING_interpreter_fail (rms->is); return; } if (GNUNET_OK != TALER_TESTING_get_trait_age_commitment_proof (coin_command, 0, &age_commitment_proof)) { GNUNET_break (0); TALER_TESTING_interpreter_fail (rms->is); return; } if (GNUNET_OK != TALER_TESTING_get_trait_h_age_commitment (coin_command, 0, &h_age_commitment)) { GNUNET_break (0); TALER_TESTING_interpreter_fail (rms->is); return; } if (GNUNET_OK != TALER_TESTING_get_trait_denom_sig (coin_command, 0, &melt_sig)) { GNUNET_break (0); TALER_TESTING_interpreter_fail (rms->is); return; } if (GNUNET_OK != TALER_TESTING_get_trait_denom_pub (coin_command, 0, &melt_denom_pub)) { GNUNET_break (0); TALER_TESTING_interpreter_fail (rms->is); return; } /* Melt amount starts with the melt fee of the old coin; we'll add the values and withdraw fees of the fresh coins next */ melt_amount = melt_denom_pub->fees.refresh; age_restricted_denom = melt_denom_pub->key.age_mask.bits != 0; GNUNET_assert (age_restricted_denom == (NULL != age_commitment_proof)); GNUNET_assert ((NULL == age_commitment_proof) || (0 < age_commitment_proof->commitment.num)); for (unsigned int i = 0; iis); return; } fresh_pk = TALER_TESTING_find_pk (TALER_TESTING_get_keys (rms->is), &fresh_amount, age_restricted_denom); if (NULL == fresh_pk) { GNUNET_break (0); /* Subroutine logs specific error */ TALER_TESTING_interpreter_fail (rms->is); return; } GNUNET_assert (0 <= TALER_amount_add (&melt_amount, &melt_amount, &fresh_amount)); GNUNET_assert (0 <= TALER_amount_add (&melt_amount, &melt_amount, &fresh_pk->fees.withdraw)); rms->fresh_pks[i] = *fresh_pk; /* Make a deep copy of the RSA key */ TALER_denom_pub_copy (&rms->fresh_pks[i].key, &fresh_pk->key); } /* end for */ rms->refresh_data.melt_priv = *rms->melt_priv; GNUNET_CRYPTO_eddsa_key_get_public (&rms->melt_priv->eddsa_priv, &rms->melt_pub.eddsa_pub); rms->refresh_data.melt_amount = melt_amount; rms->refresh_data.melt_sig = *melt_sig; rms->refresh_data.melt_pk = *melt_denom_pub; if (NULL != age_commitment_proof) { GNUNET_assert (NULL != h_age_commitment); rms->refresh_data.melt_age_commitment_proof = age_commitment_proof; rms->refresh_data.melt_h_age_commitment = h_age_commitment; } rms->refresh_data.fresh_pks = rms->fresh_pks; rms->refresh_data.fresh_pks_len = num_fresh_coins; GNUNET_assert (age_restricted_denom == (NULL != age_commitment_proof)); GNUNET_assert ((NULL == age_commitment_proof) || (0 < age_commitment_proof->commitment.num)); rms->che.type = TALER_EXCHANGE_CTT_MELT; rms->che.amount = melt_amount; if (NULL != age_commitment_proof) rms->che.details.melt.h_age_commitment = *h_age_commitment; else rms->che.details.melt.no_hac = true; rms->rmh = TALER_EXCHANGE_melt ( TALER_TESTING_interpreter_get_context (is), TALER_TESTING_get_exchange_url (is), TALER_TESTING_get_keys (is), &rms->rms, &rms->refresh_data, &melt_cb, rms); if (NULL == rms->rmh) { GNUNET_break (0); TALER_TESTING_interpreter_fail (rms->is); return; } } } /** * Free the "refresh melt" CMD state, and possibly cancel a * pending operation thereof. * * @param cls closure, must be a `struct RefreshMeltState`. * @param cmd the command which is being cleaned up. */ static void melt_cleanup (void *cls, const struct TALER_TESTING_Command *cmd) { struct RefreshMeltState *rms = cls; (void) cmd; if (NULL != rms->rmh) { TALER_TESTING_command_incomplete (rms->is, cmd->label); TALER_EXCHANGE_melt_cancel (rms->rmh); rms->rmh = NULL; } if (NULL != rms->retry_task) { GNUNET_SCHEDULER_cancel (rms->retry_task); rms->retry_task = NULL; } if (NULL != rms->fresh_pks) { for (unsigned int i = 0; i < rms->num_fresh_coins; i++) TALER_denom_pub_free (&rms->fresh_pks[i].key); GNUNET_free (rms->fresh_pks); } if (NULL != rms->mbds) { for (unsigned int i = 0; i < rms->num_fresh_coins; i++) TALER_denom_ewv_free (&rms->mbds[i].alg_value); GNUNET_free (rms->mbds); } GNUNET_free (rms->melt_fresh_amounts); GNUNET_free (rms); } /** * Offer internal data to the "refresh melt" CMD. * * @param cls closure. * @param[out] ret result (could be anything). * @param trait name of the trait. * @param index index number of the object to offer. * @return #GNUNET_OK on success. */ static enum GNUNET_GenericReturnValue melt_traits (void *cls, const void **ret, const char *trait, unsigned int index) { struct RefreshMeltState *rms = cls; if (index >= rms->num_fresh_coins) { GNUNET_break (0); return GNUNET_SYSERR; } { struct TALER_TESTING_Trait traits[] = { TALER_TESTING_make_trait_denom_pub (index, &rms->fresh_pks[index]), TALER_TESTING_make_trait_coin_priv (0, rms->melt_priv), TALER_TESTING_make_trait_coin_pub (0, &rms->melt_pub), TALER_TESTING_make_trait_coin_history (0, &rms->che), TALER_TESTING_make_trait_age_commitment_proof ( index, rms->refresh_data.melt_age_commitment_proof), TALER_TESTING_make_trait_h_age_commitment ( index, rms->refresh_data.melt_h_age_commitment), TALER_TESTING_make_trait_refresh_secret (&rms->rms), (NULL != rms->mbds) ? TALER_TESTING_make_trait_exchange_wd_value (index, &rms->mbds[index].alg_value) : TALER_TESTING_trait_end (), TALER_TESTING_trait_end () }; return TALER_TESTING_get_trait (traits, ret, trait, index); } } /** * Parse list of amounts for melt operation. * * @param[in,out] rms where to store the list * @param ap NULL-termianted list of amounts to be melted (one per fresh coin) * @return #GNUNET_OK on success */ static enum GNUNET_GenericReturnValue parse_amounts (struct RefreshMeltState *rms, va_list ap) { unsigned int len; unsigned int off; const char *amount; len = 0; off = 0; while (NULL != (amount = va_arg (ap, const char *))) { if (len == off) { struct TALER_Amount a; GNUNET_array_grow (rms->melt_fresh_amounts, len, off + 16); if (GNUNET_OK != TALER_string_to_amount (amount, &a)) { GNUNET_break (0); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to parse amount `%s' at index %u\n", amount, off); GNUNET_free (rms->melt_fresh_amounts); rms->melt_fresh_amounts = NULL; return GNUNET_SYSERR; } rms->melt_fresh_amounts[off++] = amount; } } if (0 == off) return GNUNET_OK; /* no amounts given == use defaults! */ /* ensure NULL-termination */ GNUNET_array_grow (rms->melt_fresh_amounts, len, off + 1); return GNUNET_OK; } struct TALER_TESTING_Command TALER_TESTING_cmd_melt (const char *label, const char *coin_reference, unsigned int expected_response_code, ...) { struct RefreshMeltState *rms; va_list ap; rms = GNUNET_new (struct RefreshMeltState); rms->coin_reference = coin_reference; rms->expected_response_code = expected_response_code; va_start (ap, expected_response_code); GNUNET_assert (GNUNET_OK == parse_amounts (rms, ap)); va_end (ap); { struct TALER_TESTING_Command cmd = { .label = label, .cls = rms, .run = &melt_run, .cleanup = &melt_cleanup, .traits = &melt_traits }; return cmd; } } struct TALER_TESTING_Command TALER_TESTING_cmd_melt_double (const char *label, const char *coin_reference, unsigned int expected_response_code, ...) { struct RefreshMeltState *rms; va_list ap; rms = GNUNET_new (struct RefreshMeltState); rms->coin_reference = coin_reference; rms->expected_response_code = expected_response_code; rms->double_melt = true; va_start (ap, expected_response_code); GNUNET_assert (GNUNET_OK == parse_amounts (rms, ap)); va_end (ap); { struct TALER_TESTING_Command cmd = { .label = label, .cls = rms, .run = &melt_run, .cleanup = &melt_cleanup, .traits = &melt_traits }; return cmd; } } struct TALER_TESTING_Command TALER_TESTING_cmd_melt_with_retry (struct TALER_TESTING_Command cmd) { struct RefreshMeltState *rms; GNUNET_assert (&melt_run == cmd.run); rms = cmd.cls; rms->do_retry = NUM_RETRIES; return cmd; } /** * Offer internal data from a "refresh reveal" CMD. * * @param cls closure. * @param[out] ret result (could be anything). * @param trait name of the trait. * @param index index number of the object to offer. * @return #GNUNET_OK on success. */ static enum GNUNET_GenericReturnValue refresh_reveal_traits (void *cls, const void **ret, const char *trait, unsigned int index) { struct RefreshRevealState *rrs = cls; if (index >= rrs->num_fresh_coins) return GNUNET_SYSERR; { struct TALER_TESTING_Trait traits[] = { TALER_TESTING_make_trait_coin_priv ( index, &rrs->fresh_coins[index].coin_priv), TALER_TESTING_make_trait_age_commitment_proof ( index, rrs->fresh_coins[index].age_commitment_proof), TALER_TESTING_make_trait_h_age_commitment ( index, &rrs->fresh_coins[index].h_age_commitment), TALER_TESTING_make_trait_denom_pub ( index, rrs->fresh_coins[index].pk), TALER_TESTING_make_trait_denom_sig ( index, &rrs->fresh_coins[index].sig), TALER_TESTING_make_trait_blinding_key ( index, &rrs->fresh_coins[index].blinding_key), TALER_TESTING_make_trait_array_length ( &rrs->num_fresh_coins), TALER_TESTING_make_trait_fresh_coins ( (const struct TALER_TESTING_FreshCoinData **) &rrs->fresh_coins), TALER_TESTING_make_trait_planchet_secrets (index, &rrs->psa[index]), TALER_TESTING_trait_end () }; return TALER_TESTING_get_trait (traits, ret, trait, index); } } struct TALER_TESTING_Command TALER_TESTING_cmd_refresh_reveal (const char *label, const char *melt_reference, unsigned int expected_response_code) { struct RefreshRevealState *rrs; rrs = GNUNET_new (struct RefreshRevealState); rrs->melt_reference = melt_reference; rrs->expected_response_code = expected_response_code; { struct TALER_TESTING_Command cmd = { .cls = rrs, .label = label, .run = &refresh_reveal_run, .cleanup = &refresh_reveal_cleanup, .traits = &refresh_reveal_traits }; return cmd; } } struct TALER_TESTING_Command TALER_TESTING_cmd_refresh_reveal_with_retry (struct TALER_TESTING_Command cmd) { struct RefreshRevealState *rrs; GNUNET_assert (&refresh_reveal_run == cmd.run); rrs = cmd.cls; rrs->do_retry = NUM_RETRIES; return cmd; } struct TALER_TESTING_Command TALER_TESTING_cmd_refresh_link (const char *label, const char *reveal_reference, unsigned int expected_response_code) { struct RefreshLinkState *rrs; rrs = GNUNET_new (struct RefreshLinkState); rrs->reveal_reference = reveal_reference; rrs->expected_response_code = expected_response_code; { struct TALER_TESTING_Command cmd = { .cls = rrs, .label = label, .run = &refresh_link_run, .cleanup = &refresh_link_cleanup }; return cmd; } } struct TALER_TESTING_Command TALER_TESTING_cmd_refresh_link_with_retry (struct TALER_TESTING_Command cmd) { struct RefreshLinkState *rls; GNUNET_assert (&refresh_link_run == cmd.run); rls = cmd.cls; rls->do_retry = NUM_RETRIES; return cmd; }