exchange

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

commit 79f095f2c3f125276664ddfe9d92d54996a2a182
parent 27e96db63c75da07ae1f1eb95a8c49618aa9e5f4
Author: Christian Grothoff <christian@grothoff.org>
Date:   Sat, 29 Mar 2025 14:53:11 +0100

-fix misc statistics issues

Diffstat:
Msrc/exchange-tools/taler-exchange-dbinit.c | 25++++++++++++-------------
Msrc/exchangedb/0009-statistics.sql | 3+++
Msrc/exchangedb/drop.sql | 9++++-----
Msrc/exchangedb/exchange_statistics_helpers.sql | 58+++++++++++++++++++++++++++++-----------------------------
Msrc/exchangedb/pg_disable_rules.c | 7+++----
Msrc/exchangedb/pg_enable_rules.c | 2++
Msrc/exchangedb/tops-0001.sql | 105++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Msrc/testing/taler-unified-setup.sh | 5++++-
8 files changed, 135 insertions(+), 79 deletions(-)

diff --git a/src/exchange-tools/taler-exchange-dbinit.c b/src/exchange-tools/taler-exchange-dbinit.c @@ -114,10 +114,8 @@ run (void *cls, { fprintf (stderr, "Failed to initialize database.\n"); - TALER_EXCHANGEDB_plugin_unload (plugin); - plugin = NULL; global_ret = EXIT_NOPERMISSION; - return; + goto exit; } if (gc_db || clear_shards) { @@ -126,10 +124,8 @@ run (void *cls, { fprintf (stderr, "Failed to prepare database.\n"); - TALER_EXCHANGEDB_plugin_unload (plugin); - plugin = NULL; global_ret = EXIT_NOPERMISSION; - return; + goto exit; } if (clear_shards) { @@ -167,7 +163,7 @@ run (void *cls, fprintf (stderr, "'exchange' is not a customization rule set!\n"); global_ret = EXIT_INVALIDARGUMENT; - return; + goto exit; } if (GNUNET_OK != plugin->preflight (plugin->cls)) @@ -175,7 +171,7 @@ run (void *cls, fprintf (stderr, "Preflight check failed!\n"); global_ret = EXIT_FAILURE; - return; + goto exit; } switch (plugin->disable_rules (plugin->cls, disable_rules)) @@ -184,15 +180,15 @@ run (void *cls, fprintf (stderr, "Hard DB error trying to disable customization!\n"); global_ret = EXIT_FAILURE; - return; + goto exit; case GNUNET_DB_STATUS_SOFT_ERROR: /* single call, should not be possible */ GNUNET_break (0); global_ret = EXIT_FAILURE; - return; + goto exit; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Did not find customization schema `%s'\n", + "Nothing to do to disable customization schema `%s'\n", disable_rules); break; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: @@ -201,13 +197,15 @@ run (void *cls, } if (NULL != enable_rules) { + enum GNUNET_GenericReturnValue rv; + if (0 == strcasecmp (enable_rules, "exchange")) { fprintf (stderr, "'exchange' is not a customization rule set!\n"); global_ret = EXIT_INVALIDARGUMENT; - return; + goto exit; } if (GNUNET_OK != plugin->enable_rules (plugin->cls, @@ -217,9 +215,10 @@ run (void *cls, "Enabling customization `%s' failed!\n", enable_rules); global_ret = EXIT_FAILURE; - return; + goto exit; } } +exit: TALER_EXCHANGEDB_plugin_unload (plugin); plugin = NULL; } diff --git a/src/exchangedb/0009-statistics.sql b/src/exchangedb/0009-statistics.sql @@ -131,6 +131,7 @@ BEGIN ',bucket_start INT8 NOT NULL' ',bucket_range statistic_range NOT NULL' ',cumulative_value taler_amount NOT NULL' + ',CHECK ((cumulative_value).val IS NOT NULL AND (cumulative_value).frac IS NOT NULL)' ',UNIQUE (h_payto,bmeta_serial_id,bucket_start,bucket_range)' ') %s;' ,'exchange_statistic_bucket_amount' @@ -379,6 +380,7 @@ BEGIN ',h_payto BYTEA CHECK (LENGTH(h_payto)=32)' ',slot INT8 NOT NULL' ',delta taler_amount NOT NULL' + ',CHECK ((delta).val IS NOT NULL AND (delta).frac IS NOT NULL)' ',CONSTRAINT event_key UNIQUE (h_payto,imeta_serial_id,slot)' ') %s ;' ,'exchange_statistic_amount_event' @@ -457,6 +459,7 @@ BEGIN ',event_delimiter INT8 NOT NULL' ',range INT8 NOT NULL' ',cumulative_value taler_amount NOT NULL' + ',CHECK ((cumulative_value).val IS NOT NULL AND (cumulative_value).frac IS NOT NULL)' ',UNIQUE (h_payto,imeta_serial_id,range)' ') %s ;' ,'exchange_statistic_interval_amount' diff --git a/src/exchangedb/drop.sql b/src/exchangedb/drop.sql @@ -23,16 +23,15 @@ WITH xpatches AS ( WHERE starts_with(patch_name,'exchange-') ) SELECT _v.unregister_patch(xpatches.patch_name) - FROM xpatches; - + FROM xpatches; WITH xpatches AS ( SELECT patch_name - FROM _v.patches - WHERE starts_with(patch_name,'auditor-triggers-') + FROM _v.patches + WHERE starts_with(patch_name,'auditor-triggers-') ) SELECT _v.unregister_patch(xpatches.patch_name) - FROM xpatches; + FROM xpatches; DROP SCHEMA exchange CASCADE; diff --git a/src/exchangedb/exchange_statistics_helpers.sql b/src/exchangedb/exchange_statistics_helpers.sql @@ -14,8 +14,6 @@ -- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -- --- FIXME: add helper function to drop data based on new 'origin' field in _meta - SET search_path TO exchange; DROP FUNCTION IF EXISTS interval_to_start; CREATE OR REPLACE FUNCTION interval_to_start ( @@ -148,7 +146,6 @@ BEGIN END WHERE bmeta_serial_id=my_meta AND h_payto=in_h_payto - AND curr=(in_delta).curr AND bucket_start=my_bucket_start AND bucket_range=my_range; IF NOT FOUND @@ -330,28 +327,24 @@ BEGIN (imeta_serial_id ,h_payto ,slot - ,delta_curr - ,delta_value - ,delta_frac + ,delta ) VALUES ( my_meta ,in_h_payto ,my_start - ,(in_delta).curr - ,(in_delta).val - ,(in_delta).frac + ,in_delta ) - ON CONFLICT (imeta_serial_id, h_payto, slot, delta_curr) + ON CONFLICT (imeta_serial_id, h_payto, slot) DO UPDATE SET - delta_value = msae.delta_value + (in_delta).val + delta.val = (msae.delta).val + (in_delta).val + CASE - WHEN (in_delta).frac + msae.delta_frac >= 100000000 + WHEN (in_delta).frac + (msae.delta).frac >= 100000000 THEN 1 ELSE 0 END, - delta_frac = msae.delta_frac + (in_delta).frac + delta.frac = (msae.delta).frac + (in_delta).frac - CASE - WHEN (in_delta).frac + msae.delta_frac >= 100000000 + WHEN (in_delta).frac + (msae.delta).frac >= 100000000 THEN 100000000 ELSE 0 END @@ -628,10 +621,10 @@ BEGIN END IF; -- Check if we have events that left the applicable range - SELECT SUM(delta_value) AS value_sum - ,SUM(delta_frac) AS frac_sum + SELECT SUM((esae.delta).val) AS value_sum + ,SUM((esae.delta).frac) AS frac_sum INTO my_jrec - FROM exchange_statistic_amount_event + FROM exchange_statistic_amount_event esae WHERE imeta_serial_id = my_meta AND h_payto = in_h_payto AND slot < my_time - my_range @@ -1008,27 +1001,34 @@ COMMENT ON PROCEDURE exchange_statistic_bucket_gc -DROP FUNCTIOn IF EXISTS exchange_drop_customization; +DROP FUNCTION IF EXISTS exchange_drop_customization; CREATE OR REPLACE FUNCTION exchange_drop_customization ( IN in_schema TEXT, OUT out_found BOOLEAN ) LANGUAGE plpgsql AS $$ +DECLARE + my_xpatches TEXT; BEGIN -- Update DB versioning table. - WITH xpatches AS ( + out_found = FALSE; + FOR my_xpatches IN SELECT patch_name - FROM _v.patches - WHERE starts_with(patch_name, in_schema || '-') - ) - SELECT _v.unregister_patch(xpatches.patch_name) - FROM xpatches; - out_found = FOUND; - - -- Drop the schema with all stored procedures/functions. - -- This also removes all associated triggers, hence CASCADE. - DROP SCHEMA in_schema CASCADE; + FROM _v.patches + WHERE starts_with(patch_name, in_schema || '-') + LOOP + PERFORM _v.unregister_patch(my_xpatches); + out_found = TRUE; + END LOOP; + + IF out_found + THEN + -- Drop the schema with all stored procedures/functions. + -- This also removes all associated triggers, hence CASCADE. + EXECUTE FORMAT('DROP SCHEMA %s CASCADE' + ,in_schema); + END IF; -- Finally, need to also remove entries from the statistics meta-tables. -- Doing so also DELETEs the associated statistics, hence CASCADE. diff --git a/src/exchangedb/pg_disable_rules.c b/src/exchangedb/pg_disable_rules.c @@ -38,7 +38,7 @@ TEH_PG_disable_rules ( }; bool found; struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_bool ("found", + GNUNET_PQ_result_spec_bool ("out_found", &found), GNUNET_PQ_result_spec_end }; @@ -46,9 +46,8 @@ TEH_PG_disable_rules ( PREPARE (pg, "call_exchange_drop_customization", - "SELECT " - " out_found AS found" - " FROM exchange_drop_customization" + "SELECT out_found" + " FROM exchange_drop_customization" " ($1);"); qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, "call_exchange_drop_customization", diff --git a/src/exchangedb/pg_enable_rules.c b/src/exchangedb/pg_enable_rules.c @@ -69,6 +69,8 @@ TEH_PG_enable_rules ( procfile); /* $SCHEMA-procedures MAY not exist, so only check for hard error */ GNUNET_break (GNUNET_SYSERR != ret); + if (GNUNET_NO == ret) + ret = GNUNET_OK; GNUNET_free (procfile); } GNUNET_PQ_disconnect (conn); diff --git a/src/exchangedb/tops-0001.sql b/src/exchangedb/tops-0001.sql @@ -43,28 +43,56 @@ VALUES ,'deposit-transactions' ,'number of (batch) deposits performed by this merchant, used to detect sudden increase in number of transactions' ,'number' - ,generate_series (60*60*24*7, 60*60*24*7*52, 60*60*24*7) -- weekly volume over the last year - ,array_fill (60*60*24, 52) -- precision is per day + ,ARRAY(SELECT generate_series (60*60*24*7, 60*60*24*7*52, 60*60*24*7)) -- weekly volume over the last year + ,array_fill (60*60*24, ARRAY[52]) -- precision is per day ), ('tops' -- must match schema! ,'deposit-volume' - ,'total amount deposited by this merchant in (batch) deposits, used to detect sudden increase in transaction volume' + ,'total amount deposited by this merchant in (batch) deposits including deposit fees, used to detect sudden increase in transaction volume' ,'amount' - ,generate_series (60*60*24*7, 60*60*24*7*52, 60*60*24*7) -- weekly volume over the last year - ,array_fill (60*60*24, 52) -- precision is per day + ,ARRAY(SELECT generate_series (60*60*24*7, 60*60*24*7*52, 60*60*24*7)) -- weekly volume over the last year + ,array_fill (60*60*24, ARRAY[52]) -- precision is per day ) ON CONFLICT DO NOTHING; -DROP FUNCTION IF EXISTS exchange_deposit_statistics_trigger CASCADE; -CREATE FUNCTION exchange_deposit_statistics_trigger() +INSERT INTO exchange_statistic_bucket_meta + (origin + ,slug + ,description + ,stype + ,ranges + ,ages) +VALUES +-- this first one is just for testing right now + ('tops' -- must match schema! + ,'deposit-transactions' + ,'number of (batch) deposits performed by this merchant, used to detect sudden increase in number of transactions' + ,'number' + ,ARRAY['day'::statistic_range,'week'] + ,ARRAY[5,5] + ), + ('tops' -- must match schema! + ,'deposit-volume' + ,'total amount deposited by this merchant in (batch) deposits including deposit fees, used to detect sudden increase in transaction volume' + ,'amount' + ,ARRAY['day'::statistic_range,'week'] + ,ARRAY[5,5] + ) +ON CONFLICT DO NOTHING; + +DROP FUNCTION IF EXISTS tops_deposit_statistics_trigger CASCADE; +CREATE FUNCTION tops_deposit_statistics_trigger() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE my_h_payto BYTEA; -- normalized h_payto of target account my_rec RECORD; - my_last_year taler_amount; - my_last_month taler_amount; + my_vsum INT8; + my_fsum INT8; + my_batch_amount taler_amount; -- sum of all coins (incl. fees) + my_last_year taler_amount; -- sum of deposits this year + my_last_month taler_amount; -- sum of deposits this month my_old_rules RECORD; my_properties TEXT; my_measure_name TEXT; @@ -74,22 +102,45 @@ BEGIN INTO my_h_payto FROM wire_targets wt WHERE wire_target_h_payto = NEW.wire_target_h_payto; --- FIXME: we're hard-coding an amount for testing; the --- final logic will be written once we have for Oec's --- unified deposit table patch, that'll make this WAY simpler --- as we should have the amount without having to iterate. + + -- FIXME: this does not work, coin_deposits is only initialized LATER + -- in exchange_do_deposit.sql; + -- Solution: add a new column with the sum? + FOR my_rec IN + SELECT amount_with_fee + FROM coin_deposits cdeps + WHERE batch_deposit_serial_id = NEW.batch_deposit_serial_id + LOOP + RAISE NOTICE 'XXXX has %', my_rec; + END LOOP; + SELECT SUM((cdeps.amount_with_fee).val) AS vsum, + SUM((cdeps.amount_with_fee).frac) AS fsum + INTO my_rec + FROM coin_deposits cdeps + WHERE batch_deposit_serial_id = NEW.batch_deposit_serial_id; + IF NOT FOUND + THEN + RAISE NOTICE 'Invariant failed, no coin deposits for batch deposit'; + RETURN NEW; + END IF; + RAISE NOTICE 'XXXZ SELECT yielded % - %',FOUND,my_rec; + my_vsum = my_rec.vsum; + my_fsum = my_rec.fsum; + RAISE NOTICE 'XXXY Batch deposit of %.%',my_vsum,my_fsum; + my_batch_amount.val = my_vsum + my_fsum / 100000000; + my_batch_amount.frac = my_fsum % 100000000; CALL exchange_do_bump_amount_stat ('deposit-volume' - ,h_payto - ,CURRENT_TIMESTAMP(0) - ,(1,1):taler_amount) -- should be: NEW.amount_with_fee); + ,my_h_payto + ,CURRENT_TIMESTAMP(0)::TIMESTAMP + ,my_batch_amount); -- FIXME: this is just for testing, I want to also check -- the 'counter'-based functions. - CALL exchange_do_bump_amount_stat + CALL exchange_do_bump_number_stat ('deposit-transactions' - ,h_payto - ,CURRENT_TIMESTAMP(0) + ,my_h_payto + ,CURRENT_TIMESTAMP(0)::TIMESTAMP ,1); -- Get historical deposit volumes and extract the yearly and monthly @@ -103,19 +154,19 @@ BEGIN LOOP IF (my_rec.range = 60*60*24*7*52) THEN - last_year = my_rec.rvalue; + my_last_year = my_rec.rvalue; END IF; IF (my_rec.range = 60*60*24*7*4) THEN - last_month = my_rec.rvalue; + my_last_month = my_rec.rvalue; END IF; - END LOOP + END LOOP; -- Note: it is OK to ignore '.frac', as that cannot be significant. -- Also, we effectively exclude the current month's revenue from -- "last year" as otherwise the rule makes no sense. -- Finally, we define the "current month" always as the last 4 weeks, -- just like the "last year" is the last 52 weeks. - IF last_year.val < last_month.val * 2 + IF (my_last_year.val < my_last_month.val * 2) THEN -- This is suspicious. => Flag account for AML review! -- @@ -167,7 +218,7 @@ BEGIN ,is_active ,jnew_rules ) VALUES ( - ,my_h_payto + my_h_payto ,my_now ,my_now + 366*24*60*60 ,my_properties @@ -180,14 +231,14 @@ BEGIN END IF; RETURN NEW; END $$; -COMMENT ON FUNCTION exchange_deposit_statistics_trigger +COMMENT ON FUNCTION tops_deposit_statistics_trigger IS 'creates deposit statistics'; -- Whenever a deposit is made, call our trigger to bump statistics -CREATE TRIGGER exchange_batch_deposits_on_insert +CREATE TRIGGER tops_batch_deposits_on_insert AFTER INSERT ON batch_deposits - FOR EACH ROW EXECUTE FUNCTION exchange_deposit_statistics_trigger(); + FOR EACH ROW EXECUTE FUNCTION tops_deposit_statistics_trigger(); diff --git a/src/testing/taler-unified-setup.sh b/src/testing/taler-unified-setup.sh @@ -509,7 +509,10 @@ then echo -n " patching master_pub ($MASTER_PUB)..." taler-exchange-config -c "$CONF" -s exchange -o MASTER_PUBLIC_KEY -V "$MASTER_PUB" fi - taler-exchange-dbinit -c "$CONF" --reset + taler-exchange-dbinit \ + -c "$CONF" \ + --reset +# -e tops $USE_VALGRIND taler-exchange-secmod-eddsa \ -c "$CONF" \ -L "$LOGLEVEL" \