exchange

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

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:
Msrc/exchange/taler-exchange-httpd_get-coins-COIN_PUB-history.c | 5+++++
Msrc/exchangedb/free_coin_transaction_list.c | 1+
Msrc/exchangedb/get_coin_transactions.c | 17+++++++++++++++++
Msrc/exchangedb/test_coin_history.c | 77++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/include/exchange-database/free_coin_transaction_list.h | 11+++++++++++
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;