commit 97666f86c43df5573e278aa64f6476a419fbd0d1
parent 2fdf4d18f2227880385cff40fd107e7b467a4271
Author: Christian Grothoff <christian@grothoff.org>
Date: Thu, 6 Aug 2026 17:48:37 +0200
age commitment can be NULL as per schema, handle it nicely with purses
Diffstat:
5 files changed, 111 insertions(+), 10 deletions(-)
diff --git a/src/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-create.c b/src/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-create.c
@@ -266,7 +266,9 @@ create_transaction (void *cls,
struct TALER_Amount amount;
struct TALER_CoinSpendSignatureP coin_sig;
struct TALER_DenominationHashP h_denom_pub;
- struct TALER_AgeCommitmentHashP phac;
+ struct TALER_AgeCommitmentHashP hac;
+ const struct TALER_AgeCommitmentHashP *phac = NULL;
+ bool no_age_commitment;
char *partner_url = NULL;
TALER_EXCHANGEDB_rollback (TEH_pg);
@@ -275,7 +277,8 @@ create_transaction (void *cls,
&coin->cpi.coin_pub,
&amount,
&h_denom_pub,
- &phac,
+ &hac,
+ &no_age_commitment,
&coin_sig,
&partner_url);
if (qs < 0)
@@ -289,6 +292,8 @@ create_transaction (void *cls,
"get purse deposit");
return GNUNET_DB_STATUS_HARD_ERROR;
}
+ if (! no_age_commitment)
+ phac = &hac;
*mhd_ret
= TALER_MHD_REPLY_JSON_PACK (
@@ -302,8 +307,9 @@ create_transaction (void *cls,
&coin_sig),
GNUNET_JSON_pack_data_auto ("h_denom_pub",
&h_denom_pub),
- GNUNET_JSON_pack_data_auto ("h_age_restrictions",
- &phac),
+ GNUNET_JSON_pack_allow_null (
+ GNUNET_JSON_pack_data_auto ("h_age_restrictions",
+ phac)),
GNUNET_JSON_pack_allow_null (
GNUNET_JSON_pack_string ("partner_url",
partner_url)),
diff --git a/src/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-deposit.c b/src/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-deposit.c
@@ -227,7 +227,9 @@ deposit_transaction (void *cls,
struct TALER_Amount amount;
struct TALER_CoinSpendSignatureP coin_sig;
struct TALER_DenominationHashP h_denom_pub;
- struct TALER_AgeCommitmentHashP phac;
+ struct TALER_AgeCommitmentHashP hac;
+ const struct TALER_AgeCommitmentHashP *phac = NULL;
+ bool no_age_commitment;
char *partner_url = NULL;
TALER_EXCHANGEDB_rollback (TEH_pg);
@@ -236,7 +238,8 @@ deposit_transaction (void *cls,
&coin->cpi.coin_pub,
&amount,
&h_denom_pub,
- &phac,
+ &hac,
+ &no_age_commitment,
&coin_sig,
&partner_url);
if (qs < 0)
@@ -250,6 +253,8 @@ deposit_transaction (void *cls,
"get purse deposit");
return GNUNET_DB_STATUS_HARD_ERROR;
}
+ if (! no_age_commitment)
+ phac = &hac;
*mhd_ret
= TALER_MHD_REPLY_JSON_PACK (
@@ -261,8 +266,9 @@ deposit_transaction (void *cls,
&coin->cpi.coin_pub),
GNUNET_JSON_pack_data_auto ("h_denom_pub",
&h_denom_pub),
- GNUNET_JSON_pack_data_auto ("h_age_commitment",
- &phac),
+ GNUNET_JSON_pack_allow_null (
+ GNUNET_JSON_pack_data_auto ("h_age_commitment",
+ phac)),
GNUNET_JSON_pack_data_auto ("coin_sig",
&coin_sig),
GNUNET_JSON_pack_allow_null (
diff --git a/src/exchangedb/get_purse_deposit.c b/src/exchangedb/get_purse_deposit.c
@@ -30,6 +30,7 @@ TALER_EXCHANGEDB_get_purse_deposit (
struct TALER_Amount *amount,
struct TALER_DenominationHashP *h_denom_pub,
struct TALER_AgeCommitmentHashP *phac,
+ bool *no_age_commitment,
struct TALER_CoinSpendSignatureP *coin_sig,
char **partner_url)
{
@@ -42,8 +43,10 @@ TALER_EXCHANGEDB_get_purse_deposit (
struct GNUNET_PQ_ResultSpec rs[] = {
GNUNET_PQ_result_spec_auto_from_type ("denom_pub_hash",
h_denom_pub),
- GNUNET_PQ_result_spec_auto_from_type ("age_commitment_hash",
- phac),
+ GNUNET_PQ_result_spec_allow_null (
+ GNUNET_PQ_result_spec_auto_from_type ("age_commitment_hash",
+ phac),
+ no_age_commitment),
GNUNET_PQ_result_spec_auto_from_type ("coin_sig",
coin_sig),
TALER_PQ_RESULT_SPEC_AMOUNT ("amount_with_fee",
diff --git a/src/exchangedb/test_regressions.c b/src/exchangedb/test_regressions.c
@@ -32,6 +32,7 @@
#include "exchange-database/commit.h"
#include "exchange-database/rollback.h"
#include "exchange-database/do_reserve_open.h"
+#include "exchange-database/get_purse_deposit.h"
/**
@@ -494,6 +495,86 @@ check_reserve_open_int4_overflows (void)
/**
+ * E-5: `known_coins.age_commitment_hash` is NULL for every coin without an
+ * age commitment -- the common case -- but the result spec read it without
+ * GNUNET_PQ_result_spec_allow_null(), so building the 409 conflict proof for
+ * POST /purses/$PURSE_PUB/deposit failed with an HTTP 500 instead.
+ */
+static int
+check_purse_deposit_without_age_commitment (void)
+{
+ struct TALER_PurseContractPublicKeyP purse_pub;
+ struct TALER_CoinSpendPublicKeyP coin_pub;
+ struct TALER_Amount amount;
+ struct TALER_DenominationHashP h_denom_pub;
+ struct TALER_AgeCommitmentHashP hac;
+ struct TALER_CoinSpendSignatureP coin_sig;
+ bool no_age_commitment;
+ char *partner_url = NULL;
+ char *sql;
+ char *p_hex;
+ char *c_hex;
+ enum GNUNET_GenericReturnValue ok;
+
+ memset (&purse_pub,
+ 0x55,
+ sizeof (purse_pub));
+ memset (&coin_pub,
+ 0x56,
+ sizeof (coin_pub));
+ p_hex = to_hex (&purse_pub,
+ sizeof (purse_pub));
+ c_hex = to_hex (&coin_pub,
+ sizeof (coin_pub));
+ GNUNET_asprintf (
+ &sql,
+ "INSERT INTO denominations"
+ " (denom_pub_hash,denom_type,age_mask,denom_pub,master_sig"
+ " ,valid_from,expire_withdraw,expire_deposit,expire_legal"
+ " ,coin,fee_withdraw,fee_deposit,fee_refresh,fee_refund)"
+ " VALUES (decode(repeat('e5',64),'hex'),1,0,decode('00','hex')"
+ " ,decode(repeat('00',64),'hex'),0,0,0,0"
+ " ,ROW(1,0)::taler_amount,ROW(0,0)::taler_amount"
+ " ,ROW(0,0)::taler_amount,ROW(0,0)::taler_amount"
+ " ,ROW(0,0)::taler_amount);"
+ /* age_commitment_hash deliberately left NULL */
+ "INSERT INTO known_coins"
+ " (denominations_serial,coin_pub,denom_sig,remaining)"
+ " VALUES ((SELECT denominations_serial FROM denominations"
+ " WHERE denom_pub_hash=decode(repeat('e5',64),'hex'))"
+ " ,decode('%s','hex'),decode('00','hex')"
+ " ,ROW(0,0)::taler_amount);"
+ "INSERT INTO purse_deposits"
+ " (purse_pub,coin_pub,amount_with_fee,coin_sig)"
+ " VALUES (decode('%s','hex'),decode('%s','hex')"
+ " ,ROW(1,0)::taler_amount,decode(repeat('e5',64),'hex'));",
+ c_hex,
+ p_hex,
+ c_hex);
+ ok = exec_sql (sql);
+ GNUNET_free (sql);
+ GNUNET_free (p_hex);
+ GNUNET_free (c_hex);
+ FAILIF (GNUNET_OK != ok);
+
+ /* Before the fix: HARD_ERROR from the failed NULL extraction. */
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ TALER_EXCHANGEDB_get_purse_deposit (pg,
+ &purse_pub,
+ &coin_pub,
+ &amount,
+ &h_denom_pub,
+ &hac,
+ &no_age_commitment,
+ &coin_sig,
+ &partner_url));
+ GNUNET_free (partner_url);
+ FAILIF (! no_age_commitment);
+ return 0;
+}
+
+
+/**
* All checks we know about.
*/
static const struct
@@ -509,6 +590,8 @@ static const struct
&check_reserve_open_never_expires },
{ "reserve-open-int4-overflows",
&check_reserve_open_int4_overflows },
+ { "purse-deposit-without-age-commitment",
+ &check_purse_deposit_without_age_commitment },
{ NULL, NULL }
};
diff --git a/src/include/exchange-database/get_purse_deposit.h b/src/include/exchange-database/get_purse_deposit.h
@@ -36,6 +36,8 @@
* @param[out] amount set fraction of the coin's value that was deposited (with fee)
* @param[out] h_denom_pub set to hash of denomination of the coin
* @param[out] phac set to hash of age restriction on the coin
+ * @param[out] no_age_commitment set to true if the coin has no age
+ * restriction, in which case @a phac is left unchanged
* @param[out] coin_sig set to signature affirming the operation
* @param[out] partner_url set to the URL of the partner exchange, or NULL for ourselves, must be freed by caller
* @return transaction status code
@@ -49,6 +51,7 @@ TALER_EXCHANGEDB_get_purse_deposit (struct TALER_EXCHANGEDB_PostgresContext *pg,
struct TALER_Amount *amount,
struct TALER_DenominationHashP *h_denom_pub,
struct TALER_AgeCommitmentHashP *phac,
+ bool *no_age_commitment,
struct TALER_CoinSpendSignatureP *coin_sig,
char **partner_url);