commit 3bdc7c0c70dd1dc457cdb7e900401ba1af7a2318
parent 7bc5f4e7fb42af73b9514f0655d9cc45f20e79fe
Author: Özgür Kesim <oec@codeblau.de>
Date: Mon, 14 Sep 2026 21:24:33 +0200
testing: batch and age-restricted recoup cases with history checks
test_exchange_api_revocation gets a batch withdraw of three coins, a
recoup of two of them in one request, coin and reserve history checks
and an idempotent replay. test_exchange_api_age_restriction recoups
two age-restricted coins, exercising the comparison against the hash
of the selected kappa batch.
For that, the age-withdraw command expands the planchet secrets of the
batch the exchange signed from its kappa seed and exposes them as
traits, and the age reveal command forwards the withdraw command traits
next to its denomination signatures. The recoup command prefers a
command s per-coin planchet secrets over expanding the withdraw seed.
Diffstat:
4 files changed, 104 insertions(+), 12 deletions(-)
diff --git a/src/testing/test_exchange_api_age_restriction.c b/src/testing/test_exchange_api_age_restriction.c
@@ -364,6 +364,24 @@ run (void *cls,
"age-withdraw-coins-reveal-1",
"age-withdraw-coins-1",
MHD_HTTP_OK),
+ /* Recoup of age-restricted coins: the exchange must verify the
+ batch against the hash of the selected kappa batch. */
+ TALER_TESTING_cmd_revoke (
+ "revoke-age-EUR:10",
+ MHD_HTTP_OK,
+ "age-withdraw-coins-1",
+ config_file),
+ TALER_TESTING_cmd_recoup_batch (
+ "recoup-age-withdraw",
+ MHD_HTTP_OK,
+ "age-withdraw-coins-reveal-1",
+ "0,1",
+ "EUR:10"),
+ TALER_TESTING_cmd_coin_history (
+ "coin-history-age-recouped",
+ "age-withdraw-coins-reveal-1#1",
+ "EUR:0",
+ MHD_HTTP_OK),
TALER_TESTING_cmd_end (),
};
diff --git a/src/testing/test_exchange_api_revocation.c b/src/testing/test_exchange_api_revocation.c
@@ -264,6 +264,54 @@ run (void *cls,
"create-reserve-1",
"EUR:3.79",
MHD_HTTP_OK),
+ /* Batch recoup: three coins from one withdraw operation, two of
+ them recouped in one request; the histories must show it. */
+ TALER_TESTING_cmd_admin_add_incoming ("create-reserve-2",
+ "EUR:30.04",
+ &cred.ba,
+ cred.user42_payto),
+ TALER_TESTING_cmd_check_bank_admin_transfer ("check-create-reserve-2",
+ "EUR:30.04",
+ cred.user42_payto,
+ cred.exchange_payto,
+ "create-reserve-2"),
+ CMD_EXEC_WIREWATCH ("wirewatch-5"),
+ TALER_TESTING_cmd_batch_withdraw ("batch-withdraw-revocation",
+ "create-reserve-2",
+ MHD_HTTP_OK,
+ "EUR:10",
+ "EUR:10",
+ "EUR:10",
+ NULL),
+ TALER_TESTING_cmd_revoke ("revoke-5-EUR:10",
+ MHD_HTTP_OK,
+ "batch-withdraw-revocation",
+ config_file),
+ TALER_TESTING_cmd_recoup_batch ("recoup-batch",
+ MHD_HTTP_OK,
+ "batch-withdraw-revocation",
+ "0,2",
+ "EUR:10"),
+ TALER_TESTING_cmd_coin_history ("coin-history-recouped",
+ "batch-withdraw-revocation#0",
+ "EUR:0",
+ MHD_HTTP_OK),
+ /* the coin that was not recouped is still unknown to the exchange */
+ TALER_TESTING_cmd_coin_history ("coin-history-not-recouped",
+ "batch-withdraw-revocation#1",
+ "EUR:10",
+ MHD_HTTP_NOT_FOUND),
+ TALER_TESTING_cmd_reserve_history ("reserve-history-recoup",
+ "create-reserve-2",
+ "EUR:20.01",
+ MHD_HTTP_OK),
+ /* replaying the request is idempotent (last, as the history
+ checks above would otherwise expect its entries twice) */
+ TALER_TESTING_cmd_recoup_batch ("recoup-batch-again",
+ MHD_HTTP_OK,
+ "batch-withdraw-revocation",
+ "0,2",
+ "EUR:10"),
TALER_TESTING_cmd_end ()
};
diff --git a/src/testing/testing_api_cmd_age_withdraw.c b/src/testing/testing_api_cmd_age_withdraw.c
@@ -144,6 +144,13 @@ struct AgeWithdrawState
struct CoinOutputState *coin_outputs;
/**
+ * Array of @e num_coins planchet secrets of the batch the exchange
+ * signed (the one at @e noreveal_index), expanded from @e kappa_seed
+ * once the response is known. NULL before.
+ */
+ struct TALER_PlanchetMasterSecretP *selected_secrets;
+
+ /**
* The index returned by the exchange for the "age-withdraw" operation,
* of the kappa coin candidates that we do not disclose and keep.
*/
@@ -214,6 +221,12 @@ age_withdraw_cb (
aws->kappa_seed = response->details.created.kappa_seed;
GNUNET_assert (aws->num_coins == response->details.created.num_coins);
+ GNUNET_assert (aws->noreveal_index < TALER_CNC_KAPPA);
+ aws->selected_secrets = GNUNET_new_array (aws->num_coins,
+ struct TALER_PlanchetMasterSecretP);
+ TALER_withdraw_expand_secrets (aws->num_coins,
+ &aws->kappa_seed.tuple[aws->noreveal_index],
+ aws->selected_secrets);
for (size_t n = 0; n < aws->num_coins; n++)
{
aws->coin_outputs[n].details = response->details.created.coin_details[n];
@@ -431,6 +444,8 @@ age_withdraw_cleanup (
GNUNET_free (aws->coin_outputs);
aws->coin_outputs = NULL;
}
+ GNUNET_free (aws->selected_secrets);
+ aws->selected_secrets = NULL;
GNUNET_free (aws->exchange_url);
aws->exchange_url = NULL;
@@ -480,10 +495,11 @@ age_withdraw_traits (
TALER_TESTING_make_trait_coin_priv (idx,
&details->coin_priv),
TALER_TESTING_make_trait_withdraw_seed (&aws->seed),
- /* FIXME[oec]: needed!?
- TALER_TESTING_make_trait_planchet_secrets (idx,
- &aws->secrets[k][idx]),
- */
+ TALER_TESTING_make_trait_planchet_secrets (
+ idx,
+ (NULL != aws->selected_secrets)
+ ? &aws->selected_secrets[idx]
+ : NULL),
TALER_TESTING_make_trait_blinding_key (idx,
&details->blinding_key),
TALER_TESTING_make_trait_exchange_blinding_values (idx,
@@ -799,8 +815,6 @@ age_reveal_withdraw_traits (
struct TALER_TESTING_Trait traits[] = {
TALER_TESTING_make_trait_denom_sig (idx,
&awrs->denom_sigs[idx]),
- /* FIXME: shall we provide the traits from the previous
- * call to "age withdraw" as well? */
TALER_TESTING_trait_end ()
};
@@ -808,11 +822,17 @@ age_reveal_withdraw_traits (
return GNUNET_NO;
if (idx >= awrs->num_coins)
return GNUNET_NO;
-
- return TALER_TESTING_get_trait (traits,
- ret,
- trait,
- idx);
+ if (GNUNET_OK ==
+ TALER_TESTING_get_trait (traits,
+ ret,
+ trait,
+ idx))
+ return GNUNET_OK;
+ /* everything else about the coins comes from the withdraw command */
+ return age_withdraw_traits ((void *) awrs->aws,
+ ret,
+ trait,
+ idx);
}
diff --git a/src/testing/testing_api_cmd_recoup.c b/src/testing/testing_api_cmd_recoup.c
@@ -285,7 +285,13 @@ recoup_run (void *cls,
(unsigned int) i,
&hac)) )
c->h_age_commitment = hac; /* may still be NULL */
- c->ps = &secrets[i];
+ /* commands with per-coin secrets (batch and age withdraw) expose
+ them; the single withdraw derives them from the seed */
+ if (GNUNET_OK !=
+ TALER_TESTING_get_trait_planchet_secrets (coin_cmd,
+ (unsigned int) i,
+ &c->ps))
+ c->ps = &secrets[i];
if (GNUNET_CRYPTO_BSA_CS == c->pk->key.bsign_pub_key->cipher)
have_cs = true;
for (size_t j = 0; j < ps->num_indices; j++)