test-kyc.sh (64388B)
1 #!/bin/bash 2 # 3 # This file is part of TALER 4 # Copyright (C) 2014-2026 Taler Systems SA 5 # 6 # TALER is free software; you can redistribute it and/or modify it under the 7 # terms of the GNU General Public License as published by the Free Software 8 # Foundation; either version 3, or (at your option) any later version. 9 # 10 # TALER is distributed in the hope that it will be useful, but WITHOUT ANY 11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 12 # A PARTICULAR PURPOSE. See the GNU General Public License for more details. 13 # 14 # You should have received a copy of the GNU General Public License along with 15 # TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/license> 16 # 17 # 18 # shellcheck disable=SC2317 19 # shellcheck disable=SC1091 20 # 21 # 22 # Setup database which was generated from a perfectly normal 23 # exchange-wallet-merchant interaction on a KYC-enabled exchange and run 24 # the auditor against it. 25 # 26 # The reference database (see generate-kyc-basedb.sh) contains two merchant 27 # instances that were paid the same amount and both proved that they own 28 # their bank account with a KYCAUTH wire transfer, but only one of them 29 # then passed the KYC form. The exchange therefore wired the funds of the 30 # first one out and is still holding the funds of the second one. 31 # 32 # The auditor has almost no KYC-specific logic of its own -- see issues.txt 33 # for what it could check and does not -- so this test has two jobs: 34 # 35 # 1. the KYC artefacts (KYCAUTH credits, legitimization measures and 36 # outcomes, an aggregation the exchange lawfully withheld) must not 37 # make the auditor invent findings, and the KYCAUTH totals it does 38 # track -- total_kycauth_in from the bank, total_kycauth_revenue from 39 # the exchange -- must be exactly right; 40 # 2. fault detection must still work on a KYC-enabled exchange, so the 41 # later tests inject the same kinds of faults test-auditor.sh does and 42 # require the auditor to flag them. Tests 2, 8 and 9 pull the two 43 # KYCAUTH totals apart in each of the ways they can disagree, which is 44 # what the reconciliation between them exists to catch. 45 # 46 # Requires 'jq' tool and Postgres superuser rights! 47 set -eu 48 #set -x 49 50 # Set of numbers for all the testcases. 51 # When adding new tests, increase the last number: 52 ALL_TESTS=$(seq 0 16) 53 54 # $TESTS determines which tests we should run. 55 # This construction is used to make it easy to 56 # only run a subset of the tests. To only run a subset, 57 # pass the numbers of the tests to run as the FIRST 58 # argument to test-kyc.sh, i.e.: 59 # 60 # $ test-kyc.sh "1 3" 61 # 62 # to run tests 1 and 3 only. By default, all tests are run. 63 # 64 TESTS=${1:-$ALL_TESTS} 65 66 export TALER_AUDITOR_TOKEN="secret-token:D4CST1Z6AHN3RT03M0T9NSTF2QGHTB5ZD2D3RYZB4HAWG8SX0JEFWBXCKXZHMB7Y3Z7KVFW0B3XPXD5BHCFP8EB0R6CNH2KAWDWVET0" 67 export TALER_AUDITOR_SALT="64S36D1N6RVKGC9J6CT3ADHQ70RK4CSM6MV3EE1H68SK8D9P6WW32CHK6GTKCDSR64S36D1N6RVKGC9J6CT3ADHQ70RK4CSM6MV3EE0" 68 69 # Global variable to run the auditor processes under valgrind 70 # VALGRIND=valgrind 71 VALGRIND="" 72 73 . setup.sh 74 75 76 # Cleanup exchange and libeufin between runs. 77 function cleanup() 78 { 79 if [ -n "${EPID:-}" ] 80 then 81 echo -n "Stopping exchange $EPID..." 82 kill -TERM "$EPID" 83 wait "$EPID" || true 84 echo "DONE" 85 unset EPID 86 fi 87 stop_libeufin &> /dev/null 88 } 89 90 # Cleanup to run whenever we exit 91 function exit_cleanup() 92 { 93 jobs 94 if [ -n "${POSTGRES_PATH:-}" ] 95 then 96 echo -n "Stopping Postgres at ${POSTGRES_PATH} ..." 97 "${POSTGRES_PATH}/pg_ctl" \ 98 -D "$TMPDIR" \ 99 --log="${MY_TMP_DIR}/pg_ctl.log" \ 100 stop \ 101 &> ${MY_TMP_DIR}/pg_ctl.out \ 102 || true 103 echo "DONE" 104 fi 105 echo -n "Running exit-cleanup ..." 106 cleanup 107 for n in $(jobs -p) 108 do 109 kill "$n" 2> /dev/null || true 110 done 111 wait || true 112 echo "DONE" 113 } 114 115 # Install cleanup handler (except for kill -9) 116 trap exit_cleanup EXIT 117 118 119 function await_bank () { 120 for n in $(seq 1 80) 121 do 122 echo -n "." 123 sleep 0.1 124 OK=1 125 wget http://localhost:8082/ \ 126 -o /dev/null \ 127 -O /dev/null \ 128 >/dev/null \ 129 && break 130 OK=0 131 done 132 if [ 1 != "$OK" ] 133 then 134 exit_skip "Failed to launch libeufin-bank" 135 fi 136 } 137 138 # Operations to run before the actual audit 139 function pre_audit () { 140 # Launch bank 141 echo -n "Launching libeufin-bank" 142 export CONF 143 export MY_TMP_DIR 144 launch_libeufin 145 await_bank 146 echo " DONE" 147 148 if [ "${1:-no}" = "aggregator" ] 149 then 150 # NO '-y' here: that is --kyc-off, which makes the aggregator 151 # wire funds out without consulting KYC at all and would defeat 152 # the point of this test. 153 echo -n "Running exchange aggregator ..." 154 taler-exchange-aggregator \ 155 -L "INFO" \ 156 -t \ 157 -c "$CONF" \ 158 2> "${MY_TMP_DIR}/aggregator.log" \ 159 || exit_fail "FAIL" 160 echo " DONE" 161 echo -n "Running exchange closer ..." 162 taler-exchange-closer \ 163 -L "INFO" \ 164 -t \ 165 -c "$CONF" \ 166 2> "${MY_TMP_DIR}/closer.log" \ 167 || exit_fail "FAIL" 168 echo " DONE" 169 echo -n "Running exchange transfer ..." 170 taler-exchange-transfer \ 171 -L "INFO" \ 172 -t \ 173 -c "$CONF" \ 174 2> "${MY_TMP_DIR}/transfer.log" \ 175 || exit_fail "FAIL" 176 echo " DONE" 177 fi 178 } 179 180 # actual audit run 181 function audit_only () { 182 # Run the auditor! 183 echo -n "Running audit(s) ..." 184 185 # Restart so that first run is always fresh, and second one is incremental 186 taler-auditor-dbinit \ 187 -r \ 188 -c "$CONF" 189 $VALGRIND taler-helper-auditor-aml \ 190 -i \ 191 -L DEBUG \ 192 -c "$CONF" \ 193 -t \ 194 > "${MY_TMP_DIR}/test-audit-aml.out" \ 195 2> "${MY_TMP_DIR}/test-audit-aml.err" \ 196 || exit_fail "aml audit failed (see ${MY_TMP_DIR}/test-audit-aml.*)" 197 echo -n "." 198 $VALGRIND taler-helper-auditor-aml \ 199 -i \ 200 -L DEBUG \ 201 -c "$CONF" \ 202 -t \ 203 > "${MY_TMP_DIR}/test-audit-aml-inc.out" \ 204 2> "${MY_TMP_DIR}/test-audit-aml-inc.err" \ 205 || exit_fail "incremental aml audit failed (see ${MY_TMP_DIR}/test-audit-aml-inc.*)" 206 echo -n "." 207 $VALGRIND taler-helper-auditor-aggregation \ 208 -L DEBUG \ 209 -c "$CONF" \ 210 -t \ 211 > "${MY_TMP_DIR}/test-audit-aggregation.out" \ 212 2> "${MY_TMP_DIR}/test-audit-aggregation.err" \ 213 || exit_fail "aggregation audit failed (see ${MY_TMP_DIR}/test-audit-aggregation.*)" 214 echo -n "." 215 $VALGRIND taler-helper-auditor-aggregation \ 216 -L DEBUG \ 217 -c "$CONF" \ 218 -t \ 219 > "${MY_TMP_DIR}/test-audit-aggregation-inc.out" \ 220 2> "${MY_TMP_DIR}/test-audit-aggregation-inc.err" \ 221 || exit_fail "incremental aggregation audit failed (see ${MY_TMP_DIR}/test-audit-aggregation-inc.*)" 222 echo -n "." 223 $VALGRIND taler-helper-auditor-coins \ 224 -L DEBUG \ 225 -c "$CONF" \ 226 -t \ 227 > "${MY_TMP_DIR}/test-audit-coins.out" \ 228 2> "${MY_TMP_DIR}/test-audit-coins.err" \ 229 || exit_fail "coin audit failed (see ${MY_TMP_DIR}/test-audit-coins.*)" 230 echo -n "." 231 $VALGRIND taler-helper-auditor-coins \ 232 -L DEBUG \ 233 -c "$CONF" \ 234 -t \ 235 > "${MY_TMP_DIR}/test-audit-coins-inc.out" \ 236 2> "${MY_TMP_DIR}/test-audit-coins-inc.err" \ 237 || exit_fail "incremental coin audit failed (see ${MY_TMP_DIR}/test-audit-coins-inc.*)" 238 echo -n "." 239 $VALGRIND taler-helper-auditor-deposits \ 240 -L DEBUG \ 241 -c "$CONF" \ 242 -t \ 243 > "${MY_TMP_DIR}/test-audit-deposits.out" \ 244 2> "${MY_TMP_DIR}/test-audit-deposits.err" \ 245 || exit_fail "deposits audit failed (see ${MY_TMP_DIR}/test-audit-deposits.*)" 246 echo -n "." 247 $VALGRIND taler-helper-auditor-deposits \ 248 -L DEBUG \ 249 -c "$CONF" \ 250 -t \ 251 > "${MY_TMP_DIR}/test-audit-deposits-inc.out" \ 252 2> "${MY_TMP_DIR}/test-audit-deposits-inc.err" \ 253 || exit_fail "incremental deposits audit failed (see ${MY_TMP_DIR}/test-audit-deposits-inc.*)" 254 echo -n "." 255 $VALGRIND taler-helper-auditor-reserves \ 256 -i \ 257 -L DEBUG \ 258 -c "$CONF" \ 259 -t \ 260 > "${MY_TMP_DIR}/test-audit-reserves.out" \ 261 2> "${MY_TMP_DIR}/test-audit-reserves.err" \ 262 || exit_fail "reserves audit failed (see ${MY_TMP_DIR}/test-audit-reserves.*)" 263 echo -n "." 264 $VALGRIND taler-helper-auditor-reserves \ 265 -i \ 266 -L DEBUG \ 267 -c "$CONF" \ 268 -t \ 269 > "${MY_TMP_DIR}/test-audit-reserves-inc.out" \ 270 2> "${MY_TMP_DIR}/test-audit-reserves-inc.err" \ 271 || exit_fail "incremental reserves audit failed (see ${MY_TMP_DIR}/test-audit-reserves-inc.*)" 272 echo -n "." 273 $VALGRIND taler-helper-auditor-wire-credit \ 274 -i \ 275 -L DEBUG \ 276 -c "$CONF" \ 277 -t \ 278 > "${MY_TMP_DIR}/test-audit-wire-credit.out" \ 279 2> "${MY_TMP_DIR}/test-audit-wire-credit.err" \ 280 || exit_fail "wire credit audit failed (see ${MY_TMP_DIR}/test-audit-wire-credit.*)" 281 echo -n "." 282 $VALGRIND taler-helper-auditor-wire-credit \ 283 -i \ 284 -L DEBUG \ 285 -c "$CONF" \ 286 -t \ 287 > "${MY_TMP_DIR}/test-audit-wire-credit-inc.out" \ 288 2> "${MY_TMP_DIR}/test-audit-wire-credit-inc.err" \ 289 || exit_fail "wire credit audit inc failed (see ${MY_TMP_DIR}/test-audit-wire-credit-inc.*)" 290 echo -n "." 291 $VALGRIND taler-helper-auditor-wire-debit \ 292 -i \ 293 -L DEBUG \ 294 -c "$CONF" \ 295 -t \ 296 > "${MY_TMP_DIR}/test-audit-wire-debit.out" \ 297 2> "${MY_TMP_DIR}/test-audit-wire-debit.err" \ 298 || exit_fail "wire debit audit failed (see ${MY_TMP_DIR}/test-audit-wire-debit.*)" 299 echo -n "." 300 $VALGRIND taler-helper-auditor-wire-debit \ 301 -i \ 302 -L DEBUG \ 303 -c "$CONF" \ 304 -t \ 305 > "${MY_TMP_DIR}/test-audit-wire-debit-inc.out" \ 306 2> "${MY_TMP_DIR}/test-audit-wire-debit-inc.err" \ 307 || exit_fail "wire debit audit inc failed (see ${MY_TMP_DIR}/test-audit-wire-debit-inc.*)" 308 echo -n "." 309 $VALGRIND taler-helper-auditor-purses \ 310 -i \ 311 -L DEBUG \ 312 -c "$CONF" \ 313 -t \ 314 > "${MY_TMP_DIR}/test-audit-purses.out" \ 315 2> "${MY_TMP_DIR}/test-audit-purses.err" \ 316 || exit_fail "audit purses failed" 317 echo -n "." 318 $VALGRIND taler-helper-auditor-purses \ 319 -i \ 320 -L DEBUG \ 321 -c "$CONF" \ 322 -t \ 323 > "${MY_TMP_DIR}/test-audit-purses-inc.out" \ 324 2> "${MY_TMP_DIR}/test-audit-purses-inc.err" \ 325 || exit_fail "audit purses inc failed" 326 echo -n "." 327 $VALGRIND taler-helper-auditor-transfer \ 328 -i \ 329 -L DEBUG \ 330 -c "$CONF" \ 331 -t \ 332 > "${MY_TMP_DIR}/test-audit-transfer.out" \ 333 2> "${MY_TMP_DIR}/test-audit-transfer.err" \ 334 || exit_fail "audit transfer failed" 335 echo -n "." 336 $VALGRIND taler-helper-auditor-transfer \ 337 -i \ 338 -L DEBUG \ 339 -c "$CONF" \ 340 -t \ 341 > "${MY_TMP_DIR}/test-audit-transfer-inc.out" \ 342 2> "${MY_TMP_DIR}/test-audit-transfer-inc.err" \ 343 || exit_fail "audit transfer inc failed" 344 echo -n "." 345 346 echo " DONE" 347 } 348 349 350 # Cleanup to run after the auditor 351 function post_audit () { 352 taler-exchange-dbinit \ 353 -c "$CONF" \ 354 -g \ 355 || exit_fail "exchange DB GC failed" 356 cleanup 357 } 358 359 360 # Run audit process on current database, including report 361 # generation. Pass "aggregator" as $1 to run 362 # $ taler-exchange-aggregator 363 # before auditor (to trigger pending wire transfers). 364 # Pass "drain" as $2 to run a drain operation as well. 365 function run_audit () { 366 pre_audit "${1:-no}" 367 audit_only 368 post_audit 369 } 370 371 372 function stop_auditor_httpd() { 373 if [ -n "${APID:-}" ] 374 then 375 echo -n "Stopping auditor $APID..." 376 kill -TERM "$APID" 377 wait "$APID" || true 378 echo "DONE" 379 unset APID 380 fi 381 } 382 383 384 # Do a full reload of the (original) database 385 function full_reload() 386 { 387 echo -n "Doing full reload of the database (loading ${BASEDB}.sql into $DB at ${PGHOST:-})... " 388 dropdb -f "$DB" &>> ${MY_TMP_DIR}/drop.log || true 389 createdb -T template0 "$DB" \ 390 || exit_skip "could not create database $DB (at ${PGHOST:-})" 391 # Import pre-generated database, -q(ietly) using single (-1) transaction 392 psql -Aqt "$DB" \ 393 -q \ 394 -1 \ 395 -f "${BASEDB}.sql" \ 396 &>> ${MY_TMP_DIR}/postgresql-reload.log \ 397 || exit_skip "Failed to load database $DB from ${BASEDB}.sql" 398 echo "DONE" 399 # Technically, this call shouldn't be needed as libeufin should already be stopped here... 400 stop_libeufin 401 stop_auditor_httpd 402 } 403 404 function run_auditor_httpd() { 405 echo -n "Starting auditor..." 406 $VALGRIND taler-auditor-httpd \ 407 -c "${CONF}" \ 408 -L INFO \ 409 2> "${MY_TMP_DIR}/auditor-httpd.err" & 410 APID=$! 411 412 # Wait for auditor service to be available 413 for n in $(seq 1 50) 414 do 415 echo -n "." 416 sleep 0.2 417 OK=0 418 # auditor 419 wget "http://localhost:8083/config" \ 420 -o /dev/null \ 421 -O /dev/null \ 422 >/dev/null \ 423 || continue 424 OK=1 425 break 426 done 427 echo "... DONE." 428 wget "http://localhost:8083/webui/" \ 429 -o /dev/null \ 430 -O /dev/null \ 431 >/dev/null \ 432 || exit_fail "Auditor WebUI is not available under /webui/" 433 } 434 435 436 function check_auditor_running() { 437 ARUNSTATUS=$(curl -Is http://localhost:8083/config | head -1) 438 if [ -n "${ARUNSTATUS:-}" ] 439 then 440 echo "Auditor running" 441 else 442 echo "Auditor not running, starting it" 443 run_auditor_httpd 444 fi 445 unset ARUNSTATUS 446 } 447 448 function call_endpoint() { 449 if [ -n "${2+x}" ] 450 then 451 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}" 452 echo "endpoint ${1} called (with balance_key)... " 453 else 454 curl -s -H "Accept: application/json" -H "Authorization: Bearer ${TALER_AUDITOR_TOKEN}" -o "${MY_TMP_DIR}/${1}.json" "localhost:8083/monitoring/${1}?limit=50" 455 echo "endpoint ${1} called... " 456 fi 457 } 458 459 460 function check_balance() { 461 call_endpoint "balances" "$1" 462 BAL=$(jq -r .records[0].balance_value < "${MY_TMP_DIR}/${1}.json") 463 if [ "$BAL" != "$2" ] 464 then 465 exit_fail "$3 (got $BAL, wanted $2)" 466 fi 467 echo "PASS" 468 } 469 470 471 function check_not_balance() { 472 call_endpoint "balances" "$1" 473 BAL=$(jq -r .records[0].balance_value < "${MY_TMP_DIR}/${1}.json") 474 if [ "$BAL" = "$2" ] 475 then 476 exit_fail "$3 (got $BAL, wanted NOT $2)" 477 fi 478 echo "PASS" 479 } 480 481 482 function check_report() { 483 call_endpoint "$1" 484 VAL=$(jq -r .records[0].\"$2\" < "${MY_TMP_DIR}/${1}.json") 485 if [ "$VAL" != "$3" ] 486 then 487 exit_fail "$1::$2 (got $VAL, wanted $3)" 488 fi 489 echo "PASS" 490 } 491 492 # Check that at least one entry of report $1 has field $2 set to $3. 493 # Unlike check_report, this does not depend on the order in which the 494 # auditor inserted its findings: one fault can legitimately be reported 495 # against several operations (a coin with a bad denomination signature is 496 # flagged once per operation that used it). 497 function check_report_any() { 498 call_endpoint "$1" 499 jq -e --arg want "$3" "any(.records[]; .\"$2\" == \$want)" \ 500 < "${MY_TMP_DIR}/${1}.json" \ 501 > /dev/null \ 502 || exit_fail "$1::$2 (no entry with value $3)" 503 echo "PASS" 504 } 505 506 # Check that report $1 has at least one entry, without pinning down any of 507 # its fields. Useful when the injected fault is certain to be found but 508 # the exact amounts depend on how much of a reserve happened to be left. 509 function check_some_report() { 510 call_endpoint "$1" 511 jq -e .records[0] \ 512 < "${MY_TMP_DIR}/${1}.json" \ 513 > /dev/null \ 514 || exit_fail "Wanted a report for $1, but got none" 515 echo "PASS" 516 } 517 518 function check_no_report() { 519 call_endpoint "$1" 520 jq -e .records[0] \ 521 < "${MY_TMP_DIR}/${1}.json" \ 522 > /dev/null \ 523 && exit_fail "Wanted empty report for $1, but got incidents" 524 echo "PASS" 525 } 526 527 function check_report_neg() { 528 call_endpoint "$1" 529 VAL=$(jq -r .records[0].\"$2\" < "${MY_TMP_DIR}/${1}.json") 530 if [ "$VAL" == "$3" ] 531 then 532 exit_fail "$1::$2 (got $VAL, wanted $3)" 533 fi 534 echo "PASS" 535 } 536 537 function check_row() { 538 call_endpoint "$1" 539 if [ -n "${3+x}" ] 540 then 541 RID="$2" 542 WANT="$3" 543 else 544 RID="row_id" 545 WANT="$2" 546 fi 547 ROW=$(jq -r .records[0].\"${RID}\" < "${MY_TMP_DIR}/${1}.json") 548 if [ "$ROW" != "$WANT" ] 549 then 550 exit_fail "Row ${1} wrong (got ${ROW}, wanted ${WANT})" 551 fi 552 echo "PASS" 553 } 554 555 556 557 # Amount the exchange wired to the merchant that passed KYC. 558 CLEARED_WIRED="TESTKUDOS:8.02" 559 # Amount the exchange aggregated for the merchant that did not pass KYC 560 # and then parked in aggregation_transient instead of wiring it out. 561 WITHHELD="TESTKUDOS:8.03" 562 # The account of that merchant. The IBAN libeufin derives from the account 563 # name is stable across regenerations of the reference database, so this can be 564 # hardcoded -- but the wire transfer identifier cannot: the aggregator draws it 565 # with GNUNET_CRYPTO_random_block(), so tests 14 and 15 read it out of the 566 # exchange's own tables instead of naming it. 567 BLOCKED_ACCOUNT="payto://iban/DE12500105170540732493?receiver-name=Blocked44" 568 # Two KYCAUTH wire transfers of TESTKUDOS:0.1, one per merchant instance. 569 KYCAUTH_IN="TESTKUDOS:0.2" 570 # Everything the exchange's bank account was credited with: TESTKUDOS:56 of 571 # reserve top-ups plus the two KYCAUTH transfers above. The KYCAUTH part is 572 # revenue rather than a liability, but it is money in the account all the 573 # same, so it belongs in the balance the account is expected to have. 574 WIRE_IN="TESTKUDOS:56.2" 575 576 577 # Assert that all of the auditor's loss, delta and irregularity balances 578 # are zero, i.e. that nothing about this database looks wrong to it. 579 function check_all_clean() { 580 echo -n "Test for emergencies... " 581 check_no_report "emergencies" 582 echo -n "Test for emergencies by count... " 583 check_no_report "emergencies-by-count" 584 echo -n "Test for deposit confirmation problems... " 585 check_no_report "deposit-confirmations" 586 echo -n "Test for denomination key validity problems... " 587 check_no_report "denomination-key-validity-withdraw-inconsistencies" 588 echo -n "Test for denominations without signatures... " 589 check_no_report "denominations-without-sigs" 590 echo -n "Test for wire out inconsistencies... " 591 check_no_report "wire-out-inconsistencies" 592 echo -n "Test for reserve in inconsistencies... " 593 check_no_report "reserve-in-inconsistencies" 594 echo -n "Test for KYCAUTH in inconsistencies... " 595 check_no_report "kycauth-in-inconsistencies" 596 echo -n "Test for misattribution inconsistencies... " 597 check_no_report "misattribution-in-inconsistencies" 598 echo -n "Test for row inconsistencies... " 599 check_no_report "row-inconsistencies" 600 echo -n "Test for minor row inconsistencies... " 601 check_no_report "row-minor-inconsistencies" 602 echo -n "Test for wire format inconsistencies... " 603 check_no_report "wire-format-inconsistencies" 604 echo -n "Test for arithmetic inconsistencies... " 605 check_no_report "amount-arithmetic-inconsistencies" 606 echo -n "Test for coin inconsistencies... " 607 check_no_report "coin-inconsistencies" 608 echo -n "Test for bad signature losses... " 609 check_no_report "bad-sig-losses" 610 echo -n "Test for fee time inconsistencies... " 611 check_no_report "fee-time-inconsistencies" 612 echo -n "Test for purses not closed... " 613 check_no_report "purse-not-closed-inconsistencies" 614 echo -n "Test for reserves not closed... " 615 check_no_report "reserve-not-closed-inconsistencies" 616 echo -n "Test for insufficient reserve balances... " 617 check_no_report "reserve-balance-insufficient-inconsistencies" 618 echo -n "Test for wrong reserve balance summaries... " 619 check_no_report "reserve-balance-summary-wrong-inconsistencies" 620 echo -n "Test for closure lags... " 621 check_no_report "closure-lags" 622 623 # Just to test the endpoint and for logging ... 624 call_endpoint "balances" 625 626 echo -n "Testing aggregation bad sig loss balance... " 627 check_balance \ 628 "aggregation_total_bad_sig_loss" \ 629 "TESTKUDOS:0" \ 630 "Wrong total bad sig loss from aggregation" 631 echo -n "Testing coin irregular loss balance... " 632 check_balance \ 633 "coin_irregular_loss" \ 634 "TESTKUDOS:0" \ 635 "Wrong total irregular loss from coins" 636 echo -n "Testing reserves bad sig loss balance... " 637 check_balance \ 638 "reserves_total_bad_sig_loss" \ 639 "TESTKUDOS:0" \ 640 "Wrong total bad sig loss from reserves" 641 echo -n "Testing purse bad sig loss balance... " 642 check_balance \ 643 "purse_total_bad_sig_loss" \ 644 "TESTKUDOS:0" \ 645 "Wrong total bad sig loss from purses" 646 647 echo -n "Test for bad incoming delta plus... " 648 check_balance \ 649 "total_bad_amount_in_plus" \ 650 "TESTKUDOS:0" \ 651 "Expected total wire in delta plus wrong" 652 echo -n "Test for bad incoming delta minus... " 653 check_balance \ 654 "total_bad_amount_in_minus" \ 655 "TESTKUDOS:0" \ 656 "Expected total wire in delta minus wrong" 657 echo -n "Test for bad outgoing delta plus... " 658 check_balance \ 659 "total_bad_amount_out_plus" \ 660 "TESTKUDOS:0" \ 661 "Expected total wire out delta plus wrong" 662 echo -n "Test for bad outgoing delta minus... " 663 check_balance \ 664 "total_bad_amount_out_minus" \ 665 "TESTKUDOS:0" \ 666 "Expected total wire out delta minus wrong" 667 echo -n "Test for misattribution amounts... " 668 check_balance \ 669 "total_misattribution_in" \ 670 "TESTKUDOS:0" \ 671 "Expected total misattribution in wrong" 672 673 echo -n "Test for aggregation wire out delta plus... " 674 check_balance \ 675 "aggregation_total_wire_out_delta_plus" \ 676 "TESTKUDOS:0" \ 677 "Expected total wire out delta plus wrong" 678 echo -n "Test for aggregation wire out delta minus... " 679 check_balance \ 680 "aggregation_total_wire_out_delta_minus" \ 681 "TESTKUDOS:0" \ 682 "Expected total wire out delta minus wrong" 683 684 echo -n "Checking for unexpected aggregation delta plus... " 685 check_balance \ 686 "aggregation_total_arithmetic_delta_plus" \ 687 "TESTKUDOS:0" \ 688 "Wrong arithmetic delta plus from aggregations" 689 echo -n "Checking for unexpected aggregation delta minus... " 690 check_balance \ 691 "aggregation_total_arithmetic_delta_minus" \ 692 "TESTKUDOS:0" \ 693 "Wrong arithmetic delta minus from aggregations" 694 echo -n "Checking for unexpected coin delta plus... " 695 check_balance \ 696 "coins_total_arithmetic_delta_plus" \ 697 "TESTKUDOS:0" \ 698 "Wrong arithmetic delta plus from coins" 699 echo -n "Checking for unexpected coin delta minus... " 700 check_balance \ 701 "coins_total_arithmetic_delta_minus" \ 702 "TESTKUDOS:0" \ 703 "Wrong arithmetic delta minus from coins" 704 echo -n "Checking for unexpected reserves delta plus... " 705 check_balance \ 706 "reserves_total_arithmetic_delta_plus" \ 707 "TESTKUDOS:0" \ 708 "Wrong arithmetic delta plus from reserves" 709 echo -n "Checking for unexpected reserves delta minus... " 710 check_balance \ 711 "reserves_total_arithmetic_delta_minus" \ 712 "TESTKUDOS:0" \ 713 "Wrong arithmetic delta minus from reserves" 714 } 715 716 717 # The KYC-specific assertions: how much arrived by KYCAUTH, how much left 718 # the exchange, and how much it is sitting on. 719 function check_kyc_state() { 720 echo -n "Checking the KYCAUTH credits were accounted for... " 721 check_balance \ 722 "total_kycauth_in" \ 723 "$KYCAUTH_IN" \ 724 "Wrong total for KYCAUTH wire transfers" 725 726 # KYCAUTH money is not credited to a reserve and never paid back, so 727 # the auditor books it as revenue of the exchange operator and counts 728 # it towards the balance the bank account is expected to have. 729 echo -n "Checking the KYCAUTH credits were booked as revenue... " 730 check_balance \ 731 "total_kycauth_revenue" \ 732 "$KYCAUTH_IN" \ 733 "Wrong KYCAUTH revenue" 734 echo -n "Checking the KYCAUTH credits reached the balance sheet... " 735 check_balance \ 736 "total_wire_in" \ 737 "$WIRE_IN" \ 738 "Wrong total credited to the exchange" 739 740 echo -n "Checking that only the cleared merchant was paid... " 741 check_balance \ 742 "total_wire_out" \ 743 "$CLEARED_WIRED" \ 744 "Wrong total wired out" 745 746 # The exchange aggregated the blocked merchant's coins, wrote the 747 # aggregation_tracking rows and then parked the payout in 748 # aggregation_transient rather than executing it. Confirm from the 749 # exchange's own database that the money really is still there, so 750 # that the auditor-side assertion below is about the auditor and not 751 # about the reference database having drifted. 752 echo -n "Checking that the exchange withheld the blocked payout... " 753 TRANSIENT=$(psql -Aqt "$DB" \ 754 -c "SELECT COUNT(*) FROM exchange.aggregation_transient;") 755 if [ "$TRANSIENT" != "1" ] 756 then 757 exit_fail "Expected one withheld aggregation, got ${TRANSIENT}" 758 fi 759 WIRED=$(psql -Aqt "$DB" -c "SELECT COUNT(*) FROM exchange.wire_out;") 760 if [ "$WIRED" != "1" ] 761 then 762 exit_fail "Expected exactly one wire transfer, got ${WIRED}" 763 fi 764 echo "PASS" 765 766 # The exchange also said *why*, in the append-only table that replication 767 # can carry. aggregation_transient above is the same claim, but an 768 # external auditor never gets to see it, so this is the row the auditor's 769 # classification actually rests on. Reason 2 is 770 # TALER_EXCHANGEDB_DR_KYC. 771 # 772 # Restrict this to transfers that have not gone out. Both merchants were 773 # withheld on the generator's first aggregator run -- neither had passed 774 # KYC yet -- so the cleared one has a deferral of its own, made obsolete by 775 # the wire_out row that followed. Append-only means those stay on file, 776 # which is the point: the record of a hold outlives the hold. 777 echo -n "Checking that the exchange recorded why it withheld it... " 778 DEFERRED=$(psql -Aqt "$DB" \ 779 -c "SELECT COUNT(*) FROM exchange.aggregation_deferrals d 780 WHERE d.deferral_reason=2 781 AND d.legitimization_requirement_serial_id != 0 782 AND NOT EXISTS ( 783 SELECT 1 FROM exchange.wire_out w 784 WHERE w.wtid_raw=d.wtid_raw);") 785 if [ "$DEFERRED" != "1" ] 786 then 787 exit_fail "Expected one open KYC deferral on record, got ${DEFERRED}" 788 fi 789 echo "PASS" 790 791 # ... and the auditor sees it. The withheld deposits do carry 792 # aggregation_tracking rows, so they are not "lagging" in the sense of 793 # total_amount_lag, which counts deposits the exchange has not aggregated 794 # at all; that balance is legitimately zero here. What the exchange has 795 # not done is the wire transfer, and that is what total_aml_hold counts. 796 echo -n "Checking that nothing is waiting to be aggregated... " 797 check_balance \ 798 "total_amount_lag" \ 799 "TESTKUDOS:0" \ 800 "Wrong total for deposits that were never aggregated" 801 echo -n "Checking that the withheld payout is booked as an AML hold... " 802 check_balance \ 803 "total_aml_hold" \ 804 "$WITHHELD" \ 805 "Wrong total withheld pending legitimization" 806 # The blocked merchant is the *only* transfer the exchange owes, and its 807 # KYC measure is open, so nothing may land in the unexplained bucket. 808 # Were the two ever to swap, the exchange would be stalling a payout it 809 # has no legal reason to stall, which is the case this split exists for. 810 echo -n "Checking that no payout is unexplained... " 811 check_balance \ 812 "total_transfer_lag" \ 813 "TESTKUDOS:0" \ 814 "Exchange is withholding funds it gave no reason for" 815 # Every deposit here is aggregated into a payout worth making, so the 816 # third bucket -- money parked only because the aggregate would not 817 # cover its wire fee -- must be empty. It exists so that such money 818 # does not read as either of the two above, both of which say something 819 # is wrong. 820 echo -n "Checking that nothing is parked as a small aggregate... " 821 check_balance \ 822 "total_small_aggregate" \ 823 "TESTKUDOS:0" \ 824 "Exchange parked an aggregate as too small to pay out" 825 echo -n "Checking that the hold names the blocked account... " 826 check_report_any \ 827 "aml-holds" \ 828 "account" \ 829 "$BLOCKED_ACCOUNT" 830 echo -n "Checking that the hold names the amount... " 831 check_report "aml-holds" "amount" "$WITHHELD" 832 echo -n "Checking that the hold is attributed to KYC... " 833 check_report "aml-holds" "deferral_reason" "KYC" 834 echo -n "Test for pending deposits... " 835 check_no_report "pending-deposits" 836 echo -n "Test for early aggregations... " 837 check_no_report "early-aggregations" 838 } 839 840 841 # The KYC artefacts must not make the auditor invent findings. 842 function test_0() { 843 844 echo "===========0: normal run with aggregator===========" 845 run_audit aggregator 846 check_auditor_running 847 848 echo "Checking output" 849 check_all_clean 850 check_kyc_state 851 852 # cannot easily undo aggregator, hence full reload 853 full_reload 854 cleanup 855 } 856 857 858 # Same, but without letting the aggregator run first. The aggregator 859 # cannot lawfully do anything more than it already did when the reference 860 # database was generated, so the auditor's view must be identical. 861 function test_1() { 862 863 echo "===========1: normal run===========" 864 run_audit 865 check_auditor_running 866 867 echo "Checking output" 868 check_all_clean 869 check_kyc_state 870 871 # Database was unmodified, no need to undo 872 echo "OK" 873 } 874 875 876 # A KYCAUTH credit that the bank does not agree is a KYCAUTH credit. 877 function test_2() { 878 879 echo "===========2: KYCAUTH credit reclassified by the bank===========" 880 # Modifying the bank's books needs libeufin to be restarted, it does 881 # not re-read them. 882 pre_audit 883 stop_libeufin 884 echo -n "Modifying database: " 885 # libeufin records for every credit to the exchange whether it was a 886 # reserve top-up or an account authorisation. Turn one of the two 887 # KYCAUTH credits into a reserve credit: the KYCAUTH total the auditor 888 # tracks must drop by that amount, and because the 32 bytes of KYCAUTH 889 # metadata are not a reserve public key the exchange knows, the credit 890 # now also looks like an incoming transfer the exchange never booked. 891 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')" \ 892 | psql -Aqt "$DB" 893 echo "DONE" 894 launch_libeufin 895 await_bank 896 897 audit_only 898 check_auditor_running 899 900 echo -n "Checking that the KYCAUTH total dropped... " 901 check_balance \ 902 "total_kycauth_in" \ 903 "TESTKUDOS:0.1" \ 904 "Wrong total for KYCAUTH wire transfers" 905 # Only the bank's books were touched, so the exchange still claims the 906 # KYCAUTH revenue for a transfer the bank no longer reports as one. 907 echo -n "Checking that the exchange still claims the revenue... " 908 check_balance \ 909 "total_kycauth_revenue" \ 910 "$KYCAUTH_IN" \ 911 "Wrong KYCAUTH revenue" 912 echo -n "Checking that the unbooked credit was flagged... " 913 check_report_any \ 914 "reserve-in-inconsistencies" \ 915 "diagnostic" "unknown to exchange" 916 echo -n "Checking that it was counted as an excess credit... " 917 check_balance \ 918 "total_bad_amount_in_plus" \ 919 "TESTKUDOS:0.1" \ 920 "Wrong total_bad_amount_in_plus" 921 922 # ... and the other side of the same disagreement: the exchange has a 923 # kycauths_in row for a transfer the bank now says was something else, 924 # so the auditor must not take the exchange's word for it. 925 echo -n "Checking that the unconfirmed KYCAUTH was flagged... " 926 check_report_any \ 927 "kycauth-in-inconsistencies" \ 928 "diagnostic" "KYCAUTH wire transfer claimed by exchange not found" 929 echo -n "Checking the amount the exchange claims... " 930 check_report_any \ 931 "kycauth-in-inconsistencies" \ 932 "amount_exchange_expected" "TESTKUDOS:0.1" 933 echo -n "Checking that it was counted as a missing credit... " 934 check_balance \ 935 "total_bad_amount_in_minus" \ 936 "TESTKUDOS:0.1" \ 937 "Wrong total_bad_amount_in_minus" 938 939 post_audit 940 full_reload 941 cleanup 942 } 943 944 945 # The rest of the tests inject the same kinds of faults test-auditor.sh 946 # injects, to show that KYC does not stop the auditor from finding them. 947 948 949 # Change the amount of an incoming wire transfer as recorded by the 950 # exchange. 951 function test_3() { 952 953 echo "===========3: reserves_in inconsistency===========" 954 echo -n "Modifying database: " 955 echo "UPDATE exchange.reserves_in SET credit.val=5 WHERE reserve_in_serial_id=1" \ 956 | psql -Aqt "$DB" 957 echo "DONE" 958 959 run_audit 960 check_auditor_running 961 962 echo -n "Testing inconsistency detection... " 963 check_report \ 964 "reserve-in-inconsistencies" \ 965 "row_id" 1 966 echo -n "Testing amount wired... " 967 check_report \ 968 "reserve-in-inconsistencies" \ 969 "amount_wired" "TESTKUDOS:10" 970 echo -n "Testing amount expected... " 971 check_report \ 972 "reserve-in-inconsistencies" \ 973 "amount_exchange_expected" "TESTKUDOS:5" 974 echo -n "Checking wire credit balance plus... " 975 check_balance \ 976 "total_bad_amount_in_plus" \ 977 "TESTKUDOS:5" \ 978 "Expected total_bad_amount_in_plus wrong" 979 980 full_reload 981 cleanup 982 } 983 984 985 # Test where denom_sig in the known_coins table is wrong (=> bad signature) 986 function test_4() { 987 988 echo "===========4: known_coins signature wrong===========" 989 OLD_ROW=$(echo "SELECT known_coin_id FROM exchange.known_coins LIMIT 1;" | psql "$DB" -Aqt) 990 COIN_PUB=$(echo "SELECT coin_pub FROM exchange.known_coins WHERE known_coin_id=$OLD_ROW;" | psql "$DB" -Aqt) 991 # shellcheck disable=SC2028 992 echo "UPDATE exchange.known_coins SET denom_sig='\x0000000100000000287369672d76616c200a2028727361200a2020287320233542383731423743393036444643303442424430453039353246413642464132463537303139374131313437353746324632323332394644443146324643333445393939413336363430334233413133324444464239413833353833464536354442374335434445304441453035374438363336434541423834463843323843344446304144363030343430413038353435363039373833434431333239393736423642433437313041324632414132414435413833303432434346314139464635394244434346374436323238344143354544364131373739463430353032323241373838423837363535453434423145443831364244353638303232413123290a2020290a20290b' WHERE coin_pub='$COIN_PUB'" \ 993 | psql -Aqt "$DB" 994 995 run_audit 996 check_auditor_running 997 998 echo -n "Checking bad-signature-loss detected... " 999 check_report_neg \ 1000 "bad-sig-losses" \ 1001 "loss" "TESTKUDOS:0" 1002 echo -n "Checking bad-signature-loss balance update... " 1003 check_not_balance \ 1004 "coin_irregular_loss" \ 1005 "TESTKUDOS:0" \ 1006 "Wrong total bad sig loss" 1007 1008 full_reload 1009 cleanup 1010 } 1011 1012 1013 # Test where the amount the bank actually wired differs from what the 1014 # exchange claims. 1015 function test_5() { 1016 1017 echo "===========5: incorrect wire_out amount===========" 1018 pre_audit 1019 stop_libeufin 1020 echo -n "Modifying database: " 1021 echo "UPDATE libeufin_bank.bank_account_transactions SET amount=(50,0) WHERE debtor_name='Exchange Company';" \ 1022 | psql -Aqt "$DB" 1023 echo "DONE" 1024 launch_libeufin 1025 await_bank 1026 1027 audit_only 1028 check_auditor_running 1029 1030 echo -n "Testing wire-out-inconsistency expected... " 1031 check_report \ 1032 "wire-out-inconsistencies" \ 1033 "expected" \ 1034 "$CLEARED_WIRED" 1035 echo -n "Testing wire-out-inconsistency claimed... " 1036 check_report \ 1037 "wire-out-inconsistencies" \ 1038 "claimed" \ 1039 "TESTKUDOS:50" 1040 echo -n "Testing bad_amount_out_plus balance reporting... " 1041 check_not_balance \ 1042 "total_bad_amount_out_plus" \ 1043 "TESTKUDOS:0" \ 1044 "reported total_bad_amount_out_plus wrong" 1045 1046 post_audit 1047 full_reload 1048 cleanup 1049 } 1050 1051 1052 # Test where the exchange claims to have received more than the bank sent, 1053 # which also throws its reserve balance summary off. 1054 function test_6() { 1055 1056 echo "===========6: reserve balance summary wrong===========" 1057 echo -n "Modifying database: " 1058 echo "UPDATE exchange.reserves_in SET credit.val=15 WHERE reserve_in_serial_id=1" \ 1059 | psql -Aqt "$DB" 1060 echo "DONE" 1061 1062 run_audit 1063 check_auditor_running 1064 1065 echo -n "Checking reserve balance summary inconsistency detected... " 1066 check_some_report \ 1067 "reserve-balance-summary-wrong-inconsistencies" 1068 echo -n "Testing amount the exchange expected... " 1069 check_report \ 1070 "reserve-in-inconsistencies" \ 1071 "amount_exchange_expected" "TESTKUDOS:15" 1072 echo -n "Checking wire credit balance minus... " 1073 check_balance \ 1074 "total_bad_amount_in_minus" \ 1075 "TESTKUDOS:5" \ 1076 "Wrong total_bad_amount_in_minus" 1077 1078 full_reload 1079 cleanup 1080 } 1081 1082 1083 # Test where the exchange forgot a deposit that a merchant can prove. 1084 function test_7() { 1085 1086 echo "===========7: deposit wire target malformed===========" 1087 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) 1088 # shellcheck disable=SC2028 1089 echo "INSERT INTO exchange.wire_targets (payto_uri, wire_target_h_payto) VALUES ('payto://x-taler-bank/localhost/testuser-xxlargtp', '\x1e8f31936b3cee8f8afd3aac9e38b5db42d45b721ffc4eb1e5b9ddaf1565660b');" \ 1090 | psql "$DB" -Aqt 1091 # shellcheck disable=SC2028 1092 echo "UPDATE exchange.batch_deposits SET wire_target_h_payto='\x1e8f31936b3cee8f8afd3aac9e38b5db42d45b721ffc4eb1e5b9ddaf1565660b' WHERE batch_deposit_serial_id=${SERIAL};" \ 1093 | psql -Aqt "$DB" 1094 1095 run_audit 1096 check_auditor_running 1097 1098 echo -n "Checking correct operation of loss reported... " 1099 check_report \ 1100 "bad-sig-losses" \ 1101 "operation" "deposit" 1102 echo -n "Checking correct problem row ID reported... " 1103 check_report \ 1104 "bad-sig-losses" \ 1105 "problem_row_id" "$SERIAL" 1106 echo -n "Checking irregular coin loss... " 1107 check_not_balance \ 1108 "coin_irregular_loss" \ 1109 "TESTKUDOS:0" \ 1110 "Wrong total irregular coin loss" 1111 1112 full_reload 1113 cleanup 1114 } 1115 1116 1117 # The reverse of test 2: the bank reports a KYCAUTH transfer that the 1118 # exchange has no record of. An exchange that could make such a row 1119 # disappear could also make the account authorisation it justified 1120 # disappear, so this must be flagged even though the money involved is 1121 # small. 1122 function test_8() { 1123 1124 echo "===========8: KYCAUTH credit missing from the exchange===========" 1125 echo -n "Modifying database: " 1126 echo "DELETE FROM exchange.kycauths_in WHERE kycauth_in_serial_id=1" \ 1127 | psql -Aqt "$DB" 1128 echo "DONE" 1129 1130 run_audit 1131 check_auditor_running 1132 1133 echo -n "Checking that the bank's view is unchanged... " 1134 check_balance \ 1135 "total_kycauth_in" \ 1136 "$KYCAUTH_IN" \ 1137 "Wrong total for KYCAUTH wire transfers" 1138 echo -n "Checking that the exchange claims less revenue... " 1139 check_balance \ 1140 "total_kycauth_revenue" \ 1141 "TESTKUDOS:0.1" \ 1142 "Wrong KYCAUTH revenue" 1143 echo -n "Checking that the unbooked KYCAUTH was flagged... " 1144 check_report_any \ 1145 "kycauth-in-inconsistencies" \ 1146 "diagnostic" "unknown to exchange" 1147 echo -n "Checking the amount the bank reports... " 1148 check_report_any \ 1149 "kycauth-in-inconsistencies" \ 1150 "amount_wired" "TESTKUDOS:0.1" 1151 echo -n "Checking that it was counted as an excess credit... " 1152 check_balance \ 1153 "total_bad_amount_in_plus" \ 1154 "TESTKUDOS:0.1" \ 1155 "Wrong total_bad_amount_in_plus" 1156 1157 full_reload 1158 cleanup 1159 } 1160 1161 1162 # The exchange booked a KYCAUTH transfer, but for a different amount than 1163 # the bank actually moved. 1164 function test_9() { 1165 1166 echo "===========9: KYCAUTH amount inconsistency===========" 1167 echo -n "Modifying database: " 1168 echo "UPDATE exchange.kycauths_in SET credit.val=5 WHERE kycauth_in_serial_id=1" \ 1169 | psql -Aqt "$DB" 1170 echo "DONE" 1171 1172 run_audit 1173 check_auditor_running 1174 1175 echo -n "Checking that the mismatch was flagged... " 1176 check_report_any \ 1177 "kycauth-in-inconsistencies" \ 1178 "diagnostic" "wire amount does not match" 1179 echo -n "Checking the amount the exchange claims... " 1180 check_report_any \ 1181 "kycauth-in-inconsistencies" \ 1182 "amount_exchange_expected" "TESTKUDOS:5.1" 1183 echo -n "Checking the amount the bank reports... " 1184 check_report_any \ 1185 "kycauth-in-inconsistencies" \ 1186 "amount_wired" "TESTKUDOS:0.1" 1187 echo -n "Checking that the shortfall was counted... " 1188 check_balance \ 1189 "total_bad_amount_in_minus" \ 1190 "TESTKUDOS:5" \ 1191 "Wrong total_bad_amount_in_minus" 1192 echo -n "Checking that the inflated revenue reached the balance sheet... " 1193 check_balance \ 1194 "total_kycauth_revenue" \ 1195 "TESTKUDOS:5.2" \ 1196 "Wrong KYCAUTH revenue" 1197 1198 full_reload 1199 cleanup 1200 } 1201 1202 1203 # The auditor may well see a KYCAUTH transfer at the bank before the 1204 # exchange has booked it. It reports that, and must take the report back 1205 # -- together with the excess credit it booked alongside -- once the 1206 # exchange catches up. Without the retraction of the balance, a purely 1207 # transient disagreement would inflate total_bad_amount_in_plus forever. 1208 function test_10() { 1209 1210 echo "===========10: KYCAUTH finding retracted when the exchange catches up===========" 1211 echo -n "Modifying database: " 1212 # Hide the exchange's record of the *last* KYCAUTH transfer, keeping a 1213 # copy so it can be restored byte for byte. It has to be the last one: 1214 # kycauth_in_cb() resumes from a progress point, so a row reinstated 1215 # below that point would never be looked at again. 1216 echo "CREATE TABLE stashed_kycauth AS 1217 SELECT * FROM exchange.kycauths_in 1218 WHERE kycauth_in_serial_id 1219 = (SELECT MAX(kycauth_in_serial_id) FROM exchange.kycauths_in); 1220 DELETE FROM exchange.kycauths_in 1221 WHERE kycauth_in_serial_id 1222 = (SELECT MAX(kycauth_in_serial_id) FROM exchange.kycauths_in);" \ 1223 | psql -Aqt "$DB" 1224 echo "DONE" 1225 1226 pre_audit 1227 audit_only 1228 check_auditor_running 1229 1230 echo -n "Checking that the unbooked transfer was reported... " 1231 check_report_any \ 1232 "kycauth-in-inconsistencies" \ 1233 "diagnostic" "unknown to exchange" 1234 echo -n "Checking that it was counted as an excess credit... " 1235 check_balance \ 1236 "total_bad_amount_in_plus" \ 1237 "TESTKUDOS:0.1" \ 1238 "Wrong total_bad_amount_in_plus" 1239 1240 # The exchange catches up. No progress points are touched: the credit 1241 # audit is simply run once more, exactly as a resident helper would on 1242 # its next wake-up. 1243 echo -n "Letting the exchange catch up: " 1244 echo "INSERT INTO exchange.kycauths_in SELECT * FROM stashed_kycauth; 1245 DROP TABLE stashed_kycauth;" \ 1246 | psql -Aqt "$DB" 1247 echo "DONE" 1248 1249 echo -n "Re-running the credit audit ..." 1250 $VALGRIND taler-helper-auditor-wire-credit \ 1251 -i \ 1252 -L DEBUG \ 1253 -c "$CONF" \ 1254 -t \ 1255 > "${MY_TMP_DIR}/test-audit-wire-credit-retract.out" \ 1256 2> "${MY_TMP_DIR}/test-audit-wire-credit-retract.err" \ 1257 || exit_fail "wire credit re-audit failed (see ${MY_TMP_DIR}/test-audit-wire-credit-retract.*)" 1258 echo " DONE" 1259 1260 echo -n "Checking that the report was retracted... " 1261 check_no_report "kycauth-in-inconsistencies" 1262 echo -n "Checking that the excess credit was retracted too... " 1263 check_balance \ 1264 "total_bad_amount_in_plus" \ 1265 "TESTKUDOS:0" \ 1266 "Retracted finding left total_bad_amount_in_plus inflated" 1267 1268 post_audit 1269 full_reload 1270 cleanup 1271 } 1272 1273 1274 # An AML staff appointment that the exchange's offline master key did not 1275 # sign. Without this check an exchange could appoint an AML officer, or 1276 # quietly widen a read-only officer to read-write, with the offline key never 1277 # having been involved. 1278 function test_11() { 1279 1280 echo "===========11: AML staff appointment with a bad master signature===========" 1281 echo -n "Modifying database: " 1282 # shellcheck disable=SC2028 1283 echo "INSERT INTO exchange.aml_staff 1284 (decider_pub, master_sig, decider_name, is_active, read_only, last_change) 1285 VALUES 1286 ('\x1111111111111111111111111111111111111111111111111111111111111111', 1287 '\x22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222', 1288 'Unappointed Officer', TRUE, FALSE, 1000000000000000);" \ 1289 | psql -Aqt "$DB" 1290 echo "DONE" 1291 1292 run_audit 1293 check_auditor_running 1294 1295 echo -n "Checking that the appointment was flagged... " 1296 check_report_any \ 1297 "row-inconsistencies" \ 1298 "diagnostic" "invalid master key signature on staff status" 1299 echo -n "Checking that it was blamed on the right table... " 1300 check_report_any \ 1301 "row-inconsistencies" \ 1302 "row_table" "aml_staff" 1303 1304 # aml_staff is append-only, so the helper can and must resume from where 1305 # it stopped instead of re-reading the whole table on every wake-up. 1306 # Both halves have to hold: the cursor has to be stored at all, and the 1307 # second run has to pick it up. 1308 echo -n "Checking that the aml_staff cursor was stored... " 1309 AML_PP=$(psql -Aqt "$DB" \ 1310 -c "SELECT progress_offset FROM auditor.auditor_progress \ 1311 WHERE progress_key='aml_staff_uuid';") 1312 if [ -z "$AML_PP" ] || [ "$AML_PP" = "0" ] 1313 then 1314 exit_fail "aml_staff_uuid progress point not recorded (got '$AML_PP')" 1315 fi 1316 echo "PASS ($AML_PP)" 1317 echo -n "Checking that the second run resumed from it... " 1318 if ! grep -q "Resuming AML audit at $AML_PP/" \ 1319 "${MY_TMP_DIR}/test-audit-aml-inc.err" 1320 then 1321 exit_fail "incremental AML audit did not resume at $AML_PP" 1322 fi 1323 echo "PASS" 1324 1325 full_reload 1326 cleanup 1327 } 1328 1329 1330 # An AML decision that no valid officer signature backs, by somebody the 1331 # exchange never appointed at all. Both halves have to be reported: the 1332 # signature is what makes the decision accountable, and the appointment is 1333 # what makes the signature mean anything. 1334 function test_12() { 1335 1336 echo "===========12: AML decision by an unappointed officer===========" 1337 echo -n "Modifying database: " 1338 # shellcheck disable=SC2028 1339 echo "INSERT INTO exchange.aml_history 1340 (h_payto, justification, decider_pub, decider_sig, outcome_serial_id) 1341 SELECT h_payto, 1342 'injected by test 12', 1343 '\x3333333333333333333333333333333333333333333333333333333333333333', 1344 '\x44444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444', 1345 outcome_serial_id 1346 FROM exchange.legitimization_outcomes 1347 ORDER BY outcome_serial_id 1348 LIMIT 1;" \ 1349 | psql -Aqt "$DB" 1350 echo "DONE" 1351 1352 run_audit 1353 check_auditor_running 1354 1355 echo -n "Checking that the bad signature was flagged... " 1356 check_report_any \ 1357 "row-inconsistencies" \ 1358 "diagnostic" "invalid officer signature on decision" 1359 echo -n "Checking that the missing appointment was flagged... " 1360 check_report_any \ 1361 "row-inconsistencies" \ 1362 "diagnostic" "officer was not appointed when the decision was made" 1363 echo -n "Checking that it was blamed on the right table... " 1364 check_report_any \ 1365 "row-inconsistencies" \ 1366 "row_table" "aml_history" 1367 1368 full_reload 1369 cleanup 1370 } 1371 1372 1373 # `legitimization_outcomes' is the table that says which KYC rules apply to 1374 # an account, so a row nothing produced is how an exchange would quietly 1375 # exempt a customer from KYC. The account here is fresh: it has no AML 1376 # decision pointing at the outcome, no legitimization process, and no earlier 1377 # outcome that expired, which is all three of the ways the exchange's own 1378 # code creates one. 1379 function test_13() { 1380 1381 echo "===========13: legitimization outcome without a justification===========" 1382 echo -n "Modifying database: " 1383 # shellcheck disable=SC2028 1384 echo "INSERT INTO exchange.kyc_targets 1385 (h_normalized_payto, is_wallet) 1386 VALUES 1387 ('\x5555555555555555555555555555555555555555555555555555555555555555', 1388 FALSE); 1389 INSERT INTO exchange.legitimization_outcomes 1390 (h_payto, decision_time, expiration_time, to_investigate) 1391 VALUES 1392 ('\x5555555555555555555555555555555555555555555555555555555555555555', 1393 1000000000000000, 2000000000000000, FALSE);" \ 1394 | psql -Aqt "$DB" 1395 echo "DONE" 1396 1397 run_audit 1398 check_auditor_running 1399 1400 echo -n "Checking that the unjustified outcome was flagged... " 1401 check_report_any \ 1402 "row-inconsistencies" \ 1403 "diagnostic" \ 1404 "KYC rules in force without an AML decision, a legitimization process or an expired predecessor" 1405 echo -n "Checking that it was blamed on the right table... " 1406 check_report_any \ 1407 "row-inconsistencies" \ 1408 "row_table" "legitimization_outcomes" 1409 1410 # Only the injected row is bad. The outcomes the reference database 1411 # earned honestly are each accounted for by a legitimization process or 1412 # an AML decision, and reporting those too would make the check useless. 1413 echo -n "Checking that the honest outcomes were left alone... " 1414 BAD=$(jq -r '[.records[] 1415 | select(.row_table == "legitimization_outcomes")] 1416 | length' \ 1417 < "${MY_TMP_DIR}/row-inconsistencies.json") 1418 if [ "$BAD" != "1" ] 1419 then 1420 exit_fail "wanted exactly 1 bad legitimization outcome, got $BAD" 1421 fi 1422 echo "PASS" 1423 1424 full_reload 1425 cleanup 1426 } 1427 1428 1429 # Releasing the funds must retract the alert, not just stop adding to it. 1430 # The exchange announces the release by writing the `wire_out' row, which is 1431 # the only thing that distinguishes "paid" from "decided what to pay"; so 1432 # that is what this test injects. Nothing else changes: the deposits, the 1433 # aggregation_tracking rows and the open KYC measure all stay exactly as they 1434 # were, which is the point -- the hold must end because the money moved, and 1435 # for no other reason. 1436 function test_14() { 1437 1438 echo "===========14: releasing a withheld payout===========" 1439 echo -n "Modifying database: " 1440 # Take the transfer straight from the transient the exchange parked it in, 1441 # so that the test does not have to know the randomly drawn wtid. 1442 echo "INSERT INTO exchange.wire_out 1443 (execution_date, wtid_raw, wire_target_h_payto, 1444 exchange_account_section, amount) 1445 SELECT 1785869200000000 1446 ,t.wtid_raw 1447 ,t.wire_target_h_payto 1448 ,t.exchange_account_section 1449 ,t.amount 1450 FROM exchange.aggregation_transient t;" \ 1451 | psql -Aqt "$DB" 1452 echo "DONE" 1453 1454 run_audit 1455 check_auditor_running 1456 1457 echo -n "Checking that the hold was retracted... " 1458 check_no_report "aml-holds" 1459 echo -n "Checking that the AML total went back to zero... " 1460 check_balance \ 1461 "total_aml_hold" \ 1462 "TESTKUDOS:0" \ 1463 "Released funds are still counted as withheld" 1464 echo -n "Checking that they did not turn into a lag instead... " 1465 check_balance \ 1466 "total_transfer_lag" \ 1467 "TESTKUDOS:0" \ 1468 "Released funds were reclassified rather than dropped" 1469 echo -n "Checking that they were not parked as a small aggregate... " 1470 check_balance \ 1471 "total_small_aggregate" \ 1472 "TESTKUDOS:0" \ 1473 "Released funds were reclassified rather than dropped" 1474 1475 full_reload 1476 cleanup 1477 } 1478 1479 1480 # What the exchange *claims* decides which balance a hold lands in; whether the 1481 # claim holds up is a separate question, and this is the test that they are 1482 # separate. Here the measure the exchange named is carried through to a 1483 # decision while the exchange goes on withholding the money and goes on saying 1484 # it is withholding it for that measure. 1485 # 1486 # The money must therefore stay in `total_aml_hold' -- the auditor does not get 1487 # to overrule the exchange about its own reasons -- and the auditor must report 1488 # a row_inconsistency against `aggregation_deferrals', because the one part of 1489 # the claim it can check has stopped being true. Deciding what to make of that 1490 # is a job for the humans reading the finding. 1491 function test_15() { 1492 1493 echo "===========15: a hold whose KYC measure was satisfied===========" 1494 echo -n "Modifying database: " 1495 # Carry the measure that is blocking the payout through to a decision, the 1496 # way the exchange would have if the merchant had filled in the form: a 1497 # process referring to the measure, and an outcome for the account decided 1498 # no earlier than that process started. `decision_time' is rounded up to a 1499 # whole second because the exchange stores it rounded and the AML helper 1500 # reads it as a timestamp. 1501 echo "INSERT INTO exchange.legitimization_processes 1502 (h_payto, start_time, expiration_time, provider_name, 1503 legitimization_measure_serial_id, measure_index) 1504 SELECT kt.h_normalized_payto 1505 ,lm.start_time 1506 ,0 1507 ,'full_name_and_birthdate' 1508 ,lm.legitimization_measure_serial_id 1509 ,0 1510 FROM exchange.aggregation_transient t 1511 JOIN exchange.wire_targets wt 1512 ON (wt.wire_target_h_payto=t.wire_target_h_payto) 1513 JOIN exchange.kyc_targets kt 1514 ON (kt.h_normalized_payto=wt.h_normalized_payto) 1515 JOIN exchange.legitimization_measures lm 1516 USING (access_token); 1517 INSERT INTO exchange.legitimization_outcomes 1518 (h_payto, decision_time, expiration_time, to_investigate) 1519 SELECT kt.h_normalized_payto 1520 ,(lm.start_time/1000000+1)*1000000 1521 ,2000000000000000 1522 ,FALSE 1523 FROM exchange.aggregation_transient t 1524 JOIN exchange.wire_targets wt 1525 ON (wt.wire_target_h_payto=t.wire_target_h_payto) 1526 JOIN exchange.kyc_targets kt 1527 ON (kt.h_normalized_payto=wt.h_normalized_payto) 1528 JOIN exchange.legitimization_measures lm 1529 USING (access_token);" \ 1530 | psql -Aqt "$DB" 1531 echo "DONE" 1532 1533 run_audit 1534 check_auditor_running 1535 1536 echo -n "Checking that the exchange is still taken at its word... " 1537 check_balance \ 1538 "total_aml_hold" \ 1539 "$WITHHELD" \ 1540 "Auditor reclassified a hold the exchange still attributes to KYC" 1541 echo -n "Checking that it did not become an unexplained lag... " 1542 check_balance \ 1543 "total_transfer_lag" \ 1544 "TESTKUDOS:0" \ 1545 "Auditor overruled the exchange's stated reason" 1546 echo -n "Checking that the hold still names the claimed reason... " 1547 check_report "aml-holds" "deferral_reason" "KYC" 1548 echo -n "Checking that it still names the blocked account... " 1549 check_report_any \ 1550 "aml-holds" \ 1551 "account" \ 1552 "$BLOCKED_ACCOUNT" 1553 echo -n "Checking that the stale claim was reported... " 1554 check_report_any \ 1555 "row-inconsistencies" \ 1556 "row_table" "aggregation_deferrals" 1557 1558 full_reload 1559 cleanup 1560 } 1561 1562 1563 # The other thing an exchange can lawfully be sitting on: an aggregate that 1564 # would not survive its own wire fee. That is not an irregularity and must not 1565 # read as one, so it gets a balance of its own rather than being lumped in with 1566 # payouts nobody has explained. 1567 # 1568 # The reference database has no such aggregate -- every payout in it is worth 1569 # making -- so the test restates the reason on the one deferral it does have. 1570 # Reason 1 is TALER_EXCHANGEDB_DR_AMOUNT_TOO_SMALL, 2 is ..._DR_KYC. Note that 1571 # this leaves the exchange's `aggregation_transient' still naming a 1572 # legitimization requirement, so the -i cross-check must notice that the two 1573 # no longer agree. 1574 function test_16() { 1575 1576 echo "===========16: an aggregate too small to pay out===========" 1577 echo -n "Modifying database: " 1578 # Only the transfer that is still outstanding; the cleared merchant's 1579 # deferral is on file too, but it was overtaken by its wire_out row. 1580 echo "UPDATE exchange.aggregation_deferrals d 1581 SET deferral_reason=1 1582 ,legitimization_requirement_serial_id=0 1583 WHERE NOT EXISTS ( 1584 SELECT 1 FROM exchange.wire_out w 1585 WHERE w.wtid_raw=d.wtid_raw);" \ 1586 | psql -Aqt "$DB" 1587 echo "DONE" 1588 1589 run_audit 1590 check_auditor_running 1591 1592 echo -n "Checking that the funds are booked as a small aggregate... " 1593 check_balance \ 1594 "total_small_aggregate" \ 1595 "$WITHHELD" \ 1596 "Aggregate below the wire fee was not booked as such" 1597 echo -n "Checking that they are no longer an AML hold... " 1598 check_balance \ 1599 "total_aml_hold" \ 1600 "TESTKUDOS:0" \ 1601 "Aggregate below the wire fee still counted as withheld for KYC" 1602 echo -n "Checking that they are not an unexplained lag either... " 1603 check_balance \ 1604 "total_transfer_lag" \ 1605 "TESTKUDOS:0" \ 1606 "Aggregate below the wire fee read as an unexplained delay" 1607 echo -n "Checking that the hold names the new reason... " 1608 check_report "aml-holds" "deferral_reason" "AMOUNT_TOO_SMALL" 1609 echo -n "Checking that the contradiction with the transient was reported... " 1610 check_report_any \ 1611 "row-inconsistencies" \ 1612 "row_table" "aggregation_transient" 1613 1614 full_reload 1615 cleanup 1616 } 1617 1618 1619 # *************** Main test loop starts here ************** 1620 1621 1622 # Run all the tests against the database given in $1. 1623 # Sets $fail to 0 on success, non-zero on failure. 1624 function check_with_database() 1625 { 1626 BASEDB="$1" 1627 CONF="$1.conf" 1628 export CONF 1629 echo "Running test suite with database $BASEDB using configuration $CONF" 1630 MASTER_PRIV_FILE="${BASEDB}.mpriv" 1631 taler-exchange-config \ 1632 -f \ 1633 -c "${CONF}" \ 1634 -s exchange-offline \ 1635 -o MASTER_PRIV_FILE \ 1636 -V "${MASTER_PRIV_FILE}" 1637 1638 # Load database 1639 full_reload 1640 1641 # Run test suite 1642 fail=0 1643 for i in $TESTS 1644 do 1645 "test_$i" 1646 if test 0 != $fail 1647 then 1648 break 1649 fi 1650 done 1651 echo "Cleanup (disabled, leaving database $DB behind)" 1652 # dropdb $DB 1653 } 1654 1655 # When the script is not run as root, setup a temporary directory for the 1656 # postgres database. 1657 # Sets PGHOST accordingly to the freshly created socket. 1658 function perform_initdb() { 1659 # Available directly in path? 1660 INITDB_BIN=$(command -v initdb) || true 1661 if [[ -n "$INITDB_BIN" ]]; then 1662 echo " FOUND (in path) at $INITDB_BIN" 1663 else 1664 HAVE_INITDB=$(find /usr -name "initdb" 2> /dev/null \ 1665 | head -1 2> /dev/null \ 1666 | grep postgres) \ 1667 || exit_skip " MISSING" 1668 echo " FOUND at $(dirname "$HAVE_INITDB")" 1669 INITDB_BIN=$(echo "$HAVE_INITDB" | grep bin/initdb | grep postgres | sort -n | tail -n1) 1670 fi 1671 POSTGRES_PATH=$(dirname "$INITDB_BIN") 1672 1673 TMPDIR="$MY_TMP_DIR/postgres" 1674 mkdir -p "$TMPDIR" 1675 echo -n "Setting up Postgres DB at $TMPDIR ..." 1676 $INITDB_BIN \ 1677 --no-sync \ 1678 --auth=trust \ 1679 -D "${TMPDIR}" \ 1680 > "${MY_TMP_DIR}/postgres-dbinit.log" \ 1681 2> "${MY_TMP_DIR}/postgres-dbinit.err" \ 1682 || { 1683 echo "FAILED!" 1684 echo "Last entries in ${MY_TMP_DIR}/postgres-dbinit.err:" 1685 tail "${MY_TMP_DIR}/postgres-dbinit.err" 1686 exit 1 1687 } 1688 echo "DONE" 1689 1690 # Once we move to PG16, we can use: 1691 # --set listen_addresses='' \ 1692 # --set fsync=off \ 1693 # --set max_wal_senders=0 \ 1694 # --set synchronous_commit=off \ 1695 # --set wal_level=minimal \ 1696 # --set unix_socket_directories="${TMPDIR}/sockets" \ 1697 1698 1699 SOCKETDIR="${TMPDIR}/sockets" 1700 mkdir "${SOCKETDIR}" 1701 1702 echo -n "Launching Postgres service" 1703 1704 cat - >> "$TMPDIR/postgresql.conf" <<EOF 1705 unix_socket_directories='${TMPDIR}/sockets' 1706 fsync=off 1707 max_wal_senders=0 1708 synchronous_commit=off 1709 wal_level=minimal 1710 listen_addresses='' 1711 EOF 1712 1713 grep -v host \ 1714 < "$TMPDIR/pg_hba.conf" \ 1715 > "$TMPDIR/pg_hba.conf.new" 1716 mv "$TMPDIR/pg_hba.conf.new" "$TMPDIR/pg_hba.conf" 1717 "${POSTGRES_PATH}/pg_ctl" \ 1718 -D "$TMPDIR" \ 1719 -l "${MY_TMP_DIR}/postgres.log" \ 1720 start \ 1721 > "${MY_TMP_DIR}/postgres-start.log" \ 1722 2> "${MY_TMP_DIR}/postgres-start.err" 1723 echo " DONE" 1724 PGHOST="$TMPDIR/sockets" 1725 export PGHOST 1726 } 1727 1728 1729 # *************** Main logic starts here ************** 1730 1731 # ####### Setup globals ###### 1732 # Postgres database to use (must match configuration file) 1733 export DB="auditor-basedb" 1734 1735 # test required commands exist 1736 echo "Testing for jq" 1737 jq -h > /dev/null || exit_skip "jq required" 1738 echo "Testing for taler-merchant-config" 1739 taler-merchant-config -h > /dev/null || exit_skip "taler-merchant-config required" 1740 echo "Testing for taler-merchant-httpd" 1741 taler-merchant-httpd -h > /dev/null || exit_skip "taler-merchant-httpd required" 1742 echo "Testing for faketime" 1743 faketime -h > /dev/null || exit_skip "faketime required" 1744 # NOTE: really check for all three libeufin commands? 1745 echo "Testing for libeufin" 1746 libeufin-bank --help >/dev/null 2> /dev/null </dev/null || exit_skip "libeufin required" 1747 echo "Testing for taler-wallet-cli" 1748 taler-wallet-cli -h >/dev/null </dev/null 2>/dev/null || exit_skip "taler-wallet-cli required" 1749 1750 1751 echo -n "Testing for Postgres" 1752 1753 MY_TMP_DIR=$(mktemp -d /tmp/taler-auditor-basedbXXXXXX) 1754 echo "Using $MY_TMP_DIR for logging and temporary data" 1755 1756 # If run as root, simply use the running postgres instance. 1757 # Otherwise create a temporary storage space for postgres. 1758 [ $(id -u) == 0 ] || perform_initdb 1759 1760 MYDIR="${MY_TMP_DIR}/basedb" 1761 mkdir -p "${MYDIR}" 1762 1763 if [ -z ${REUSE_BASEDB_DIR+x} ] 1764 then 1765 echo "Generating fresh database at $MYDIR" 1766 1767 if faketime -f '-1 d' ./generate-kyc-basedb.sh -d "$MYDIR/$DB" 1768 then 1769 echo -n "Reset 'auditor-basedb' database at ${PGHOST:-} ..." 1770 dropdb --if-exists "auditor-basedb" > /dev/null 2> /dev/null || true 1771 createdb "auditor-basedb" || exit_skip "Could not create database '$BASEDB' at ${PGHOST:-}" 1772 echo " DONE" 1773 else 1774 echo "Generation failed" 1775 exit 1 1776 fi 1777 echo "To reuse this database in the future, use:" 1778 echo "export REUSE_BASEDB_DIR=$MY_TMP_DIR" 1779 else 1780 echo "Reusing existing database from ${REUSE_BASEDB_DIR}" 1781 cp -r "${REUSE_BASEDB_DIR}/basedb"/* "${MYDIR}/" 1782 fi 1783 1784 check_with_database "$MYDIR/$DB" 1785 if [ "$fail" != "0" ] 1786 then 1787 exit "$fail" 1788 fi 1789 1790 if [ -z "${REUSE_BASEDB_DIR+x}" ] 1791 then 1792 echo "Run 'export REUSE_BASEDB_DIR=${MY_TMP_DIR}' to re-run tests against the same database" 1793 fi 1794 1795 exit 0