exchange

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

commit 5f6d203e072e430f5db0956ee42ef5f460ccc5dc
parent 9d6dc3cdc14b574ddff38224f6ad9fa8e0ec0a6e
Author: Christian Grothoff <christian@grothoff.org>
Date:   Sat,  1 Aug 2026 00:33:19 +0200

start with KYC auditor test logic

Diffstat:
Msrc/auditor/generate-kyc-basedb.conf | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/auditor/meson.build | 28+++++++++++++++++-----------
Msrc/auditor/test-kyc.sh | 1161++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
3 files changed, 911 insertions(+), 376 deletions(-)

diff --git a/src/auditor/generate-kyc-basedb.conf b/src/auditor/generate-kyc-basedb.conf @@ -1,4 +1,99 @@ # This file is in the public domain. +# +# Configuration for the KYC/AML reference database used by test-kyc.sh. +# It is the ordinary auditor reference configuration plus a KYC setup that +# is deliberately easy to drive from a shell script: every rule that we +# trigger resolves to a FORM measure, and a form can be satisfied with two +# plain HTTP requests (GET /kyc-info/$TOKEN, POST /kyc-upload/$ID). @INLINE@ generate-auditor-basedb.conf -# FIXME: add options for KYC here! -\ No newline at end of file +[exchange] +ENABLE_KYC = YES + + +# ------------------------------------------------------------------ +# Boilerplate that every KYC configuration needs: the FREEZE measure +# is the fallback whenever an AML program fails or a check expires, +# and NONE is a program that imposes no new rules at all. +# ------------------------------------------------------------------ + +[KYC-MEASURE-FREEZE] +CHECK_NAME = SKIP +CONTEXT = {} +PROGRAM = FREEZE + +[AML-PROGRAM-FREEZE] +COMMAND = taler-exchange-helper-measure-freeze +ENABLED = true +DESCRIPTION = Freeze the account +DESCRIPTION_I18N = {} +FALLBACK = FREEZE + +[AML-PROGRAM-NONE] +COMMAND = /bin/true +ENABLED = true +DESCRIPTION = Impose no new rules +DESCRIPTION_I18N = {} +FALLBACK = FREEZE + + +# ------------------------------------------------------------------ +# C1/M1/P1: ask for a full name and a date of birth. Once the form +# has been submitted, taler-exchange-helper-measure-test-form raises +# every limit to TESTKUDOS:1000, which un-blocks the account. +# ------------------------------------------------------------------ + +[KYC-CHECK-C1] +TYPE = FORM +FORM_NAME = full_name_and_birthdate +DESCRIPTION = Provide your full name and date of birth +DESCRIPTION_I18N = {} +OUTPUTS = FULL_NAME DATE_OF_BIRTH +FALLBACK = FREEZE + +[KYC-MEASURE-M1] +CHECK_NAME = C1 +CONTEXT = {} +PROGRAM = P1 + +[AML-PROGRAM-P1] +COMMAND = taler-exchange-helper-measure-test-form +ENABLED = true +DESCRIPTION = Check FULL_NAME and DATE_OF_BIRTH +DESCRIPTION_I18N = {} +FALLBACK = FREEZE + + +# ------------------------------------------------------------------ +# The rules. The AGGREGATE threshold sits below the TESTKUDOS:4 that +# the reference wallet pays each merchant but above zero, which is +# what makes the two interesting states reachable from a script: a +# merchant may still accept the order (nothing has been aggregated +# yet, so it is within its limit), the exchange accepts the coins, +# and only the wire transfer is held back until measure M1 has been +# passed. That held-back transfer is the KYC-induced lag the auditor +# has to report. +# +# WITHDRAW has a threshold well above what the reference wallet ever +# withdraws, so the rule is recorded in the configuration -- and thus +# exercised by the exchange -- without blocking the interaction that +# produces the rest of the reference database. +# ------------------------------------------------------------------ + +[KYC-RULE-AGGREGATE] +OPERATION_TYPE = AGGREGATE +ENABLED = YES +EXPOSED = YES +IS_AND_COMBINATOR = YES +THRESHOLD = TESTKUDOS:3 +TIMEFRAME = 1d +NEXT_MEASURES = M1 + +[KYC-RULE-WITHDRAW] +OPERATION_TYPE = WITHDRAW +ENABLED = YES +EXPOSED = YES +IS_AND_COMBINATOR = YES +THRESHOLD = TESTKUDOS:100 +TIMEFRAME = 1d +NEXT_MEASURES = M1 diff --git a/src/auditor/meson.build b/src/auditor/meson.build @@ -297,17 +297,22 @@ test( is_parallel: false, timeout: 1800, ) -#test_kyc = configure_file( -# input: 'test-kyc.sh', -# output: 'test-kyc.sh', -# copy: true, -#) -#test( -# 'test-kyc', -# test_kyc, -# workdir: meson.current_build_dir(), -# suite: ['auditor', 'installcheck'], -#) +# test-kyc audits a reference database produced by a KYC-enabled exchange +# (generate-kyc-basedb.sh). See issues.txt for the KYC/AML checks the +# auditor does not implement yet and that this test therefore leaves out. +test_kyc = configure_file( + input: 'test-kyc.sh', + output: 'test-kyc.sh', + copy: true, +) +test( + 'test-kyc', + test_kyc, + workdir: meson.current_build_dir(), + suite: ['auditor', 'installcheck'], + is_parallel: false, + timeout: 1800, +) # test-revocation audits denomination revocation, which relies on recoup. # The exchange's recoup handlers are currently compiled out (FIXME_9828, # https://bugs.gnunet.org/view.php?id=9828), so the test detects the missing @@ -347,6 +352,7 @@ EXTRA_DIST = [ 'test-sync-out.conf', 'generate-auditor-basedb.sh', 'generate-auditor-basedb.conf', + 'generate-kyc-basedb.sh', 'generate-kyc-basedb.conf', 'generate-revoke-basedb.sh', ] diff --git a/src/auditor/test-kyc.sh b/src/auditor/test-kyc.sh @@ -1,7 +1,7 @@ -#!/bin/sh +#!/bin/bash # # This file is part of TALER -# Copyright (C) 2014-2023 Taler Systems SA +# Copyright (C) 2014-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 @@ -20,19 +20,33 @@ # # # Setup database which was generated from a perfectly normal -# exchange-wallet interaction with KYC enabled and transactions -# blocked due to KYC and run the auditor against it. +# exchange-wallet-merchant interaction on a KYC-enabled exchange and run +# the auditor against it. # -# Check that the auditor report is as expected. +# The reference database (see generate-kyc-basedb.sh) contains two merchant +# instances that were paid the same amount and both proved that they own +# their bank account with a KYCAUTH wire transfer, but only one of them +# then passed the KYC form. The exchange therefore wired the funds of the +# first one out and is still holding the funds of the second one. # -# Requires 'jq' tool and Postgres superuser rights! +# The auditor has almost no KYC-specific logic of its own -- see issues.txt +# for what it could check and does not -- so this test has two jobs: +# +# 1. the KYC artefacts (KYCAUTH credits, legitimization measures and +# outcomes, an aggregation the exchange lawfully withheld) must not +# make the auditor invent findings, and the one KYC-specific balance it +# does track, total_kycauth_in, must be exactly right; +# 2. fault detection must still work on a KYC-enabled exchange, so the +# later tests inject the same kinds of faults test-auditor.sh does and +# require the auditor to flag them. # +# Requires 'jq' tool and Postgres superuser rights! set -eu #set -x # Set of numbers for all the testcases. # When adding new tests, increase the last number: -ALL_TESTS=$(seq 0 1) +ALL_TESTS=$(seq 0 7) # $TESTS determines which tests we should run. # This construction is used to make it easy to @@ -46,22 +60,20 @@ ALL_TESTS=$(seq 0 1) # TESTS=${1:-$ALL_TESTS} +export TALER_AUDITOR_TOKEN="secret-token:D4CST1Z6AHN3RT03M0T9NSTF2QGHTB5ZD2D3RYZB4HAWG8SX0JEFWBXCKXZHMB7Y3Z7KVFW0B3XPXD5BHCFP8EB0R6CNH2KAWDWVET0" +export TALER_AUDITOR_SALT="64S36D1N6RVKGC9J6CT3ADHQ70RK4CSM6MV3EE1H68SK8D9P6WW32CHK6GTKCDSR64S36D1N6RVKGC9J6CT3ADHQ70RK4CSM6MV3EE0" + # Global variable to run the auditor processes under valgrind # VALGRIND=valgrind VALGRIND="" -# Number of seconds to let libeuifn background -# tasks apply a cycle of payment submission and -# history request. -LIBEUFIN_SETTLE_TIME=1 - . setup.sh # Cleanup exchange and libeufin between runs. function cleanup() { - if test ! -z "${EPID:-}" + if [ -n "${EPID:-}" ] then echo -n "Stopping exchange $EPID..." kill -TERM "$EPID" @@ -69,23 +81,25 @@ function cleanup() echo "DONE" unset EPID fi - stop_libeufin + stop_libeufin &> /dev/null } # Cleanup to run whenever we exit function exit_cleanup() { - echo "Running exit-cleanup" - if test ! -z "${POSTGRES_PATH:-}" + jobs + if [ -n "${POSTGRES_PATH:-}" ] then - echo "Stopping Postgres at ${POSTGRES_PATH}" + echo -n "Stopping Postgres at ${POSTGRES_PATH} ..." "${POSTGRES_PATH}/pg_ctl" \ -D "$TMPDIR" \ - -l /dev/null \ + --log="${MY_TMP_DIR}/pg_ctl.log" \ stop \ - &> /dev/null \ + &> ${MY_TMP_DIR}/pg_ctl.out \ || true + echo "DONE" fi + echo -n "Running exit-cleanup ..." cleanup for n in $(jobs -p) do @@ -99,29 +113,7 @@ function exit_cleanup() trap exit_cleanup EXIT - -# Operations to run before the actual audit -function pre_audit () { - # Launch bank - echo -n "Launching bank" - launch_libeufin - for n in $(seq 1 80) - do - echo -n "." - sleep 0.1 - OK=1 - wget http://localhost:18082/ \ - -o /dev/null \ - -O /dev/null \ - >/dev/null \ - && break - OK=0 - done - if [ 1 != "$OK" ] - then - exit_skip "Failed to launch Sandbox" - fi - sleep "$LIBEUFIN_SETTLE_TIME" +function await_bank () { for n in $(seq 1 80) do echo -n "." @@ -136,14 +128,27 @@ function pre_audit () { done if [ 1 != "$OK" ] then - exit_skip "Failed to launch Nexus" + exit_skip "Failed to launch libeufin-bank" fi + } + +# Operations to run before the actual audit +function pre_audit () { + # Launch bank + echo -n "Launching libeufin-bank" + export CONF + export MY_TMP_DIR + launch_libeufin + await_bank echo " DONE" - if test "${1:-no}" = "aggregator" + + if [ "${1:-no}" = "aggregator" ] then + # NO '-y' here: that is --kyc-off, which makes the aggregator + # wire funds out without consulting KYC at all and would defeat + # the point of this test. echo -n "Running exchange aggregator ..." taler-exchange-aggregator \ - -y \ -L "INFO" \ -t \ -c "$CONF" \ @@ -181,91 +186,140 @@ function audit_only () { $VALGRIND taler-helper-auditor-aggregation \ -L DEBUG \ -c "$CONF" \ - -m "$MASTER_PUB" \ - > test-audit-aggregation.json \ - 2> "${MY_TMP_DIR}/test-audit-aggregation.log" \ - || exit_fail "aggregation audit failed" + -t \ + > "${MY_TMP_DIR}/test-audit-aggregation.out" \ + 2> "${MY_TMP_DIR}/test-audit-aggregation.err" \ + || exit_fail "aggregation audit failed (see ${MY_TMP_DIR}/test-audit-aggregation.*)" echo -n "." $VALGRIND taler-helper-auditor-aggregation \ -L DEBUG \ -c "$CONF" \ - -m "$MASTER_PUB" \ - > test-audit-aggregation-inc.json \ - 2> "${MY_TMP_DIR}/test-audit-aggregation-inc.log" \ - || exit_fail "incremental aggregation audit failed" + -t \ + > "${MY_TMP_DIR}/test-audit-aggregation-inc.out" \ + 2> "${MY_TMP_DIR}/test-audit-aggregation-inc.err" \ + || exit_fail "incremental aggregation audit failed (see ${MY_TMP_DIR}/test-audit-aggregation-inc.*)" echo -n "." $VALGRIND taler-helper-auditor-coins \ -L DEBUG \ -c "$CONF" \ - -m "$MASTER_PUB" \ - > test-audit-coins.json \ - 2> "${MY_TMP_DIR}/test-audit-coins.log" \ - || exit_fail "coin audit failed" + -t \ + > "${MY_TMP_DIR}/test-audit-coins.out" \ + 2> "${MY_TMP_DIR}/test-audit-coins.err" \ + || exit_fail "coin audit failed (see ${MY_TMP_DIR}/test-audit-coins.*)" echo -n "." $VALGRIND taler-helper-auditor-coins \ -L DEBUG \ -c "$CONF" \ - -m "$MASTER_PUB" \ - > test-audit-coins-inc.json \ - 2> "${MY_TMP_DIR}/test-audit-coins-inc.log" \ - || exit_fail "incremental coin audit failed" + -t \ + > "${MY_TMP_DIR}/test-audit-coins-inc.out" \ + 2> "${MY_TMP_DIR}/test-audit-coins-inc.err" \ + || exit_fail "incremental coin audit failed (see ${MY_TMP_DIR}/test-audit-coins-inc.*)" echo -n "." $VALGRIND taler-helper-auditor-deposits \ -L DEBUG \ -c "$CONF" \ - -m "$MASTER_PUB" \ - > test-audit-deposits.json \ - 2> "${MY_TMP_DIR}/test-audit-deposits.log" \ - || exit_fail "deposits audit failed" + -t \ + > "${MY_TMP_DIR}/test-audit-deposits.out" \ + 2> "${MY_TMP_DIR}/test-audit-deposits.err" \ + || exit_fail "deposits audit failed (see ${MY_TMP_DIR}/test-audit-deposits.*)" echo -n "." $VALGRIND taler-helper-auditor-deposits \ -L DEBUG \ -c "$CONF" \ - -m "$MASTER_PUB" \ - > test-audit-deposits-inc.json \ - 2> "${MY_TMP_DIR}/test-audit-deposits-inc.log" \ - || exit_fail "incremental deposits audit failed" + -t \ + > "${MY_TMP_DIR}/test-audit-deposits-inc.out" \ + 2> "${MY_TMP_DIR}/test-audit-deposits-inc.err" \ + || exit_fail "incremental deposits audit failed (see ${MY_TMP_DIR}/test-audit-deposits-inc.*)" echo -n "." $VALGRIND taler-helper-auditor-reserves \ -i \ -L DEBUG \ -c "$CONF" \ - -m "$MASTER_PUB" \ - > test-audit-reserves.json \ - 2> "${MY_TMP_DIR}/test-audit-reserves.log" \ - || exit_fail "reserves audit failed" + -t \ + > "${MY_TMP_DIR}/test-audit-reserves.out" \ + 2> "${MY_TMP_DIR}/test-audit-reserves.err" \ + || exit_fail "reserves audit failed (see ${MY_TMP_DIR}/test-audit-reserves.*)" echo -n "." $VALGRIND taler-helper-auditor-reserves \ -i \ -L DEBUG \ -c "$CONF" \ - -m "$MASTER_PUB" \ - > test-audit-reserves-inc.json \ - 2> "${MY_TMP_DIR}/test-audit-reserves-inc.log" \ - || exit_fail "incremental reserves audit failed" + -t \ + > "${MY_TMP_DIR}/test-audit-reserves-inc.out" \ + 2> "${MY_TMP_DIR}/test-audit-reserves-inc.err" \ + || exit_fail "incremental reserves audit failed (see ${MY_TMP_DIR}/test-audit-reserves-inc.*)" echo -n "." - rm -f "${MY_TMP_DIR}/test-wire-audit.log" - thaw() { - $VALGRIND taler-helper-auditor-wire \ - -i \ - -L DEBUG \ - -c "$CONF" \ - -m "$MASTER_PUB" \ - > test-audit-wire.json \ - 2>> "${MY_TMP_DIR}/test-wire-audit.log" - } - thaw || ( echo -e " FIRST CALL TO taler-helper-auditor-wire FAILED,\nRETRY AFTER TWO SECONDS..." | tee -a "${MY_TMP_DIR}/test-wire-audit.log" - sleep 2 - thaw || exit_fail "wire audit failed" ) + $VALGRIND taler-helper-auditor-wire-credit \ + -i \ + -L DEBUG \ + -c "$CONF" \ + -t \ + > "${MY_TMP_DIR}/test-audit-wire-credit.out" \ + 2> "${MY_TMP_DIR}/test-audit-wire-credit.err" \ + || exit_fail "wire credit audit failed (see ${MY_TMP_DIR}/test-audit-wire-credit.*)" + echo -n "." + $VALGRIND taler-helper-auditor-wire-credit \ + -i \ + -L DEBUG \ + -c "$CONF" \ + -t \ + > "${MY_TMP_DIR}/test-audit-wire-credit-inc.out" \ + 2> "${MY_TMP_DIR}/test-audit-wire-credit-inc.err" \ + || exit_fail "wire credit audit inc failed (see ${MY_TMP_DIR}/test-audit-wire-credit-inc.*)" + echo -n "." + $VALGRIND taler-helper-auditor-wire-debit \ + -i \ + -L DEBUG \ + -c "$CONF" \ + -t \ + > "${MY_TMP_DIR}/test-audit-wire-debit.out" \ + 2> "${MY_TMP_DIR}/test-audit-wire-debit.err" \ + || exit_fail "wire debit audit failed (see ${MY_TMP_DIR}/test-audit-wire-debit.*)" + echo -n "." + $VALGRIND taler-helper-auditor-wire-debit \ + -i \ + -L DEBUG \ + -c "$CONF" \ + -t \ + > "${MY_TMP_DIR}/test-audit-wire-debit-inc.out" \ + 2> "${MY_TMP_DIR}/test-audit-wire-debit-inc.err" \ + || exit_fail "wire debit audit inc failed (see ${MY_TMP_DIR}/test-audit-wire-debit-inc.*)" + echo -n "." + $VALGRIND taler-helper-auditor-purses \ + -i \ + -L DEBUG \ + -c "$CONF" \ + -t \ + > "${MY_TMP_DIR}/test-audit-purses.out" \ + 2> "${MY_TMP_DIR}/test-audit-purses.err" \ + || exit_fail "audit purses failed" echo -n "." - $VALGRIND taler-helper-auditor-wire \ + $VALGRIND taler-helper-auditor-purses \ -i \ -L DEBUG \ -c "$CONF" \ - -m "$MASTER_PUB" \ - > test-audit-wire-inc.json \ - 2> "${MY_TMP_DIR}/test-wire-audit-inc.log" \ - || exit_fail "wire audit inc failed" + -t \ + > "${MY_TMP_DIR}/test-audit-purses-inc.out" \ + 2> "${MY_TMP_DIR}/test-audit-purses-inc.err" \ + || exit_fail "audit purses inc failed" + echo -n "." + $VALGRIND taler-helper-auditor-transfer \ + -i \ + -L DEBUG \ + -c "$CONF" \ + -t \ + > "${MY_TMP_DIR}/test-audit-transfer.out" \ + 2> "${MY_TMP_DIR}/test-audit-transfer.err" \ + || exit_fail "audit transfer failed" + echo -n "." + $VALGRIND taler-helper-auditor-transfer \ + -i \ + -L DEBUG \ + -c "$CONF" \ + -t \ + > "${MY_TMP_DIR}/test-audit-transfer-inc.out" \ + 2> "${MY_TMP_DIR}/test-audit-transfer-inc.err" \ + || exit_fail "audit transfer inc failed" echo -n "." echo " DONE" @@ -279,7 +333,6 @@ function post_audit () { -g \ || exit_fail "exchange DB GC failed" cleanup - echo " DONE" } @@ -290,73 +343,28 @@ function post_audit () { # Pass "drain" as $2 to run a drain operation as well. function run_audit () { pre_audit "${1:-no}" - if test "${2:-no}" = "drain" - then - echo -n "Starting exchange..." - taler-exchange-httpd \ - -c "${CONF}" \ - -L INFO \ - 2> "${MY_TMP_DIR}/exchange-httpd-drain.err" & - EPID=$! - - # Wait for all services to be available - for n in $(seq 1 50) - do - echo -n "." - sleep 0.1 - OK=0 - # exchange - wget "http://localhost:8081/seed" \ - -o /dev/null \ - -O /dev/null \ - >/dev/null \ - || continue - OK=1 - break - done - echo "... DONE." - export CONF - - echo -n "Running taler-exchange-offline drain " - - taler-exchange-offline \ - -L DEBUG \ - -c "${CONF}" \ - drain TESTKUDOS:0.1 \ - exchange-account-1 payto://iban/DE360679?receiver-name=Exchange+Drain \ - upload \ - 2> "${MY_TMP_DIR}/taler-exchange-offline-drain.log" \ - || exit_fail "offline draining failed" - kill -TERM "$EPID" - wait "$EPID" || true - unset EPID - echo -n "Running taler-exchange-drain ..." - printf "\n" | taler-exchange-drain \ - -L DEBUG \ - -c "$CONF" \ - 2> "${MY_TMP_DIR}/taler-exchange-drain.log" \ - || exit_fail "FAIL" - echo " DONE" - fi - echo -n "Running taler-exchange-transfer ..." - taler-exchange-transfer \ - -L INFO \ - -t \ - -c "$CONF" \ - 2> "${MY_TMP_DIR}/drain-transfer.log" \ - || exit_fail "FAIL" - echo " DONE" - audit_only post_audit } +function stop_auditor_httpd() { + if [ -n "${APID:-}" ] + then + echo -n "Stopping auditor $APID..." + kill -TERM "$APID" + wait "$APID" || true + echo "DONE" + unset APID + fi +} + + # Do a full reload of the (original) database function full_reload() { echo -n "Doing full reload of the database (loading ${BASEDB}.sql into $DB at ${PGHOST:-})... " - dropdb "$DB" 2> /dev/null || true + dropdb -f "$DB" &>> ${MY_TMP_DIR}/drop.log || true createdb -T template0 "$DB" \ || exit_skip "could not create database $DB (at ${PGHOST:-})" # Import pre-generated database, -q(ietly) using single (-1) transaction @@ -364,238 +372,626 @@ function full_reload() -q \ -1 \ -f "${BASEDB}.sql" \ - > /dev/null \ + &>> ${MY_TMP_DIR}/postgresql-reload.log \ || exit_skip "Failed to load database $DB from ${BASEDB}.sql" echo "DONE" # Technically, this call shouldn't be needed as libeufin should already be stopped here... stop_libeufin + stop_auditor_httpd } +function run_auditor_httpd() { + echo -n "Starting auditor..." + $VALGRIND taler-auditor-httpd \ + -c "${CONF}" \ + -L INFO \ + 2> "${MY_TMP_DIR}/auditor-httpd.err" & + APID=$! + + # Wait for auditor service to be available + for n in $(seq 1 50) + do + echo -n "." + sleep 0.2 + OK=0 + # auditor + wget "http://localhost:8083/config" \ + -o /dev/null \ + -O /dev/null \ + >/dev/null \ + || continue + OK=1 + break + done + echo "... DONE." +} -function test_0() { - - echo "===========0: normal run with aggregator===========" - run_audit aggregator - echo "Checking output" - # if an emergency was detected, that is a bug and we should fail - echo -n "Test for emergencies... " - jq -e .emergencies[0] < test-audit-coins.json > /dev/null && exit_fail "Unexpected emergency detected in ordinary run" || echo PASS - echo -n "Test for deposit confirmation emergencies... " - jq -e .deposit_confirmation_inconsistencies[0] < test-audit-deposits.json > /dev/null && exit_fail "Unexpected deposit confirmation inconsistency detected" || echo PASS - echo -n "Test for emergencies by count... " - jq -e .emergencies_by_count[0] < test-audit-coins.json > /dev/null && exit_fail "Unexpected emergency by count detected in ordinary run" || echo PASS - - echo -n "Test for wire inconsistencies... " - jq -e .wire_out_amount_inconsistencies[0] < test-audit-wire.json > /dev/null && exit_fail "Unexpected wire out inconsistency detected in ordinary run" - jq -e .reserve_in_amount_inconsistencies[0] < test-audit-wire.json > /dev/null && exit_fail "Unexpected reserve in inconsistency detected in ordinary run" - jq -e .misattribution_inconsistencies[0] < test-audit-wire.json > /dev/null && exit_fail "Unexpected misattribution inconsistency detected in ordinary run" - jq -e .row_inconsistencies[0] < test-audit-wire.json > /dev/null && exit_fail "Unexpected row inconsistency detected in ordinary run" - jq -e .denomination_key_validity_withdraw_inconsistencies[0] < test-audit-reserves.json > /dev/null && exit_fail "Unexpected denomination key withdraw inconsistency detected in ordinary run" - jq -e .row_minor_inconsistencies[0] < test-audit-wire.json > /dev/null && exit_fail "Unexpected minor row inconsistency detected in ordinary run" - jq -e .lag_details[0] < test-audit-wire.json > /dev/null && exit_fail "Unexpected lag detected in ordinary run" - jq -e .wire_format_inconsistencies[0] < test-audit-wire.json > /dev/null && exit_fail "Unexpected wire format inconsistencies detected in ordinary run" - - - # TODO: check operation balances are correct (once we have all transaction types and wallet is deterministic) - # TODO: check revenue summaries are correct (once we have all transaction types and wallet is deterministic) - - echo PASS - LOSS=$(jq -r .total_bad_sig_loss < test-audit-aggregation.json) - if [ "$LOSS" != "TESTKUDOS:0" ] - then - exit_fail "Wrong total bad sig loss from aggregation, got unexpected loss of $LOSS" - fi - LOSS=$(jq -r .irregular_loss < test-audit-coins.json) - if [ "$LOSS" != "TESTKUDOS:0" ] +function check_auditor_running() { + ARUNSTATUS=$(curl -Is http://localhost:8083/config | head -1) + if [ -n "${ARUNSTATUS:-}" ] then - exit_fail "Wrong total bad sig loss from coins, got unexpected loss of $LOSS" - fi - LOSS=$(jq -r .total_bad_sig_loss < test-audit-reserves.json) - if [ "$LOSS" != "TESTKUDOS:0" ] - then - exit_fail "Wrong total bad sig loss from reserves, got unexpected loss of $LOSS" - fi + echo "Auditor running" + else + echo "Auditor not running, starting it" + run_auditor_httpd + fi + unset ARUNSTATUS +} - echo -n "Test for wire amounts... " - WIRED=$(jq -r .total_wire_in_delta_plus < test-audit-wire.json) - if [ "$WIRED" != "TESTKUDOS:0" ] - then - exit_fail "Expected total wire delta plus wrong, got $WIRED" - fi - WIRED=$(jq -r .total_wire_in_delta_minus < test-audit-wire.json) - if [ "$WIRED" != "TESTKUDOS:0" ] - then - exit_fail "Expected total wire delta minus wrong, got $WIRED" - fi - WIRED=$(jq -r .total_wire_out_delta_plus < test-audit-wire.json) - if [ "$WIRED" != "TESTKUDOS:0" ] +function call_endpoint() { + if [ -n "${2+x}" ] then - exit_fail "Expected total wire delta plus wrong, got $WIRED" + curl -s -H "Accept: application/json" -H "Authorization: Bearer ${TALER_AUDITOR_TOKEN}" -o "${MY_TMP_DIR}/${2}.json" "localhost:8083/monitoring/${1}?limit=50&balance_key=${2}" + echo "endpoint ${1} called (with balance_key)... " + else + curl -s -H "Accept: application/json" -H "Authorization: Bearer ${TALER_AUDITOR_TOKEN}" -o "${MY_TMP_DIR}/${1}.json" "localhost:8083/monitoring/${1}?limit=50" + echo "endpoint ${1} called... " fi - WIRED=$(jq -r .total_wire_out_delta_minus < test-audit-wire.json) - if [ "$WIRED" != "TESTKUDOS:0" ] +} + + +function check_balance() { + call_endpoint "balances" "$1" + BAL=$(jq -r .balances[0].balance_value < "${MY_TMP_DIR}/${1}.json") + if [ "$BAL" != "$2" ] then - exit_fail "Expected total wire delta minus wrong, got $WIRED" + exit_fail "$3 (got $BAL, wanted $2)" fi - WIRED=$(jq -r .total_misattribution_in < test-audit-wire.json) - if [ "$WIRED" != "TESTKUDOS:0" ] + echo "PASS" +} + + +function check_not_balance() { + call_endpoint "balances" "$1" + BAL=$(jq -r .balances[0].balance_value < "${MY_TMP_DIR}/${1}.json") + if [ "$BAL" = "$2" ] then - exit_fail "Expected total misattribution in wrong, got $WIRED" + exit_fail "$3 (got $BAL, wanted NOT $2)" fi echo "PASS" +} - echo -n "Checking for unexpected arithmetic differences " - LOSS=$(jq -r .total_arithmetic_delta_plus < test-audit-aggregation.json) - if [ "$LOSS" != "TESTKUDOS:0" ] + +function check_report() { + call_endpoint "$1" + NAME=$(echo "$1" | tr '-' '_') + # shellcheck disable=SC2086 + VAL=$(jq -r .\"${NAME}\"[0].\"$2\" < "${MY_TMP_DIR}/${1}.json") + if [ "$VAL" != "$3" ] then - exit_fail "Wrong arithmetic delta from aggregations, got unexpected plus of $LOSS" + exit_fail "$1::$2 (got $VAL, wanted $3)" fi - LOSS=$(jq -r .total_arithmetic_delta_minus < test-audit-aggregation.json) - if [ "$LOSS" != "TESTKUDOS:0" ] + echo "PASS" +} + +# Check that at least one entry of report $1 has field $2 set to $3. +# Unlike check_report, this does not depend on the order in which the +# auditor inserted its findings: one fault can legitimately be reported +# against several operations (a coin with a bad denomination signature is +# flagged once per operation that used it). +function check_report_any() { + call_endpoint "$1" + NAME=$(echo "$1" | tr '-' '_') + # shellcheck disable=SC2086 + jq -e --arg want "$3" "any(.\"${NAME}\"[]; .\"$2\" == \$want)" \ + < "${MY_TMP_DIR}/${1}.json" \ + > /dev/null \ + || exit_fail "$1::$2 (no entry with value $3)" + echo "PASS" +} + +# Check that report $1 has at least one entry, without pinning down any of +# its fields. Useful when the injected fault is certain to be found but +# the exact amounts depend on how much of a reserve happened to be left. +function check_some_report() { + call_endpoint "$1" + NAME=$(echo "$1" | tr '-' '_') + # shellcheck disable=SC2086 + jq -e .\"${NAME}\"[0] \ + < "${MY_TMP_DIR}/${1}.json" \ + > /dev/null \ + || exit_fail "Wanted a report for $1, but got none" + echo "PASS" +} + +function check_no_report() { + call_endpoint "$1" + NAME=$(echo "$1" | tr '-' '_') + # shellcheck disable=SC2086 + jq -e .\"${NAME}\"[0] \ + < "${MY_TMP_DIR}/${1}.json" \ + > /dev/null \ + && exit_fail "Wanted empty report for $1, but got incidents" + echo "PASS" +} + +function check_report_neg() { + call_endpoint "$1" + NAME=$(echo "$1" | tr '-' '_') + # shellcheck disable=SC2086 + VAL=$(jq -r .\"${NAME}\"[0].\"$2\" < "${MY_TMP_DIR}/${1}.json") + if [ "$VAL" == "$3" ] then - exit_fail "Wrong arithmetic delta from aggregation, got unexpected minus of $LOSS" + exit_fail "$1::$2 (got $VAL, wanted $3)" fi - LOSS=$(jq -r .total_arithmetic_delta_plus < test-audit-coins.json) - if [ "$LOSS" != "TESTKUDOS:0" ] + echo "PASS" +} + +function check_row() { + call_endpoint "$1" + NAME=$(echo "$1" | tr '-' '_') + if [ -n "${3+x}" ] then - exit_fail "Wrong arithmetic delta from coins, got unexpected plus of $LOSS" + RID="$2" + WANT="$3" + else + RID="row_id" + WANT="$2" fi - LOSS=$(jq -r .total_arithmetic_delta_minus < test-audit-coins.json) - if [ "$LOSS" != "TESTKUDOS:0" ] + # shellcheck disable=SC2086 + ROW=$(jq -r .\"${NAME}\"[0].\"${RID}\" < "${MY_TMP_DIR}/${1}.json") + if [ "$ROW" != "$WANT" ] then - exit_fail "Wrong arithmetic delta from coins, got unexpected minus of $LOSS" + exit_fail "Row ${1} wrong (got ${ROW}, wanted ${WANT})" fi - LOSS=$(jq -r .total_arithmetic_delta_plus < test-audit-reserves.json) - if [ "$LOSS" != "TESTKUDOS:0" ] + echo "PASS" +} + + + +# Amount the exchange wired to the merchant that passed KYC. +CLEARED_WIRED="TESTKUDOS:8.02" +# Amount the exchange aggregated for the merchant that did not pass KYC +# and then parked in aggregation_transient instead of wiring it out. +WITHHELD="TESTKUDOS:8.03" +# Two KYCAUTH wire transfers of TESTKUDOS:0.1, one per merchant instance. +KYCAUTH_IN="TESTKUDOS:0.2" + + +# Assert that all of the auditor's loss, delta and irregularity balances +# are zero, i.e. that nothing about this database looks wrong to it. +function check_all_clean() { + echo -n "Test for emergencies... " + check_no_report "emergency" + echo -n "Test for emergencies by count... " + check_no_report "emergency-by-count" + echo -n "Test for deposit confirmation problems... " + check_no_report "deposit-confirmation" + echo -n "Test for denomination key validity problems... " + check_no_report "denomination-key-validity-withdraw-inconsistency" + echo -n "Test for denominations without signatures... " + check_no_report "denominations-without-sigs" + echo -n "Test for wire out inconsistencies... " + check_no_report "wire-out-inconsistency" + echo -n "Test for reserve in inconsistencies... " + check_no_report "reserve-in-inconsistency" + echo -n "Test for misattribution inconsistencies... " + check_no_report "misattribution-in-inconsistency" + echo -n "Test for row inconsistencies... " + check_no_report "row-inconsistency" + echo -n "Test for minor row inconsistencies... " + check_no_report "row-minor-inconsistencies" + echo -n "Test for wire format inconsistencies... " + check_no_report "wire-format-inconsistency" + echo -n "Test for arithmetic inconsistencies... " + check_no_report "amount-arithmetic-inconsistency" + echo -n "Test for coin inconsistencies... " + check_no_report "coin-inconsistency" + echo -n "Test for bad signature losses... " + check_no_report "bad-sig-losses" + echo -n "Test for fee time inconsistencies... " + check_no_report "fee-time-inconsistency" + echo -n "Test for purses not closed... " + check_no_report "purse-not-closed-inconsistencies" + echo -n "Test for reserves not closed... " + check_no_report "reserve-not-closed-inconsistency" + echo -n "Test for insufficient reserve balances... " + check_no_report "reserve-balance-insufficient-inconsistency" + echo -n "Test for wrong reserve balance summaries... " + check_no_report "reserve-balance-summary-wrong-inconsistency" + echo -n "Test for closure lags... " + check_no_report "closure-lags" + + # Just to test the endpoint and for logging ... + call_endpoint "balances" + + echo -n "Testing aggregation bad sig loss balance... " + check_balance \ + "aggregation_total_bad_sig_loss" \ + "TESTKUDOS:0" \ + "Wrong total bad sig loss from aggregation" + echo -n "Testing coin irregular loss balance... " + check_balance \ + "coin_irregular_loss" \ + "TESTKUDOS:0" \ + "Wrong total irregular loss from coins" + echo -n "Testing reserves bad sig loss balance... " + check_balance \ + "reserves_total_bad_sig_loss" \ + "TESTKUDOS:0" \ + "Wrong total bad sig loss from reserves" + echo -n "Testing purse bad sig loss balance... " + check_balance \ + "purse_total_bad_sig_loss" \ + "TESTKUDOS:0" \ + "Wrong total bad sig loss from purses" + + echo -n "Test for bad incoming delta plus... " + check_balance \ + "total_bad_amount_in_plus" \ + "TESTKUDOS:0" \ + "Expected total wire in delta plus wrong" + echo -n "Test for bad incoming delta minus... " + check_balance \ + "total_bad_amount_in_minus" \ + "TESTKUDOS:0" \ + "Expected total wire in delta minus wrong" + echo -n "Test for bad outgoing delta plus... " + check_balance \ + "total_bad_amount_out_plus" \ + "TESTKUDOS:0" \ + "Expected total wire out delta plus wrong" + echo -n "Test for bad outgoing delta minus... " + check_balance \ + "total_bad_amount_out_minus" \ + "TESTKUDOS:0" \ + "Expected total wire out delta minus wrong" + echo -n "Test for misattribution amounts... " + check_balance \ + "total_misattribution_in" \ + "TESTKUDOS:0" \ + "Expected total misattribution in wrong" + + echo -n "Test for aggregation wire out delta plus... " + check_balance \ + "aggregation_total_wire_out_delta_plus" \ + "TESTKUDOS:0" \ + "Expected total wire out delta plus wrong" + echo -n "Test for aggregation wire out delta minus... " + check_balance \ + "aggregation_total_wire_out_delta_minus" \ + "TESTKUDOS:0" \ + "Expected total wire out delta minus wrong" + + echo -n "Checking for unexpected aggregation delta plus... " + check_balance \ + "aggregation_total_arithmetic_delta_plus" \ + "TESTKUDOS:0" \ + "Wrong arithmetic delta plus from aggregations" + echo -n "Checking for unexpected aggregation delta minus... " + check_balance \ + "aggregation_total_arithmetic_delta_minus" \ + "TESTKUDOS:0" \ + "Wrong arithmetic delta minus from aggregations" + echo -n "Checking for unexpected coin delta plus... " + check_balance \ + "coins_total_arithmetic_delta_plus" \ + "TESTKUDOS:0" \ + "Wrong arithmetic delta plus from coins" + echo -n "Checking for unexpected coin delta minus... " + check_balance \ + "coins_total_arithmetic_delta_minus" \ + "TESTKUDOS:0" \ + "Wrong arithmetic delta minus from coins" + echo -n "Checking for unexpected reserves delta plus... " + check_balance \ + "reserves_total_arithmetic_delta_plus" \ + "TESTKUDOS:0" \ + "Wrong arithmetic delta plus from reserves" + echo -n "Checking for unexpected reserves delta minus... " + check_balance \ + "reserves_total_arithmetic_delta_minus" \ + "TESTKUDOS:0" \ + "Wrong arithmetic delta minus from reserves" +} + + +# The KYC-specific assertions: how much arrived by KYCAUTH, how much left +# the exchange, and how much it is sitting on. +function check_kyc_state() { + echo -n "Checking the KYCAUTH credits were accounted for... " + check_balance \ + "total_kycauth_in" \ + "$KYCAUTH_IN" \ + "Wrong total for KYCAUTH wire transfers" + + echo -n "Checking that only the cleared merchant was paid... " + check_balance \ + "total_wire_out" \ + "$CLEARED_WIRED" \ + "Wrong total wired out" + + # The exchange aggregated the blocked merchant's coins, wrote the + # aggregation_tracking rows and then parked the payout in + # aggregation_transient rather than executing it. Confirm from the + # exchange's own database that the money really is still there, so + # that the auditor-side assertion below is about the auditor and not + # about the reference database having drifted. + echo -n "Checking that the exchange withheld the blocked payout... " + TRANSIENT=$(psql -Aqt "$DB" \ + -c "SELECT COUNT(*) FROM exchange.aggregation_transient;") + if [ "$TRANSIENT" != "1" ] then - exit_fail "Wrong arithmetic delta from reserves, got unexpected plus of $LOSS" + exit_fail "Expected one withheld aggregation, got ${TRANSIENT}" fi - LOSS=$(jq -r .total_arithmetic_delta_minus < test-audit-reserves.json) - if [ "$LOSS" != "TESTKUDOS:0" ] + WIRED=$(psql -Aqt "$DB" -c "SELECT COUNT(*) FROM exchange.wire_out;") + if [ "$WIRED" != "1" ] then - exit_fail "Wrong arithmetic delta from reserves, got unexpected minus of $LOSS" + exit_fail "Expected exactly one wire transfer, got ${WIRED}" fi - - jq -e .amount_arithmetic_inconsistencies[0] < test-audit-aggregation.json > /dev/null && exit_fail "Unexpected arithmetic inconsistencies from aggregations detected in ordinary run" - jq -e .amount_arithmetic_inconsistencies[0] < test-audit-coins.json > /dev/null && exit_fail "Unexpected arithmetic inconsistencies from coins detected in ordinary run" - jq -e .amount_arithmetic_inconsistencies[0] < test-audit-reserves.json > /dev/null && exit_fail "Unexpected arithmetic inconsistencies from reserves detected in ordinary run" echo "PASS" - echo -n "Checking for unexpected wire out differences " - jq -e .wire_out_inconsistencies[0] < test-audit-aggregation.json > /dev/null && exit_fail "Unexpected wire out inconsistencies detected in ordinary run" - echo "PASS" + # ... and the auditor does not notice. See issues.txt, issue 9: the + # withheld deposits carry aggregation_tracking rows, which + # taler-helper-auditor-transfer.c accepts as proof that they were + # paid, so no lag is reported for the WITHHELD amount. This + # assertion pins down the current behaviour; when the auditor learns + # to require a wire_out row it will fail here and should then be + # changed to expect the withheld amount instead. + echo -n "Checking the (unimplemented) view of withheld funds... " + check_balance \ + "total_amount_lag" \ + "TESTKUDOS:0" \ + "Withheld funds are now visible to the auditor: update this test and issues.txt (issue 9), expected ${WITHHELD}" + echo -n "Test for pending deposits... " + check_no_report "pending-deposits" + echo -n "Test for early aggregations... " + check_no_report "early-aggregation" +} + + +# The KYC artefacts must not make the auditor invent findings. +function test_0() { + + echo "===========0: normal run with aggregator===========" + run_audit aggregator + check_auditor_running + + echo "Checking output" + check_all_clean + check_kyc_state # cannot easily undo aggregator, hence full reload full_reload - + cleanup } -# Run without aggregator, hence auditor should detect wire -# transfer lag! +# Same, but without letting the aggregator run first. The aggregator +# cannot lawfully do anything more than it already did when the reference +# database was generated, so the auditor's view must be identical. function test_1() { echo "===========1: normal run===========" run_audit + check_auditor_running echo "Checking output" - # if an emergency was detected, that is a bug and we should fail - echo -n "Test for emergencies... " - jq -e .emergencies[0] \ - < test-audit-coins.json \ - > /dev/null \ - && exit_fail "Unexpected emergency detected in ordinary run"; - echo "PASS" - echo -n "Test for emergencies by count... " - jq -e .emergencies_by_count[0] \ - < test-audit-coins.json \ - > /dev/null \ - && exit_fail "Unexpected emergency by count detected in ordinary run" - echo "PASS" + check_all_clean + check_kyc_state - echo -n "Test for wire inconsistencies... " - jq -e .wire_out_amount_inconsistencies[0] \ - < test-audit-wire.json \ - > /dev/null \ - && exit_fail "Unexpected wire out inconsistency detected in ordinary run" - jq -e .reserve_in_amount_inconsistencies[0] \ - < test-audit-wire.json \ - > /dev/null \ - && exit_fail "Unexpected reserve in inconsistency detected in ordinary run" - jq -e .misattribution_inconsistencies[0] \ - < test-audit-wire.json \ - > /dev/null \ - && exit_fail "Unexpected misattribution inconsistency detected in ordinary run" - jq -e .row_inconsistencies[0] \ - < test-audit-wire.json \ - > /dev/null \ - && exit_fail "Unexpected row inconsistency detected in ordinary run" - jq -e .row_minor_inconsistencies[0] \ - < test-audit-wire.json \ - > /dev/null \ - && exit_fail "Unexpected minor row inconsistency detected in ordinary run" - jq -e .wire_format_inconsistencies[0] \ - < test-audit-wire.json \ - > /dev/null \ - && exit_fail "Unexpected wire format inconsistencies detected in ordinary run" + # Database was unmodified, no need to undo + echo "OK" +} - # TODO: check operation balances are correct (once we have all transaction types and wallet is deterministic) - # TODO: check revenue summaries are correct (once we have all transaction types and wallet is deterministic) - echo "PASS" +# A KYCAUTH credit that the bank does not agree is a KYCAUTH credit. +function test_2() { - echo -n "Check for lag detection... " + echo "===========2: KYCAUTH credit reclassified by the bank===========" + # Modifying the bank's books needs libeufin to be restarted, it does + # not re-read them. + pre_audit + stop_libeufin + echo -n "Modifying database: " + # libeufin records for every credit to the exchange whether it was a + # reserve top-up or an account authorisation. Turn one of the two + # KYCAUTH credits into a reserve credit: the KYCAUTH total the auditor + # tracks must drop by that amount, and because the 32 bytes of KYCAUTH + # metadata are not a reserve public key the exchange knows, the credit + # now also looks like an incoming transfer the exchange never booked. + echo "UPDATE libeufin_bank.taler_exchange_incoming SET type='reserve' WHERE exchange_incoming_id=(SELECT MIN(exchange_incoming_id) FROM libeufin_bank.taler_exchange_incoming WHERE type='kyc')" \ + | psql -Aqt "$DB" + echo "DONE" + launch_libeufin + await_bank - # Check wire transfer lag reported (no aggregator!) - # NOTE: This test is EXPECTED to fail for ~1h after - # re-generating the test database as we do not - # report lag of less than 1h (see GRACE_PERIOD in - # taler-helper-auditor-wire.c) - jq -e .lag_details[0] \ - < test-audit-wire.json \ - > /dev/null \ - || exit_fail "Lag not detected in run without aggregator" + audit_only + check_auditor_running + + echo -n "Checking that the KYCAUTH total dropped... " + check_balance \ + "total_kycauth_in" \ + "TESTKUDOS:0.1" \ + "Wrong total for KYCAUTH wire transfers" + echo -n "Checking that the unbooked credit was flagged... " + check_report_any \ + "reserve-in-inconsistency" \ + "diagnostic" "unknown to exchange" + echo -n "Checking that it was counted as an excess credit... " + check_balance \ + "total_bad_amount_in_plus" \ + "TESTKUDOS:0.1" \ + "Wrong total_bad_amount_in_plus" - LAG=$(jq -r .total_amount_lag < test-audit-wire.json) - if [ "$LAG" = "TESTKUDOS:0" ] - then - exit_fail "Expected total lag to be non-zero" - fi - echo "PASS" + post_audit + full_reload + cleanup +} - echo -n "Test for wire amounts... " - WIRED=$(jq -r .total_wire_in_delta_plus < test-audit-wire.json) - if [ "$WIRED" != "TESTKUDOS:0" ] - then - exit_fail "Expected total wire delta plus wrong, got $WIRED" - fi - WIRED=$(jq -r .total_wire_in_delta_minus < test-audit-wire.json) - if [ "$WIRED" != "TESTKUDOS:0" ] - then - exit_fail "Expected total wire delta minus wrong, got $WIRED" - fi - WIRED=$(jq -r .total_wire_out_delta_plus < test-audit-wire.json) - if [ "$WIRED" != "TESTKUDOS:0" ] - then - exit_fail "Expected total wire delta plus wrong, got $WIRED" - fi - WIRED=$(jq -r .total_wire_out_delta_minus < test-audit-wire.json) - if [ "$WIRED" != "TESTKUDOS:0" ] - then - exit_fail "Expected total wire delta minus wrong, got $WIRED" - fi - WIRED=$(jq -r .total_misattribution_in < test-audit-wire.json) - if [ "$WIRED" != "TESTKUDOS:0" ] - then - exit_fail "Expected total misattribution in wrong, got $WIRED" - fi - # Database was unmodified, no need to undo - echo "OK" +# The rest of the tests inject the same kinds of faults test-auditor.sh +# injects, to show that KYC does not stop the auditor from finding them. + + +# Change the amount of an incoming wire transfer as recorded by the +# exchange. +function test_3() { + + echo "===========3: reserves_in inconsistency===========" + echo -n "Modifying database: " + echo "UPDATE exchange.reserves_in SET credit.val=5 WHERE reserve_in_serial_id=1" \ + | psql -Aqt "$DB" + echo "DONE" + + run_audit + check_auditor_running + + echo -n "Testing inconsistency detection... " + check_report \ + "reserve-in-inconsistency" \ + "row_id" 1 + echo -n "Testing amount wired... " + check_report \ + "reserve-in-inconsistency" \ + "amount_wired" "TESTKUDOS:10" + echo -n "Testing amount expected... " + check_report \ + "reserve-in-inconsistency" \ + "amount_exchange_expected" "TESTKUDOS:5" + echo -n "Checking wire credit balance plus... " + check_balance \ + "total_bad_amount_in_plus" \ + "TESTKUDOS:5" \ + "Expected total_bad_amount_in_plus wrong" + + full_reload + cleanup } +# Test where denom_sig in the known_coins table is wrong (=> bad signature) +function test_4() { + + echo "===========4: known_coins signature wrong===========" + OLD_ROW=$(echo "SELECT known_coin_id FROM exchange.known_coins LIMIT 1;" | psql "$DB" -Aqt) + COIN_PUB=$(echo "SELECT coin_pub FROM exchange.known_coins WHERE known_coin_id=$OLD_ROW;" | psql "$DB" -Aqt) +# shellcheck disable=SC2028 + echo "UPDATE exchange.known_coins SET denom_sig='\x0000000100000000287369672d76616c200a2028727361200a2020287320233542383731423743393036444643303442424430453039353246413642464132463537303139374131313437353746324632323332394644443146324643333445393939413336363430334233413133324444464239413833353833464536354442374335434445304441453035374438363336434541423834463843323843344446304144363030343430413038353435363039373833434431333239393736423642433437313041324632414132414435413833303432434346314139464635394244434346374436323238344143354544364131373739463430353032323241373838423837363535453434423145443831364244353638303232413123290a2020290a20290b' WHERE coin_pub='$COIN_PUB'" \ + | psql -Aqt "$DB" + + run_audit + check_auditor_running + + echo -n "Checking bad-signature-loss detected... " + check_report_neg \ + "bad-sig-losses" \ + "loss" "TESTKUDOS:0" + echo -n "Checking bad-signature-loss balance update... " + check_not_balance \ + "coin_irregular_loss" \ + "TESTKUDOS:0" \ + "Wrong total bad sig loss" + + full_reload + cleanup +} + + +# Test where the amount the bank actually wired differs from what the +# exchange claims. +function test_5() { + + echo "===========5: incorrect wire_out amount===========" + pre_audit + stop_libeufin + echo -n "Modifying database: " + echo "UPDATE libeufin_bank.bank_account_transactions SET amount=(50,0) WHERE debtor_name='Exchange Company';" \ + | psql -Aqt "$DB" + echo "DONE" + launch_libeufin + await_bank + + audit_only + check_auditor_running + + echo -n "Testing wire-out-inconsistency expected... " + check_report \ + "wire-out-inconsistency" \ + "expected" \ + "$CLEARED_WIRED" + echo -n "Testing wire-out-inconsistency claimed... " + check_report \ + "wire-out-inconsistency" \ + "claimed" \ + "TESTKUDOS:50" + echo -n "Testing bad_amount_out_plus balance reporting... " + check_not_balance \ + "total_bad_amount_out_plus" \ + "TESTKUDOS:0" \ + "reported total_bad_amount_out_plus wrong" + + post_audit + full_reload + cleanup +} + + +# Test where the exchange claims to have received more than the bank sent, +# which also throws its reserve balance summary off. +function test_6() { + + echo "===========6: reserve balance summary wrong===========" + echo -n "Modifying database: " + echo "UPDATE exchange.reserves_in SET credit.val=15 WHERE reserve_in_serial_id=1" \ + | psql -Aqt "$DB" + echo "DONE" + + run_audit + check_auditor_running + + echo -n "Checking reserve balance summary inconsistency detected... " + check_some_report \ + "reserve-balance-summary-wrong-inconsistency" + echo -n "Testing amount the exchange expected... " + check_report \ + "reserve-in-inconsistency" \ + "amount_exchange_expected" "TESTKUDOS:15" + echo -n "Checking wire credit balance minus... " + check_balance \ + "total_bad_amount_in_minus" \ + "TESTKUDOS:5" \ + "Wrong total_bad_amount_in_minus" + + full_reload + cleanup +} + + +# Test where the exchange forgot a deposit that a merchant can prove. +function test_7() { + + echo "===========7: deposit wire target malformed===========" + SERIAL=$(echo "SELECT batch_deposit_serial_id FROM exchange.coin_deposits WHERE (amount_with_fee).val=3 ORDER BY batch_deposit_serial_id LIMIT 1" | psql "$DB" -Aqt) +# shellcheck disable=SC2028 + echo "INSERT INTO exchange.wire_targets (payto_uri, wire_target_h_payto) VALUES ('payto://x-taler-bank/localhost/testuser-xxlargtp', '\x1e8f31936b3cee8f8afd3aac9e38b5db42d45b721ffc4eb1e5b9ddaf1565660b');" \ + | psql "$DB" -Aqt +# shellcheck disable=SC2028 + echo "UPDATE exchange.batch_deposits SET wire_target_h_payto='\x1e8f31936b3cee8f8afd3aac9e38b5db42d45b721ffc4eb1e5b9ddaf1565660b' WHERE batch_deposit_serial_id=${SERIAL};" \ + | psql -Aqt "$DB" + + run_audit + check_auditor_running + + echo -n "Checking correct operation of loss reported... " + check_report \ + "bad-sig-losses" \ + "operation" "deposit" + echo -n "Checking correct problem row ID reported... " + check_report \ + "bad-sig-losses" \ + "problem_row_id" "$SERIAL" + echo -n "Checking irregular coin loss... " + check_not_balance \ + "coin_irregular_loss" \ + "TESTKUDOS:0" \ + "Wrong total irregular coin loss" + + full_reload + cleanup +} + # *************** Main test loop starts here ************** @@ -606,6 +1002,7 @@ function check_with_database() { BASEDB="$1" CONF="$1.conf" + export CONF echo "Running test suite with database $BASEDB using configuration $CONF" MASTER_PRIV_FILE="${BASEDB}.mpriv" taler-exchange-config \ @@ -614,9 +1011,6 @@ function check_with_database() -s exchange-offline \ -o MASTER_PRIV_FILE \ -V "${MASTER_PRIV_FILE}" - MASTER_PUB=$(gnunet-ecc -p "$MASTER_PRIV_FILE") - - echo "MASTER PUB is ${MASTER_PUB} using file ${MASTER_PRIV_FILE}" # Load database full_reload @@ -635,23 +1029,24 @@ function check_with_database() # dropdb $DB } -# If this script is not run as root, create the temporary -# storage space for postgres. -# Sets PGHOST accordingly +# When the script is not run as root, setup a temporary directory for the +# postgres database. +# Sets PGHOST accordingly to the freshly created socket. function perform_initdb() { # Available directly in path? INITDB_BIN=$(command -v initdb) || true if [[ -n "$INITDB_BIN" ]]; then echo " FOUND (in path) at $INITDB_BIN" else - HAVE_INITDB=$(find /usr -name "initdb" | head -1 2> /dev/null | grep postgres) \ + HAVE_INITDB=$(find /usr -name "initdb" 2> /dev/null \ + | head -1 2> /dev/null \ + | grep postgres) \ || exit_skip " MISSING" echo " FOUND at $(dirname "$HAVE_INITDB")" INITDB_BIN=$(echo "$HAVE_INITDB" | grep bin/initdb | grep postgres | sort -n | tail -n1) fi POSTGRES_PATH=$(dirname "$INITDB_BIN") - echo "Using $MY_TMP_DIR for logging and temporary data" TMPDIR="$MY_TMP_DIR/postgres" mkdir -p "$TMPDIR" echo -n "Setting up Postgres DB at $TMPDIR ..." @@ -660,11 +1055,29 @@ function perform_initdb() { --auth=trust \ -D "${TMPDIR}" \ > "${MY_TMP_DIR}/postgres-dbinit.log" \ - 2> "${MY_TMP_DIR}/postgres-dbinit.err" + 2> "${MY_TMP_DIR}/postgres-dbinit.err" \ + || { + echo "FAILED!" + echo "Last entries in ${MY_TMP_DIR}/postgres-dbinit.err:" + tail "${MY_TMP_DIR}/postgres-dbinit.err" + exit 1 + } echo "DONE" + + # Once we move to PG16, we can use: + # --set listen_addresses='' \ + # --set fsync=off \ + # --set max_wal_senders=0 \ + # --set synchronous_commit=off \ + # --set wal_level=minimal \ + # --set unix_socket_directories="${TMPDIR}/sockets" \ + + SOCKETDIR="${TMPDIR}/sockets" mkdir "${SOCKETDIR}" + echo -n "Launching Postgres service" + cat - >> "$TMPDIR/postgresql.conf" <<EOF unix_socket_directories='${TMPDIR}/sockets' fsync=off @@ -673,24 +1086,23 @@ synchronous_commit=off wal_level=minimal listen_addresses='' EOF + grep -v host \ < "$TMPDIR/pg_hba.conf" \ > "$TMPDIR/pg_hba.conf.new" mv "$TMPDIR/pg_hba.conf.new" "$TMPDIR/pg_hba.conf" "${POSTGRES_PATH}/pg_ctl" \ - -D "$TMPDIR" \ - -l /dev/null \ - start \ - > "${MY_TMP_DIR}/postgres-start.log" \ - 2> "${MY_TMP_DIR}/postgres-start.err" + -D "$TMPDIR" \ + -l "${MY_TMP_DIR}/postgres.log" \ + start \ + > "${MY_TMP_DIR}/postgres-start.log" \ + 2> "${MY_TMP_DIR}/postgres-start.err" echo " DONE" PGHOST="$TMPDIR/sockets" export PGHOST - } - # *************** Main logic starts here ************** # ####### Setup globals ###### @@ -700,38 +1112,61 @@ export DB="auditor-basedb" # test required commands exist echo "Testing for jq" jq -h > /dev/null || exit_skip "jq required" +echo "Testing for taler-merchant-config" +taler-merchant-config -h > /dev/null || exit_skip "taler-merchant-config required" +echo "Testing for taler-merchant-httpd" +taler-merchant-httpd -h > /dev/null || exit_skip "taler-merchant-httpd required" echo "Testing for faketime" faketime -h > /dev/null || exit_skip "faketime required" # NOTE: really check for all three libeufin commands? -echo "Testing for libeufin-bank" -libeufin-bank --help >/dev/null 2> /dev/null </dev/null || exit_skip "libeufin-bank required" +echo "Testing for libeufin" +libeufin-bank --help >/dev/null 2> /dev/null </dev/null || exit_skip "libeufin required" echo "Testing for taler-wallet-cli" taler-wallet-cli -h >/dev/null </dev/null 2>/dev/null || exit_skip "taler-wallet-cli required" -MY_TMP_DIR=$(mktemp -d /tmp/taler-auditor-basedbXXXXXX) echo -n "Testing for Postgres" + +MY_TMP_DIR=$(mktemp -d /tmp/taler-auditor-basedbXXXXXX) +echo "Using $MY_TMP_DIR for logging and temporary data" + +# If run as root, simply use the running postgres instance. +# Otherwise create a temporary storage space for postgres. [ $(id -u) == 0 ] || perform_initdb MYDIR="${MY_TMP_DIR}/basedb" mkdir -p "${MYDIR}" -echo "Generating fresh database at $MYDIR" -if faketime -f '-1 d' ./generate-auditor-basedb.sh \ - -c generate-kyc-basedb.conf \ - -d "$MYDIR/$DB" + +if [ -z ${REUSE_BASEDB_DIR+x} ] then - echo -n "Reset 'auditor-basedb' database at ${PGHOST:-} ..." - dropdb "auditor-basedb" >/dev/null 2>/dev/null || true - createdb "auditor-basedb" || exit_skip "Could not create database '$BASEDB' at ${PGHOST:-}" - echo " DONE" - check_with_database "$MYDIR/$DB" - if [ "$fail" != "0" ] + echo "Generating fresh database at $MYDIR" + + if faketime -f '-1 d' ./generate-kyc-basedb.sh -d "$MYDIR/$DB" then - exit "$fail" + echo -n "Reset 'auditor-basedb' database at ${PGHOST:-} ..." + dropdb --if-exists "auditor-basedb" > /dev/null 2> /dev/null || true + createdb "auditor-basedb" || exit_skip "Could not create database '$BASEDB' at ${PGHOST:-}" + echo " DONE" + else + echo "Generation failed" + exit 1 fi + echo "To reuse this database in the future, use:" + echo "export REUSE_BASEDB_DIR=$MY_TMP_DIR" else - echo "Generation failed" - exit 1 + echo "Reusing existing database from ${REUSE_BASEDB_DIR}" + cp -r "${REUSE_BASEDB_DIR}/basedb"/* "${MYDIR}/" +fi + +check_with_database "$MYDIR/$DB" +if [ "$fail" != "0" ] +then + exit "$fail" +fi + +if [ -z "${REUSE_BASEDB_DIR+x}" ] +then + echo "Run 'export REUSE_BASEDB_DIR=${MY_TMP_DIR}' to re-run tests against the same database" fi exit 0