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:
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,