commit 0095cbecbdabd9fafaf47b90719decfd2d437d3e parent f3fe43e48b0c39031b8626b2f5acc4ea0befb8d3 Author: Christian Grothoff <christian@grothoff.org> Date: Sun, 25 Oct 2026 20:41:37 +0100 change profitable to tri-state (-1/0/1) Diffstat:
17 files changed, 279 insertions(+), 52 deletions(-)
diff --git a/src/auditor/taler-auditor-httpd.c b/src/auditor/taler-auditor-httpd.c @@ -85,7 +85,7 @@ * release version, and the format is NOT the same that semantic * versioning uses either. */ -#define AUDITOR_PROTOCOL_VERSION "2:0:0" +#define AUDITOR_PROTOCOL_VERSION "3:0:0" /** * Salt we use when doing the KDF for access. diff --git a/src/auditor/taler-auditor-httpd_get-monitoring-amount-arithmetic-inconsistency.c b/src/auditor/taler-auditor-httpd_get-monitoring-amount-arithmetic-inconsistency.c @@ -52,8 +52,8 @@ add_amount_arithmetic_inconsistency ( &dc->exchange_amount), TALER_JSON_pack_amount ("auditor_amount", &dc->auditor_amount), - GNUNET_JSON_pack_bool ("profitable", - dc->profitable), + GNUNET_JSON_pack_int64 ("profitable", + dc->profitable), GNUNET_JSON_pack_bool ("suppressed", dc->suppressed) ); @@ -93,15 +93,15 @@ TAH_get_monitoring_amount_arithmetic_inconsistency ( NULL); } TAH_PARSE_REQUEST_LIMIT (connection, - "limit", - &limit); + "limit", + &limit); if (limit < 0) offset = TAH_SAFE_UINT64_MAX; else offset = 0; TAH_PARSE_SAFE_REQUEST_NUMBER (connection, - "offset", - &offset); + "offset", + &offset); { const char *ret_s; diff --git a/src/auditor/taler-auditor-httpd_get-monitoring-coin-inconsistency.c b/src/auditor/taler-auditor-httpd_get-monitoring-coin-inconsistency.c @@ -49,7 +49,7 @@ add_coin_inconsistency ( TALER_JSON_pack_amount ("exchange_amount", &dc->exchange_amount), TALER_JSON_pack_amount ("auditor_amount", &dc->auditor_amount), GNUNET_JSON_pack_data_auto ("coin_pub",&dc->coin_pub), - GNUNET_JSON_pack_bool ("profitable", dc->profitable), + GNUNET_JSON_pack_int64 ("profitable", dc->profitable), GNUNET_JSON_pack_bool ("suppressed", dc->suppressed) ); GNUNET_break (0 == @@ -84,15 +84,15 @@ TAH_get_monitoring_coin_inconsistency ( NULL); } TAH_PARSE_REQUEST_LIMIT (connection, - "limit", - &limit); + "limit", + &limit); if (limit < 0) offset = TAH_SAFE_UINT64_MAX; else offset = 0; TAH_PARSE_SAFE_REQUEST_NUMBER (connection, - "offset", - &offset); + "offset", + &offset); { const char *ret_s = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, diff --git a/src/auditor/taler-auditor-httpd_mhd.c b/src/auditor/taler-auditor-httpd_mhd.c @@ -33,7 +33,10 @@ /** - * Check recursively that integers in @a value are valid SafeUint64 values. + * Check recursively that integers in @a value can be represented exactly by + * JavaScript clients, that is, that their magnitude does not exceed the + * largest SafeUint64. Negative values occur (such as "profitable" being -1 + * for a loss). * * @param value JSON value to inspect * @return true if all integers are safe @@ -45,8 +48,9 @@ json_integers_are_safe (const json_t *value) { json_int_t number = json_integer_value (value); - return (number >= 0) && - ((uint64_t) number <= TAH_SAFE_UINT64_MAX); + if (number < 0) + return ((uint64_t) (-(number + 1))) < TAH_SAFE_UINT64_MAX; + return ((uint64_t) number <= TAH_SAFE_UINT64_MAX); } if (json_is_array (value)) { @@ -54,8 +58,8 @@ json_integers_are_safe (const json_t *value) json_t *child; json_array_foreach ((json_t *) value, index, child) - if (! json_integers_are_safe (child)) - return false; + if (! json_integers_are_safe (child)) + return false; } if (json_is_object (value)) { @@ -63,8 +67,8 @@ json_integers_are_safe (const json_t *value) json_t *child; json_object_foreach ((json_t *) value, key, child) - if (! json_integers_are_safe (child)) - return false; + if (! json_integers_are_safe (child)) + return false; } return true; } diff --git a/src/auditor/taler-helper-auditor-transfer.c b/src/auditor/taler-helper-auditor-transfer.c @@ -661,6 +661,8 @@ evaluate_hold (struct HoldContext *hc, liabilities, which is the direction that favours the exchange */ .profitable = (0 > TALER_amount_cmp (&claimed, &amount)) + ? 1 + : -1 }; qs = TALER_AUDITORDB_insert_amount_arithmetic_inconsistency ( @@ -749,6 +751,8 @@ evaluate_hold (struct HoldContext *hc, liabilities, which is the direction that favours the exchange */ .profitable = (0 > TALER_amount_cmp (&transient, &amount)) + ? 1 + : -1 }; qs = TALER_AUDITORDB_insert_amount_arithmetic_inconsistency ( diff --git a/src/auditor/test-auditor.sh b/src/auditor/test-auditor.sh @@ -1946,9 +1946,10 @@ function test_25() { check_auditor_running echo -n "Testing inconsistency detection... " + # The coin is now overspent: a loss for the exchange. check_report \ "coin-inconsistencies" \ - "profitable" "true" + "profitable" "-1" echo -n "Testing emergency risk reporting... " check_report \ "emergencies" \ @@ -2087,10 +2088,12 @@ function test_29() { check_report_neg \ "bad-sig-losses" \ "loss" "TESTKUDOS:0" - echo -n "Testing inconsistency was reported as profitable... " + echo -n "Testing inconsistency was reported as a loss... " + # With the higher melt fee, the melted coins no longer cover the + # cost of the fresh coins: the exchange lost money. check_report \ "amount-arithmetic-inconsistencies" \ - "profitable" "true" + "profitable" "-1" echo -n "Testing no emergency was raised... " check_no_report "emergencies" diff --git a/src/auditordb/0006-auditor_amount_arithmetic_inconsistency.sql b/src/auditordb/0006-auditor_amount_arithmetic_inconsistency.sql @@ -0,0 +1,29 @@ +-- +-- 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/> +-- + +-- "profitable" is tri-state: the helpers report 1 if the inconsistency +-- favours the exchange, -1 if it is a loss for the exchange and 0 if +-- that is unclear. It used to be a BOOLEAN, which stored both 1 and -1 +-- as TRUE and 0 as FALSE. Old rows therefore map TRUE to 1 and FALSE to +-- 0; a loss recorded before this migration stays reported as a profit, as +-- the original sign cannot be recovered. +ALTER TABLE auditor_amount_arithmetic_inconsistency + ALTER COLUMN profitable TYPE SMALLINT + USING (CASE WHEN profitable THEN 1 ELSE 0 END) + ,ADD CONSTRAINT auditor_amount_arithmetic_inconsistency_profitable_check + CHECK (profitable BETWEEN -1 AND 1); +COMMENT ON COLUMN auditor_amount_arithmetic_inconsistency.profitable + IS '1 if the inconsistency is profitable for the exchange, -1 if it is a loss for the exchange, 0 if unclear; rows created before auditor-0006 only have 1 (profit or loss) and 0'; diff --git a/src/auditordb/0006-auditor_coin_inconsistency.sql b/src/auditordb/0006-auditor_coin_inconsistency.sql @@ -0,0 +1,29 @@ +-- +-- 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/> +-- + +-- "profitable" is tri-state: the helpers report 1 if the inconsistency +-- favours the exchange, -1 if it is a loss for the exchange and 0 if +-- that is unclear. It used to be a BOOLEAN, which stored both 1 and -1 +-- as TRUE and 0 as FALSE. Old rows therefore map TRUE to 1 and FALSE to +-- 0; a loss recorded before this migration stays reported as a profit, as +-- the original sign cannot be recovered. +ALTER TABLE auditor_coin_inconsistency + ALTER COLUMN profitable TYPE SMALLINT + USING (CASE WHEN profitable THEN 1 ELSE 0 END) + ,ADD CONSTRAINT auditor_coin_inconsistency_profitable_check + CHECK (profitable BETWEEN -1 AND 1); +COMMENT ON COLUMN auditor_coin_inconsistency.profitable + IS '1 if the inconsistency is profitable for the exchange, -1 if it is a loss for the exchange, 0 if unclear; rows created before auditor-0006 only have 1 (profit or loss) and 0'; diff --git a/src/auditordb/0006-preamble.sql b/src/auditordb/0006-preamble.sql @@ -0,0 +1,21 @@ +-- +-- 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('auditor-0006', NULL, NULL); + +SET search_path TO auditor; diff --git a/src/auditordb/insert_amount_arithmetic_inconsistency.c b/src/auditordb/insert_amount_arithmetic_inconsistency.c @@ -33,7 +33,7 @@ TALER_AUDITORDB_insert_amount_arithmetic_inconsistency (struct &dc->exchange_amount), TALER_PQ_query_param_amount (pg->conn, &dc->auditor_amount), - GNUNET_PQ_query_param_bool (dc->profitable), + GNUNET_PQ_query_param_int16 (&dc->profitable), GNUNET_PQ_query_param_end }; diff --git a/src/auditordb/insert_coin_inconsistency.c b/src/auditordb/insert_coin_inconsistency.c @@ -32,7 +32,7 @@ TALER_AUDITORDB_insert_coin_inconsistency (struct TALER_PQ_query_param_amount (pg->conn, &dc->auditor_amount), GNUNET_PQ_query_param_auto_from_type (&dc->coin_pub), - GNUNET_PQ_query_param_bool (dc->profitable), + GNUNET_PQ_query_param_int16 (&dc->profitable), GNUNET_PQ_query_param_end }; diff --git a/src/auditordb/iterate_amount_arithmetic_inconsistencies.c b/src/auditordb/iterate_amount_arithmetic_inconsistencies.c @@ -75,6 +75,9 @@ amount_arithmetic_inconsistency_cb (void *cls, for (unsigned int i = 0; i < num_results; i++) { struct TALER_AUDITORDB_AmountArithmeticInconsistency dc; + /* libgnunetpq declares GNUNET_PQ_result_spec_int16() but does + not implement it, so the SMALLINT is read as INT8 */ + int64_t profitable; struct GNUNET_PQ_ResultSpec rs[] = { GNUNET_PQ_result_spec_uint64 ("row_id", &dc.row_id), @@ -86,8 +89,8 @@ amount_arithmetic_inconsistency_cb (void *cls, &dc.exchange_amount), TALER_PQ_RESULT_SPEC_AMOUNT ("auditor_amount", &dc.auditor_amount), - GNUNET_PQ_result_spec_bool ("profitable", - &dc.profitable), + GNUNET_PQ_result_spec_int64 ("profitable", + &profitable), GNUNET_PQ_result_spec_bool ("suppressed", &dc.suppressed), GNUNET_PQ_result_spec_end @@ -103,6 +106,7 @@ amount_arithmetic_inconsistency_cb (void *cls, dcc->qs = GNUNET_DB_STATUS_HARD_ERROR; return; } + dc.profitable = (int16_t) profitable; dcc->qs = i + 1; rval = dcc->cb (dcc->cb_cls, &dc); @@ -145,7 +149,7 @@ TALER_AUDITORDB_iterate_amount_arithmetic_inconsistencies ( ",operation" ",exchange_amount" ",auditor_amount" - ",profitable" + ",profitable::INT8 AS profitable" ",suppressed" " FROM auditor_amount_arithmetic_inconsistency" " WHERE (row_id<$1)" @@ -161,7 +165,7 @@ TALER_AUDITORDB_iterate_amount_arithmetic_inconsistencies ( ",operation" ",exchange_amount" ",auditor_amount" - ",profitable" + ",profitable::INT8 AS profitable" ",suppressed" " FROM auditor_amount_arithmetic_inconsistency" " WHERE (row_id>$1)" diff --git a/src/auditordb/iterate_coin_inconsistencies.c b/src/auditordb/iterate_coin_inconsistencies.c @@ -76,6 +76,10 @@ coin_inconsistency_cb (void *cls, for (unsigned int i = 0; i < num_results; i++) { struct TALER_AUDITORDB_CoinInconsistency dc; + /* libgnunetpq only recently added support for + GNUNET_PQ_result_spec_int16() change this after + the next GNUnet release! */ + int64_t profitable; struct GNUNET_PQ_ResultSpec rs[] = { GNUNET_PQ_result_spec_uint64 ("row_id", @@ -86,9 +90,10 @@ coin_inconsistency_cb (void *cls, &dc.exchange_amount), TALER_PQ_RESULT_SPEC_AMOUNT ("auditor_amount", &dc.auditor_amount), - GNUNET_PQ_result_spec_auto_from_type ("coin_pub", &dc.coin_pub), - GNUNET_PQ_result_spec_bool ("profitable", - &dc.profitable), + GNUNET_PQ_result_spec_auto_from_type ("coin_pub", + &dc.coin_pub), + GNUNET_PQ_result_spec_int64 ("profitable", + &profitable), GNUNET_PQ_result_spec_bool ("suppressed", &dc.suppressed), @@ -106,9 +111,8 @@ coin_inconsistency_cb (void *cls, return; } + dc.profitable = (int16_t) profitable; dcc->qs = i + 1; - - rval = dcc->cb (dcc->cb_cls, dc.row_id, &dc); @@ -128,7 +132,6 @@ TALER_AUDITORDB_iterate_coin_inconsistencies ( TALER_AUDITORDB_CoinInconsistencyCallback cb, void *cb_cls) { - uint64_t plimit = GNUNET_MIN ((uint64_t) MAX_RECORDS, TALER_AUDITORDB_abs_limit (limit)); struct GNUNET_PQ_QueryParam params[] = { @@ -144,7 +147,6 @@ TALER_AUDITORDB_iterate_coin_inconsistencies ( }; enum GNUNET_DB_QueryStatus qs; - PREPARE (pg, "iterate_coin_inconsistencies_inconsistency_select_desc", "SELECT" @@ -153,7 +155,7 @@ TALER_AUDITORDB_iterate_coin_inconsistencies ( ",exchange_amount" ",auditor_amount" ",coin_pub" - ",profitable" + ",profitable::INT8 AS profitable" ",suppressed" " FROM auditor_coin_inconsistency" " WHERE (row_id < $1)" @@ -169,7 +171,7 @@ TALER_AUDITORDB_iterate_coin_inconsistencies ( ",exchange_amount" ",auditor_amount" ",coin_pub" - ",profitable" + ",profitable::INT8 AS profitable" ",suppressed" " FROM auditor_coin_inconsistency" " WHERE (row_id > $1)" @@ -177,16 +179,14 @@ TALER_AUDITORDB_iterate_coin_inconsistencies ( " ORDER BY row_id ASC" " LIMIT $3" ); - qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - (limit > 0) ? - "iterate_coin_inconsistencies_inconsistency_select_asc" - : - "iterate_coin_inconsistencies_inconsistency_select_desc", - params, - &coin_inconsistency_cb, - &dcc); - - + qs = GNUNET_PQ_eval_prepared_multi_select ( + pg->conn, + (limit > 0) + ? "iterate_coin_inconsistencies_inconsistency_select_asc" + : "iterate_coin_inconsistencies_inconsistency_select_desc", + params, + &coin_inconsistency_cb, + &dcc); if (qs > 0) return dcc.qs; GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR != qs); diff --git a/src/auditordb/meson.build b/src/auditordb/meson.build @@ -73,6 +73,13 @@ auditor_0005_sql = [ 'commit.sql', ] +auditor_0006_sql = [ + '0006-preamble.sql', + '0006-auditor_amount_arithmetic_inconsistency.sql', + '0006-auditor_coin_inconsistency.sql', + 'commit.sql', +] + generated_sql = [ ['versioning.sql', ['versioning.sql']], ['drop.sql', ['drop.sql']], @@ -83,6 +90,7 @@ generated_sql = [ ['auditor-0003.sql', auditor_0003_sql], ['auditor-0004.sql', auditor_0004_sql], ['auditor-0005.sql', auditor_0005_sql], + ['auditor-0006.sql', auditor_0006_sql], ] foreach g : generated_sql diff --git a/src/auditordb/test_auditordb.c b/src/auditordb/test_auditordb.c @@ -29,12 +29,16 @@ #include "auditor-database/get_denomination_balance.h" #include "auditor-database/get_exists_historic_denom_revenue.h" #include "auditor-database/get_reserve_info.h" +#include "auditor-database/insert_amount_arithmetic_inconsistency.h" #include "auditor-database/insert_auditor_closure_lags.h" +#include "auditor-database/insert_coin_inconsistency.h" #include "auditor-database/insert_denomination_balance.h" #include "auditor-database/insert_historic_denom_revenue.h" #include "auditor-database/insert_historic_reserve_revenue.h" #include "auditor-database/insert_reserve_info.h" #include "auditor-database/preflight.h" +#include "auditor-database/iterate_amount_arithmetic_inconsistencies.h" +#include "auditor-database/iterate_coin_inconsistencies.h" #include "auditor-database/iterate_historic_denom_revenue.h" #include "auditor-database/iterate_historic_reserve_revenue.h" #include "auditor-database/start.h" @@ -194,6 +198,68 @@ select_historic_reserve_revenue_result ( /** + * Check a row returned by + * #TALER_AUDITORDB_iterate_amount_arithmetic_inconsistencies(). + * The rows were inserted with "profitable" set to 1, 0 and -1, in + * that order. + * + * @param cls pointer to the number of rows seen so far + * @param dc the row + * @return #GNUNET_OK if the row is as expected + */ +static enum GNUNET_GenericReturnValue +select_amount_arithmetic_inconsistency_result ( + void *cls, + const struct TALER_AUDITORDB_AmountArithmeticInconsistency *dc) +{ + int *n = cls; + + if ( (3 <= *n) || + (1 - *n != dc->profitable) || + (0 != strcmp (dc->operation, + "test")) ) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + (*n)++; + return GNUNET_OK; +} + + +/** + * Check a row returned by #TALER_AUDITORDB_iterate_coin_inconsistencies(). + * The rows were inserted with "profitable" set to 1, 0 and -1, in + * that order. + * + * @param cls pointer to the number of rows seen so far + * @param serial_id row in the table + * @param dc the row + * @return #GNUNET_OK if the row is as expected + */ +static enum GNUNET_GenericReturnValue +select_coin_inconsistency_result ( + void *cls, + uint64_t serial_id, + const struct TALER_AUDITORDB_CoinInconsistency *dc) +{ + int *n = cls; + + (void) serial_id; + if ( (3 <= *n) || + (1 - *n != dc->profitable) || + (0 != strcmp (dc->operation, + "test")) ) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + (*n)++; + return GNUNET_OK; +} + + +/** * Main function that will be run by the scheduler. * * @param cls closure with config @@ -518,6 +584,56 @@ run (void *cls) &select_historic_reserve_revenue_result, NULL)); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Test: profitable round-trip\n"); + { + struct TALER_AUDITORDB_AmountArithmeticInconsistency aai = { + .problem_row_id = 42, + .operation = (char *) "test", + .exchange_amount = value, + .auditor_amount = fee_withdraw + }; + struct TALER_AUDITORDB_CoinInconsistency ci = { + .operation = (char *) "test", + .exchange_amount = value, + .auditor_amount = fee_withdraw + }; + int n; + + RND_BLK (&ci.coin_pub); + for (int profitable = 1; profitable >= -1; profitable--) + { + aai.profitable = profitable; + ci.profitable = profitable; + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_AUDITORDB_insert_amount_arithmetic_inconsistency (pg, + &aai)); + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_AUDITORDB_insert_coin_inconsistency (pg, + &ci)); + } + n = 0; + FAILIF (3 != + TALER_AUDITORDB_iterate_amount_arithmetic_inconsistencies ( + pg, + 3, /* limit */ + 0, /* offset */ + true, /* return_suppressed */ + &select_amount_arithmetic_inconsistency_result, + &n)); + FAILIF (3 != n); + n = 0; + FAILIF (3 != + TALER_AUDITORDB_iterate_coin_inconsistencies ( + pg, + 3, /* limit */ + 0, /* offset */ + true, /* return_suppressed */ + &select_coin_inconsistency_result, + &n)); + FAILIF (3 != n); + } + FAILIF (0 > TALER_AUDITORDB_commit (pg)); diff --git a/src/include/auditordb_lib.h b/src/include/auditordb_lib.h @@ -151,7 +151,11 @@ struct TALER_AUDITORDB_CoinInconsistency struct TALER_Amount exchange_amount; struct TALER_Amount auditor_amount; struct GNUNET_CRYPTO_EddsaPublicKey coin_pub; - bool profitable; + /** + * 1 if the inconsistency is profitable for the exchange, -1 if it + * is a loss for the exchange, 0 if that is unclear. + */ + int16_t profitable; bool suppressed; }; @@ -725,7 +729,11 @@ struct TALER_AUDITORDB_AmountArithmeticInconsistency char *operation; struct TALER_Amount exchange_amount; struct TALER_Amount auditor_amount; - bool profitable; + /** + * 1 if the inconsistency is profitable for the exchange, -1 if it + * is a loss for the exchange, 0 if that is unclear. + */ + int16_t profitable; bool suppressed; }; diff --git a/src/lib/auditor_api_get_config.c b/src/lib/auditor_api_get_config.c @@ -32,12 +32,13 @@ * Which revision of the Taler auditor protocol is implemented * by this library? Used to determine compatibility. */ -#define TALER_PROTOCOL_CURRENT 2 +#define TALER_PROTOCOL_CURRENT 3 /** - * How many revisions back are we compatible to? + * How many revisions back are we compatible to? v3 only changed + * the /monitoring API, which this library does not use. */ -#define TALER_PROTOCOL_AGE 0 +#define TALER_PROTOCOL_AGE 1 /**