exchange

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

commit e6f102bf5b8767b9f99921dd92be928d7023b762
parent 36929158c3b344c4f807401436a9424f686a8855
Author: Christian Grothoff <christian@grothoff.org>
Date:   Sun, 11 Oct 2026 12:14:17 +0200

check successive /keys replies in the cherry-picking tests

Diffstat:
Msrc/include/taler/taler_testing_lib.h | 32++++++++++++++++++++++++++++++++
Msrc/testing/meson.build | 1+
Msrc/testing/test_exchange_api_keys_cherry_picking.c | 24+++++++++++++++++++++++-
Msrc/testing/test_exchange_api_overlapping_keys_bug.c | 6++++++
Asrc/testing/testing_api_cmd_check_keys.c | 664+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/testing/testing_api_cmd_get_exchange.c | 20++++++++++++++++++++
6 files changed, 746 insertions(+), 1 deletion(-)

diff --git a/src/include/taler/taler_testing_lib.h b/src/include/taler/taler_testing_lib.h @@ -706,6 +706,38 @@ TALER_TESTING_cmd_get_exchange ( /** + * Check the /keys of successive "get exchange" commands. + * + * The /keys of @a later_ref must have the same master public key and + * currency as those of @a earlier_ref, contain every denomination of + * @a earlier_ref that was still valid when @a later_ref received its + * reply (with the same meta data), contain no denomination that had + * expired when @a later_ref sent its request, and list no denomination + * twice. + * + * If @a full_ref is given, it must be a "get exchange" command that + * downloaded the full /keys after @a later_ref (typically, @a later_ref + * used cherry picking). The /keys of both must then be the same: the + * same denominations, signing keys and auditors, except for + * denominations expiring between the two downloads. + * + * Denominations must not be revoked between the downloads. + * + * @param label command label + * @param earlier_ref label of a "get exchange" command + * @param later_ref label of a later "get exchange" command + * @param full_ref label of a "get exchange" command that downloaded the + * full /keys after @a later_ref, or NULL + * @return the command + */ +struct TALER_TESTING_Command +TALER_TESTING_cmd_check_keys (const char *label, + const char *earlier_ref, + const char *later_ref, + const char *full_ref); + + +/** * Connects to the auditor. * * @param label command label diff --git a/src/testing/meson.build b/src/testing/meson.build @@ -35,6 +35,7 @@ libtalertesting_la_SOURCES = [ 'testing_api_cmd_batch_deposit.c', 'testing_api_cmd_batch_withdraw.c', 'testing_api_cmd_check_aml_decisions.c', + 'testing_api_cmd_check_keys.c', 'testing_api_cmd_coin_history.c', 'testing_api_cmd_common.c', 'testing_api_cmd_contract_get.c', diff --git a/src/testing/test_exchange_api_keys_cherry_picking.c b/src/testing/test_exchange_api_keys_cherry_picking.c @@ -64,18 +64,40 @@ run (void *cls, NULL, true, true), + /* Denominations can be deposited for about 12 s after their start + (see the -rsa/-cs configurations), so after this sleep the ones + that were valid when we first downloaded /keys have expired, + while those that started in the future are still valid. */ TALER_TESTING_cmd_sleep ("sleep", - 6 /* seconds */), + 12 /* seconds */), TALER_TESTING_cmd_get_exchange ("get-exchange-1", cred.cfg, "get-exchange", true, true), + TALER_TESTING_cmd_get_exchange ("get-exchange-1-full", + cred.cfg, + NULL, + true, + true), + TALER_TESTING_cmd_check_keys ("check-keys-1", + "get-exchange", + "get-exchange-1", + "get-exchange-1-full"), TALER_TESTING_cmd_get_exchange ("get-exchange-2", cred.cfg, "get-exchange-1", true, true), + TALER_TESTING_cmd_get_exchange ("get-exchange-2-full", + cred.cfg, + NULL, + true, + true), + TALER_TESTING_cmd_check_keys ("check-keys-2", + "get-exchange-1", + "get-exchange-2", + "get-exchange-2-full"), TALER_TESTING_cmd_end () }; diff --git a/src/testing/test_exchange_api_overlapping_keys_bug.c b/src/testing/test_exchange_api_overlapping_keys_bug.c @@ -78,6 +78,12 @@ run (void *cls, NULL, true, true), + /* The cherry-picked /keys overlap with the ones we already had: + merging them must neither lose nor duplicate denominations. */ + TALER_TESTING_cmd_check_keys ("check-keys", + "get-exchange", + "get-exchange-1", + "get-exchange-2"), TALER_TESTING_cmd_end () }; diff --git a/src/testing/testing_api_cmd_check_keys.c b/src/testing/testing_api_cmd_check_keys.c @@ -0,0 +1,664 @@ +/* + This file is part of TALER + (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 3, or + (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public + License along with TALER; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file testing/testing_api_cmd_check_keys.c + * @brief command to compare the /keys of successive "get exchange" commands + * @author Christian Grothoff + */ +#include "taler/taler_json_lib.h" +#include <gnunet/gnunet_curl_lib.h> +#include "taler/taler_testing_lib.h" + + +/** + * State for a "check keys" CMD. + */ +struct CheckKeysState +{ + /** + * Label of the "get exchange" command with the earlier /keys. + */ + const char *earlier_ref; + + /** + * Label of the "get exchange" command with the later /keys. + */ + const char *later_ref; + + /** + * Label of a "get exchange" command that downloaded the full /keys + * after @e later_ref, or NULL. + */ + const char *full_ref; +}; + + +/** + * /keys of a "get exchange" command and when they were requested. + */ +struct KeysSnapshot +{ + /** + * Label of the command. + */ + const char *label; + + /** + * The keys. + */ + const struct TALER_EXCHANGE_Keys *keys; + + /** + * When the request for @e keys was sent. + */ + struct GNUNET_TIME_Absolute request_time; + + /** + * When the reply with @e keys was received. + */ + struct GNUNET_TIME_Absolute reply_time; +}; + + +/** + * Obtain the /keys of the "get exchange" command @a label. + * + * @param is interpreter state + * @param label label of the command + * @param[out] ks set to the keys of the command + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +get_snapshot (struct TALER_TESTING_Interpreter *is, + const char *label, + struct KeysSnapshot *ks) +{ + const struct TALER_TESTING_Command *cmd; + struct TALER_EXCHANGE_Keys *keys; + const struct GNUNET_TIME_Absolute *request_time; + const struct GNUNET_TIME_Absolute *reply_time; + + cmd = TALER_TESTING_interpreter_lookup_command (is, + label); + if (NULL == cmd) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if ( (GNUNET_OK != + TALER_TESTING_get_trait_keys (cmd, + &keys)) || + (NULL == keys) || + (GNUNET_OK != + TALER_TESTING_get_trait_absolute_time (cmd, + 0, + &request_time)) || + (GNUNET_OK != + TALER_TESTING_get_trait_absolute_time (cmd, + 1, + &reply_time)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Command `%s' did not download /keys\n", + label); + return GNUNET_SYSERR; + } + ks->label = label; + ks->keys = keys; + ks->request_time = *request_time; + ks->reply_time = *reply_time; + return GNUNET_OK; +} + + +/** + * Find the denomination @a dk in @a keys. + * + * @param keys keys to search + * @param dk denomination to find (by its hash) + * @return NULL if @a keys does not contain @a dk + */ +static const struct TALER_EXCHANGE_DenomPublicKey * +find_denom (const struct TALER_EXCHANGE_Keys *keys, + const struct TALER_EXCHANGE_DenomPublicKey *dk) +{ + for (unsigned int i = 0; i<keys->num_denom_keys; i++) + if (0 == + GNUNET_memcmp (&dk->h_key, + &keys->denom_keys[i].h_key)) + return &keys->denom_keys[i]; + return NULL; +} + + +/** + * Check if @a a and @a b are the same amount. + * + * @param a an amount + * @param b another amount + * @return true if they are equal + */ +static bool +amount_equal (const struct TALER_Amount *a, + const struct TALER_Amount *b) +{ + return (GNUNET_YES == + TALER_amount_cmp_currency (a, + b)) && + (0 == TALER_amount_cmp (a, + b)); +} + + +/** + * Check if @a a and @a b describe the same denomination with the + * same meta data. + * + * @param a a denomination + * @param b another denomination + * @return true if they are the same + */ +static bool +denom_equal (const struct TALER_EXCHANGE_DenomPublicKey *a, + const struct TALER_EXCHANGE_DenomPublicKey *b) +{ + return (0 == GNUNET_memcmp (&a->h_key, + &b->h_key)) && + (0 == TALER_denom_pub_cmp (&a->key, + &b->key)) && + (0 == GNUNET_memcmp (&a->master_sig, + &b->master_sig)) && + (GNUNET_TIME_timestamp_cmp (a->valid_from, + ==, + b->valid_from)) && + (GNUNET_TIME_timestamp_cmp (a->withdraw_valid_until, + ==, + b->withdraw_valid_until)) && + (GNUNET_TIME_timestamp_cmp (a->expire_deposit, + ==, + b->expire_deposit)) && + (GNUNET_TIME_timestamp_cmp (a->expire_legal, + ==, + b->expire_legal)) && + amount_equal (&a->value, + &b->value) && + amount_equal (&a->fees.withdraw, + &b->fees.withdraw) && + amount_equal (&a->fees.deposit, + &b->fees.deposit) && + amount_equal (&a->fees.refresh, + &b->fees.refresh) && + amount_equal (&a->fees.refund, + &b->fees.refund) && + (a->revoked == b->revoked); +} + + +/** + * Check that every denomination of @a a that is still valid after + * @a valid_after is also in @a b, with the same meta data. + * + * @param a keys with the denominations to look for + * @param b keys to search + * @param valid_after denominations of @a a that expire before this + * time are not checked + * @return true if all of them were found + */ +static bool +denoms_included (const struct KeysSnapshot *a, + const struct KeysSnapshot *b, + struct GNUNET_TIME_Absolute valid_after) +{ + bool ok = true; + + for (unsigned int i = 0; i<a->keys->num_denom_keys; i++) + { + const struct TALER_EXCHANGE_DenomPublicKey *dk + = &a->keys->denom_keys[i]; + const struct TALER_EXCHANGE_DenomPublicKey *dkb; + + if (GNUNET_TIME_absolute_cmp (dk->expire_deposit.abs_time, + <=, + valid_after)) + continue; + dkb = find_denom (b->keys, + dk); + if (NULL == dkb) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Denomination %s (valid until %s) of `%s' missing in `%s'\n", + GNUNET_h2s (&dk->h_key.hash), + GNUNET_TIME_timestamp2s (dk->expire_deposit), + a->label, + b->label); + ok = false; + continue; + } + if (! denom_equal (dk, + dkb)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Denomination %s differs between `%s' and `%s'\n", + GNUNET_h2s (&dk->h_key.hash), + a->label, + b->label); + ok = false; + } + } + return ok; +} + + +/** + * Check that @a ks lists no denomination that had expired + * when its /keys were requested. + * + * @param ks keys to check + * @return true if no expired denomination is listed + */ +static bool +no_expired_denoms (const struct KeysSnapshot *ks) +{ + bool ok = true; + + for (unsigned int i = 0; i<ks->keys->num_denom_keys; i++) + { + const struct TALER_EXCHANGE_DenomPublicKey *dk + = &ks->keys->denom_keys[i]; + + if (GNUNET_TIME_absolute_cmp (dk->expire_deposit.abs_time, + <, + ks->request_time)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "`%s' lists denomination %s that expired at %s\n", + ks->label, + GNUNET_h2s (&dk->h_key.hash), + GNUNET_TIME_timestamp2s (dk->expire_deposit)); + ok = false; + } + } + return ok; +} + + +/** + * Check that @a ks lists each denomination only once. + * + * @param ks keys to check + * @return true if there are no duplicates + */ +static bool +no_duplicate_denoms (const struct KeysSnapshot *ks) +{ + for (unsigned int i = 0; i<ks->keys->num_denom_keys; i++) + for (unsigned int j = i + 1; j<ks->keys->num_denom_keys; j++) + if (0 == + GNUNET_memcmp (&ks->keys->denom_keys[i].h_key, + &ks->keys->denom_keys[j].h_key)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "`%s' lists denomination %s twice\n", + ks->label, + GNUNET_h2s (&ks->keys->denom_keys[i].h_key.hash)); + return false; + } + return true; +} + + +/** + * Check that @a later is a valid successor of @a earlier: same master + * public key and currency, all denominations of @a earlier that are + * still valid are kept, none that expired, and none twice. + * + * @param earlier earlier /keys + * @param later later /keys + * @return true if @a later is a valid successor of @a earlier + */ +static bool +check_successor (const struct KeysSnapshot *earlier, + const struct KeysSnapshot *later) +{ + bool ok = true; + + if (0 != + GNUNET_memcmp (&earlier->keys->master_pub, + &later->keys->master_pub)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Master public key changed between `%s' and `%s'\n", + earlier->label, + later->label); + ok = false; + } + if (0 != strcmp (earlier->keys->currency, + later->keys->currency)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Currency changed between `%s' and `%s'\n", + earlier->label, + later->label); + ok = false; + } + /* Denominations that were still valid when the later reply was + received must have been kept. */ + if (! denoms_included (earlier, + later, + later->reply_time)) + ok = false; + if (! no_expired_denoms (later)) + ok = false; + if (! no_duplicate_denoms (later)) + ok = false; + return ok; +} + + +/** + * Check that the auditors of @a a are also auditors in @a b, auditing + * at least the same denominations, ignoring denominations that expire + * before @a valid_after. + * + * @param a keys with the auditors to look for + * @param b keys to search + * @param valid_after ignore denominations that expire before this time + * @return true if all of them were found + */ +static bool +auditors_included (const struct KeysSnapshot *a, + const struct KeysSnapshot *b, + struct GNUNET_TIME_Absolute valid_after) +{ + for (unsigned int i = 0; i<a->keys->num_auditors; i++) + { + const struct TALER_EXCHANGE_AuditorInformation *ai + = &a->keys->auditors[i]; + const struct TALER_EXCHANGE_AuditorInformation *bi = NULL; + + for (unsigned int j = 0; j<b->keys->num_auditors; j++) + if ( (0 == + GNUNET_memcmp (&ai->auditor_pub, + &b->keys->auditors[j].auditor_pub)) && + (0 == strcmp (ai->auditor_url, + b->keys->auditors[j].auditor_url)) ) + bi = &b->keys->auditors[j]; + if (NULL == bi) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Auditor %s of `%s' missing in `%s'\n", + ai->auditor_url, + a->label, + b->label); + return false; + } + for (unsigned int k = 0; k<ai->num_denom_keys; k++) + { + const struct TALER_EXCHANGE_DenomPublicKey *dk + = &a->keys->denom_keys[ai->denom_keys[k].denom_key_offset]; + bool found = false; + + if (GNUNET_TIME_absolute_cmp (dk->expire_deposit.abs_time, + <=, + valid_after)) + continue; + for (unsigned int l = 0; l<bi->num_denom_keys; l++) + if (0 == + GNUNET_memcmp ( + &dk->h_key, + &b->keys->denom_keys[bi->denom_keys[l].denom_key_offset].h_key)) + found = true; + if (! found) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Auditor %s does not audit denomination %s in `%s'\n", + ai->auditor_url, + GNUNET_h2s (&dk->h_key.hash), + b->label); + return false; + } + } + } + return true; +} + + +/** + * Check that the signing keys of @a a are also in @a b. + * + * @param a keys with the signing keys to look for + * @param b keys to search + * @return true if all of them were found + */ +static bool +signkeys_included (const struct KeysSnapshot *a, + const struct KeysSnapshot *b) +{ + for (unsigned int i = 0; i<a->keys->num_sign_keys; i++) + { + const struct TALER_EXCHANGE_SigningPublicKey *sk + = &a->keys->sign_keys[i]; + bool found = false; + + for (unsigned int j = 0; j<b->keys->num_sign_keys; j++) + if ( (0 == + GNUNET_memcmp (&sk->key, + &b->keys->sign_keys[j].key)) && + (0 == + GNUNET_memcmp (&sk->master_sig, + &b->keys->sign_keys[j].master_sig)) ) + found = true; + if (! found) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Signing key %s of `%s' missing in `%s'\n", + TALER_B2S (&sk->key), + a->label, + b->label); + return false; + } + } + return true; +} + + +/** + * Check that @a later (cherry-picked and merged by the client) equals + * @a full, downloaded in full right after it. Denominations that + * expire between the two downloads may legitimately differ and are + * ignored. + * + * @param later cherry-picked /keys + * @param full full /keys + * @return true if they are the same + */ +static bool +check_equal (const struct KeysSnapshot *later, + const struct KeysSnapshot *full) +{ + bool ok = true; + + if (0 != + GNUNET_memcmp (&later->keys->master_pub, + &full->keys->master_pub)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Master public key differs between `%s' and `%s'\n", + later->label, + full->label); + ok = false; + } + if (0 != strcmp (later->keys->currency, + full->keys->currency)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Currency differs between `%s' and `%s'\n", + later->label, + full->label); + ok = false; + } + if (! denoms_included (later, + full, + full->reply_time)) + ok = false; + if (! denoms_included (full, + later, + full->reply_time)) + ok = false; + if (! auditors_included (later, + full, + full->reply_time)) + ok = false; + if (! auditors_included (full, + later, + full->reply_time)) + ok = false; + if (! signkeys_included (later, + full)) + ok = false; + if (! signkeys_included (full, + later)) + ok = false; + if (! no_expired_denoms (full)) + ok = false; + if (! no_duplicate_denoms (full)) + ok = false; + return ok; +} + + +/** + * Run the "check keys" command. + * + * @param cls closure. + * @param cmd the command currently being executed. + * @param is the interpreter state. + */ +static void +check_keys_run (void *cls, + const struct TALER_TESTING_Command *cmd, + struct TALER_TESTING_Interpreter *is) +{ + struct CheckKeysState *cks = cls; + struct KeysSnapshot earlier; + struct KeysSnapshot later; + + (void) cmd; + if ( (GNUNET_OK != + get_snapshot (is, + cks->earlier_ref, + &earlier)) || + (GNUNET_OK != + get_snapshot (is, + cks->later_ref, + &later)) ) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (is); + return; + } + if (! check_successor (&earlier, + &later)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (is); + return; + } + if (NULL != cks->full_ref) + { + struct KeysSnapshot full; + + if (GNUNET_OK != + get_snapshot (is, + cks->full_ref, + &full)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (is); + return; + } + if (GNUNET_TIME_absolute_cmp (full.request_time, + <, + later.reply_time)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "`%s' must download /keys after `%s'\n", + cks->full_ref, + cks->later_ref); + GNUNET_break (0); + TALER_TESTING_interpreter_fail (is); + return; + } + if (! check_equal (&later, + &full)) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (is); + return; + } + } + TALER_TESTING_interpreter_next (is); +} + + +/** + * Cleanup the state. + * + * @param cls closure. + * @param cmd the command which is being cleaned up. + */ +static void +check_keys_cleanup (void *cls, + const struct TALER_TESTING_Command *cmd) +{ + struct CheckKeysState *cks = cls; + + (void) cmd; + GNUNET_free (cks); +} + + +struct TALER_TESTING_Command +TALER_TESTING_cmd_check_keys (const char *label, + const char *earlier_ref, + const char *later_ref, + const char *full_ref) +{ + struct CheckKeysState *cks; + + GNUNET_assert (NULL != earlier_ref); + GNUNET_assert (NULL != later_ref); + cks = GNUNET_new (struct CheckKeysState); + cks->earlier_ref = earlier_ref; + cks->later_ref = later_ref; + cks->full_ref = full_ref; + { + struct TALER_TESTING_Command cmd = { + .cls = cks, + .label = label, + .run = &check_keys_run, + .cleanup = &check_keys_cleanup + }; + + return cmd; + } +} + + +/* end of testing_api_cmd_check_keys.c */ diff --git a/src/testing/testing_api_cmd_get_exchange.c b/src/testing/testing_api_cmd_get_exchange.c @@ -76,6 +76,16 @@ struct GetExchangeState struct GNUNET_TIME_Timestamp my_denom_date; /** + * When did we send the /keys request? + */ + struct GNUNET_TIME_Absolute request_time; + + /** + * When did we receive the reply to the /keys request? + */ + struct GNUNET_TIME_Absolute reply_time; + + /** * Are we waiting for /keys before continuing? */ bool wait_for_keys; @@ -99,6 +109,7 @@ cert_cb (struct GetExchangeState *ges, struct TALER_TESTING_Interpreter *is = ges->is; ges->exchange = NULL; + ges->reply_time = GNUNET_TIME_absolute_get (); if (NULL != ges->keys) TALER_EXCHANGE_keys_decref (ges->keys); ges->keys = keys; @@ -249,6 +260,7 @@ get_exchange_run (void *cls, TALER_EXCHANGE_get_keys_option_last_keys (xkeys)); } TALER_EXCHANGE_keys_decref (xkeys); + ges->request_time = GNUNET_TIME_absolute_get (); if (TALER_EC_NONE != TALER_EXCHANGE_get_keys_start (ges->exchange, &cert_cb, @@ -317,6 +329,10 @@ get_exchange_traits (void *cls, TALER_TESTING_make_trait_exchange_url (ges->exchange_url), TALER_TESTING_make_trait_timestamp (0, &ges->my_denom_date), + TALER_TESTING_make_trait_absolute_time (0, + &ges->request_time), + TALER_TESTING_make_trait_absolute_time (1, + &ges->reply_time), TALER_TESTING_trait_end () }; @@ -332,6 +348,10 @@ get_exchange_traits (void *cls, TALER_TESTING_make_trait_exchange_url (ges->exchange_url), TALER_TESTING_make_trait_timestamp (0, &ges->my_denom_date), + TALER_TESTING_make_trait_absolute_time (0, + &ges->request_time), + TALER_TESTING_make_trait_absolute_time (1, + &ges->reply_time), TALER_TESTING_trait_end () };