merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

commit 2436287b5d7747f058cc6fae818f549a5221ebaa
parent 52d6b14afc917ca78a5a9d19161a2115343b98b5
Author: Christian Grothoff <christian@grothoff.org>
Date:   Wed,  5 Aug 2026 23:48:16 +0200

complain if exchange changed its wire fee

Diffstat:
Msrc/backenddb/insert_exchange_wire_fee.c | 72+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msrc/backenddb/test_merchantdb.c | 24++++++++++++++++++++++++
Msrc/include/merchant-database/insert_exchange_wire_fee.h | 7++++++-
3 files changed, 89 insertions(+), 14 deletions(-)

diff --git a/src/backenddb/insert_exchange_wire_fee.c b/src/backenddb/insert_exchange_wire_fee.c @@ -46,27 +46,73 @@ TALER_MERCHANTDB_insert_exchange_wire_fee ( GNUNET_PQ_query_param_auto_from_type (master_sig), GNUNET_PQ_query_param_end }; + bool inserted; + bool identical; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_bool ("inserted", + &inserted), + GNUNET_PQ_result_spec_bool ("identical", + &identical), + GNUNET_PQ_result_spec_end + }; + enum GNUNET_DB_QueryStatus qs; /* no preflight check here, run in its own transaction by the caller */ + /* Note: a bare 'ON CONFLICT DO NOTHING' would silently discard a + *changed* fee for an existing (master_pub, h_wire_method, + start_date), leaving us computing with the stale one. The CTE + therefore also reports whether the row that is already there is the + one we were asked to store; if it is not, the exchange contradicted + a fee it signed earlier and the caller must not proceed. */ PREPARE (pg, "insert_exchange_wire_fee", - "INSERT INTO merchant.merchant_exchange_wire_fees" - "(master_pub" - ",h_wire_method" - ",wire_fee" - ",closing_fee" - ",start_date" - ",end_date" - ",master_sig)" + "WITH ins AS (" + " INSERT INTO merchant.merchant_exchange_wire_fees" + " (master_pub" + " ,h_wire_method" + " ,wire_fee" + " ,closing_fee" + " ,start_date" + " ,end_date" + " ,master_sig)" " VALUES " - "($1, $2, $3, $4, $5, $6, $7)" - " ON CONFLICT DO NOTHING"); + " ($1, $2, $3, $4, $5, $6, $7)" + " ON CONFLICT (master_pub,h_wire_method,start_date)" + " DO NOTHING" + " RETURNING 1)" + "SELECT" + " EXISTS (SELECT 1 FROM ins) AS inserted" + ",EXISTS (SELECT 1" + " FROM merchant.merchant_exchange_wire_fees" + " WHERE master_pub=$1" + " AND h_wire_method=$2" + " AND start_date=$5" + " AND end_date=$6" + " AND wire_fee=$3" + " AND closing_fee=$4" + " AND master_sig=$7) AS identical"); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Storing wire fee for %s starting at %s of %s\n", TALER_B2S (master_pub), GNUNET_TIME_timestamp2s (start_date), TALER_amount2s (&fees->wire)); - return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "insert_exchange_wire_fee", - params); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "insert_exchange_wire_fee", + params, + rs); + if (qs < 0) + return qs; + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs); + if (inserted) + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; + if (identical) + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + /* FIXME: should properly distinguish this error from + actual database failures; change signature of function eventually! */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Exchange %s signed a different wire fee for the same wire method and start date %s; keeping the fee we already have on file\n", + TALER_B2S (master_pub), + GNUNET_TIME_timestamp2s (start_date)); + GNUNET_break (0); + return GNUNET_DB_STATUS_HARD_ERROR; } diff --git a/src/backenddb/test_merchantdb.c b/src/backenddb/test_merchantdb.c @@ -6231,6 +6231,30 @@ run_test_transfers (struct TestTransfers_Closure *cls) /* Test lookup wire fee by exchange */ TEST_RET_ON_FAIL (test_lookup_wire_fee (&cls->signkey, &cls->wire_fee[0])); + /* A *different* fee for the same (master_pub, h_wire_method, + start_date) must be reported, not silently dropped: the stored fee + is the one the exchange signed, and continuing to compute with a + stale fee is an accounting error. Regression test: the INSERT used + a bare 'ON CONFLICT DO NOTHING' and returned NO_RESULTS here. */ + { + struct WireFeeData changed = cls->wire_fee[0]; + + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount ("EUR:0.99", + &changed.fees.wire)); + TALER_exchange_offline_wire_fee_sign (changed.wire_method, + changed.wire_fee_start, + changed.wire_fee_end, + &changed.fees, + &cls->signkey.master_priv, + &changed.fee_sig); + TEST_RET_ON_FAIL (test_insert_wire_fee (&cls->signkey, + &changed, + GNUNET_DB_STATUS_HARD_ERROR)); + /* ... and the fee on file must be untouched. */ + TEST_RET_ON_FAIL (test_lookup_wire_fee (&cls->signkey, + &cls->wire_fee[0])); + } /* Test different wire fees for different methods. */ TEST_RET_ON_FAIL (test_insert_wire_fee (&cls->signkey, &cls->wire_fee[1], diff --git a/src/include/merchant-database/insert_exchange_wire_fee.h b/src/include/merchant-database/insert_exchange_wire_fee.h @@ -38,7 +38,12 @@ struct TALER_MERCHANTDB_PostgresContext; * @param start_date start of fee being used * @param end_date end of fee being used * @param master_sig signature of exchange over fee structure - * @return transaction status code + * @return #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT if the fee was stored, + * #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if we already had exactly this + * fee on file, #GNUNET_DB_STATUS_HARD_ERROR if we already have a + * *different* fee for this @a master_pub, @a h_wire_method and + * @a start_date (which the exchange previously signed, and which we + * thus keep) -- or the usual negative status on a database failure */ enum GNUNET_DB_QueryStatus TALER_MERCHANTDB_insert_exchange_wire_fee (struct TALER_MERCHANTDB_PostgresContext *pg,