commit 3cf90625b1cef76be71dd6ff56742ff84c03918e
parent feca982974a05c2fe157a4a1265096717c9e1843
Author: Florian Dold <dold@taler.net>
Date: Wed, 9 Sep 2026 15:37:56 +0200
exchange: return refresh denominations in coin history
Include denoms_h in MELT history entries so wallets can reconstruct the
original refresh request. Resolve stored denomination serials to hashes
in coin order, preserving duplicates before and after reveal.
Diffstat:
5 files changed, 104 insertions(+), 7 deletions(-)
diff --git a/src/exchange/taler-exchange-httpd_get-coins-COIN_PUB-history.c b/src/exchange/taler-exchange-httpd_get-coins-COIN_PUB-history.c
@@ -213,6 +213,11 @@ compile_transaction_history (
&melt->h_denom_pub),
GNUNET_JSON_pack_data_auto ("refresh_seed",
&melt->refresh_seed),
+ TALER_JSON_pack_array_of_data (
+ "denoms_h",
+ melt->num_coins,
+ melt->denom_pub_hashes,
+ sizeof(melt->denom_pub_hashes[0])),
GNUNET_JSON_pack_allow_null (
GNUNET_JSON_pack_data_auto ("blinding_seed",
pbs)),
diff --git a/src/exchangedb/free_coin_transaction_list.c b/src/exchangedb/free_coin_transaction_list.c
@@ -40,6 +40,7 @@ TALER_EXCHANGEDB_free_coin_transaction_list (
break;
}
case TALER_EXCHANGEDB_TT_MELT:
+ GNUNET_free (tl->details.melt->denom_pub_hashes);
GNUNET_free (tl->details.melt);
break;
case TALER_EXCHANGEDB_TT_RECOUP_REFRESH_RECEIVER:
diff --git a/src/exchangedb/get_coin_transactions.c b/src/exchangedb/get_coin_transactions.c
@@ -258,6 +258,7 @@ add_coin_melt (void *cls,
melt = GNUNET_new (struct TALER_EXCHANGEDB_MeltListEntry);
{
+ struct TALER_DenominationHashP *denom_pub_hashes = NULL;
struct GNUNET_PQ_ResultSpec rs[] = {
GNUNET_PQ_result_spec_auto_from_type ("rc",
&melt->rc),
@@ -268,6 +269,10 @@ add_coin_melt (void *cls,
&melt->coin_sig),
GNUNET_PQ_result_spec_auto_from_type ("refresh_seed",
&melt->refresh_seed),
+ TALER_PQ_result_spec_array_denom_hash (pg->conn,
+ "denom_pub_hashes",
+ &melt->num_coins,
+ &denom_pub_hashes),
GNUNET_PQ_result_spec_allow_null (
GNUNET_PQ_result_spec_auto_from_type ("blinding_seed",
&melt->blinding_seed),
@@ -291,10 +296,14 @@ add_coin_melt (void *cls,
i))
{
GNUNET_break (0);
+ GNUNET_PQ_cleanup_result (rs);
GNUNET_free (melt);
chc->failed = true;
return;
}
+ melt->denom_pub_hashes = denom_pub_hashes;
+ denom_pub_hashes = NULL;
+ GNUNET_PQ_cleanup_result (rs);
}
tl = GNUNET_new (struct TALER_EXCHANGEDB_TransactionList);
tl->next = chc->head;
@@ -930,6 +939,14 @@ TALER_EXCHANGEDB_get_coin_transactions (
",denoms.fee_refresh"
",kc.age_commitment_hash"
",refresh_id"
+ ",ARRAY("
+ " SELECT fresh_denoms.denom_pub_hash"
+ " FROM unnest(refresh.denom_serials) WITH ORDINALITY"
+ " AS fresh(denom_serial, coin_index)"
+ " LEFT JOIN denominations fresh_denoms"
+ " ON fresh_denoms.denominations_serial=fresh.denom_serial"
+ " ORDER BY fresh.coin_index"
+ ") AS denom_pub_hashes"
" FROM refresh"
" JOIN known_coins kc"
" ON (refresh.old_coin_pub = kc.coin_pub)"
diff --git a/src/exchangedb/test_coin_history.c b/src/exchangedb/test_coin_history.c
@@ -37,6 +37,7 @@
#include "exchange-database/do_refund.h"
#include "exchange-database/get_coin_transactions.h"
#include "exchange-database/insert_reserve_open_deposit.h"
+#include "exchange-database/update_to_refresh_revealed.h"
/**
@@ -52,6 +53,12 @@ static struct TDB_Denom denom;
/**
+ * A different denomination used among the refresh outputs.
+ */
+static struct TDB_Denom fresh_denom;
+
+
+/**
* The coin whose history we build up.
*/
static struct TALER_CoinPublicInfo coin;
@@ -486,12 +493,24 @@ check_other_spends (struct TALER_EXCHANGEDB_PostgresContext *pg)
TDB_FILL (rf.selected_h,
44);
rf.amount_with_fee = one;
- rf.num_coins = 1;
- rf.denom_serials = GNUNET_new (uint64_t);
- rf.denom_serials[0] = denom.serial;
- rf.denom_sigs = GNUNET_new (struct TALER_BlindedDenominationSignature);
- TDB_blinded_denom_sig (43,
- &rf.denom_sigs[0]);
+ TDB_denom (pg,
+ 11,
+ "0.1",
+ "0.01",
+ &fresh_denom);
+ rf.num_coins = 3;
+ rf.denom_serials = GNUNET_new_array (rf.num_coins,
+ uint64_t);
+ /* Deliberately neither sorted nor distinct. */
+ rf.denom_serials[0] = fresh_denom.serial;
+ rf.denom_serials[1] = denom.serial;
+ rf.denom_serials[2] = fresh_denom.serial;
+ rf.denom_sigs = GNUNET_new_array (
+ rf.num_coins,
+ struct TALER_BlindedDenominationSignature);
+ for (unsigned int i = 0; i < rf.num_coins; i++)
+ TDB_blinded_denom_sig (43 + i,
+ &rf.denom_sigs[i]);
rf.noreveal_index = 1;
rf.is_v27_refresh = true;
rf.no_blinding_seed = true;
@@ -504,11 +523,54 @@ check_other_spends (struct TALER_EXCHANGEDB_PostgresContext *pg)
&nonce_reuse,
&melt_balance_ok,
&coin_balance);
- TALER_blinded_denom_sig_free (&rf.denom_sigs[0]);
+ for (unsigned int i = 0; i < rf.num_coins; i++)
+ TALER_blinded_denom_sig_free (&rf.denom_sigs[i]);
GNUNET_free (rf.denom_sigs);
GNUNET_free (rf.denom_serials);
FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs);
FAILIF (! melt_balance_ok);
+
+ /* The original output list is available before and after reveal. */
+ for (unsigned int revealed = 0; revealed < 2; revealed++)
+ {
+ bool found_melt = false;
+
+ if (revealed)
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ TALER_EXCHANGEDB_update_to_refresh_revealed (pg,
+ &rf.rc));
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ history (pg,
+ 0,
+ 0,
+ &etag,
+ &tl));
+ for (const struct TALER_EXCHANGEDB_TransactionList *pos = tl;
+ NULL != pos;
+ pos = pos->next)
+ {
+ const struct TALER_EXCHANGEDB_MeltListEntry *melt;
+
+ if (TALER_EXCHANGEDB_TT_MELT != pos->type)
+ continue;
+ found_melt = true;
+ melt = pos->details.melt;
+ FAILIF_C (3 != melt->num_coins,
+ TALER_EXCHANGEDB_free_coin_transaction_list (tl));
+ FAILIF_C (0 != GNUNET_memcmp (&melt->denom_pub_hashes[0],
+ &fresh_denom.h_denom_pub),
+ TALER_EXCHANGEDB_free_coin_transaction_list (tl));
+ FAILIF_C (0 != GNUNET_memcmp (&melt->denom_pub_hashes[1],
+ &denom.h_denom_pub),
+ TALER_EXCHANGEDB_free_coin_transaction_list (tl));
+ FAILIF_C (0 != GNUNET_memcmp (&melt->denom_pub_hashes[2],
+ &fresh_denom.h_denom_pub),
+ TALER_EXCHANGEDB_free_coin_transaction_list (tl));
+ }
+ TALER_EXCHANGEDB_free_coin_transaction_list (tl);
+ tl = NULL;
+ FAILIF (! found_melt);
+ }
}
FAILIF (5 != TDB_count (pg,
@@ -706,6 +768,7 @@ main (int argc,
tests);
TDB_coin_free (&coin);
TDB_denom_free (&denom);
+ TDB_denom_free (&fresh_denom);
TDB_account_free (&account);
return ret;
}
diff --git a/src/include/exchange-database/free_coin_transaction_list.h b/src/include/exchange-database/free_coin_transaction_list.h
@@ -305,6 +305,17 @@ struct TALER_EXCHANGEDB_MeltListEntry
struct TALER_PublicRefreshMasterSeedP refresh_seed;
/**
+ * Number of fresh coins requested by this melt.
+ */
+ size_t num_coins;
+
+ /**
+ * Denomination hashes for the fresh coins, in the original request order.
+ * Contains @e num_coins entries, including repeated denominations.
+ */
+ struct TALER_DenominationHashP *denom_pub_hashes;
+
+ /**
* If false, @e blinding_seed is present
*/
bool no_blinding_seed;