commit 9ff5cad379154ed45845d02890428362a1a4b2d3 parent 2a5d98f79cec4fe444050974008a0a36647e257a Author: Özgür Kesim <oec@codeblau.de> Date: Mon, 14 Sep 2026 20:23:46 +0200 exchangedb: record the coin index of recoups and expose it in histories Schema patch exchange-0016 adds coin_index to the recoup and recoup_refresh tables: the batch recoup protocol (#9828) identifies a coin by its position in the batch of coins signed in the withdraw or refresh operation, and the coin history reports that position so a wallet can replay the recoup. do_recoup() and do_recoup_refresh() take the index. get_withdraw() and get_refresh() now return the row id of the operation, which the new handlers need to link a recoup to it. The coin history entries carry the commitment of the originating operation (planchets_h resp. rc) and the coin index; the reserve history RECOUP entry carries planchets_h. The recoup-by-old-coin history query joins refresh directly. Table tests: the coin history checks assert the new fields for a withdraw recoup and, new, for a refresh recoup on both coins; the reserve history gets a recoup case. The old per-coin handlers only pass a zero index until they are replaced. Diffstat:
24 files changed, 853 insertions(+), 21 deletions(-)
diff --git a/src/exchange/taler-exchange-httpd_post-recoup-refresh.c b/src/exchange/taler-exchange-httpd_post-recoup-refresh.c @@ -114,6 +114,7 @@ recoup_refresh_transaction (void *cls, qs = TALER_EXCHANGEDB_do_recoup_refresh (TEH_pg, &pc->old_coin_pub, pc->rrc_serial, + 0, /* coin_index, FIXME_9828 */ pc->coin_bks, &pc->coin->coin_pub, pc->known_coin_id, diff --git a/src/exchange/taler-exchange-httpd_post-recoup-withdraw.c b/src/exchange/taler-exchange-httpd_post-recoup-withdraw.c @@ -117,6 +117,7 @@ recoup_transaction (void *cls, qs = TALER_EXCHANGEDB_do_recoup (TEH_pg, &pc->reserve_pub, pc->withdraw_serial_id, + 0, /* coin_index, FIXME_9828 */ pc->coin_bks, &pc->coin->coin_pub, pc->known_coin_id, diff --git a/src/exchangedb/do_recoup.c b/src/exchangedb/do_recoup.c @@ -28,6 +28,7 @@ TALER_EXCHANGEDB_do_recoup ( struct TALER_EXCHANGEDB_PostgresContext *pg, const struct TALER_ReservePublicKeyP *reserve_pub, uint64_t withdraw_id, + uint32_t coin_index, const union GNUNET_CRYPTO_BlindingSecretP *coin_bks, const struct TALER_CoinSpendPublicKeyP *coin_pub, uint64_t known_coin_id, @@ -43,6 +44,7 @@ TALER_EXCHANGEDB_do_recoup ( struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_auto_from_type (reserve_pub), GNUNET_PQ_query_param_uint64 (&withdraw_id), + GNUNET_PQ_query_param_uint32 (&coin_index), GNUNET_PQ_query_param_auto_from_type (coin_bks), GNUNET_PQ_query_param_auto_from_type (coin_pub), GNUNET_PQ_query_param_uint64 (&known_coin_id), @@ -73,7 +75,7 @@ TALER_EXCHANGEDB_do_recoup ( ",out_recoup_ok AS recoup_ok" ",out_internal_failure AS internal_failure" " FROM exchange_do_recoup_to_reserve" - " ($1,$2,$3,$4,$5,$6,$7,$8,$9);"); + " ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10);"); return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, "do_recoup", params, diff --git a/src/exchangedb/do_recoup.sql b/src/exchangedb/do_recoup.sql @@ -21,6 +21,7 @@ DROP FUNCTION IF EXISTS exchange_do_recoup_to_reserve; CREATE FUNCTION exchange_do_recoup_to_reserve( IN in_reserve_pub BYTEA, IN in_withdraw_id INT8, + IN in_coin_index INT4, IN in_coin_blind BYTEA, IN in_coin_pub BYTEA, IN in_known_coin_id INT8, @@ -68,13 +69,17 @@ tmp := rval.remaining; IF tmp.val + tmp.frac = 0 THEN - -- Check for idempotency + -- Check for idempotency: only a recoup for this very + -- withdraw operation and position counts, a recoup of the same + -- coin for another operation means the coin has nothing left. SELECT recoup_timestamp INTO out_recoup_timestamp FROM exchange.recoup - WHERE coin_pub=in_coin_pub; + WHERE coin_pub=in_coin_pub + AND withdraw_id=in_withdraw_id + AND coin_index=in_coin_index; out_recoup_ok=FOUND; RETURN; @@ -134,6 +139,7 @@ INSERT INTO exchange.recoup ,amount ,recoup_timestamp ,withdraw_id + ,coin_index ) VALUES (in_coin_pub @@ -141,7 +147,8 @@ VALUES ,in_coin_blind ,tmp ,in_recoup_timestamp - ,in_withdraw_id); + ,in_withdraw_id + ,in_coin_index); -- Normal end, everything is fine. out_recoup_ok=TRUE; diff --git a/src/exchangedb/do_recoup_refresh.c b/src/exchangedb/do_recoup_refresh.c @@ -28,6 +28,7 @@ TALER_EXCHANGEDB_do_recoup_refresh ( struct TALER_EXCHANGEDB_PostgresContext *pg, const struct TALER_CoinSpendPublicKeyP *old_coin_pub, uint64_t refresh_id, + uint32_t coin_index, const union GNUNET_CRYPTO_BlindingSecretP *coin_bks, const struct TALER_CoinSpendPublicKeyP *coin_pub, uint64_t known_coin_id, @@ -39,6 +40,7 @@ TALER_EXCHANGEDB_do_recoup_refresh ( struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_auto_from_type (old_coin_pub), GNUNET_PQ_query_param_uint64 (&refresh_id), + GNUNET_PQ_query_param_uint32 (&coin_index), GNUNET_PQ_query_param_auto_from_type (coin_bks), GNUNET_PQ_query_param_auto_from_type (coin_pub), GNUNET_PQ_query_param_uint64 (&known_coin_id), @@ -67,7 +69,7 @@ TALER_EXCHANGEDB_do_recoup_refresh ( ",out_recoup_ok AS recoup_ok" ",out_internal_failure AS internal_failure" " FROM exchange_do_recoup_to_coin" - " ($1,$2,$3,$4,$5,$6,$7);"); + " ($1,$2,$3,$4,$5,$6,$7,$8);"); return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, "do_recoup_refresh", diff --git a/src/exchangedb/do_recoup_refresh.sql b/src/exchangedb/do_recoup_refresh.sql @@ -18,6 +18,7 @@ DROP FUNCTION IF EXISTS exchange_do_recoup_to_coin; CREATE FUNCTION exchange_do_recoup_to_coin( IN in_old_coin_pub BYTEA, IN in_refresh_id INT8, + IN in_coin_index INT4, IN in_coin_blind BYTEA, IN in_coin_pub BYTEA, IN in_known_coin_id INT8, @@ -60,13 +61,17 @@ tmp := rval.remaining; IF tmp.val + tmp.frac = 0 THEN - -- Check for idempotency + -- Check for idempotency: only a recoup for this very + -- refresh operation and position counts, a recoup of the same + -- coin for another operation means the coin has nothing left. SELECT recoup_timestamp INTO out_recoup_timestamp FROM recoup_refresh - WHERE coin_pub=in_coin_pub; + WHERE coin_pub=in_coin_pub + AND refresh_id=in_refresh_id + AND coin_index=in_coin_index; out_recoup_ok=FOUND; RETURN; END IF; @@ -112,6 +117,7 @@ INSERT INTO recoup_refresh ,amount ,recoup_timestamp ,refresh_id + ,coin_index ) VALUES (in_coin_pub @@ -120,7 +126,8 @@ VALUES ,in_coin_blind ,tmp ,in_recoup_timestamp - ,in_refresh_id); + ,in_refresh_id + ,in_coin_index); -- Normal end, everything is fine. out_recoup_ok=TRUE; diff --git a/src/exchangedb/get_coin_transactions.c b/src/exchangedb/get_coin_transactions.c @@ -473,6 +473,10 @@ add_old_coin_recoup (void *cls, &recoup->value), GNUNET_PQ_result_spec_timestamp ("recoup_timestamp", &recoup->timestamp), + GNUNET_PQ_result_spec_uint32 ("coin_index", + &recoup->coin_index), + GNUNET_PQ_result_spec_auto_from_type ("rc", + &recoup->rc), GNUNET_PQ_result_spec_auto_from_type ("denom_pub_hash", &recoup->coin.denom_pub_hash), TALER_PQ_result_spec_denom_sig ("denom_sig", @@ -542,6 +546,10 @@ add_coin_recoup (void *cls, &recoup->value), GNUNET_PQ_result_spec_timestamp ("recoup_timestamp", &recoup->timestamp), + GNUNET_PQ_result_spec_uint32 ("coin_index", + &recoup->coin_index), + GNUNET_PQ_result_spec_auto_from_type ("planchets_h", + &recoup->planchets_h), GNUNET_PQ_result_spec_uint64 ("recoup_uuid", &serial_id), GNUNET_PQ_result_spec_end @@ -604,6 +612,10 @@ add_coin_recoup_refresh (void *cls, &recoup->value), GNUNET_PQ_result_spec_timestamp ("recoup_timestamp", &recoup->timestamp), + GNUNET_PQ_result_spec_uint32 ("coin_index", + &recoup->coin_index), + GNUNET_PQ_result_spec_auto_from_type ("rc", + &recoup->rc), GNUNET_PQ_result_spec_auto_from_type ("denom_pub_hash", &recoup->coin.denom_pub_hash), TALER_PQ_result_spec_denom_sig ("denom_sig", @@ -1025,19 +1037,20 @@ TALER_EXCHANGEDB_get_coin_transactions ( ",rr.coin_blind" ",rr.amount" ",rr.recoup_timestamp" + ",rr.coin_index" + ",rfc.rc" ",denoms.denom_pub_hash" ",coins.denom_sig" ",rr.recoup_refresh_uuid" " FROM recoup_refresh rr" + " JOIN refresh rfc" + " ON (rfc.refresh_id = rr.refresh_id)" " JOIN known_coins coins" - " USING (coin_pub)" + " ON (coins.coin_pub = rr.coin_pub)" " JOIN denominations denoms" - " USING (denominations_serial)" - " WHERE recoup_refresh_uuid=$2" - " AND refresh_id IN" - " (SELECT refresh_id" - " FROM refresh" - " WHERE refresh.old_coin_pub=$1);"); + " ON (denoms.denominations_serial = coins.denominations_serial)" + " WHERE rr.recoup_refresh_uuid=$2" + " AND rfc.old_coin_pub=$1;"); PREPARE (pg, "get_coin_transactions_recoup_by_coin", "SELECT" @@ -1047,6 +1060,8 @@ TALER_EXCHANGEDB_get_coin_transactions ( ",rcp.coin_blind" ",rcp.amount" ",rcp.recoup_timestamp" + ",rcp.coin_index" + ",ro.planchets_h" ",rcp.recoup_uuid" " FROM recoup rcp" " JOIN withdraw ro" @@ -1069,6 +1084,8 @@ TALER_EXCHANGEDB_get_coin_transactions ( ",rr.coin_blind" ",rr.amount" ",rr.recoup_timestamp" + ",rr.coin_index" + ",rfc.rc" ",denoms.denom_pub_hash" ",coins.denom_sig" ",recoup_refresh_uuid" diff --git a/src/exchangedb/get_refresh.c b/src/exchangedb/get_refresh.c @@ -43,6 +43,8 @@ TALER_EXCHANGEDB_get_refresh ( struct TALER_TransferPublicKeyP *transfer_pubs = NULL; uint64_t *denom_serials = NULL; struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_uint64 ("refresh_id", + &refresh->refresh_id), TALER_PQ_RESULT_SPEC_AMOUNT ("amount_with_fee", &refresh->amount_with_fee), GNUNET_PQ_result_spec_auto_from_type ("old_coin_pub", @@ -102,7 +104,8 @@ TALER_EXCHANGEDB_get_refresh ( PREPARE (pg, "get_refresh", "SELECT" - " amount_with_fee" + " refresh_id" + ",amount_with_fee" ",old_coin_pub" ",kc.age_commitment_hash AS age_commitment_hash" ",old_coin_sig" diff --git a/src/exchangedb/get_reserve_history.c b/src/exchangedb/get_reserve_history.c @@ -299,6 +299,8 @@ add_recoup (void *cls, &recoup->coin_sig), GNUNET_PQ_result_spec_timestamp ("recoup_timestamp", &recoup->timestamp), + GNUNET_PQ_result_spec_auto_from_type ("planchets_h", + &recoup->planchets_h), GNUNET_PQ_result_spec_auto_from_type ("denom_pub_hash", &recoup->coin.denom_pub_hash), TALER_PQ_result_spec_denom_sig ( @@ -787,6 +789,7 @@ TALER_EXCHANGEDB_get_reserve_history ( ",rec.coin_blind" ",rec.amount" ",rec.recoup_timestamp" + ",ro.planchets_h" ",denom.denom_pub_hash" ",kc.denom_sig" " FROM recoup rec" diff --git a/src/exchangedb/get_withdraw.c b/src/exchangedb/get_withdraw.c @@ -48,6 +48,8 @@ TALER_EXCHANGEDB_get_withdraw ( bool no_cs_r_choices; struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_uint64 ("withdraw_id", + &wd->withdraw_id), GNUNET_PQ_result_spec_auto_from_type ("planchets_h", &wd->planchets_h), GNUNET_PQ_result_spec_auto_from_type ("reserve_sig", @@ -100,7 +102,8 @@ TALER_EXCHANGEDB_get_withdraw ( PREPARE (pg, "get_withdraw", "SELECT" - " planchets_h" + " withdraw_id" + ",planchets_h" ",blinding_seed" ",reserve_sig" ",reserve_pub" diff --git a/src/exchangedb/insert_records_by_table.c b/src/exchangedb/insert_records_by_table.c @@ -1311,6 +1311,7 @@ irbt_cb_table_recoup (struct TALER_EXCHANGEDB_PostgresContext *pg, GNUNET_PQ_query_param_auto_from_type ( &td->details.recoup.coin_pub), GNUNET_PQ_query_param_uint64 (&td->details.recoup.withdraw_serial_id), + GNUNET_PQ_query_param_uint32 (&td->details.recoup.coin_index), GNUNET_PQ_query_param_end }; @@ -1324,8 +1325,9 @@ irbt_cb_table_recoup (struct TALER_EXCHANGEDB_PostgresContext *pg, ",recoup_timestamp" ",coin_pub" ",withdraw_id" + ",coin_index" ") VALUES " - "($1, $2, $3, $4, $5, $6, $7);"); + "($1, $2, $3, $4, $5, $6, $7, $8);"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, "insert_records_by_table_into_table_recoup", params); @@ -1360,6 +1362,8 @@ irbt_cb_table_recoup_refresh (struct TALER_EXCHANGEDB_PostgresContext *pg, &td->details.recoup_refresh.coin_pub), GNUNET_PQ_query_param_uint64 ( &td->details.recoup_refresh.refresh_id), + GNUNET_PQ_query_param_uint32 ( + &td->details.recoup_refresh.coin_index), GNUNET_PQ_query_param_end }; @@ -1374,8 +1378,9 @@ irbt_cb_table_recoup_refresh (struct TALER_EXCHANGEDB_PostgresContext *pg, ",known_coin_id" ",coin_pub" ",refresh_id" + ",coin_index" ") VALUES " - "($1, $2, $3, $4, $5, $6, $7, $8);"); + "($1, $2, $3, $4, $5, $6, $7, $8, $9);"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, "insert_records_by_table_into_table_recoup_refresh", params); diff --git a/src/exchangedb/iterate_records_by_table.c b/src/exchangedb/iterate_records_by_table.c @@ -1522,6 +1522,8 @@ lrbt_cb_table_recoup (void *cls, &td.details.recoup.coin_pub), GNUNET_PQ_result_spec_uint64 ("withdraw_id", &td.details.recoup.withdraw_serial_id), + GNUNET_PQ_result_spec_uint32 ("coin_index", + &td.details.recoup.coin_index), GNUNET_PQ_result_spec_end }; @@ -1586,6 +1588,9 @@ lrbt_cb_table_recoup_refresh (void *cls, GNUNET_PQ_result_spec_uint64 ( "refresh_id", &td.details.recoup_refresh.refresh_id), + GNUNET_PQ_result_spec_uint32 ( + "coin_index", + &td.details.recoup_refresh.coin_index), GNUNET_PQ_result_spec_end }; @@ -3345,6 +3350,7 @@ TALER_EXCHANGEDB_iterate_records_by_table ( ",recoup_timestamp" ",coin_pub" ",withdraw_id" + ",coin_index" " FROM recoup" " WHERE recoup_uuid > $1" " ORDER BY recoup_uuid ASC;"); @@ -3361,6 +3367,7 @@ TALER_EXCHANGEDB_iterate_records_by_table ( ",coin_pub" ",known_coin_id" ",refresh_id" + ",coin_index" " FROM recoup_refresh" " WHERE recoup_refresh_uuid > $1" " ORDER BY recoup_refresh_uuid ASC;"); diff --git a/src/exchangedb/sql-schema/exchange-0016.sql b/src/exchangedb/sql-schema/exchange-0016.sql @@ -0,0 +1,76 @@ +-- +-- This file is part of TALER +-- Copyright (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/> +-- + + +BEGIN; + +SELECT _v.register_patch('exchange-0016', NULL, NULL); + +SET search_path TO exchange; + + +-- Record the position of a recouped coin in the batch of coins that the +-- exchange signed in the original withdraw or refresh operation. The batch +-- recoup protocol (see #9828) identifies coins by that position, and the +-- coin history reports it so that a wallet can replay the recoup. +-- +-- The recoup endpoints were disabled before this column was introduced, so +-- no rows are expected to predate it; the default only keeps the migration +-- from failing on a database that has some. + +CREATE FUNCTION alter_table_recoup16() +RETURNS void +LANGUAGE plpgsql +AS $$ +BEGIN + ALTER TABLE recoup + ADD COLUMN coin_index INT4 NOT NULL DEFAULT (0); + COMMENT ON COLUMN recoup.coin_index + IS 'index of the coin in the batch of coins signed in the withdraw operation referenced by withdraw_id, starting at 0'; +END +$$; + +CREATE FUNCTION alter_table_recoup_refresh16() +RETURNS void +LANGUAGE plpgsql +AS $$ +BEGIN + ALTER TABLE recoup_refresh + ADD COLUMN coin_index INT4 NOT NULL DEFAULT (0); + COMMENT ON COLUMN recoup_refresh.coin_index + IS 'index of the coin in the batch of coins signed in the refresh operation referenced by refresh_id, starting at 0'; +END +$$; + +INSERT INTO exchange_tables + (name + ,version + ,action + ,partitioned + ,by_range) + VALUES + ('recoup16' + ,'exchange-0016' + ,'alter' + ,TRUE + ,FALSE), + ('recoup_refresh16' + ,'exchange-0016' + ,'alter' + ,TRUE + ,FALSE); + +COMMIT; diff --git a/src/exchangedb/sql-schema/meson.build b/src/exchangedb/sql-schema/meson.build @@ -191,6 +191,7 @@ generated_sql = [ ['exchange-0013.sql', exchange_0013_sql], ['exchange-0014.sql', ['exchange-0014.sql']], ['exchange-0015.sql', ['exchange-0015.sql']], + ['exchange-0016.sql', ['exchange-0016.sql']], ['tops-0001.sql', ['tops-0001.sql']], ] diff --git a/src/exchangedb/test_coin_history.c b/src/exchangedb/test_coin_history.c @@ -33,6 +33,7 @@ #include "exchange-database/rollback.h" #include "exchange-database/start.h" #include "exchange-database/do_recoup.h" +#include "exchange-database/do_recoup_refresh.h" #include "exchange-database/do_refresh.h" #include "exchange-database/do_refund.h" #include "exchange-database/get_coin_transactions.h" @@ -538,7 +539,7 @@ check_other_spends (struct TALER_EXCHANGEDB_PostgresContext *pg) if (revealed) FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != TALER_EXCHANGEDB_update_to_refresh_revealed (pg, - &rf.rc)); + &rf.rc)); FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != history (pg, 0, @@ -655,6 +656,7 @@ check_recoup (struct TALER_EXCHANGEDB_PostgresContext *pg) TALER_EXCHANGEDB_do_recoup (pg, &reserve_pub, withdraw_id, + 3, &coin_bks, &coin.coin_pub, known_coin_id, @@ -682,6 +684,36 @@ check_recoup (struct TALER_EXCHANGEDB_PostgresContext *pg) TALER_EXCHANGEDB_free_coin_transaction_list (tl)); FAILIF_C (0 == (types & (1U << TALER_EXCHANGEDB_TT_RECOUP_WITHDRAW)), TALER_EXCHANGEDB_free_coin_transaction_list (tl)); + /* the entry names the withdraw operation (by its commitment, seeded + like the withdraw in check_fresh_coin()) and the coin's position */ + { + const struct TALER_EXCHANGEDB_RecoupListEntry *recoup = NULL; + struct TALER_HashBlindedPlanchetsP planchets_h; + + TDB_FILL (planchets_h, + 11); + for (const struct TALER_EXCHANGEDB_TransactionList *pos = tl; + NULL != pos; + pos = pos->next) + if (TALER_EXCHANGEDB_TT_RECOUP_WITHDRAW == pos->type) + recoup = pos->details.recoup; + FAILIF_C (NULL == recoup, + TALER_EXCHANGEDB_free_coin_transaction_list (tl)); + FAILIF_C (3 != recoup->coin_index, + TALER_EXCHANGEDB_free_coin_transaction_list (tl)); + FAILIF_C (0 != GNUNET_memcmp (&recoup->planchets_h, + &planchets_h), + TALER_EXCHANGEDB_free_coin_transaction_list (tl)); + FAILIF_C (0 != GNUNET_memcmp (&recoup->reserve_pub, + &reserve_pub), + TALER_EXCHANGEDB_free_coin_transaction_list (tl)); + FAILIF_C (0 != GNUNET_memcmp (&recoup->coin_sig, + &coin_sig), + TALER_EXCHANGEDB_free_coin_transaction_list (tl)); + FAILIF_C (0 != GNUNET_memcmp (&recoup->coin_blind, + &coin_bks), + TALER_EXCHANGEDB_free_coin_transaction_list (tl)); + } TALER_EXCHANGEDB_free_coin_transaction_list (tl); FAILIF (0 != TALER_amount_cmp (&balance, &zero)); @@ -732,6 +764,184 @@ check_in_transaction (struct TALER_EXCHANGEDB_PostgresContext *pg) /** + * Recouping a coin that was refreshed from @e coin credits @e coin and adds + * a RECOUP-REFRESH-RECEIVER entry to its history that names the refresh + * operation (by its commitment, seeded like the melt in + * check_other_spends()), the recouped coin and its position in the batch. + * The recouped coin's own history gets the matching RECOUP-REFRESH entry. + * + * @param pg the database context + * @return 0 on success + */ +static int +check_recoup_refresh (struct TALER_EXCHANGEDB_PostgresContext *pg) +{ + struct TALER_EXCHANGEDB_TransactionList *tl = NULL; + const struct TALER_EXCHANGEDB_RecoupRefreshListEntry *rr = NULL; + struct TALER_CoinPublicInfo fresh_coin; + struct TALER_RefreshCommitmentP rc; + struct TALER_CoinSpendSignatureP coin_sig; + union GNUNET_CRYPTO_BlindingSecretP coin_bks; + struct GNUNET_TIME_Timestamp recoup_timestamp; + struct TALER_Amount balance; + struct TALER_Amount zero = TDB_amount ("0"); + struct TALER_Amount credited = TDB_amount ("0.1"); + struct TALER_DenominationHashP h_denom_pub; + uint64_t fresh_known_coin_id; + uint64_t refresh_id; + uint64_t etag = 0; + unsigned int types; + bool recoup_ok; + bool internal_failure; + + TDB_FILL (rc, + 43); + /* row of the melt made in check_other_spends() */ + { + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (&rc), + GNUNET_PQ_query_param_end + }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_uint64 ("refresh_id", + &refresh_id), + GNUNET_PQ_result_spec_end + }; + + FAILIF (GNUNET_OK != + GNUNET_PQ_prepare_anon (pg->conn, + "SELECT refresh_id" + " FROM refresh" + " WHERE rc=$1;")); + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "", + params, + rs)); + } + TDB_coin (pg, + &fresh_denom, + 60, + &fresh_coin, + &fresh_known_coin_id); + TDB_FILL (coin_sig, + 60); + TDB_FILL (coin_bks, + 60); + recoup_timestamp = GNUNET_TIME_timestamp_get (); + FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_EXCHANGEDB_do_recoup_refresh (pg, + &coin.coin_pub, + refresh_id, + 2, + &coin_bks, + &fresh_coin.coin_pub, + fresh_known_coin_id, + &coin_sig, + &recoup_timestamp, + &recoup_ok, + &internal_failure), + TDB_coin_free (&fresh_coin)); + FAILIF_C (internal_failure || ! recoup_ok, + TDB_coin_free (&fresh_coin)); + + /* the old coin is credited and sees the receiver entry */ + FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_EXCHANGEDB_get_coin_transactions (pg, + true, + &coin.coin_pub, + 0, + 0, + &etag, + &balance, + &h_denom_pub, + &tl), + TDB_coin_free (&fresh_coin)); + FAILIF_C (7 != summarize (tl, + &types), + TALER_EXCHANGEDB_free_coin_transaction_list (tl); + TDB_coin_free (&fresh_coin)); + for (const struct TALER_EXCHANGEDB_TransactionList *pos = tl; + NULL != pos; + pos = pos->next) + if (TALER_EXCHANGEDB_TT_RECOUP_REFRESH_RECEIVER == pos->type) + rr = pos->details.old_coin_recoup; + FAILIF_C (NULL == rr, + TALER_EXCHANGEDB_free_coin_transaction_list (tl); + TDB_coin_free (&fresh_coin)); + FAILIF_C (2 != rr->coin_index, + TALER_EXCHANGEDB_free_coin_transaction_list (tl); + TDB_coin_free (&fresh_coin)); + FAILIF_C (0 != GNUNET_memcmp (&rr->rc, + &rc), + TALER_EXCHANGEDB_free_coin_transaction_list (tl); + TDB_coin_free (&fresh_coin)); + FAILIF_C (0 != GNUNET_memcmp (&rr->coin.coin_pub, + &fresh_coin.coin_pub), + TALER_EXCHANGEDB_free_coin_transaction_list (tl); + TDB_coin_free (&fresh_coin)); + FAILIF_C (0 != GNUNET_memcmp (&rr->old_coin_pub, + &coin.coin_pub), + TALER_EXCHANGEDB_free_coin_transaction_list (tl); + TDB_coin_free (&fresh_coin)); + FAILIF_C (0 != TALER_amount_cmp (&rr->value, + &credited), + TALER_EXCHANGEDB_free_coin_transaction_list (tl); + TDB_coin_free (&fresh_coin)); + FAILIF_C (0 != TALER_amount_cmp (&balance, + &credited), + TALER_EXCHANGEDB_free_coin_transaction_list (tl); + TDB_coin_free (&fresh_coin)); + TALER_EXCHANGEDB_free_coin_transaction_list (tl); + tl = NULL; + + /* the recouped coin is emptied and sees the debit entry */ + etag = 0; + FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_EXCHANGEDB_get_coin_transactions (pg, + true, + &fresh_coin.coin_pub, + 0, + 0, + &etag, + &balance, + &h_denom_pub, + &tl), + TDB_coin_free (&fresh_coin)); + FAILIF_C (1 != summarize (tl, + &types), + TALER_EXCHANGEDB_free_coin_transaction_list (tl); + TDB_coin_free (&fresh_coin)); + FAILIF_C (TALER_EXCHANGEDB_TT_RECOUP_REFRESH != tl->type, + TALER_EXCHANGEDB_free_coin_transaction_list (tl); + TDB_coin_free (&fresh_coin)); + rr = tl->details.recoup_refresh; + FAILIF_C (2 != rr->coin_index, + TALER_EXCHANGEDB_free_coin_transaction_list (tl); + TDB_coin_free (&fresh_coin)); + FAILIF_C (0 != GNUNET_memcmp (&rr->rc, + &rc), + TALER_EXCHANGEDB_free_coin_transaction_list (tl); + TDB_coin_free (&fresh_coin)); + FAILIF_C (0 != GNUNET_memcmp (&rr->old_coin_pub, + &coin.coin_pub), + TALER_EXCHANGEDB_free_coin_transaction_list (tl); + TDB_coin_free (&fresh_coin)); + FAILIF_C (0 != GNUNET_memcmp (&rr->coin_sig, + &coin_sig), + TALER_EXCHANGEDB_free_coin_transaction_list (tl); + TDB_coin_free (&fresh_coin)); + FAILIF_C (0 != TALER_amount_cmp (&balance, + &zero), + TALER_EXCHANGEDB_free_coin_transaction_list (tl); + TDB_coin_free (&fresh_coin)); + TALER_EXCHANGEDB_free_coin_transaction_list (tl); + TDB_coin_free (&fresh_coin); + return 0; +} + + +/** * The checks to run, in order. */ static const struct TDB_Test tests[] = { @@ -751,6 +961,8 @@ static const struct TDB_Test tests[] = { &check_recoup }, { "coin-history-in-transaction", &check_in_transaction }, + { "coin-history-recoup-refresh", + &check_recoup_refresh }, { NULL, NULL } }; diff --git a/src/exchangedb/test_recoup.c b/src/exchangedb/test_recoup.c @@ -30,6 +30,8 @@ #include "exchange-database/do_recoup.h" #include "exchange-database/get_reserve.h" #include "exchange-database/iterate_recoups_above_serial_id.h" +#include "exchange-database/iterate_records_by_table.h" +#include "exchange-database/insert_records_by_table.h" /** @@ -193,6 +195,7 @@ run_recoup (struct TALER_EXCHANGEDB_PostgresContext *pg, return TALER_EXCHANGEDB_do_recoup (pg, reserve_pub, withdraw_id, + 0, &coin_bks, coin_pub, known_coin_id, @@ -448,12 +451,166 @@ check_recoup (struct TALER_EXCHANGEDB_PostgresContext *pg) FAILIF_C (1 != TDB_count (pg, "FROM recoup"), TDB_coin_free (&coin)); + + /* ...but a recoup of the same coin for another withdraw operation + is not the same recoup: the coin has nothing left for it */ + { + uint64_t other_withdraw_id; + + other_withdraw_id = TDB_withdraw (pg, + &denom, + &reserve_pub, + 13, + "5"); + FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + run_recoup (pg, + &reserve_pub, + other_withdraw_id, + &coin.coin_pub, + known_coin_id, + 21, + &st), + TDB_coin_free (&coin)); + FAILIF_C (st.recoup_ok, + TDB_coin_free (&coin)); + FAILIF_C (st.internal_failure, + TDB_coin_free (&coin)); + FAILIF_C (1 != TDB_count (pg, + "FROM recoup"), + TDB_coin_free (&coin)); + } TDB_coin_free (&coin); return 0; } /** + * Closure for #replication_cb(). + */ +struct ReplicationContext +{ + /** + * Coin index every record must carry. + */ + uint32_t expected_coin_index; + + /** + * Records seen. + */ + unsigned int seen; + + /** + * Set if a record carried another coin index. + */ + bool mismatch; +}; + + +/** + * Counts the replication records of the recoup table and checks + * their coin index. + * + * @param cls a `struct ReplicationContext *` + * @param td the record + * @return #GNUNET_OK to continue + */ +static int +replication_cb (void *cls, + const struct TALER_EXCHANGEDB_TableData *td) +{ + struct ReplicationContext *rc = cls; + + if (TALER_EXCHANGEDB_RT_RECOUP != td->table) + { + rc->mismatch = true; + return GNUNET_OK; + } + rc->seen++; + if (rc->expected_coin_index != td->details.recoup.coin_index) + rc->mismatch = true; + return GNUNET_OK; +} + + +/** + * Replication carries the coin index of a recoup. + * + * @param pg the database context + * @return 0 on success + */ +static int +check_replication (struct TALER_EXCHANGEDB_PostgresContext *pg) +{ + struct ReplicationContext rc = { + .expected_coin_index = 0 + }; + struct TALER_EXCHANGEDB_TableData td = { + .table = TALER_EXCHANGEDB_RT_RECOUP, + .serial = 77, + .details.recoup.coin_index = 7, + .details.recoup.amount = TDB_amount ("5"), + .details.recoup.timestamp = GNUNET_TIME_timestamp_get () + }; + + /* the row made by check_recoup() is reported with its index 0 */ + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_EXCHANGEDB_iterate_records_by_table (pg, + TALER_EXCHANGEDB_RT_RECOUP, + 0, + &replication_cb, + &rc)); + FAILIF (1 != rc.seen); + /* a replicated row keeps its index */ + { + struct TALER_CoinPublicInfo coin; + uint64_t known_coin_id; + + TDB_coin (pg, + &denom, + 21, + &coin, + &known_coin_id); + td.details.recoup.coin_pub = coin.coin_pub; + TDB_coin_free (&coin); + } + { + struct TALER_ReservePublicKeyP reserve_pub; + + TDB_reserve_in (pg, + &account, + 14, + "10", + &reserve_pub); + td.details.recoup.withdraw_serial_id = TDB_withdraw (pg, + &denom, + &reserve_pub, + 14, + "5"); + } + TDB_FILL (td.details.recoup.coin_sig, + 77); + TDB_FILL (td.details.recoup.coin_blind, + 77); + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_EXCHANGEDB_insert_records_by_table (pg, + &td)); + FAILIF (1 != TDB_count (pg, + "FROM recoup WHERE coin_index=7")); + rc.seen = 0; + rc.expected_coin_index = 7; + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_EXCHANGEDB_iterate_records_by_table (pg, + TALER_EXCHANGEDB_RT_RECOUP, + 76, + &replication_cb, + &rc)); + FAILIF (1 != rc.seen); + FAILIF (rc.mismatch); + return 0; +} + + +/** * The iterator's serial bound and abort return behave as documented. * * @param pg the database context @@ -507,6 +664,8 @@ static const struct TDB_Test tests[] = { &check_recoup }, { "recoup-iterate", &check_iterate }, + { "recoup-replication", + &check_replication }, { NULL, NULL } }; diff --git a/src/exchangedb/test_recoup_refresh.c b/src/exchangedb/test_recoup_refresh.c @@ -34,6 +34,8 @@ #include "exchange-database/do_recoup_refresh.h" #include "exchange-database/do_refresh.h" #include "exchange-database/iterate_recoup_refreshes_above_serial_id.h" +#include "exchange-database/iterate_records_by_table.h" +#include "exchange-database/insert_records_by_table.h" /** @@ -246,6 +248,7 @@ run_recoup (struct TALER_EXCHANGEDB_PostgresContext *pg, return TALER_EXCHANGEDB_do_recoup_refresh (pg, old_coin_pub, refresh_id, + 0, &coin_bks, coin_pub, known_coin_id, @@ -458,6 +461,33 @@ check_recoup_refresh (struct TALER_EXCHANGEDB_PostgresContext *pg) FAILIF_C (1 != TDB_count (pg, "FROM recoup_refresh"), TDB_coin_free (&fresh); TDB_coin_free (&old_coin)); + + /* ...but a recoup of the same coin for another refresh operation + is not the same recoup: the coin has nothing left for it */ + { + uint64_t other_refresh_id; + + other_refresh_id = melt (pg, + &old_coin, + 5, + "1"); + FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + run_recoup (pg, + &old_coin.coin_pub, + other_refresh_id, + &fresh.coin_pub, + known_coin_id, + 24, + &st), + TDB_coin_free (&fresh); TDB_coin_free (&old_coin)); + FAILIF_C (st.recoup_ok, + TDB_coin_free (&fresh); TDB_coin_free (&old_coin)); + FAILIF_C (st.internal_failure, + TDB_coin_free (&fresh); TDB_coin_free (&old_coin)); + FAILIF_C (1 != TDB_count (pg, + "FROM recoup_refresh"), + TDB_coin_free (&fresh); TDB_coin_free (&old_coin)); + } TDB_coin_free (&fresh); TDB_coin_free (&old_coin); return 0; @@ -465,6 +495,128 @@ check_recoup_refresh (struct TALER_EXCHANGEDB_PostgresContext *pg) /** + * Closure for #replication_cb(). + */ +struct ReplicationContext +{ + /** + * Coin index every record must carry. + */ + uint32_t expected_coin_index; + + /** + * Records seen. + */ + unsigned int seen; + + /** + * Set if a record carried another coin index. + */ + bool mismatch; +}; + + +/** + * Counts the replication records of the recoup table and checks + * their coin index. + * + * @param cls a `struct ReplicationContext *` + * @param td the record + * @return #GNUNET_OK to continue + */ +static int +replication_cb (void *cls, + const struct TALER_EXCHANGEDB_TableData *td) +{ + struct ReplicationContext *rc = cls; + + if (TALER_EXCHANGEDB_RT_RECOUP_REFRESH != td->table) + { + rc->mismatch = true; + return GNUNET_OK; + } + rc->seen++; + if (rc->expected_coin_index != td->details.recoup_refresh.coin_index) + rc->mismatch = true; + return GNUNET_OK; +} + + +/** + * Replication carries the coin index of a recoup-refresh. + * + * @param pg the database context + * @return 0 on success + */ +static int +check_replication (struct TALER_EXCHANGEDB_PostgresContext *pg) +{ + struct ReplicationContext rc = { + .expected_coin_index = 0 + }; + struct TALER_EXCHANGEDB_TableData td = { + .table = TALER_EXCHANGEDB_RT_RECOUP_REFRESH, + .serial = 77, + .details.recoup_refresh.coin_index = 7, + .details.recoup_refresh.amount = TDB_amount ("1"), + .details.recoup_refresh.recoup_timestamp = GNUNET_TIME_timestamp_get () + }; + struct TALER_CoinPublicInfo old_coin; + struct TALER_CoinPublicInfo fresh; + + /* the row made by check_recoup_refresh() is reported with its index 0 */ + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_EXCHANGEDB_iterate_records_by_table ( + pg, + TALER_EXCHANGEDB_RT_RECOUP_REFRESH, + 0, + &replication_cb, + &rc)); + FAILIF (1 != rc.seen); + FAILIF (rc.mismatch); + /* a replicated row keeps its index */ + TDB_coin (pg, + &denom, + 25, + &old_coin, + NULL); + TDB_coin (pg, + &denom, + 26, + &fresh, + &td.details.recoup_refresh.known_coin_id); + td.details.recoup_refresh.coin_pub = fresh.coin_pub; + td.details.recoup_refresh.refresh_id = melt (pg, + &old_coin, + 6, + "1"); + TDB_coin_free (&old_coin); + TDB_coin_free (&fresh); + TDB_FILL (td.details.recoup_refresh.coin_sig, + 77); + TDB_FILL (td.details.recoup_refresh.coin_blind, + 77); + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_EXCHANGEDB_insert_records_by_table (pg, + &td)); + FAILIF (1 != TDB_count (pg, + "FROM recoup_refresh WHERE coin_index=7")); + rc.seen = 0; + rc.expected_coin_index = 7; + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_EXCHANGEDB_iterate_records_by_table ( + pg, + TALER_EXCHANGEDB_RT_RECOUP_REFRESH, + 76, + &replication_cb, + &rc)); + FAILIF (1 != rc.seen); + FAILIF (rc.mismatch); + return 0; +} + + +/** * The iterator must not report a recoup-refresh that is not there. * * This is all it can be checked for today: its statement joins @@ -503,6 +655,8 @@ static const struct TDB_Test tests[] = { &check_recoup_refresh }, { "recoup-refresh-iterate", &check_iterate }, + { "recoup-refresh-replication", + &check_replication }, { NULL, NULL } }; diff --git a/src/exchangedb/test_reserve_history.c b/src/exchangedb/test_reserve_history.c @@ -29,6 +29,7 @@ * short-circuit -- which must not leave a transaction open. */ #include "test_common.h" +#include "exchange-database/do_recoup.h" #include "exchange-database/get_reserve_history.h" #include "exchange-database/insert_close_request.h" #include "exchange-database/start.h" @@ -294,6 +295,129 @@ check_two_entries (struct TALER_EXCHANGEDB_PostgresContext *pg) /** + * A recoup shows up as a RECOUP entry that names the coin, the amount + * and the withdraw operation the coin came from. + * + * @param pg the database context + * @return 0 on success + */ +static int +check_recoup (struct TALER_EXCHANGEDB_PostgresContext *pg) +{ + struct TDB_Denom denom; + struct TALER_CoinPublicInfo coin; + struct TALER_ReservePublicKeyP reserve_pub; + struct TALER_HashBlindedPlanchetsP planchets_h; + struct TALER_CoinSpendSignatureP coin_sig; + union GNUNET_CRYPTO_BlindingSecretP coin_bks; + struct GNUNET_TIME_Timestamp recoup_timestamp; + struct TALER_EXCHANGEDB_ReserveHistory *rh = NULL; + struct TALER_Amount balance; + struct TALER_Amount expect_value = TDB_amount ("5"); + struct TALER_Amount expect_balance = TDB_amount ("10"); + const struct TALER_EXCHANGEDB_Recoup *recoup = NULL; + unsigned int matched = 0; + uint64_t withdraw_id; + uint64_t known_coin_id; + uint64_t etag = 0; + bool recoup_ok; + bool internal_failure; + + TDB_denom (pg, + 30, + "5", + "0.1", + &denom); + TDB_reserve_in (pg, + &account, + 30, + "10", + &reserve_pub); + withdraw_id = TDB_withdraw (pg, + &denom, + &reserve_pub, + 30, + "5"); + /* TDB_withdraw() seeds the commitment with its seed */ + TDB_FILL (planchets_h, + 30); + TDB_coin (pg, + &denom, + 31, + &coin, + &known_coin_id); + TDB_FILL (coin_sig, + 31); + TDB_FILL (coin_bks, + 31); + recoup_timestamp = GNUNET_TIME_timestamp_get (); + FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_EXCHANGEDB_do_recoup (pg, + &reserve_pub, + withdraw_id, + 2, + &coin_bks, + &coin.coin_pub, + known_coin_id, + &coin_sig, + &recoup_timestamp, + &recoup_ok, + &internal_failure), + TDB_coin_free (&coin); TDB_denom_free (&denom)); + FAILIF_C (internal_failure || ! recoup_ok, + TDB_coin_free (&coin); TDB_denom_free (&denom)); + FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_EXCHANGEDB_get_reserve_history (pg, + &reserve_pub, + 0, + 0, + &etag, + &balance, + &rh), + TDB_coin_free (&coin); TDB_denom_free (&denom)); + /* bank transfer in, withdraw, recoup */ + FAILIF_C (3 != count_history (rh, + TALER_EXCHANGEDB_RO_RECOUP_COIN, + &matched), + TALER_EXCHANGEDB_free_reserve_history (rh); + TDB_coin_free (&coin); TDB_denom_free (&denom)); + FAILIF_C (1 != matched, + TALER_EXCHANGEDB_free_reserve_history (rh); + TDB_coin_free (&coin); TDB_denom_free (&denom)); + for (const struct TALER_EXCHANGEDB_ReserveHistory *p = rh; + NULL != p; + p = p->next) + if (TALER_EXCHANGEDB_RO_RECOUP_COIN == p->type) + recoup = p->details.recoup; + FAILIF_C (0 != GNUNET_memcmp (&recoup->coin.coin_pub, + &coin.coin_pub), + TALER_EXCHANGEDB_free_reserve_history (rh); + TDB_coin_free (&coin); TDB_denom_free (&denom)); + FAILIF_C (0 != GNUNET_memcmp (&recoup->planchets_h, + &planchets_h), + TALER_EXCHANGEDB_free_reserve_history (rh); + TDB_coin_free (&coin); TDB_denom_free (&denom)); + FAILIF_C (0 != GNUNET_memcmp (&recoup->reserve_pub, + &reserve_pub), + TALER_EXCHANGEDB_free_reserve_history (rh); + TDB_coin_free (&coin); TDB_denom_free (&denom)); + FAILIF_C (0 != TALER_amount_cmp (&recoup->value, + &expect_value), + TALER_EXCHANGEDB_free_reserve_history (rh); + TDB_coin_free (&coin); TDB_denom_free (&denom)); + /* 10 in, 5 withdrawn, 5 recouped */ + FAILIF_C (0 != TALER_amount_cmp (&balance, + &expect_balance), + TALER_EXCHANGEDB_free_reserve_history (rh); + TDB_coin_free (&coin); TDB_denom_free (&denom)); + TALER_EXCHANGEDB_free_reserve_history (rh); + TDB_coin_free (&coin); + TDB_denom_free (&denom); + return 0; +} + + +/** * Passing the current ETag short-circuits the lookup -- and must not leave * the transaction it opened behind. * @@ -363,6 +487,8 @@ static const struct TDB_Test tests[] = { &check_bank_transfer }, { "reserve-history-two-entries", &check_two_entries }, + { "reserve-history-recoup", + &check_recoup }, { "reserve-history-etag", &check_etag }, { NULL, NULL } diff --git a/src/include/exchange-database/do_recoup.h b/src/include/exchange-database/do_recoup.h @@ -33,6 +33,7 @@ * @param pg the database context * @param reserve_pub public key of the reserve to credit * @param withdraw_serial_id row in the withdraw table justifying the recoup + * @param coin_index index of the coin in the batch signed in that withdraw operation * @param coin_bks coin blinding key secret to persist * @param coin_pub public key of the coin being recouped * @param known_coin_id row of the @a coin_pub in the known_coins table @@ -46,6 +47,7 @@ enum GNUNET_DB_QueryStatus TALER_EXCHANGEDB_do_recoup (struct TALER_EXCHANGEDB_PostgresContext *pg, const struct TALER_ReservePublicKeyP *reserve_pub, uint64_t withdraw_serial_id, + uint32_t coin_index, const union GNUNET_CRYPTO_BlindingSecretP *coin_bks, const struct TALER_CoinSpendPublicKeyP *coin_pub, uint64_t known_coin_id, diff --git a/src/include/exchange-database/do_recoup_refresh.h b/src/include/exchange-database/do_recoup_refresh.h @@ -33,7 +33,8 @@ * * @param pg the database context * @param old_coin_pub public key of the old coin to credit - * @param rrc_serial row in the refresh_revealed_coins table justifying the recoup-refresh + * @param rrc_serial row in the refresh table justifying the recoup-refresh + * @param coin_index index of the coin in the batch signed in that refresh operation * @param coin_bks coin blinding key secret to persist * @param coin_pub public key of the coin being recouped * @param known_coin_id row of the @a coin_pub in the known_coins table @@ -50,6 +51,7 @@ TALER_EXCHANGEDB_do_recoup_refresh (struct TALER_CoinSpendPublicKeyP * old_coin_pub, uint64_t rrc_serial, + uint32_t coin_index, const union GNUNET_CRYPTO_BlindingSecretP * coin_bks, diff --git a/src/include/exchange-database/free_coin_transaction_list.h b/src/include/exchange-database/free_coin_transaction_list.h @@ -368,6 +368,17 @@ struct TALER_EXCHANGEDB_RecoupListEntry */ struct GNUNET_TIME_Timestamp timestamp; + /** + * Commitment of the withdraw operation the coin originated from. + */ + struct TALER_HashBlindedPlanchetsP planchets_h; + + /** + * Index of the coin in the batch of coins signed in that + * withdraw operation, starting at 0. + */ + uint32_t coin_index; + }; @@ -411,6 +422,18 @@ struct TALER_EXCHANGEDB_RecoupRefreshListEntry */ struct GNUNET_TIME_Timestamp timestamp; + /** + * Commitment of the refresh operation the recouped coin + * originated from. + */ + struct TALER_RefreshCommitmentP rc; + + /** + * Index of the recouped coin in the batch of coins signed in that + * refresh operation, starting at 0. + */ + uint32_t coin_index; + }; diff --git a/src/include/exchange-database/free_reserve_history.h b/src/include/exchange-database/free_reserve_history.h @@ -181,6 +181,12 @@ struct TALER_EXCHANGEDB_Withdraw * get_reserve_history(). */ struct TALER_DenominationHashP *denom_pub_hashes; + + /** + * [out]-Row of this operation in the `withdraw` table. + * Set by get_withdraw(), zero otherwise. + */ + uint64_t withdraw_id; }; @@ -223,6 +229,11 @@ struct TALER_EXCHANGEDB_Recoup */ struct GNUNET_TIME_Timestamp timestamp; + /** + * Commitment of the withdraw operation the coin originated from. + */ + struct TALER_HashBlindedPlanchetsP planchets_h; + }; diff --git a/src/include/exchange-database/iterate_records_by_table.h b/src/include/exchange-database/iterate_records_by_table.h @@ -424,6 +424,7 @@ struct TALER_EXCHANGEDB_TableData struct TALER_Amount amount; struct GNUNET_TIME_Timestamp timestamp; uint64_t withdraw_serial_id; + uint32_t coin_index; } recoup; struct @@ -435,6 +436,7 @@ struct TALER_EXCHANGEDB_TableData struct TALER_Amount amount; struct GNUNET_TIME_Timestamp recoup_timestamp; uint64_t refresh_id; + uint32_t coin_index; } recoup_refresh; struct diff --git a/src/include/exchangedb_lib.h b/src/include/exchangedb_lib.h @@ -796,6 +796,12 @@ struct TALER_EXCHANGEDB_Refresh_vDOLDPLUS * get_refresh */ struct TALER_DenominationHashP *denom_pub_hashes; + + /** + * [out]-Row of this operation in the `refresh` table. + * Set by get_refresh(), zero otherwise. + */ + uint64_t refresh_id; };