/* This file is part of TALER Copyright (C) 2014-2017 GNUnet e.V. and Inria 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 exchange/test_exchange_api.c * @brief testcase to test exchange's HTTP API interface * @author Sree Harsha Totakura * @author Christian Grothoff */ #include "platform.h" #include "taler_util.h" #include "taler_signatures.h" #include "taler_exchange_service.h" #include "taler_json_lib.h" #include #include #include "taler_bank_service.h" #include "taler_fakebank_lib.h" /** * Is the configuration file is set to include wire format 'test'? */ #define WIRE_TEST 1 /** * Is the configuration file is set to include wire format 'sepa'? */ #define WIRE_SEPA 1 /** * Account number of the exchange at the bank. */ #define EXCHANGE_ACCOUNT_NO 2 /** * Main execution context for the main loop. */ static struct GNUNET_CURL_Context *ctx; /** * Handle to access the exchange. */ static struct TALER_EXCHANGE_Handle *exchange; /** * Context for running the CURL event loop. */ static struct GNUNET_CURL_RescheduleContext *rc; /** * Handle to the exchange process. */ static struct GNUNET_OS_Process *exchanged; /** * Task run on timeout. */ static struct GNUNET_SCHEDULER_Task *timeout_task; /** * Handle to our fakebank. */ static struct TALER_FAKEBANK_Handle *fakebank; /** * Result of the testcases, #GNUNET_OK on success */ static int result; /** * Opcodes for the interpreter. */ enum OpCode { /** * Termination code, stops the interpreter loop (with success). */ OC_END = 0, /** * Add funds to a reserve by (faking) incoming wire transfer. */ OC_ADMIN_ADD_INCOMING, /** * Check status of a reserve. */ OC_WITHDRAW_STATUS, /** * Withdraw a coin from a reserve. */ OC_WITHDRAW_SIGN, /** * Deposit a coin (pay with it). */ OC_DEPOSIT, /** * Melt a (set of) coins. */ OC_REFRESH_MELT, /** * Complete melting session by withdrawing melted coins. */ OC_REFRESH_REVEAL, /** * Verify exchange's /refresh/link by linking original private key to * results from #OC_REFRESH_REVEAL step. */ OC_REFRESH_LINK, /** * Verify the exchange's /wire-method. */ OC_WIRE, /** * Verify exchange's /track/transfer method. */ OC_WIRE_DEPOSITS, /** * Verify exchange's /track/transaction method. */ OC_DEPOSIT_WTID, /** * Run the aggregator to execute deposits. */ OC_RUN_AGGREGATOR, /** * Run the wirewatcher to check for incoming transactions. */ OC_RUN_WIREWATCH, /** * Check that the fakebank has received a certain transaction. */ OC_CHECK_BANK_TRANSFER, /** * Check that the fakebank has not received any other transactions. */ OC_CHECK_BANK_TRANSFERS_EMPTY, /** * Refund some deposit. */ OC_REFUND, /** * Revoke some denomination key. */ OC_REVOKE, /** * Payback some coin. */ OC_PAYBACK }; /** * Structure specifying details about a coin to be melted. * Used in a NULL-terminated array as part of command * specification. */ struct MeltDetails { /** * Amount to melt (including fee). */ const char *amount; /** * Reference to reserve_withdraw operations for coin to * be used for the /refresh/melt operation. */ const char *coin_ref; }; /** * Information about a fresh coin generated by the refresh operation. */ struct FreshCoin { /** * 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; }; /** * Details for a exchange operation to execute. */ struct Command { /** * Opcode of the command. */ enum OpCode oc; /** * Label for the command, can be NULL. */ const char *label; /** * Which response code do we expect for this command? */ unsigned int expected_response_code; /** * Details about the command. */ union { /** * Information for a #OC_ADMIN_ADD_INCOMING command. */ struct { /** * Label to another admin_add_incoming command if we * should deposit into an existing reserve, NULL if * a fresh reserve should be created. */ const char *reserve_reference; /** * String describing the amount to add to the reserve. */ const char *amount; /** * Wire transfer subject. NULL to use public key corresponding * to @e reserve_priv or @e reserve_reference. Should only be * set manually to test invalid wire transfer subjects. */ const char *subject; /** * Sender (debit) account number. */ uint64_t debit_account_no; /** * Receiver (credit) account number. */ uint64_t credit_account_no; /** * Username to use for authentication. */ const char *auth_username; /** * Password to use for authentication. */ const char *auth_password; /** * Set (by the interpreter) to the reserve's private key * we used to fill the reserve. */ struct TALER_ReservePrivateKeyP reserve_priv; /** * Set to the API's handle during the operation. */ struct TALER_BANK_AdminAddIncomingHandle *aih; /** * Set to the wire transfer's unique ID. */ uint64_t serial_id; } admin_add_incoming; /** * Information for a #OC_WITHDRAW_STATUS command. */ struct { /** * Label to the #OC_ADMIN_ADD_INCOMING command which * created the reserve. */ const char *reserve_reference; /** * Set to the API's handle during the operation. */ struct TALER_EXCHANGE_ReserveStatusHandle *wsh; /** * Expected reserve balance. */ const char *expected_balance; } reserve_status; /** * Information for a #OC_WITHDRAW_SIGN command. */ struct { /** * Which reserve should we withdraw from? */ const char *reserve_reference; /** * String describing the denomination value we should withdraw. * A corresponding denomination key must exist in the exchange's * offerings. Can be NULL if @e pk is set instead. */ const char *amount; /** * 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; /** * Private key material of the coin, set by the interpreter. */ struct TALER_PlanchetSecretsP ps; /** * Withdraw handle (while operation is running). */ struct TALER_EXCHANGE_ReserveWithdrawHandle *wsh; } reserve_withdraw; /** * Information for a #OC_DEPOSIT command. */ struct { /** * Amount to deposit. */ const char *amount; /** * Reference to a reserve_withdraw operation for a coin to * be used for the /deposit operation. */ const char *coin_ref; /** * If this @e coin_ref refers to an operation that generated * an array of coins, this value determines which coin to use. */ unsigned int coin_idx; /** * JSON string describing the merchant's "wire details". */ const char *wire_details; /** * JSON string describing what a proposal is about. */ const char *contract_terms; /** * Relative time (to add to 'now') to compute the refund deadline. * Zero for no refunds. */ struct GNUNET_TIME_Relative refund_deadline; /** * Set (by the interpreter) to a fresh private key of the merchant, * if @e refund_deadline is non-zero. */ struct TALER_MerchantPrivateKeyP merchant_priv; /** * Deposit handle while operation is running. */ struct TALER_EXCHANGE_DepositHandle *dh; } deposit; /** * Information for a #OC_REFRESH_MELT command. */ struct { /** * Information about coins to be melted. */ struct MeltDetails melted_coin; /** * Denominations of the fresh coins to withdraw. */ const char **fresh_amounts; /** * Array of the public keys corresponding to * the @e fresh_amounts, set by the interpreter. */ const struct TALER_EXCHANGE_DenomPublicKey **fresh_pks; /** * Melt handle while operation is running. */ struct TALER_EXCHANGE_RefreshMeltHandle *rmh; /** * Data used in the refresh operation, set by the interpreter. */ char *refresh_data; /** * Number of bytes in @e refresh_data, set by the interpreter. */ size_t refresh_data_length; /** * Set by the interpreter (upon completion) to the noreveal * index selected by the exchange. */ uint16_t noreveal_index; } refresh_melt; /** * Information for a #OC_REFRESH_REVEAL command. */ struct { /** * Melt operation this is the matching reveal for. */ const char *melt_ref; /** * Reveal handle while operation is running. */ struct TALER_EXCHANGE_RefreshRevealHandle *rrh; /** * Number of fresh coins withdrawn, set by the interpreter. * Length of the @e fresh_coins array. */ unsigned int num_fresh_coins; /** * Information about coins withdrawn, set by the interpreter. */ struct FreshCoin *fresh_coins; } refresh_reveal; /** * Information for a #OC_REFRESH_LINK command. */ struct { /** * Reveal operation this is the matching link for. */ const char *reveal_ref; /** * Link handle while operation is running. */ struct TALER_EXCHANGE_RefreshLinkHandle *rlh; /** * Which of the melted coins should be used for the linkage? */ unsigned int coin_idx; } refresh_link; /** * Information for the /wire command. */ struct { /** * Handle to the wire request. */ struct TALER_EXCHANGE_WireHandle *wh; /** * Format we expect to see, others will be *ignored*. */ const char *format; /** * Expected wire fee. */ const char *expected_fee; } wire; /** * Information for the /track/transfer's command. */ struct { /** * Handle to the wire deposits request. */ struct TALER_EXCHANGE_TrackTransferHandle *wdh; /** * Reference to a command providing a WTID. If set, we use the * WTID from that command. The command can be either an * #OC_DEPOSIT_WTID or an #OC_CHECK_BANK_TRANSFER. In the * case of the bank transfer, we check that the total amount * claimed by the exchange matches the total amount transferred * by the bank. In the case of a /track/transaction, we check * that the wire details match. */ const char *wtid_ref; /** * WTID to use (used if @e wtid_ref is NULL). */ struct TALER_WireTransferIdentifierRawP wtid; /** * What is the expected total amount? Only used if * @e expected_response_code was #MHD_HTTP_OK. */ const char *total_amount_expected; /** * What is the expected wire fee? Only used if * @e expected_response_code was #MHD_HTTP_OK. */ const char *wire_fee_expected; /* TODO: may want to add list of deposits we expected to see aggregated here in the future. */ } wire_deposits; /** * Information for the /track/transaction command. */ struct { /** * Handle to the deposit wtid request. */ struct TALER_EXCHANGE_TrackTransactionHandle *dwh; /** * Which /deposit operation should we obtain WTID data for? */ const char *deposit_ref; /** * Which #OC_CHECK_BANK_TRANSFER wtid should this match? NULL for none. */ const char *bank_transfer_ref; /** * Wire transfer identifier, set if #MHD_HTTP_OK was the response code. */ struct TALER_WireTransferIdentifierRawP wtid; } deposit_wtid; struct { /** * Process for the aggregator. */ struct GNUNET_OS_Process *aggregator_proc; /** * ID of task called whenever we get a SIGCHILD. */ struct GNUNET_SCHEDULER_Task *child_death_task; } run_aggregator; struct { /** * Process for the wirewatcher. */ struct GNUNET_OS_Process *wirewatch_proc; /** * ID of task called whenever we get a SIGCHILD. */ struct GNUNET_SCHEDULER_Task *child_death_task; } run_wirewatch; struct { /** * Which amount do we expect to see transferred? */ const char *amount; /** * Which account do we expect to be debited? */ uint64_t account_debit; /** * Which account do we expect to be credited? */ uint64_t account_credit; /** * Which exchange base URL is expected? */ const char *exchange_base_url; /** * Set (!) to the wire transfer subject observed. */ char *subject; } check_bank_transfer; struct { /** * Amount that should be refunded. */ const char *amount; /** * Expected refund fee. */ const char *fee; /** * Reference to the corresponding deposit operation. * Used to obtain proposal details, merchant keys, * fee structure, etc. */ const char *deposit_ref; /** * Refund transaction identifier. */ uint64_t rtransaction_id; /** * Handle to the refund operation (while it is ongoing). */ struct TALER_EXCHANGE_RefundHandle *rh; } refund; struct { /** * Reference to a _coin's_ withdraw operation where the coin's denomination key * is the denomination key to be revoked. */ const char *ref; /** * Process for the revocation process. */ struct GNUNET_OS_Process *revoke_proc; /** * ID of task called whenever we get a SIGCHILD. */ struct GNUNET_SCHEDULER_Task *child_death_task; } revoke; struct { /** * Reference to the _coin's_ withdraw operation. */ const char *ref; /** * Amount that should be paid back. */ const char *amount; /** * Handle to the ongoing /payback operation. */ struct TALER_EXCHANGE_PaybackHandle *ph; } payback; } details; }; /** * State of the interpreter loop. */ struct InterpreterState { /** * Keys from the exchange. */ const struct TALER_EXCHANGE_Keys *keys; /** * Commands the interpreter will run. */ struct Command *commands; /** * Interpreter task (if one is scheduled). */ struct GNUNET_SCHEDULER_Task *task; /** * Instruction pointer. Tells #interpreter_run() which * instruction to run next. */ unsigned int ip; }; /** * Pipe used to communicate child death via signal. */ static struct GNUNET_DISK_PipeHandle *sigpipe; /** * The testcase failed, return with an error code. * * @param is interpreter state to clean up */ static void fail (struct InterpreterState *is) { result = GNUNET_SYSERR; GNUNET_SCHEDULER_shutdown (); } /** * Find a command by label. * * @param is interpreter state to search * @param label label to look for * @return NULL if command was not found */ static const struct Command * find_command (const struct InterpreterState *is, const char *label) { unsigned int i; const struct Command *cmd; if (NULL == label) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Attempt to lookup command for empty label\n"); return NULL; } for (i=0;OC_END != (cmd = &is->commands[i])->oc;i++) if ( (NULL != cmd->label) && (0 == strcmp (cmd->label, label)) ) return cmd; GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Command not found: %s\n", label); return NULL; } /** * Run the main interpreter loop that performs exchange operations. * * @param cls contains the `struct InterpreterState` */ static void interpreter_run (void *cls); /** * Run the next command with the interpreter. * * @param is current interpeter state. */ static void next_command (struct InterpreterState *is) { if (GNUNET_SYSERR == result) return; /* ignore, we already failed! */ is->ip++; is->task = GNUNET_SCHEDULER_add_now (&interpreter_run, is); } /** * Function called upon completion of our /admin/add/incoming request. * * @param cls closure with the interpreter state * @param http_status HTTP response code, #MHD_HTTP_OK (200) for successful status request * 0 if the exchange's reply is bogus (fails to follow the protocol) * @param ec taler-specific error code, #TALER_EC_NONE on success * @param serial_id unique ID of the wire transfer * @param full_response full response from the exchange (for logging, in case of errors) */ static void add_incoming_cb (void *cls, unsigned int http_status, enum TALER_ErrorCode ec, uint64_t serial_id, const json_t *full_response) { struct InterpreterState *is = cls; struct Command *cmd = &is->commands[is->ip]; cmd->details.admin_add_incoming.aih = NULL; cmd->details.admin_add_incoming.serial_id = serial_id; if (MHD_HTTP_OK != http_status) { GNUNET_break (0); fail (is); return; } next_command (is); } /** * Check if the given historic event @a h corresponds to the given * command @a cmd. * * @param h event in history * @param cmd an #OC_ADMIN_ADD_INCOMING command * @return #GNUNET_OK if they match, #GNUNET_SYSERR if not */ static int compare_admin_add_incoming_history (const struct TALER_EXCHANGE_ReserveHistory *h, const struct Command *cmd) { struct TALER_Amount amount; if (TALER_EXCHANGE_RTT_DEPOSIT != h->type) { GNUNET_break_op (0); return GNUNET_SYSERR; } GNUNET_assert (GNUNET_OK == TALER_string_to_amount (cmd->details.admin_add_incoming.amount, &amount)); if (0 != TALER_amount_cmp (&amount, &h->amount)) { GNUNET_break_op (0); return GNUNET_SYSERR; } return GNUNET_OK; } /** * Check if the given historic event @a h corresponds to the given * command @a cmd. * * @param h event in history * @param cmd an #OC_WITHDRAW_SIGN command * @return #GNUNET_OK if they match, #GNUNET_SYSERR if not */ static int compare_reserve_withdraw_history (const struct TALER_EXCHANGE_ReserveHistory *h, const struct Command *cmd) { struct TALER_Amount amount; struct TALER_Amount amount_with_fee; if (TALER_EXCHANGE_RTT_WITHDRAWAL != h->type) { GNUNET_break_op (0); return GNUNET_SYSERR; } GNUNET_assert (GNUNET_OK == TALER_string_to_amount (cmd->details.reserve_withdraw.amount, &amount)); GNUNET_assert (GNUNET_OK == TALER_amount_add (&amount_with_fee, &amount, &cmd->details.reserve_withdraw.pk->fee_withdraw)); if (0 != TALER_amount_cmp (&amount_with_fee, &h->amount)) { GNUNET_break_op (0); return GNUNET_SYSERR; } return GNUNET_OK; } /** * Check if the given historic event @a h corresponds to the given * command @a cmd. * * @param h event in history * @param cmd an #OC_WITHDRAW_SIGN command * @return #GNUNET_OK if they match, #GNUNET_SYSERR if not */ static int compare_reserve_payback_history (const struct TALER_EXCHANGE_ReserveHistory *h, const struct Command *cmd) { struct TALER_Amount amount; if (TALER_EXCHANGE_RTT_PAYBACK != h->type) { GNUNET_break_op (0); return GNUNET_SYSERR; } GNUNET_assert (GNUNET_OK == TALER_string_to_amount (cmd->details.payback.amount, &amount)); if (0 != TALER_amount_cmp (&amount, &h->amount)) { GNUNET_break_op (0); return GNUNET_SYSERR; } return GNUNET_OK; } /** * Function called with the result of a /reserve/status request. * * @param cls closure with the interpreter state * @param http_status HTTP response code, #MHD_HTTP_OK (200) for successful status request * 0 if the exchange's reply is bogus (fails to follow the protocol) * @param ec taler-specific error code, #TALER_EC_NONE on success * @param[in] json original response in JSON format (useful only for diagnostics) * @param balance current balance in the reserve, NULL on error * @param history_length number of entries in the transaction history, 0 on error * @param history detailed transaction history, NULL on error */ static void reserve_status_cb (void *cls, unsigned int http_status, enum TALER_ErrorCode ec, const json_t *json, const struct TALER_Amount *balance, unsigned int history_length, const struct TALER_EXCHANGE_ReserveHistory *history) { struct InterpreterState *is = cls; struct Command *cmd = &is->commands[is->ip]; struct Command *rel; const struct Command *xrel; unsigned int i; unsigned int j; struct TALER_Amount amount; cmd->details.reserve_status.wsh = NULL; if (cmd->expected_response_code != http_status) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u to command %s\n", http_status, cmd->label); GNUNET_break (0); json_dumpf (json, stderr, 0); fail (is); return; } switch (http_status) { case MHD_HTTP_OK: /* FIXME: note that history events may come in a different order than the commands. However, for now this works... */ j = 0; for (i=0;iip;i++) { switch ((rel = &is->commands[i])->oc) { case OC_ADMIN_ADD_INCOMING: if ( ( (NULL != rel->label) && (0 == strcmp (cmd->details.reserve_status.reserve_reference, rel->label) ) ) || ( (NULL != rel->details.admin_add_incoming.reserve_reference) && (0 == strcmp (cmd->details.reserve_status.reserve_reference, rel->details.admin_add_incoming.reserve_reference) ) ) ) { if ( (j >= history_length) || (GNUNET_OK != compare_admin_add_incoming_history (&history[j], rel)) ) { GNUNET_break (0); fail (is); return; } j++; } break; case OC_WITHDRAW_SIGN: if (0 == strcmp (cmd->details.reserve_status.reserve_reference, rel->details.reserve_withdraw.reserve_reference)) { if ( (j >= history_length) || (GNUNET_OK != compare_reserve_withdraw_history (&history[j], rel)) ) { GNUNET_break (0); fail (is); return; } j++; } break; case OC_PAYBACK: xrel = find_command (is, rel->details.payback.ref); GNUNET_assert (NULL != xrel); if (0 == strcmp (cmd->details.reserve_status.reserve_reference, xrel->details.reserve_withdraw.reserve_reference)) { if ( (j >= history_length) || (GNUNET_OK != compare_reserve_payback_history (&history[j], rel)) ) { GNUNET_break (0); fail (is); return; } j++; } break; default: /* unreleated, just skip */ break; } } if (j != history_length) { GNUNET_break (0); fail (is); return; } if (NULL != cmd->details.reserve_status.expected_balance) { GNUNET_assert (GNUNET_OK == TALER_string_to_amount (cmd->details.reserve_status.expected_balance, &amount)); if (0 != TALER_amount_cmp (&amount, balance)) { GNUNET_break (0); fail (is); return; } } break; default: /* Unsupported status code (by test harness) */ GNUNET_break (0); break; } next_command (is); } /** * Function called upon completion of our /reserve/withdraw request. * * @param cls closure with the interpreter state * @param http_status HTTP response code, #MHD_HTTP_OK (200) for successful status request * 0 if the exchange's reply is bogus (fails to follow the protocol) * @param ec taler-specific error code, #TALER_EC_NONE on success * @param sig signature over the coin, NULL on error * @param full_response full response from the exchange (for logging, in case of errors) */ static void reserve_withdraw_cb (void *cls, unsigned int http_status, enum TALER_ErrorCode ec, const struct TALER_DenominationSignature *sig, const json_t *full_response) { struct InterpreterState *is = cls; struct Command *cmd = &is->commands[is->ip]; cmd->details.reserve_withdraw.wsh = NULL; if (cmd->expected_response_code != http_status) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u to command %s\n", http_status, cmd->label); json_dumpf (full_response, stderr, 0); GNUNET_break (0); fail (is); return; } switch (http_status) { case MHD_HTTP_OK: if (NULL == sig) { GNUNET_break (0); fail (is); return; } cmd->details.reserve_withdraw.sig.rsa_signature = GNUNET_CRYPTO_rsa_signature_dup (sig->rsa_signature); break; case MHD_HTTP_FORBIDDEN: /* nothing to check */ break; case MHD_HTTP_NOT_FOUND: /* nothing to check */ break; default: /* Unsupported status code (by test harness) */ GNUNET_break (0); break; } next_command (is); } /** * Function called with the result of a /deposit operation. * * @param cls closure with the interpreter state * @param http_status HTTP response code, #MHD_HTTP_OK (200) for successful deposit; * 0 if the exchange's reply is bogus (fails to follow the protocol) * @param ec taler-specific error code, #TALER_EC_NONE on success * @param exchange_pub public key the exchange used for signing * @param obj the received JSON reply, should be kept as proof (and, in case of errors, * be forwarded to the customer) */ static void deposit_cb (void *cls, unsigned int http_status, enum TALER_ErrorCode ec, const struct TALER_ExchangePublicKeyP *exchange_pub, const json_t *obj) { struct InterpreterState *is = cls; struct Command *cmd = &is->commands[is->ip]; cmd->details.deposit.dh = NULL; if (cmd->expected_response_code != http_status) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u to command %s\n", http_status, cmd->label); json_dumpf (obj, stderr, 0); fail (is); return; } next_command (is); } /** * Function called with the result of the /refresh/melt operation. * * @param cls closure with the interpreter state * @param http_status HTTP response code, never #MHD_HTTP_OK (200) as for successful intermediate response this callback is skipped. * 0 if the exchange's reply is bogus (fails to follow the protocol) * @param ec taler-specific error code, #TALER_EC_NONE on success * @param noreveal_index choice by the exchange in the cut-and-choose protocol, * UINT16_MAX on error * @param exchange_pub public key the exchange used for signing * @param full_response full response from the exchange (for logging, in case of errors) */ static void melt_cb (void *cls, unsigned int http_status, enum TALER_ErrorCode ec, uint32_t noreveal_index, const struct TALER_ExchangePublicKeyP *exchange_pub, const json_t *full_response) { struct InterpreterState *is = cls; struct Command *cmd = &is->commands[is->ip]; cmd->details.refresh_melt.rmh = NULL; if (cmd->expected_response_code != http_status) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u to command %s\n", http_status, cmd->label); json_dumpf (full_response, stderr, 0); fail (is); return; } cmd->details.refresh_melt.noreveal_index = noreveal_index; next_command (is); } /** * Function called with the result of the /refresh/reveal operation. * * @param cls closure with the interpreter state * @param http_status HTTP response code, #MHD_HTTP_OK (200) for successful status request * 0 if the exchange's reply is bogus (fails to follow the protocol) * @param ec taler-specific error code, #TALER_EC_NONE on success * @param num_coins number of fresh coins created, length of the @a sigs and @a coin_privs arrays, 0 if the operation failed * @param coin_privs array of @a num_coins private keys for the coins that were created, NULL on error * @param sigs array of signature over @a num_coins coins, NULL on error * @param full_response full response from the exchange (for logging, in case of errors) */ static void reveal_cb (void *cls, unsigned int http_status, enum TALER_ErrorCode ec, unsigned int num_coins, const struct TALER_CoinSpendPrivateKeyP *coin_privs, const struct TALER_DenominationSignature *sigs, const json_t *full_response) { struct InterpreterState *is = cls; struct Command *cmd = &is->commands[is->ip]; const struct Command *ref; unsigned int i; cmd->details.refresh_reveal.rrh = NULL; if (cmd->expected_response_code != http_status) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u to command %s\n", http_status, cmd->label); json_dumpf (full_response, stderr, 0); fail (is); return; } ref = find_command (is, cmd->details.refresh_reveal.melt_ref); GNUNET_assert (NULL != ref); cmd->details.refresh_reveal.num_fresh_coins = num_coins; switch (http_status) { case MHD_HTTP_OK: cmd->details.refresh_reveal.fresh_coins = GNUNET_new_array (num_coins, struct FreshCoin); for (i=0;idetails.refresh_reveal.fresh_coins[i]; fc->pk = ref->details.refresh_melt.fresh_pks[i]; fc->coin_priv = coin_privs[i]; fc->sig.rsa_signature = GNUNET_CRYPTO_rsa_signature_dup (sigs[i].rsa_signature); } break; default: break; } next_command (is); } /** * Function called with the result of a /refresh/link operation. * * @param cls closure with the interpreter state * @param http_status HTTP response code, #MHD_HTTP_OK (200) for successful status request * 0 if the exchange's reply is bogus (fails to follow the protocol) * @param ec taler-specific error code, #TALER_EC_NONE on success * @param num_coins number of fresh coins created, length of the @a sigs and @a coin_privs arrays, 0 if the operation failed * @param coin_privs array of @a num_coins private keys for the coins that were created, NULL on error * @param sigs array of signature over @a num_coins coins, NULL on error * @param pubs array of public keys for the @a sigs, NULL on error * @param full_response full response from the exchange (for logging, in case of errors) */ static void link_cb (void *cls, unsigned int http_status, enum TALER_ErrorCode ec, unsigned int num_coins, const struct TALER_CoinSpendPrivateKeyP *coin_privs, const struct TALER_DenominationSignature *sigs, const struct TALER_DenominationPublicKey *pubs, const json_t *full_response) { struct InterpreterState *is = cls; struct Command *cmd = &is->commands[is->ip]; const struct Command *ref; unsigned int found; cmd->details.refresh_link.rlh = NULL; if (cmd->expected_response_code != http_status) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u to command %s\n", http_status, cmd->label); json_dumpf (full_response, stderr, 0); fail (is); return; } ref = find_command (is, cmd->details.refresh_link.reveal_ref); GNUNET_assert (NULL != ref); switch (http_status) { case MHD_HTTP_OK: /* check that number of coins returned matches */ if (num_coins != ref->details.refresh_reveal.num_fresh_coins) { GNUNET_break (0); fail (is); return; } /* check that the coins match */ for (unsigned int i=0;idetails.refresh_reveal.fresh_coins[j]; if ( (0 == memcmp (&coin_privs[i], &fc->coin_priv, sizeof (struct TALER_CoinSpendPrivateKeyP))) && (0 == GNUNET_CRYPTO_rsa_signature_cmp (fc->sig.rsa_signature, sigs[i].rsa_signature)) && (0 == GNUNET_CRYPTO_rsa_public_key_cmp (fc->pk->key.rsa_public_key, pubs[i].rsa_public_key)) ) { found++; break; } } if (found != num_coins) { fprintf (stderr, "Only %u/%u coins match expectations\n", found, num_coins); GNUNET_break (0); fail (is); return; } break; default: break; } next_command (is); } /** * Task triggered whenever we receive a SIGCHLD (child * process died). * * @param cls closure, NULL if we need to self-restart */ static void maint_child_death (void *cls) { struct InterpreterState *is = cls; struct Command *cmd = &is->commands[is->ip]; const struct GNUNET_DISK_FileHandle *pr; char c[16]; switch (cmd->oc) { case OC_RUN_AGGREGATOR: cmd->details.run_aggregator.child_death_task = NULL; pr = GNUNET_DISK_pipe_handle (sigpipe, GNUNET_DISK_PIPE_END_READ); GNUNET_break (0 < GNUNET_DISK_file_read (pr, &c, sizeof (c))); GNUNET_OS_process_wait (cmd->details.run_aggregator.aggregator_proc); GNUNET_OS_process_destroy (cmd->details.run_aggregator.aggregator_proc); cmd->details.run_aggregator.aggregator_proc = NULL; break; case OC_RUN_WIREWATCH: cmd->details.run_wirewatch.child_death_task = NULL; pr = GNUNET_DISK_pipe_handle (sigpipe, GNUNET_DISK_PIPE_END_READ); GNUNET_break (0 < GNUNET_DISK_file_read (pr, &c, sizeof (c))); GNUNET_OS_process_wait (cmd->details.run_wirewatch.wirewatch_proc); GNUNET_OS_process_destroy (cmd->details.run_wirewatch.wirewatch_proc); cmd->details.run_wirewatch.wirewatch_proc = NULL; break; case OC_REVOKE: cmd->details.revoke.child_death_task = NULL; pr = GNUNET_DISK_pipe_handle (sigpipe, GNUNET_DISK_PIPE_END_READ); GNUNET_break (0 < GNUNET_DISK_file_read (pr, &c, sizeof (c))); GNUNET_OS_process_wait (cmd->details.revoke.revoke_proc); GNUNET_OS_process_destroy (cmd->details.revoke.revoke_proc); cmd->details.revoke.revoke_proc = NULL; /* trigger reload of denomination key information */ GNUNET_break (0 == GNUNET_OS_process_kill (exchanged, SIGUSR1)); sleep (5); /* make sure signal was received and processed */ break; default: GNUNET_break (0); fail (is); return; } next_command (is); } /** * Find denomination key matching the given amount. * * @param keys array of keys to search * @param amount coin value to look for * @return NULL if no matching key was found */ static const struct TALER_EXCHANGE_DenomPublicKey * find_pk (const struct TALER_EXCHANGE_Keys *keys, const struct TALER_Amount *amount) { unsigned int i; struct GNUNET_TIME_Absolute now; struct TALER_EXCHANGE_DenomPublicKey *pk; char *str; now = GNUNET_TIME_absolute_get (); for (i=0;inum_denom_keys;i++) { pk = &keys->denom_keys[i]; if ( (0 == TALER_amount_cmp (amount, &pk->value)) && (now.abs_value_us >= pk->valid_from.abs_value_us) && (now.abs_value_us < pk->withdraw_valid_until.abs_value_us) ) return pk; } /* do 2nd pass to check if expiration times are to blame for failure */ str = TALER_amount_to_string (amount); for (i=0;inum_denom_keys;i++) { pk = &keys->denom_keys[i]; if ( (0 == TALER_amount_cmp (amount, &pk->value)) && ( (now.abs_value_us < pk->valid_from.abs_value_us) || (now.abs_value_us > pk->withdraw_valid_until.abs_value_us) ) ) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Have denomination key for `%s', but with wrong expiration range %llu vs [%llu,%llu)\n", str, (unsigned long long) now.abs_value_us, (unsigned long long) pk->valid_from.abs_value_us, (unsigned long long) pk->withdraw_valid_until.abs_value_us); GNUNET_free (str); return NULL; } } GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "No denomination key for amount %s found\n", str); GNUNET_free (str); return NULL; } /** * Function called with information about the wire fees * for each wire method. * * @param cls closure * @param wire_method name of the wire method (i.e. "sepa") * @param fees fee structure for this method */ static void check_fee_cb (void *cls, const char *wire_method, const struct TALER_EXCHANGE_WireAggregateFees *fees) { struct InterpreterState *is = cls; struct Command *cmd = &is->commands[is->ip]; struct TALER_Amount expected_amount; GNUNET_break ( (0 == strcasecmp ("test", wire_method)) || (0 == strcasecmp ("sepa", wire_method)) ); if (GNUNET_OK != TALER_string_to_amount (cmd->details.wire.expected_fee, &expected_amount)) { GNUNET_break (0); fail (is); return; } while (NULL != fees) { if (0 != TALER_amount_cmp (&fees->wire_fee, &expected_amount)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Wire fee missmatch to command %s\n", cmd->label); fail (is); return; } fees = fees->next; } } /** * Callbacks called with the result(s) of a * wire format inquiry request to the exchange. * * @param cls closure with the interpreter state * @param http_status HTTP response code, #MHD_HTTP_OK (200) for successful request; * 0 if the exchange's reply is bogus (fails to follow the protocol) * @param ec taler-specific error code, #TALER_EC_NONE on success * @param obj the received JSON reply, if successful this should be the wire * format details as provided by /wire. */ static void wire_cb (void *cls, unsigned int http_status, enum TALER_ErrorCode ec, const json_t *obj) { struct InterpreterState *is = cls; struct Command *cmd = &is->commands[is->ip]; cmd->details.wire.wh = NULL; if (cmd->expected_response_code != http_status) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u to command %s\n", http_status, cmd->label); json_dumpf (obj, stderr, 0); fail (is); return; } switch (http_status) { case MHD_HTTP_OK: { json_t *method; method = json_object_get (obj, cmd->details.wire.format); if (NULL == method) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Expected method `%s' not included in response to command %s\n", cmd->details.wire.format, cmd->label); json_dumpf (obj, stderr, 0); fail (is); return; } if (GNUNET_OK != TALER_EXCHANGE_wire_get_fees (&TALER_EXCHANGE_get_keys (exchange)->master_pub, obj, &check_fee_cb, is)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Wire fee extraction in command %s failed\n", cmd->label); json_dumpf (obj, stderr, 0); fail (is); return; } } break; default: break; } next_command (is); } /** * Function called with detailed wire transfer data, including all * of the coin transactions that were combined into the wire transfer. * * @param cls closure * @param http_status HTTP status code we got, 0 on exchange protocol violation * @param ec taler-specific error code, #TALER_EC_NONE on success * @param exchange_pub public key the exchange used for signing * @param json original json reply (may include signatures, those have then been * validated already) * @param h_wire hash of the wire transfer address the transfer went to, or NULL on error * @param execution_time time when the exchange claims to have performed the wire transfer * @param total_amount total amount of the wire transfer, or NULL if the exchange could * not provide any @a wtid (set only if @a http_status is #MHD_HTTP_OK) * @param wire_fee wire fee that was charged by the exchange * @param details_length length of the @a details array * @param details array with details about the combined transactions */ static void wire_deposits_cb (void *cls, unsigned int http_status, enum TALER_ErrorCode ec, const struct TALER_ExchangePublicKeyP *exchange_pub, const json_t *json, const struct GNUNET_HashCode *h_wire, struct GNUNET_TIME_Absolute execution_time, const struct TALER_Amount *total_amount, const struct TALER_Amount *wire_fee, unsigned int details_length, const struct TALER_TrackTransferDetails *details) { struct InterpreterState *is = cls; struct Command *cmd = &is->commands[is->ip]; const struct Command *ref; struct TALER_Amount expected_amount; cmd->details.wire_deposits.wdh = NULL; if (cmd->expected_response_code != http_status) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u to command %s\n", http_status, cmd->label); json_dumpf (json, stderr, 0); fail (is); return; } switch (http_status) { case MHD_HTTP_OK: if (GNUNET_OK != TALER_string_to_amount (cmd->details.wire_deposits.total_amount_expected, &expected_amount)) { GNUNET_break (0); fail (is); return; } if (0 != TALER_amount_cmp (total_amount, &expected_amount)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Total amount missmatch to command %s\n", cmd->label); json_dumpf (json, stderr, 0); fail (is); return; } if (GNUNET_OK != TALER_string_to_amount (cmd->details.wire_deposits.wire_fee_expected, &expected_amount)) { GNUNET_break (0); fail (is); return; } if (0 != TALER_amount_cmp (wire_fee, &expected_amount)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Wire fee missmatch to command %s\n", cmd->label); json_dumpf (json, stderr, 0); fail (is); return; } ref = find_command (is, cmd->details.wire_deposits.wtid_ref); GNUNET_assert (NULL != ref); switch (ref->oc) { case OC_DEPOSIT_WTID: if (NULL != ref->details.deposit_wtid.deposit_ref) { const struct Command *dep; struct GNUNET_HashCode hw; json_t *wire; dep = find_command (is, ref->details.deposit_wtid.deposit_ref); GNUNET_assert (NULL != dep); wire = json_loads (dep->details.deposit.wire_details, JSON_REJECT_DUPLICATES, NULL); GNUNET_assert (GNUNET_OK == TALER_JSON_hash (wire, &hw)); json_decref (wire); if (0 != memcmp (&hw, h_wire, sizeof (struct GNUNET_HashCode))) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Wire hash missmatch to command %s\n", cmd->label); json_dumpf (json, stderr, 0); fail (is); return; } } break; case OC_CHECK_BANK_TRANSFER: if (GNUNET_OK != TALER_string_to_amount (ref->details.check_bank_transfer.amount, &expected_amount)) { GNUNET_break (0); fail (is); return; } if (0 != TALER_amount_cmp (total_amount, &expected_amount)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Total amount missmatch to command %s\n", cmd->label); json_dumpf (json, stderr, 0); fail (is); return; } break; default: GNUNET_break (0); fail (is); return; } break; default: break; } next_command (is); } /** * Function called with detailed wire transfer data. * * @param cls closure * @param http_status HTTP status code we got, 0 on exchange protocol violation * @param ec taler-specific error code, #TALER_EC_NONE on success * @param exchange_pub public key the exchange used for signing * @param json original json reply (may include signatures, those have then been * validated already) * @param wtid wire transfer identifier used by the exchange, NULL if exchange did not * yet execute the transaction * @param execution_time actual or planned execution time for the wire transfer * @param coin_contribution contribution to the @a total_amount of the deposited coin (may be NULL) * @param total_amount total amount of the wire transfer, or NULL if the exchange could * not provide any @a wtid (set only if @a http_status is #MHD_HTTP_OK) */ static void deposit_wtid_cb (void *cls, unsigned int http_status, enum TALER_ErrorCode ec, const struct TALER_ExchangePublicKeyP *exchange_pub, const json_t *json, const struct TALER_WireTransferIdentifierRawP *wtid, struct GNUNET_TIME_Absolute execution_time, const struct TALER_Amount *coin_contribution) { struct InterpreterState *is = cls; struct Command *cmd = &is->commands[is->ip]; cmd->details.deposit_wtid.dwh = NULL; if (cmd->expected_response_code != http_status) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u to command %s\n", http_status, cmd->label); json_dumpf (json, stderr, 0); fail (is); return; } switch (http_status) { case MHD_HTTP_OK: cmd->details.deposit_wtid.wtid = *wtid; if (NULL != cmd->details.deposit_wtid.bank_transfer_ref) { const struct Command *ref; char *ws; ws = GNUNET_STRINGS_data_to_string_alloc (wtid, sizeof (*wtid)); ref = find_command (is, cmd->details.deposit_wtid.bank_transfer_ref); GNUNET_assert (NULL != ref); if (0 != strcmp (ws, ref->details.check_bank_transfer.subject)) { GNUNET_break (0); GNUNET_free (ws); fail (is); return; } GNUNET_free (ws); } break; case MHD_HTTP_ACCEPTED: /* allowed, nothing to check here */ break; case MHD_HTTP_NOT_FOUND: /* allowed, nothing to check here */ break; default: GNUNET_break (0); break; } next_command (is); } /** * Check the result for the refund request. * * @param cls closure * @param http_status HTTP response code, #MHD_HTTP_OK (200) for successful deposit; * 0 if the exchange's reply is bogus (fails to follow the protocol) * @param ec taler-specific error code, #TALER_EC_NONE on success * @param exchange_pub public key the exchange used for signing @a obj * @param obj the received JSON reply, should be kept as proof (and, in particular, * be forwarded to the customer) */ static void refund_cb (void *cls, unsigned int http_status, enum TALER_ErrorCode ec, const struct TALER_ExchangePublicKeyP *exchange_pub, const json_t *obj) { struct InterpreterState *is = cls; struct Command *cmd = &is->commands[is->ip]; cmd->details.refund.rh = NULL; if (cmd->expected_response_code != http_status) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u to command %s\n", http_status, cmd->label); json_dumpf (obj, stderr, 0); fail (is); return; } switch (http_status) { case MHD_HTTP_OK: break; default: break; } next_command (is); } /** * Check the result of the payback request. * * @param cls closure * @param http_status HTTP response code, #MHD_HTTP_OK (200) for successful status request * 0 if the exchange's reply is bogus (fails to follow the protocol) * @param ec taler-specific error code, #TALER_EC_NONE on success * @param amount amount the exchange will wire back for this coin * @param timestamp what time did the exchange receive the /payback request * @param reserve_pub public key of the reserve receiving the payback * @param full_response full response from the exchange (for logging, in case of errors) */ static void payback_cb (void *cls, unsigned int http_status, enum TALER_ErrorCode ec, const struct TALER_Amount *amount, struct GNUNET_TIME_Absolute timestamp, const struct TALER_ReservePublicKeyP *reserve_pub, const json_t *full_response) { struct InterpreterState *is = cls; struct Command *cmd = &is->commands[is->ip]; const struct Command *withdraw; const struct Command *reserve; struct TALER_Amount expected_amount; struct TALER_ReservePublicKeyP rp; cmd->details.payback.ph = NULL; if (cmd->expected_response_code != http_status) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected response code %u to command %s\n", http_status, cmd->label); json_dumpf (full_response, stderr, 0); fail (is); return; } withdraw = find_command (is, cmd->details.payback.ref); GNUNET_assert (NULL != withdraw); reserve = find_command (is, withdraw->details.reserve_withdraw.reserve_reference); GNUNET_assert (NULL != reserve); GNUNET_CRYPTO_eddsa_key_get_public (&reserve->details.admin_add_incoming.reserve_priv.eddsa_priv, &rp.eddsa_pub); switch (http_status) { case MHD_HTTP_OK: if (GNUNET_OK != TALER_string_to_amount (cmd->details.payback.amount, &expected_amount)) { GNUNET_break (0); fail (is); return; } if (0 != TALER_amount_cmp (amount, &expected_amount)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Total amount missmatch to command %s\n", cmd->label); json_dumpf (full_response, stderr, 0); fail (is); return; } if (0 != memcmp (reserve_pub, &rp, sizeof (rp))) { GNUNET_break (0); fail (is); return; } break; default: break; } next_command (is); } /** * Given a command that is used to withdraw coins, * extract the corresponding public key of the coin. * * @param coin command relating to coin withdrawal or refresh * @param idx index to use if we got multiple coins from the @a coin command * @param[out] coin_pub where to store the public key of the coin */ static void get_public_key_from_coin_command (const struct Command *coin, unsigned int idx, struct TALER_CoinSpendPublicKeyP *coin_pub) { switch (coin->oc) { case OC_WITHDRAW_SIGN: GNUNET_CRYPTO_eddsa_key_get_public (&coin->details.reserve_withdraw.ps.coin_priv.eddsa_priv, &coin_pub->eddsa_pub); break; case OC_REFRESH_REVEAL: { const struct FreshCoin *fc; GNUNET_assert (idx < coin->details.refresh_reveal.num_fresh_coins); fc = &coin->details.refresh_reveal.fresh_coins[idx]; GNUNET_CRYPTO_eddsa_key_get_public (&fc->coin_priv.eddsa_priv, &coin_pub->eddsa_pub); } break; default: GNUNET_assert (0); } } /** * Run the main interpreter loop that performs exchange operations. * * @param cls contains the `struct InterpreterState` */ static void interpreter_run (void *cls) { struct InterpreterState *is = cls; struct Command *cmd = &is->commands[is->ip]; const struct Command *ref; struct TALER_ReservePublicKeyP reserve_pub; struct TALER_Amount amount; const struct GNUNET_SCHEDULER_TaskContext *tc; is->task = NULL; tc = GNUNET_SCHEDULER_get_task_context (); if (0 != (tc->reason & GNUNET_SCHEDULER_REASON_SHUTDOWN)) { fprintf (stderr, "Test aborted by shutdown request\n"); fail (is); return; } GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Running command `%s'\n", cmd->label); switch (cmd->oc) { case OC_END: result = GNUNET_OK; GNUNET_SCHEDULER_shutdown (); return; case OC_ADMIN_ADD_INCOMING: { char *subject; struct TALER_BANK_AuthenticationData auth; if (NULL != cmd->details.admin_add_incoming.subject) { subject = GNUNET_strdup (cmd->details.admin_add_incoming.subject); } else { /* Use reserve public key as subject */ if (NULL != cmd->details.admin_add_incoming.reserve_reference) { ref = find_command (is, cmd->details.admin_add_incoming.reserve_reference); GNUNET_assert (NULL != ref); GNUNET_assert (OC_ADMIN_ADD_INCOMING == ref->oc); cmd->details.admin_add_incoming.reserve_priv = ref->details.admin_add_incoming.reserve_priv; } else { struct GNUNET_CRYPTO_EddsaPrivateKey *priv; priv = GNUNET_CRYPTO_eddsa_key_create (); cmd->details.admin_add_incoming.reserve_priv.eddsa_priv = *priv; GNUNET_free (priv); } GNUNET_CRYPTO_eddsa_key_get_public (&cmd->details.admin_add_incoming.reserve_priv.eddsa_priv, &reserve_pub.eddsa_pub); subject = GNUNET_STRINGS_data_to_string_alloc (&reserve_pub, sizeof (reserve_pub)); } if (GNUNET_OK != TALER_string_to_amount (cmd->details.admin_add_incoming.amount, &amount)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to parse amount `%s' at %u\n", cmd->details.admin_add_incoming.amount, is->ip); fail (is); return; } auth.method = TALER_BANK_AUTH_BASIC; auth.details.basic.username = (char *) cmd->details.admin_add_incoming.auth_username; auth.details.basic.password = (char *) cmd->details.admin_add_incoming.auth_password; cmd->details.admin_add_incoming.aih = TALER_BANK_admin_add_incoming (ctx, "http://localhost:8082/", /* bank URL */ &auth, "https://exchange.com/", /* exchange URL */ subject, &amount, cmd->details.admin_add_incoming.debit_account_no, cmd->details.admin_add_incoming.credit_account_no, &add_incoming_cb, is); GNUNET_free (subject); if (NULL == cmd->details.admin_add_incoming.aih) { GNUNET_break (0); fail (is); return; } } return; case OC_WITHDRAW_STATUS: GNUNET_assert (NULL != cmd->details.reserve_status.reserve_reference); ref = find_command (is, cmd->details.reserve_status.reserve_reference); GNUNET_assert (NULL != ref); GNUNET_assert (OC_ADMIN_ADD_INCOMING == ref->oc); GNUNET_CRYPTO_eddsa_key_get_public (&ref->details.admin_add_incoming.reserve_priv.eddsa_priv, &reserve_pub.eddsa_pub); cmd->details.reserve_status.wsh = TALER_EXCHANGE_reserve_status (exchange, &reserve_pub, &reserve_status_cb, is); return; case OC_WITHDRAW_SIGN: GNUNET_assert (NULL != cmd->details.reserve_withdraw.reserve_reference); ref = find_command (is, cmd->details.reserve_withdraw.reserve_reference); GNUNET_assert (NULL != ref); GNUNET_assert (OC_ADMIN_ADD_INCOMING == ref->oc); if (NULL != cmd->details.reserve_withdraw.amount) { if (GNUNET_OK != TALER_string_to_amount (cmd->details.reserve_withdraw.amount, &amount)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to parse amount `%s' at %u\n", cmd->details.reserve_withdraw.amount, is->ip); fail (is); return; } cmd->details.reserve_withdraw.pk = find_pk (is->keys, &amount); } if (NULL == cmd->details.reserve_withdraw.pk) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to determine denomination key at %u\n", is->ip); fail (is); return; } TALER_planchet_setup_random (&cmd->details.reserve_withdraw.ps); cmd->details.reserve_withdraw.wsh = TALER_EXCHANGE_reserve_withdraw (exchange, cmd->details.reserve_withdraw.pk, &ref->details.admin_add_incoming.reserve_priv, &cmd->details.reserve_withdraw.ps, &reserve_withdraw_cb, is); if (NULL == cmd->details.reserve_withdraw.wsh) { GNUNET_break (0); fail (is); return; } return; case OC_DEPOSIT: { struct GNUNET_HashCode h_contract_terms; const struct TALER_CoinSpendPrivateKeyP *coin_priv; const struct TALER_EXCHANGE_DenomPublicKey *coin_pk; const struct TALER_DenominationSignature *coin_pk_sig; struct TALER_CoinSpendPublicKeyP coin_pub; struct TALER_CoinSpendSignatureP coin_sig; struct GNUNET_TIME_Absolute refund_deadline; struct GNUNET_TIME_Absolute wire_deadline; struct GNUNET_TIME_Absolute timestamp; struct GNUNET_CRYPTO_EddsaPrivateKey *priv; struct TALER_MerchantPublicKeyP merchant_pub; json_t *contract_terms; json_t *wire; GNUNET_assert (NULL != cmd->details.deposit.coin_ref); ref = find_command (is, cmd->details.deposit.coin_ref); GNUNET_assert (NULL != ref); switch (ref->oc) { case OC_WITHDRAW_SIGN: coin_priv = &ref->details.reserve_withdraw.ps.coin_priv; coin_pk = ref->details.reserve_withdraw.pk; coin_pk_sig = &ref->details.reserve_withdraw.sig; break; case OC_REFRESH_REVEAL: { const struct FreshCoin *fc; unsigned int idx; idx = cmd->details.deposit.coin_idx; GNUNET_assert (idx < ref->details.refresh_reveal.num_fresh_coins); fc = &ref->details.refresh_reveal.fresh_coins[idx]; coin_priv = &fc->coin_priv; coin_pk = fc->pk; coin_pk_sig = &fc->sig; } break; default: GNUNET_assert (0); } if (GNUNET_OK != TALER_string_to_amount (cmd->details.deposit.amount, &amount)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to parse amount `%s' at %u\n", cmd->details.deposit.amount, is->ip); fail (is); return; } contract_terms = json_loads (cmd->details.deposit.contract_terms, JSON_REJECT_DUPLICATES, NULL); if (NULL == contract_terms) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to parse proposal data `%s' at %u/%s\n", cmd->details.deposit.contract_terms, is->ip, cmd->label); fail (is); return; } GNUNET_assert (GNUNET_OK == TALER_JSON_hash (contract_terms, &h_contract_terms)); json_decref (contract_terms); wire = json_loads (cmd->details.deposit.wire_details, JSON_REJECT_DUPLICATES, NULL); if (NULL == wire) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to parse wire details `%s' at %u/%s\n", cmd->details.deposit.wire_details, is->ip, cmd->label); fail (is); return; } GNUNET_CRYPTO_eddsa_key_get_public (&coin_priv->eddsa_priv, &coin_pub.eddsa_pub); priv = GNUNET_CRYPTO_eddsa_key_create (); cmd->details.deposit.merchant_priv.eddsa_priv = *priv; GNUNET_free (priv); if (0 != cmd->details.deposit.refund_deadline.rel_value_us) { refund_deadline = GNUNET_TIME_relative_to_absolute (cmd->details.deposit.refund_deadline); wire_deadline = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_relative_multiply (cmd->details.deposit.refund_deadline, 2)); } else { refund_deadline = GNUNET_TIME_UNIT_ZERO_ABS; wire_deadline = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_ZERO); } GNUNET_CRYPTO_eddsa_key_get_public (&cmd->details.deposit.merchant_priv.eddsa_priv, &merchant_pub.eddsa_pub); timestamp = GNUNET_TIME_absolute_get (); GNUNET_TIME_round_abs (×tamp); GNUNET_TIME_round_abs (&refund_deadline); GNUNET_TIME_round_abs (&wire_deadline); { struct TALER_DepositRequestPS dr; memset (&dr, 0, sizeof (dr)); dr.purpose.size = htonl (sizeof (struct TALER_DepositRequestPS)); dr.purpose.purpose = htonl (TALER_SIGNATURE_WALLET_COIN_DEPOSIT); dr.h_contract_terms = h_contract_terms; GNUNET_assert (GNUNET_OK == TALER_JSON_hash (wire, &dr.h_wire)); dr.timestamp = GNUNET_TIME_absolute_hton (timestamp); dr.refund_deadline = GNUNET_TIME_absolute_hton (refund_deadline); TALER_amount_hton (&dr.amount_with_fee, &amount); TALER_amount_hton (&dr.deposit_fee, &coin_pk->fee_deposit); dr.merchant = merchant_pub; dr.coin_pub = coin_pub; GNUNET_assert (GNUNET_OK == GNUNET_CRYPTO_eddsa_sign (&coin_priv->eddsa_priv, &dr.purpose, &coin_sig.eddsa_signature)); } cmd->details.deposit.dh = TALER_EXCHANGE_deposit (exchange, &amount, wire_deadline, wire, &h_contract_terms, &coin_pub, coin_pk_sig, &coin_pk->key, timestamp, &merchant_pub, refund_deadline, &coin_sig, &deposit_cb, is); if (NULL == cmd->details.deposit.dh) { GNUNET_break (0); json_decref (wire); fail (is); return; } json_decref (wire); return; } case OC_REFRESH_MELT: { unsigned int num_fresh_coins; cmd->details.refresh_melt.noreveal_index = UINT16_MAX; for (num_fresh_coins=0; NULL != cmd->details.refresh_melt.fresh_amounts[num_fresh_coins]; num_fresh_coins++) ; cmd->details.refresh_melt.fresh_pks = GNUNET_new_array (num_fresh_coins, const struct TALER_EXCHANGE_DenomPublicKey *); { struct TALER_CoinSpendPrivateKeyP melt_priv; struct TALER_Amount melt_amount; struct TALER_DenominationSignature melt_sig; struct TALER_EXCHANGE_DenomPublicKey melt_pk; struct TALER_EXCHANGE_DenomPublicKey fresh_pks[num_fresh_coins]; unsigned int i; const struct MeltDetails *md = &cmd->details.refresh_melt.melted_coin; ref = find_command (is, md->coin_ref); GNUNET_assert (NULL != ref); GNUNET_assert (OC_WITHDRAW_SIGN == ref->oc); melt_priv = ref->details.reserve_withdraw.ps.coin_priv; if (GNUNET_OK != TALER_string_to_amount (md->amount, &melt_amount)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to parse amount `%s' at %u\n", md->amount, is->ip); fail (is); return; } melt_sig = ref->details.reserve_withdraw.sig; melt_pk = *ref->details.reserve_withdraw.pk; for (i=0;idetails.refresh_melt.fresh_amounts[i], &amount)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to parse amount `%s' at %u\n", cmd->details.reserve_withdraw.amount, is->ip); fail (is); return; } if (NULL == (cmd->details.refresh_melt.fresh_pks[i] = find_pk (is->keys, &amount))) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to find denomination key for amount `%s' at %u\n", cmd->details.reserve_withdraw.amount, is->ip); fail (is); return; } fresh_pks[i] = *cmd->details.refresh_melt.fresh_pks[i]; } cmd->details.refresh_melt.refresh_data = TALER_EXCHANGE_refresh_prepare (&melt_priv, &melt_amount, &melt_sig, &melt_pk, GNUNET_YES, num_fresh_coins, fresh_pks, &cmd->details.refresh_melt.refresh_data_length); if (NULL == cmd->details.refresh_melt.refresh_data) { GNUNET_break (0); fail (is); return; } cmd->details.refresh_melt.rmh = TALER_EXCHANGE_refresh_melt (exchange, cmd->details.refresh_melt.refresh_data_length, cmd->details.refresh_melt.refresh_data, &melt_cb, is); if (NULL == cmd->details.refresh_melt.rmh) { GNUNET_break (0); fail (is); return; } } } return; case OC_REFRESH_REVEAL: ref = find_command (is, cmd->details.refresh_reveal.melt_ref); GNUNET_assert (NULL != ref); cmd->details.refresh_reveal.rrh = TALER_EXCHANGE_refresh_reveal (exchange, ref->details.refresh_melt.refresh_data_length, ref->details.refresh_melt.refresh_data, ref->details.refresh_melt.noreveal_index, &reveal_cb, is); if (NULL == cmd->details.refresh_reveal.rrh) { GNUNET_break (0); fail (is); return; } return; case OC_REFRESH_LINK: /* find reveal command */ ref = find_command (is, cmd->details.refresh_link.reveal_ref); GNUNET_assert (NULL != ref); /* find melt command */ ref = find_command (is, ref->details.refresh_reveal.melt_ref); GNUNET_assert (NULL != ref); /* find reserve_withdraw command */ { const struct MeltDetails *md; md = &ref->details.refresh_melt.melted_coin; ref = find_command (is, md->coin_ref); GNUNET_assert (NULL != ref); } GNUNET_assert (OC_WITHDRAW_SIGN == ref->oc); /* finally, use private key from withdraw sign command */ cmd->details.refresh_link.rlh = TALER_EXCHANGE_refresh_link (exchange, &ref->details.reserve_withdraw.ps.coin_priv, &link_cb, is); if (NULL == cmd->details.refresh_link.rlh) { GNUNET_break (0); fail (is); return; } return; case OC_WIRE: cmd->details.wire.wh = TALER_EXCHANGE_wire (exchange, &wire_cb, is); return; case OC_WIRE_DEPOSITS: if (NULL != cmd->details.wire_deposits.wtid_ref) { ref = find_command (is, cmd->details.wire_deposits.wtid_ref); GNUNET_assert (NULL != ref); switch (ref->oc) { case OC_DEPOSIT_WTID: cmd->details.wire_deposits.wtid = ref->details.deposit_wtid.wtid; break; case OC_CHECK_BANK_TRANSFER: GNUNET_assert (GNUNET_OK == GNUNET_STRINGS_string_to_data (ref->details.check_bank_transfer.subject, strlen (ref->details.check_bank_transfer.subject), &cmd->details.wire_deposits.wtid, sizeof (cmd->details.wire_deposits.wtid))); break; default: GNUNET_break (0); fail (is); return; } } cmd->details.wire_deposits.wdh = TALER_EXCHANGE_track_transfer (exchange, &cmd->details.wire_deposits.wtid, &wire_deposits_cb, is); return; case OC_DEPOSIT_WTID: { struct GNUNET_HashCode h_wire; struct GNUNET_HashCode h_contract_terms; json_t *wire; json_t *contract_terms; const struct Command *coin; struct TALER_CoinSpendPublicKeyP coin_pub; ref = find_command (is, cmd->details.deposit_wtid.deposit_ref); GNUNET_assert (NULL != ref); coin = find_command (is, ref->details.deposit.coin_ref); GNUNET_assert (NULL != coin); get_public_key_from_coin_command (coin, ref->details.deposit.coin_idx, &coin_pub); wire = json_loads (ref->details.deposit.wire_details, JSON_REJECT_DUPLICATES, NULL); GNUNET_assert (NULL != wire); GNUNET_assert (GNUNET_OK == TALER_JSON_hash (wire, &h_wire)); json_decref (wire); contract_terms = json_loads (ref->details.deposit.contract_terms, JSON_REJECT_DUPLICATES, NULL); GNUNET_assert (NULL != contract_terms); GNUNET_assert (GNUNET_OK == TALER_JSON_hash (contract_terms, &h_contract_terms)); json_decref (contract_terms); cmd->details.deposit_wtid.dwh = TALER_EXCHANGE_track_transaction (exchange, &ref->details.deposit.merchant_priv, &h_wire, &h_contract_terms, &coin_pub, &deposit_wtid_cb, is); } return; case OC_RUN_AGGREGATOR: { const struct GNUNET_DISK_FileHandle *pr; cmd->details.run_aggregator.aggregator_proc = GNUNET_OS_start_process (GNUNET_NO, GNUNET_OS_INHERIT_STD_ALL, NULL, NULL, NULL, "taler-exchange-aggregator", "taler-exchange-aggregator", "-c", "test_exchange_api.conf", "-t", /* exit when done */ NULL); if (NULL == cmd->details.run_aggregator.aggregator_proc) { GNUNET_break (0); fail (is); return; } pr = GNUNET_DISK_pipe_handle (sigpipe, GNUNET_DISK_PIPE_END_READ); cmd->details.run_aggregator.child_death_task = GNUNET_SCHEDULER_add_read_file (GNUNET_TIME_UNIT_FOREVER_REL, pr, &maint_child_death, is); return; } case OC_RUN_WIREWATCH: { const struct GNUNET_DISK_FileHandle *pr; cmd->details.run_wirewatch.wirewatch_proc = GNUNET_OS_start_process (GNUNET_NO, GNUNET_OS_INHERIT_STD_ALL, NULL, NULL, NULL, "taler-exchange-wirewatch", "taler-exchange-wirewatch", "-c", "test_exchange_api.conf", "-t", "test", /* use Taler's bank/fakebank */ "-T", /* exit when done */ NULL); if (NULL == cmd->details.run_wirewatch.wirewatch_proc) { GNUNET_break (0); fail (is); return; } pr = GNUNET_DISK_pipe_handle (sigpipe, GNUNET_DISK_PIPE_END_READ); cmd->details.run_wirewatch.child_death_task = GNUNET_SCHEDULER_add_read_file (GNUNET_TIME_UNIT_FOREVER_REL, pr, &maint_child_death, is); return; } case OC_CHECK_BANK_TRANSFER: { if (GNUNET_OK != TALER_string_to_amount (cmd->details.check_bank_transfer.amount, &amount)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to parse amount `%s' at %u\n", cmd->details.reserve_withdraw.amount, is->ip); fail (is); return; } if (GNUNET_OK != TALER_FAKEBANK_check (fakebank, &amount, cmd->details.check_bank_transfer.account_debit, cmd->details.check_bank_transfer.account_credit, cmd->details.check_bank_transfer.exchange_base_url, &cmd->details.check_bank_transfer.subject)) { GNUNET_break (0); fail (is); return; } next_command (is); return; } case OC_CHECK_BANK_TRANSFERS_EMPTY: { if (GNUNET_OK != TALER_FAKEBANK_check_empty (fakebank)) { GNUNET_break (0); fail (is); return; } next_command (is); return; } case OC_REFUND: { const struct Command *coin; struct GNUNET_HashCode h_contract_terms; json_t *contract_terms; struct TALER_CoinSpendPublicKeyP coin_pub; struct TALER_Amount refund_fee; if (GNUNET_OK != TALER_string_to_amount (cmd->details.refund.amount, &amount)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to parse amount `%s' at %u\n", cmd->details.refund.amount, is->ip); fail (is); return; } if (GNUNET_OK != TALER_string_to_amount (cmd->details.refund.fee, &refund_fee)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to parse amount `%s' at %u\n", cmd->details.refund.fee, is->ip); fail (is); return; } ref = find_command (is, cmd->details.refund.deposit_ref); GNUNET_assert (NULL != ref); contract_terms = json_loads (ref->details.deposit.contract_terms, JSON_REJECT_DUPLICATES, NULL); GNUNET_assert (NULL != contract_terms); GNUNET_assert (GNUNET_OK == TALER_JSON_hash (contract_terms, &h_contract_terms)); json_decref (contract_terms); coin = find_command (is, ref->details.deposit.coin_ref); GNUNET_assert (NULL != coin); get_public_key_from_coin_command (coin, ref->details.deposit.coin_idx, &coin_pub); cmd->details.refund.rh = TALER_EXCHANGE_refund (exchange, &amount, &refund_fee, &h_contract_terms, &coin_pub, cmd->details.refund.rtransaction_id, &ref->details.deposit.merchant_priv, &refund_cb, is); if (NULL == cmd->details.refund.rh) { GNUNET_break (0); fail (is); return; } return; } case OC_REVOKE: { const struct GNUNET_DISK_FileHandle *pr; char *dhks; const struct Command *ref; ref = find_command (is, cmd->details.revoke.ref); GNUNET_assert (NULL != ref); dhks = GNUNET_STRINGS_data_to_string_alloc (&ref->details.reserve_withdraw.pk->h_key, sizeof (struct GNUNET_HashCode)); cmd->details.revoke.revoke_proc = GNUNET_OS_start_process (GNUNET_NO, GNUNET_OS_INHERIT_STD_ALL, NULL, NULL, NULL, "taler-exchange-keyup", "taler-exchange-keyup", "-c", "test_exchange_api.conf", "-r", dhks, NULL); GNUNET_free (dhks); if (NULL == cmd->details.revoke.revoke_proc) { GNUNET_break (0); fail (is); return; } pr = GNUNET_DISK_pipe_handle (sigpipe, GNUNET_DISK_PIPE_END_READ); cmd->details.revoke.child_death_task = GNUNET_SCHEDULER_add_read_file (GNUNET_TIME_UNIT_FOREVER_REL, pr, &maint_child_death, is); return; } case OC_PAYBACK: { const struct Command *ref; ref = find_command (is, cmd->details.payback.ref); GNUNET_assert (NULL != ref); cmd->details.payback.ph = TALER_EXCHANGE_payback (exchange, ref->details.reserve_withdraw.pk, &ref->details.reserve_withdraw.sig, &ref->details.reserve_withdraw.ps, &payback_cb, is); return; } default: GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unknown instruction %d at %u (%s)\n", cmd->oc, is->ip, cmd->label); fail (is); return; } } /** * Signal handler called for SIGCHLD. Triggers the * respective handler by writing to the trigger pipe. */ static void sighandler_child_death () { static char c; int old_errno = errno; /* back-up errno */ GNUNET_break (1 == GNUNET_DISK_file_write (GNUNET_DISK_pipe_handle (sigpipe, GNUNET_DISK_PIPE_END_WRITE), &c, sizeof (c))); errno = old_errno; /* restore errno */ } /** * Function run when the test terminates (good or bad) with timeout. * * @param cls NULL */ static void do_timeout (void *cls) { timeout_task = NULL; GNUNET_SCHEDULER_shutdown (); } /** * Function run when the test terminates (good or bad). * Cleans up our state. * * @param cls the interpreter state. */ static void do_shutdown (void *cls) { struct InterpreterState *is = cls; struct Command *cmd; fprintf (stderr, "Executing shutdown at `%s'\n", is->commands[is->ip].label); for (unsigned int i=0;OC_END != (cmd = &is->commands[i])->oc;i++) { switch (cmd->oc) { case OC_END: GNUNET_assert (0); break; case OC_ADMIN_ADD_INCOMING: if (NULL != cmd->details.admin_add_incoming.aih) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Command %u (%s) did not complete\n", i, cmd->label); TALER_BANK_admin_add_incoming_cancel (cmd->details.admin_add_incoming.aih); cmd->details.admin_add_incoming.aih = NULL; } break; case OC_WITHDRAW_STATUS: if (NULL != cmd->details.reserve_status.wsh) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Command %u (%s) did not complete\n", i, cmd->label); TALER_EXCHANGE_reserve_status_cancel (cmd->details.reserve_status.wsh); cmd->details.reserve_status.wsh = NULL; } break; case OC_WITHDRAW_SIGN: if (NULL != cmd->details.reserve_withdraw.wsh) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Command %u (%s) did not complete\n", i, cmd->label); TALER_EXCHANGE_reserve_withdraw_cancel (cmd->details.reserve_withdraw.wsh); cmd->details.reserve_withdraw.wsh = NULL; } if (NULL != cmd->details.reserve_withdraw.sig.rsa_signature) { GNUNET_CRYPTO_rsa_signature_free (cmd->details.reserve_withdraw.sig.rsa_signature); cmd->details.reserve_withdraw.sig.rsa_signature = NULL; } break; case OC_DEPOSIT: if (NULL != cmd->details.deposit.dh) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Command %u (%s) did not complete\n", i, cmd->label); TALER_EXCHANGE_deposit_cancel (cmd->details.deposit.dh); cmd->details.deposit.dh = NULL; } break; case OC_REFRESH_MELT: if (NULL != cmd->details.refresh_melt.rmh) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Command %u (%s) did not complete\n", i, cmd->label); TALER_EXCHANGE_refresh_melt_cancel (cmd->details.refresh_melt.rmh); cmd->details.refresh_melt.rmh = NULL; } GNUNET_free_non_null (cmd->details.refresh_melt.fresh_pks); cmd->details.refresh_melt.fresh_pks = NULL; GNUNET_free_non_null (cmd->details.refresh_melt.refresh_data); cmd->details.refresh_melt.refresh_data = NULL; cmd->details.refresh_melt.refresh_data_length = 0; break; case OC_REFRESH_REVEAL: if (NULL != cmd->details.refresh_reveal.rrh) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Command %u (%s) did not complete\n", i, cmd->label); TALER_EXCHANGE_refresh_reveal_cancel (cmd->details.refresh_reveal.rrh); cmd->details.refresh_reveal.rrh = NULL; } { unsigned int j; struct FreshCoin *fresh_coins; fresh_coins = cmd->details.refresh_reveal.fresh_coins; for (j=0;jdetails.refresh_reveal.num_fresh_coins;j++) GNUNET_CRYPTO_rsa_signature_free (fresh_coins[j].sig.rsa_signature); } GNUNET_free_non_null (cmd->details.refresh_reveal.fresh_coins); cmd->details.refresh_reveal.fresh_coins = NULL; cmd->details.refresh_reveal.num_fresh_coins = 0; break; case OC_REFRESH_LINK: if (NULL != cmd->details.refresh_link.rlh) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Command %u (%s) did not complete\n", i, cmd->label); TALER_EXCHANGE_refresh_link_cancel (cmd->details.refresh_link.rlh); cmd->details.refresh_link.rlh = NULL; } break; case OC_WIRE: if (NULL != cmd->details.wire.wh) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Command %u (%s) did not complete\n", i, cmd->label); TALER_EXCHANGE_wire_cancel (cmd->details.wire.wh); cmd->details.wire.wh = NULL; } break; case OC_WIRE_DEPOSITS: if (NULL != cmd->details.wire_deposits.wdh) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Command %u (%s) did not complete\n", i, cmd->label); TALER_EXCHANGE_track_transfer_cancel (cmd->details.wire_deposits.wdh); cmd->details.wire_deposits.wdh = NULL; } break; case OC_DEPOSIT_WTID: if (NULL != cmd->details.deposit_wtid.dwh) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Command %u (%s) did not complete\n", i, cmd->label); TALER_EXCHANGE_track_transaction_cancel (cmd->details.deposit_wtid.dwh); cmd->details.deposit_wtid.dwh = NULL; } break; case OC_RUN_AGGREGATOR: if (NULL != cmd->details.run_aggregator.aggregator_proc) { GNUNET_break (0 == GNUNET_OS_process_kill (cmd->details.run_aggregator.aggregator_proc, SIGKILL)); GNUNET_OS_process_wait (cmd->details.run_aggregator.aggregator_proc); GNUNET_OS_process_destroy (cmd->details.run_aggregator.aggregator_proc); cmd->details.run_aggregator.aggregator_proc = NULL; } if (NULL != cmd->details.run_aggregator.child_death_task) { GNUNET_SCHEDULER_cancel (cmd->details.run_aggregator.child_death_task); cmd->details.run_aggregator.child_death_task = NULL; } break; case OC_RUN_WIREWATCH: if (NULL != cmd->details.run_wirewatch.wirewatch_proc) { GNUNET_break (0 == GNUNET_OS_process_kill (cmd->details.run_wirewatch.wirewatch_proc, SIGKILL)); GNUNET_OS_process_wait (cmd->details.run_wirewatch.wirewatch_proc); GNUNET_OS_process_destroy (cmd->details.run_wirewatch.wirewatch_proc); cmd->details.run_wirewatch.wirewatch_proc = NULL; } if (NULL != cmd->details.run_wirewatch.child_death_task) { GNUNET_SCHEDULER_cancel (cmd->details.run_wirewatch.child_death_task); cmd->details.run_wirewatch.child_death_task = NULL; } break; case OC_CHECK_BANK_TRANSFER: GNUNET_free_non_null (cmd->details.check_bank_transfer.subject); cmd->details.check_bank_transfer.subject = NULL; break; case OC_CHECK_BANK_TRANSFERS_EMPTY: break; case OC_REFUND: if (NULL != cmd->details.refund.rh) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Command %u (%s) did not complete\n", i, cmd->label); TALER_EXCHANGE_refund_cancel (cmd->details.refund.rh); cmd->details.refund.rh = NULL; } break; case OC_REVOKE: if (NULL != cmd->details.revoke.revoke_proc) { GNUNET_break (0 == GNUNET_OS_process_kill (cmd->details.revoke.revoke_proc, SIGKILL)); GNUNET_OS_process_wait (cmd->details.revoke.revoke_proc); GNUNET_OS_process_destroy (cmd->details.revoke.revoke_proc); cmd->details.revoke.revoke_proc = NULL; } if (NULL != cmd->details.revoke.child_death_task) { GNUNET_SCHEDULER_cancel (cmd->details.revoke.child_death_task); cmd->details.revoke.child_death_task = NULL; } break; case OC_PAYBACK: if (NULL != cmd->details.payback.ph) { TALER_EXCHANGE_payback_cancel (cmd->details.payback.ph); cmd->details.payback.ph = NULL; } break; default: GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unknown instruction %d at %u (%s)\n", cmd->oc, i, cmd->label); break; } } if (NULL != is->task) { GNUNET_SCHEDULER_cancel (is->task); is->task = NULL; } GNUNET_free (is); if (NULL != fakebank) { TALER_FAKEBANK_stop (fakebank); fakebank = NULL; } if (NULL != exchange) { TALER_EXCHANGE_disconnect (exchange); exchange = NULL; } if (NULL != ctx) { GNUNET_CURL_fini (ctx); ctx = NULL; } if (NULL != rc) { GNUNET_CURL_gnunet_rc_destroy (rc); rc = NULL; } if (NULL != timeout_task) { GNUNET_SCHEDULER_cancel (timeout_task); timeout_task = NULL; } } /** * Functions of this type are called to provide the retrieved signing and * denomination keys of the exchange. No TALER_EXCHANGE_*() functions should be called * in this callback. * * @param cls closure * @param keys information about keys of the exchange * @param vc version compatibility */ static void cert_cb (void *cls, const struct TALER_EXCHANGE_Keys *keys, enum TALER_EXCHANGE_VersionCompatibility vc) { struct InterpreterState *is = cls; /* check that keys is OK */ #define ERR(cond) do { if(!(cond)) break; GNUNET_break (0); GNUNET_SCHEDULER_shutdown(); return; } while (0) ERR (NULL == keys); ERR (0 == keys->num_sign_keys); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Read %u signing keys\n", keys->num_sign_keys); ERR (0 == keys->num_denom_keys); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Read %u denomination keys\n", keys->num_denom_keys); #undef ERR /* run actual tests via interpreter-loop */ is->keys = keys; is->task = GNUNET_SCHEDULER_add_now (&interpreter_run, is); } /** * Main function that will be run by the scheduler. * * @param cls closure */ static void run (void *cls) { struct InterpreterState *is; static const char *melt_fresh_amounts_1[] = { "EUR:1", "EUR:1", "EUR:1", "EUR:0.1", "EUR:0.1", "EUR:0.1", "EUR:0.1", "EUR:0.1", "EUR:0.1", "EUR:0.1", "EUR:0.1", "EUR:0.01", "EUR:0.01", "EUR:0.01", "EUR:0.01", "EUR:0.01", "EUR:0.01", /* with 0.01 withdraw fees (except for 1ct coins), this totals up to exactly EUR:3.97, and with the 0.03 refresh fee, to EUR:4.0*/ NULL }; static struct Command commands[] = { /* *************** start of /wire testing ************** */ #if WIRE_TEST { .oc = OC_WIRE, .label = "wire-test", /* expecting 'test' method in response */ .expected_response_code = MHD_HTTP_OK, .details.wire.format = "test", .details.wire.expected_fee = "EUR:0.01" }, #endif #if WIRE_SEPA { .oc = OC_WIRE, .label = "wire-sepa", /* expecting 'sepa' method in response */ .expected_response_code = MHD_HTTP_OK, .details.wire.format = "sepa", .details.wire.expected_fee = "EUR:0.01" }, #endif /* *************** end of /wire testing ************** */ #if WIRE_TEST /* None of this works if 'test' is not allowed as we do /admin/add/incoming with format 'test' */ /* Fill reserve with EUR:5.01, as withdraw fee is 1 ct per config */ { .oc = OC_ADMIN_ADD_INCOMING, .label = "create-reserve-1", .expected_response_code = MHD_HTTP_OK, .details.admin_add_incoming.debit_account_no = 42, .details.admin_add_incoming.credit_account_no = EXCHANGE_ACCOUNT_NO, .details.admin_add_incoming.auth_username = "user42", .details.admin_add_incoming.auth_password = "pass42", .details.admin_add_incoming.amount = "EUR:5.01" }, /* Run wirewatch to observe /admin/add/incoming */ { .oc = OC_RUN_WIREWATCH, .label = "wirewatch-1" }, /* Withdraw a 5 EUR coin, at fee of 1 ct */ { .oc = OC_WITHDRAW_SIGN, .label = "withdraw-coin-1", .expected_response_code = MHD_HTTP_OK, .details.reserve_withdraw.reserve_reference = "create-reserve-1", .details.reserve_withdraw.amount = "EUR:5" }, /* Check that deposit and withdraw operation are in history, and that the balance is now at zero */ { .oc = OC_WITHDRAW_STATUS, .label = "withdraw-status-1", .expected_response_code = MHD_HTTP_OK, .details.reserve_status.reserve_reference = "create-reserve-1", .details.reserve_status.expected_balance = "EUR:0" }, /* Try to deposit the 5 EUR coin (in full) */ { .oc = OC_DEPOSIT, .label = "deposit-simple", .expected_response_code = MHD_HTTP_OK, .details.deposit.amount = "EUR:5", .details.deposit.coin_ref = "withdraw-coin-1", .details.deposit.wire_details = "{ \"type\":\"test\", \"bank_uri\":\"http://localhost:8082/\", \"account_number\":42 }", .details.deposit.contract_terms = "{ \"items\": [ { \"name\":\"ice cream\", \"value\":1 } ] }" }, /* Try to overdraw funds ... */ { .oc = OC_WITHDRAW_SIGN, .label = "withdraw-coin-2", .expected_response_code = MHD_HTTP_FORBIDDEN, .details.reserve_withdraw.reserve_reference = "create-reserve-1", .details.reserve_withdraw.amount = "EUR:5" }, /* Try to double-spend the 5 EUR coin with different wire details */ { .oc = OC_DEPOSIT, .label = "deposit-double-1", .expected_response_code = MHD_HTTP_FORBIDDEN, .details.deposit.amount = "EUR:5", .details.deposit.coin_ref = "withdraw-coin-1", .details.deposit.wire_details = "{ \"type\":\"test\", \"bank_uri\":\"http://localhost:8082/\", \"account_number\":43 }", .details.deposit.contract_terms = "{ \"items\": [ { \"name\":\"ice cream\", \"value\":1 } ] }" }, /* Try to double-spend the 5 EUR coin at the same merchant (but different transaction ID) */ { .oc = OC_DEPOSIT, .label = "deposit-double-2", .expected_response_code = MHD_HTTP_FORBIDDEN, .details.deposit.amount = "EUR:5", .details.deposit.coin_ref = "withdraw-coin-1", .details.deposit.wire_details = "{ \"type\":\"test\", \"bank_uri\":\"http://localhost:8082/\", \"account_number\":42 }", .details.deposit.contract_terms = "{ \"items\": [ { \"name\":\"ice cream\", \"value\":1 } ] }" }, /* Try to double-spend the 5 EUR coin at the same merchant (but different proposal) */ { .oc = OC_DEPOSIT, .label = "deposit-double-3", .expected_response_code = MHD_HTTP_FORBIDDEN, .details.deposit.amount = "EUR:5", .details.deposit.coin_ref = "withdraw-coin-1", .details.deposit.wire_details = "{ \"type\":\"test\", \"bank_uri\":\"http://localhost:8082/\", \"account_number\":42 }", .details.deposit.contract_terms = "{ \"items\":[{ \"name\":\"ice cream\", \"value\":2 } ] }" }, /* ***************** /refresh testing ******************** */ /* Fill reserve with EUR:5.01, as withdraw fee is 1 ct */ { .oc = OC_ADMIN_ADD_INCOMING, .label = "refresh-create-reserve-1", .expected_response_code = MHD_HTTP_OK, .details.admin_add_incoming.debit_account_no = 424, .details.admin_add_incoming.credit_account_no = EXCHANGE_ACCOUNT_NO, .details.admin_add_incoming.auth_username = "user424", .details.admin_add_incoming.auth_password = "pass424", .details.admin_add_incoming.amount = "EUR:5.01" }, /* Run wirewatch to observe /admin/add/incoming */ { .oc = OC_RUN_WIREWATCH, .label = "wirewatch-2" }, /* Withdraw a 5 EUR coin, at fee of 1 ct */ { .oc = OC_WITHDRAW_SIGN, .label = "refresh-withdraw-coin-1", .expected_response_code = MHD_HTTP_OK, .details.reserve_withdraw.reserve_reference = "refresh-create-reserve-1", .details.reserve_withdraw.amount = "EUR:5" }, /* Try to partially spend (deposit) 1 EUR of the 5 EUR coin (in full) (merchant would receive EUR:0.99 due to 1 ct deposit fee) */ { .oc = OC_DEPOSIT, .label = "refresh-deposit-partial", .expected_response_code = MHD_HTTP_OK, .details.deposit.amount = "EUR:1", .details.deposit.coin_ref = "refresh-withdraw-coin-1", .details.deposit.wire_details = "{ \"type\":\"test\", \"bank_uri\":\"http://localhost:8082/\", \"account_number\":42 }", .details.deposit.contract_terms = "{ \"items\" : [ { \"name\":\"ice cream\", \"value\":\"EUR:1\" } ] }" }, /* Melt the rest of the coin's value (EUR:4.00 = 3x EUR:1.03 + 7x EUR:0.13) */ { .oc = OC_REFRESH_MELT, .label = "refresh-melt-1", .expected_response_code = MHD_HTTP_OK, .details.refresh_melt.melted_coin = { .amount = "EUR:4", .coin_ref = "refresh-withdraw-coin-1" }, .details.refresh_melt.fresh_amounts = melt_fresh_amounts_1 }, /* Complete (successful) melt operation, and withdraw the coins */ { .oc = OC_REFRESH_REVEAL, .label = "refresh-reveal-1", .expected_response_code = MHD_HTTP_OK, .details.refresh_reveal.melt_ref = "refresh-melt-1" }, /* do it again to check idempotency */ { .oc = OC_REFRESH_REVEAL, .label = "refresh-reveal-1-idempotency", .expected_response_code = MHD_HTTP_OK, .details.refresh_reveal.melt_ref = "refresh-melt-1" }, /* Test that /refresh/link works */ { .oc = OC_REFRESH_LINK, .label = "refresh-link-1", .expected_response_code = MHD_HTTP_OK, .details.refresh_link.reveal_ref = "refresh-reveal-1" }, /* Test successfully spending coins from the refresh operation: first EUR:1 */ { .oc = OC_DEPOSIT, .label = "refresh-deposit-refreshed-1a", .expected_response_code = MHD_HTTP_OK, .details.deposit.amount = "EUR:1", .details.deposit.coin_ref = "refresh-reveal-1-idempotency", .details.deposit.coin_idx = 0, .details.deposit.wire_details = "{ \"type\":\"test\", \"bank_uri\":\"http://localhost:8082/\", \"account_number\":42 }", .details.deposit.contract_terms = "{ \"items\": [ { \"name\":\"ice cream\", \"value\":3 } ] }" }, /* Test successfully spending coins from the refresh operation: finally EUR:0.1 */ { .oc = OC_DEPOSIT, .label = "refresh-deposit-refreshed-1b", .expected_response_code = MHD_HTTP_OK, .details.deposit.amount = "EUR:0.1", .details.deposit.coin_ref = "refresh-reveal-1", .details.deposit.coin_idx = 4, .details.deposit.wire_details = "{ \"type\":\"test\", \"bank_uri\":\"http://localhost:8082/\", \"account_number\":43 }", .details.deposit.contract_terms = "{ \"items\": [ { \"name\":\"ice cream\", \"value\":3 } ] }" }, /* Test running a failing melt operation (same operation again must fail) */ { .oc = OC_REFRESH_MELT, .label = "refresh-melt-failing", .expected_response_code = MHD_HTTP_FORBIDDEN, .details.refresh_melt.melted_coin = { .amount = "EUR:4", .coin_ref = "refresh-withdraw-coin-1" }, .details.refresh_melt.fresh_amounts = melt_fresh_amounts_1 }, // FIXME: also test with coin that was already melted // (signature differs from coin that was deposited...) /* *************** end of /refresh testing ************** */ /* ************** Test tracking API ******************** */ /* Try resolving a deposit's WTID, as we never triggered execution of transactions, the answer should be that the exchange knows about the deposit, but has no WTID yet. */ { .oc = OC_DEPOSIT_WTID, .label = "deposit-wtid-found", .expected_response_code = MHD_HTTP_ACCEPTED, .details.deposit_wtid.deposit_ref = "deposit-simple" }, /* Try resolving a deposit's WTID for a failed deposit. As the deposit failed, the answer should be that the exchange does NOT know about the deposit. */ { .oc = OC_DEPOSIT_WTID, .label = "deposit-wtid-failing", .expected_response_code = MHD_HTTP_NOT_FOUND, .details.deposit_wtid.deposit_ref = "deposit-double-2" }, /* Try resolving an undefined (all zeros) WTID; this should fail as obviously the exchange didn't use that WTID value for any transaction. */ { .oc = OC_WIRE_DEPOSITS, .label = "wire-deposit-failing", .expected_response_code = MHD_HTTP_NOT_FOUND }, /* Run transfers. Note that _actual_ aggregation will NOT happen here, as each deposit operation is run with a fresh merchant public key! */ { .oc = OC_RUN_AGGREGATOR, .label = "run-aggregator" }, { .oc = OC_CHECK_BANK_TRANSFER, .label = "check_bank_transfer-499c", .details.check_bank_transfer.exchange_base_url = "https://exchange.com/", .details.check_bank_transfer.amount = "EUR:4.98", .details.check_bank_transfer.account_debit = 2, .details.check_bank_transfer.account_credit = 42 }, { .oc = OC_CHECK_BANK_TRANSFER, .label = "check_bank_transfer-99c1", .details.check_bank_transfer.exchange_base_url = "https://exchange.com/", .details.check_bank_transfer.amount = "EUR:0.98", .details.check_bank_transfer.account_debit = 2, .details.check_bank_transfer.account_credit = 42 }, { .oc = OC_CHECK_BANK_TRANSFER, .label = "check_bank_transfer-99c2", .details.check_bank_transfer.exchange_base_url = "https://exchange.com/", .details.check_bank_transfer.amount = "EUR:0.98", .details.check_bank_transfer.account_debit = 2, .details.check_bank_transfer.account_credit = 42 }, { .oc = OC_CHECK_BANK_TRANSFER, .label = "check_bank_transfer-9c", .details.check_bank_transfer.exchange_base_url = "https://exchange.com/", .details.check_bank_transfer.amount = "EUR:0.08", .details.check_bank_transfer.account_debit = 2, .details.check_bank_transfer.account_credit = 43 }, { .oc = OC_CHECK_BANK_TRANSFER, .label = "check_bank_transfer-aai-1", .details.check_bank_transfer.exchange_base_url = "https://exchange.com/", .details.check_bank_transfer.amount = "EUR:5.01", .details.check_bank_transfer.account_debit = 42, .details.check_bank_transfer.account_credit = 2 }, { .oc = OC_CHECK_BANK_TRANSFER, .label = "check_bank_transfer-aai-2", .details.check_bank_transfer.exchange_base_url = "https://exchange.com/", .details.check_bank_transfer.amount = "EUR:5.01", .details.check_bank_transfer.account_debit = 424, .details.check_bank_transfer.account_credit = 2 }, { .oc = OC_CHECK_BANK_TRANSFERS_EMPTY, .label = "check_bank_empty" }, { .oc = OC_DEPOSIT_WTID, .label = "deposit-wtid-ok", .expected_response_code = MHD_HTTP_OK, .details.deposit_wtid.deposit_ref = "deposit-simple", .details.deposit_wtid.bank_transfer_ref = "check_bank_transfer-499c" }, { .oc = OC_WIRE_DEPOSITS, .label = "wire-deposits-success-bank", .expected_response_code = MHD_HTTP_OK, .details.wire_deposits.wtid_ref = "check_bank_transfer-99c1", .details.wire_deposits.total_amount_expected = "EUR:0.98", .details.wire_deposits.wire_fee_expected = "EUR:0.01" }, { .oc = OC_WIRE_DEPOSITS, .label = "wire-deposits-success-wtid", .expected_response_code = MHD_HTTP_OK, .details.wire_deposits.wtid_ref = "deposit-wtid-ok", .details.wire_deposits.total_amount_expected = "EUR:4.98", .details.wire_deposits.wire_fee_expected = "EUR:0.01" }, /* ************** End of tracking API testing************* */ /* ************** Test /refund API ************* */ /* Fill reserve with EUR:5.01, as withdraw fee is 1 ct per config */ { .oc = OC_ADMIN_ADD_INCOMING, .label = "create-reserve-r1", .expected_response_code = MHD_HTTP_OK, .details.admin_add_incoming.debit_account_no = 42, .details.admin_add_incoming.credit_account_no = EXCHANGE_ACCOUNT_NO, .details.admin_add_incoming.auth_username = "user42", .details.admin_add_incoming.auth_password = "pass42", .details.admin_add_incoming.amount = "EUR:5.01" }, /* Run wirewatch to observe /admin/add/incoming */ { .oc = OC_RUN_WIREWATCH, .label = "wirewatch-3" }, /* Withdraw a 5 EUR coin, at fee of 1 ct */ { .oc = OC_WITHDRAW_SIGN, .label = "withdraw-coin-r1", .expected_response_code = MHD_HTTP_OK, .details.reserve_withdraw.reserve_reference = "create-reserve-r1", .details.reserve_withdraw.amount = "EUR:5" }, /* Spend 5 EUR of the 5 EUR coin (in full) (merchant would receive EUR:4.99 due to 1 ct deposit fee) */ { .oc = OC_DEPOSIT, .label = "deposit-refund-1", .expected_response_code = MHD_HTTP_OK, .details.deposit.amount = "EUR:5", .details.deposit.coin_ref = "withdraw-coin-r1", .details.deposit.wire_details = "{ \"type\":\"test\", \"bank_uri\":\"http://localhost:8082/\", \"account_number\":42 }", .details.deposit.contract_terms = "{ \"items\" : [ { \"name\":\"ice cream\", \"value\":\"EUR:5\" } ] }", .details.deposit.refund_deadline = { 60LL * 1000 * 1000 } /* 60 s */, }, /* Run transfers. Should do nothing as refund deadline blocks it */ { .oc = OC_RUN_AGGREGATOR, .label = "run-aggregator-refund" }, { .oc = OC_CHECK_BANK_TRANSFER, .label = "check_bank_transfer-aai-3", .details.check_bank_transfer.exchange_base_url = "https://exchange.com/", .details.check_bank_transfer.amount = "EUR:5.01", .details.check_bank_transfer.account_debit = 42, .details.check_bank_transfer.account_credit = 2 }, /* check that aggregator didn't do anything, as expected */ { .oc = OC_CHECK_BANK_TRANSFERS_EMPTY, .label = "check-refund-not-run" }, /* Trigger refund */ { .oc = OC_REFUND, .label = "refund-ok", .expected_response_code = MHD_HTTP_OK, .details.refund.amount = "EUR:5", .details.refund.fee = "EUR:0.01", .details.refund.deposit_ref = "deposit-refund-1", }, /* Spend 4.99 EUR of the refunded 4.99 EUR coin (1ct gone due to refund) (merchant would receive EUR:4.98 due to 1 ct deposit fee) */ { .oc = OC_DEPOSIT, .label = "deposit-refund-2", .expected_response_code = MHD_HTTP_OK, .details.deposit.amount = "EUR:4.99", .details.deposit.coin_ref = "withdraw-coin-r1", .details.deposit.wire_details = "{ \"type\":\"test\", \"bank_uri\":\"http://localhost:8082/\", \"account_number\":42 }", .details.deposit.contract_terms = "{ \"items\" : [ { \"name\":\"more ice cream\", \"value\":\"EUR:5\" } ] }", }, /* Run transfers. This will do the transfer as refund deadline was 0 */ { .oc = OC_RUN_AGGREGATOR, .label = "run-aggregator-3" }, /* Check that deposit did run */ { .oc = OC_CHECK_BANK_TRANSFER, .label = "check_bank_transfer-pre-refund", .details.check_bank_transfer.exchange_base_url = "https://exchange.com/", .details.check_bank_transfer.amount = "EUR:4.97", .details.check_bank_transfer.account_debit = 2, .details.check_bank_transfer.account_credit = 42 }, /* Run failing refund, as past deadline & aggregation */ { .oc = OC_REFUND, .label = "refund-fail", .expected_response_code = MHD_HTTP_GONE, .details.refund.amount = "EUR:4.99", .details.refund.fee = "EUR:0.01", .details.refund.deposit_ref = "deposit-refund-2", }, { .oc = OC_CHECK_BANK_TRANSFERS_EMPTY, .label = "check-empty-after-refund" }, #if 0 /* Test refunded coins are never executed, even past refund deadline */ { .oc = OC_ADMIN_ADD_INCOMING, .label = "create-reserve-rb", .expected_response_code = MHD_HTTP_OK, .details.admin_add_incoming.debit_account_no = 42, .details.admin_add_incoming.credit_account_no = EXCHANGE_ACCOUNT_NO, .details.admin_add_incoming.auth_username = "user42", .details.admin_add_incoming.auth_password = "pass42", .details.admin_add_incoming.amount = "EUR:5.01" }, /* Run wirewatch to observe /admin/add/incoming */ { .oc = OC_RUN_WIREWATCH, .label = "wirewatch-3b" }, /* Withdraw a 5 EUR coin, at fee of 1 ct */ { .oc = OC_WITHDRAW_SIGN, .label = "withdraw-coin-rb", .expected_response_code = MHD_HTTP_OK, .details.reserve_withdraw.reserve_reference = "create-reserve-rb", .details.reserve_withdraw.amount = "EUR:5" }, /* Spend 5 EUR of the 5 EUR coin (in full) (merchant would receive EUR:4.99 due to 1 ct deposit fee) */ { .oc = OC_DEPOSIT, .label = "deposit-refund-1b", .expected_response_code = MHD_HTTP_OK, .details.deposit.amount = "EUR:5", .details.deposit.coin_ref = "withdraw-coin-rb", .details.deposit.wire_details = "{ \"type\":\"test\", \"bank_uri\":\"http://localhost:8082/\", \"account_number\":42 }", .details.deposit.contract_terms = "{ \"items\" : [ { \"name\":\"ice cream\", \"value\":\"EUR:5\" } ] }", .details.deposit.refund_deadline = { 0 }, }, { .oc = OC_CHECK_BANK_TRANSFER, .label = "check_bank_transfer-aai-3b", .details.check_bank_transfer.exchange_base_url = "https://exchange.com/", .details.check_bank_transfer.amount = "EUR:5.01", .details.check_bank_transfer.account_debit = 42, .details.check_bank_transfer.account_credit = 2 }, /* Trigger refund (before aggregator had a chance to execute deposit, even though refund deadline was zero) */ { .oc = OC_REFUND, .label = "refund-ok-fast", .expected_response_code = MHD_HTTP_OK, .details.refund.amount = "EUR:5", .details.refund.fee = "EUR:0.01", .details.refund.deposit_ref = "deposit-refund-1b", }, /* Run transfers. This will do the transfer as refund deadline was 0, except of course because the refund succeeded, the transfer should no longer be done. */ { .oc = OC_RUN_AGGREGATOR, .label = "run-aggregator-3b" }, /* check that aggregator didn't do anything, as expected */ { .oc = OC_CHECK_BANK_TRANSFERS_EMPTY, .label = "check-refund-fast-not-run" }, #endif /* ************** End of refund API testing************* */ /* ************** Test /payback API ************* */ /* Fill reserve with EUR:5.01, as withdraw fee is 1 ct per config, then withdraw a coin and then have it be paid back. */ { .oc = OC_ADMIN_ADD_INCOMING, .label = "payback-create-reserve-1", .expected_response_code = MHD_HTTP_OK, .details.admin_add_incoming.debit_account_no = 42, .details.admin_add_incoming.credit_account_no = EXCHANGE_ACCOUNT_NO, .details.admin_add_incoming.auth_username = "user42", .details.admin_add_incoming.auth_password = "pass42", .details.admin_add_incoming.amount = "EUR:5.01" }, /* Run wirewatch to observe /admin/add/incoming */ { .oc = OC_RUN_WIREWATCH, .label = "wirewatch-4" }, /* Withdraw a 5 EUR coin, at fee of 1 ct */ { .oc = OC_WITHDRAW_SIGN, .label = "payback-withdraw-coin-1", .expected_response_code = MHD_HTTP_OK, .details.reserve_withdraw.reserve_reference = "payback-create-reserve-1", .details.reserve_withdraw.amount = "EUR:5" }, { .oc = OC_REVOKE, .label = "revoke-1", .expected_response_code = MHD_HTTP_OK, .details.revoke.ref = "payback-withdraw-coin-1" }, { .oc = OC_PAYBACK, .label = "payback-1", .expected_response_code = MHD_HTTP_OK, .details.payback.ref = "payback-withdraw-coin-1", .details.payback.amount = "EUR:5" }, /* Check the money is back with the reserve */ { .oc = OC_WITHDRAW_STATUS, .label = "payback-reserve-status-1", .expected_response_code = MHD_HTTP_OK, .details.reserve_status.reserve_reference = "payback-create-reserve-1", .details.reserve_status.expected_balance = "EUR:5.00" }, /* Fill reserve with EUR:2.02, as withdraw fee is 1 ct per config, then withdraw two coin, partially spend one, and then have the rest paid back. Check deposit of other coin fails. (Do not use EUR:5 here as the EUR:5 coin was revoked and we did not bother to create a new one...) */ { .oc = OC_ADMIN_ADD_INCOMING, .label = "payback-create-reserve-2", .expected_response_code = MHD_HTTP_OK, .details.admin_add_incoming.debit_account_no = 42, .details.admin_add_incoming.credit_account_no = EXCHANGE_ACCOUNT_NO, .details.admin_add_incoming.auth_username = "user42", .details.admin_add_incoming.auth_password = "pass42", .details.admin_add_incoming.amount = "EUR:2.02" }, /* Run wirewatch to observe /admin/add/incoming */ { .oc = OC_RUN_WIREWATCH, .label = "wirewatch-5" }, /* Withdraw a 1 EUR coin, at fee of 1 ct */ { .oc = OC_WITHDRAW_SIGN, .label = "payback-withdraw-coin-2a", .expected_response_code = MHD_HTTP_OK, .details.reserve_withdraw.reserve_reference = "payback-create-reserve-2", .details.reserve_withdraw.amount = "EUR:1" }, /* Withdraw a 1 EUR coin, at fee of 1 ct */ { .oc = OC_WITHDRAW_SIGN, .label = "payback-withdraw-coin-2b", .expected_response_code = MHD_HTTP_OK, .details.reserve_withdraw.reserve_reference = "payback-create-reserve-2", .details.reserve_withdraw.amount = "EUR:1" }, { .oc = OC_DEPOSIT, .label = "payback-deposit-partial", .expected_response_code = MHD_HTTP_OK, .details.deposit.amount = "EUR:0.5", .details.deposit.coin_ref = "payback-withdraw-coin-2a", .details.deposit.wire_details = "{ \"type\":\"test\", \"bank_uri\":\"http://localhost:8082/\", \"account_number\":42 }", .details.deposit.contract_terms = "{ \"items\": [ { \"name\":\"more ice cream\", \"value\":1 } ] }" }, { .oc = OC_REVOKE, .label = "revoke-2", .expected_response_code = MHD_HTTP_OK, .details.revoke.ref = "payback-withdraw-coin-2a" }, { .oc = OC_PAYBACK, .label = "payback-2", .expected_response_code = MHD_HTTP_OK, .details.payback.ref = "payback-withdraw-coin-2a", .details.payback.amount = "EUR:0.5" }, { .oc = OC_PAYBACK, .label = "payback-2b", .expected_response_code = MHD_HTTP_FORBIDDEN, .details.payback.ref = "payback-withdraw-coin-2a", .details.payback.amount = "EUR:0.5" }, { .oc = OC_DEPOSIT, .label = "payback-deposit-revoked", .expected_response_code = MHD_HTTP_NOT_FOUND, .details.deposit.amount = "EUR:1", .details.deposit.coin_ref = "payback-withdraw-coin-2b", .details.deposit.wire_details = "{ \"type\":\"test\", \"bank_uri\":\"http://localhost:8082/\", \"account_number\":42 }", .details.deposit.contract_terms = "{ \"items\": [ { \"name\":\"more ice cream\", \"value\":1 } ] }" }, /* Test deposit fails after payback, with proof in payback */ /* FIXME: #3887: right now, the exchange will never return the coin's transaction history with payback data, as we get a 404 on the DK! */ { .oc = OC_DEPOSIT, .label = "payback-deposit-partial-after-payback", .expected_response_code = MHD_HTTP_NOT_FOUND, .details.deposit.amount = "EUR:0.5", .details.deposit.coin_ref = "payback-withdraw-coin-2a", .details.deposit.wire_details = "{ \"type\":\"test\", \"bank_uri\":\"http://localhost:8082/\", \"account_number\":42 }", .details.deposit.contract_terms = "{ \"items\": [ { \"name\":\"extra ice cream\", \"value\":1 } ] }" }, /* Test that revoked coins cannot be withdrawn */ { .oc = OC_ADMIN_ADD_INCOMING, .label = "payback-create-reserve-3", .expected_response_code = MHD_HTTP_OK, .details.admin_add_incoming.debit_account_no = 42, .details.admin_add_incoming.credit_account_no = EXCHANGE_ACCOUNT_NO, .details.admin_add_incoming.auth_username = "user42", .details.admin_add_incoming.auth_password = "pass42", .details.admin_add_incoming.amount = "EUR:1.01" }, /* Run wirewatch to observe /admin/add/incoming */ { .oc = OC_RUN_WIREWATCH, .label = "wirewatch-6" }, { .oc = OC_WITHDRAW_SIGN, .label = "payback-withdraw-coin-3-revoked", .expected_response_code = MHD_HTTP_NOT_FOUND, .details.reserve_withdraw.reserve_reference = "payback-create-reserve-3", .details.reserve_withdraw.amount = "EUR:1" }, /* check that we are empty before the rejection test */ { .oc = OC_CHECK_BANK_TRANSFER, .label = "check_bank_transfer-pr1", .details.check_bank_transfer.exchange_base_url = "https://exchange.com/", .details.check_bank_transfer.amount = "EUR:5.01", .details.check_bank_transfer.account_debit = 42, .details.check_bank_transfer.account_credit = 2 }, { .oc = OC_CHECK_BANK_TRANSFER, .label = "check_bank_transfer-pr2", .details.check_bank_transfer.exchange_base_url = "https://exchange.com/", .details.check_bank_transfer.amount = "EUR:2.02", .details.check_bank_transfer.account_debit = 42, .details.check_bank_transfer.account_credit = 2 }, { .oc = OC_CHECK_BANK_TRANSFER, .label = "check_bank_transfer-pr3", .details.check_bank_transfer.exchange_base_url = "https://exchange.com/", .details.check_bank_transfer.amount = "EUR:1.01", .details.check_bank_transfer.account_debit = 42, .details.check_bank_transfer.account_credit = 2 }, { .oc = OC_CHECK_BANK_TRANSFERS_EMPTY, .label = "check-empty-again" }, /* Test rejection of bogus wire transfers */ { .oc = OC_ADMIN_ADD_INCOMING, .label = "bogus-subject", .expected_response_code = MHD_HTTP_OK, .details.admin_add_incoming.subject = "not a reserve public key", .details.admin_add_incoming.debit_account_no = 42, .details.admin_add_incoming.credit_account_no = EXCHANGE_ACCOUNT_NO, .details.admin_add_incoming.auth_username = "user42", .details.admin_add_incoming.auth_password = "pass42", .details.admin_add_incoming.amount = "EUR:1.01" }, /* Run wirewatch to observe rejection */ { .oc = OC_RUN_WIREWATCH, .label = "wirewatch-7" }, { .oc = OC_CHECK_BANK_TRANSFERS_EMPTY, .label = "check-empty-from-reject" }, /* ************** End of payback API testing************* */ #endif { .oc = OC_END } }; is = GNUNET_new (struct InterpreterState); is->commands = commands; ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, &rc); GNUNET_assert (NULL != ctx); rc = GNUNET_CURL_gnunet_rc_create (ctx); fakebank = TALER_FAKEBANK_start (8082); exchange = TALER_EXCHANGE_connect (ctx, "http://localhost:8081", &cert_cb, is, TALER_EXCHANGE_OPTION_END); GNUNET_assert (NULL != exchange); timeout_task = GNUNET_SCHEDULER_add_delayed (GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 300), &do_timeout, NULL); GNUNET_SCHEDULER_add_shutdown (&do_shutdown, is); } /** * Remove files from previous runs */ static void cleanup_files () { struct GNUNET_CONFIGURATION_Handle *cfg; char *dir; cfg = GNUNET_CONFIGURATION_create (); if (GNUNET_OK != GNUNET_CONFIGURATION_load (cfg, "test_exchange_api.conf")) { GNUNET_break (0); GNUNET_CONFIGURATION_destroy (cfg); exit (77); } GNUNET_assert (GNUNET_OK == GNUNET_CONFIGURATION_get_value_filename (cfg, "exchange", "keydir", &dir)); if (GNUNET_YES == GNUNET_DISK_directory_test (dir, GNUNET_NO)) GNUNET_break (GNUNET_OK == GNUNET_DISK_directory_remove (dir)); GNUNET_free (dir); GNUNET_CONFIGURATION_destroy (cfg); } /** * Main function for the testcase for the exchange API. * * @param argc expected to be 1 * @param argv expected to only contain the program name */ int main (int argc, char * const *argv) { struct GNUNET_OS_Process *proc; struct GNUNET_SIGNAL_Context *shc_chld; enum GNUNET_OS_ProcessStatusType type; unsigned long code; unsigned int iter; /* These might get in the way... */ unsetenv ("XDG_DATA_HOME"); unsetenv ("XDG_CONFIG_HOME"); GNUNET_log_setup ("test-exchange-api", "INFO", NULL); if (GNUNET_OK != GNUNET_NETWORK_test_port_free (IPPROTO_TCP, 8081)) { fprintf (stderr, "Required port %u not available, skipping.\n", 8081); return 77; } if (GNUNET_OK != GNUNET_NETWORK_test_port_free (IPPROTO_TCP, 8082)) { fprintf (stderr, "Required port %u not available, skipping.\n", 8082); return 77; } cleanup_files (); proc = GNUNET_OS_start_process (GNUNET_NO, GNUNET_OS_INHERIT_STD_ALL, NULL, NULL, NULL, "taler-exchange-keyup", "taler-exchange-keyup", "-c", "test_exchange_api.conf", "-o", "auditor.in", NULL); if (NULL == proc) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to run `taler-exchange-keyup`, is your PATH correct?\n"); return 77; } GNUNET_OS_process_wait (proc); GNUNET_OS_process_destroy (proc); proc = GNUNET_OS_start_process (GNUNET_NO, GNUNET_OS_INHERIT_STD_ALL, NULL, NULL, NULL, "taler-auditor-sign", "taler-auditor-sign", "-c", "test_exchange_api.conf", "-u", "http://auditor/", "-m", "98NJW3CQHZQGQXTY3K85K531XKPAPAVV4Q5V8PYYRR00NJGZWNVG", "-r", "auditor.in", "-o", "test_exchange_api_home/.local/share/taler/auditors/auditor.out", NULL); if (NULL == proc) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to run `taler-exchange-keyup`, is your PATH correct?\n"); return 77; } GNUNET_OS_process_wait (proc); GNUNET_OS_process_destroy (proc); proc = GNUNET_OS_start_process (GNUNET_NO, GNUNET_OS_INHERIT_STD_ALL, NULL, NULL, NULL, "taler-exchange-dbinit", "taler-exchange-dbinit", "-c", "test_exchange_api.conf", "-r", NULL); if (NULL == proc) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to run `taler-exchange-dbinit`, is your PATH correct?\n"); return 77; } if (GNUNET_SYSERR == GNUNET_OS_process_wait_status (proc, &type, &code)) { GNUNET_break (0); GNUNET_OS_process_destroy (proc); return 1; } GNUNET_OS_process_destroy (proc); if ( (type == GNUNET_OS_PROCESS_EXITED) && (0 != code) ) { fprintf (stderr, "Failed to setup database\n"); return 77; } if ( (type != GNUNET_OS_PROCESS_EXITED) || (0 != code) ) { fprintf (stderr, "Unexpected error running `taler-exchange-dbinit'!\n"); return 1; } exchanged = GNUNET_OS_start_process (GNUNET_NO, GNUNET_OS_INHERIT_STD_ALL, NULL, NULL, NULL, "taler-exchange-httpd", "taler-exchange-httpd", "-c", "test_exchange_api.conf", "-i", NULL); /* give child time to start and bind against the socket */ fprintf (stderr, "Waiting for `taler-exchange-httpd' to be ready"); iter = 0; do { if (10 == iter) { fprintf (stderr, "Failed to launch `taler-exchange-httpd' (or `wget')\n"); GNUNET_OS_process_kill (exchanged, SIGTERM); GNUNET_OS_process_wait (exchanged); GNUNET_OS_process_destroy (exchanged); return 77; } fprintf (stderr, "."); sleep (1); iter++; } while (0 != system ("wget -q -t 1 -T 1 http://127.0.0.1:8081/keys -o /dev/null -O /dev/null")); fprintf (stderr, "\n"); result = GNUNET_NO; sigpipe = GNUNET_DISK_pipe (GNUNET_NO, GNUNET_NO, GNUNET_NO, GNUNET_NO); GNUNET_assert (NULL != sigpipe); shc_chld = GNUNET_SIGNAL_handler_install (GNUNET_SIGCHLD, &sighandler_child_death); GNUNET_SCHEDULER_run (&run, NULL); GNUNET_SIGNAL_handler_uninstall (shc_chld); shc_chld = NULL; GNUNET_DISK_pipe_close (sigpipe); GNUNET_break (0 == GNUNET_OS_process_kill (exchanged, SIGTERM)); GNUNET_break (GNUNET_OK == GNUNET_OS_process_wait (exchanged)); GNUNET_OS_process_destroy (exchanged); return (GNUNET_OK == result) ? 0 : 1; } /* end of test_exchange_api.c */