/*
  This file is part of TALER
  Copyright (C) 2026 Taler Systems SA

  TALER is free software; you can redistribute it and/or modify it under the
  terms of the GNU Affero Public License as published by the Free Software
  Foundation; either version 3, or (at your option) any later version.

  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  A PARTICULAR PURPOSE.  See the GNU Affero Public License for more details.

  You should have received a copy of the GNU Affero Public License along with
  TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
*/
/**
 * @file auditor/taler-helper-auditor-aml.c
 * @brief audits the AML decisions and the appointments of the AML staff who
 *        made them
 * @author Christian Grothoff
 *
 * Three checks, in one transaction and in this order, because each is judged
 * against the result of the one before it:
 *
 * 1) every row in `aml_staff` must carry a valid signature by the exchange's
 *    *offline* master key.  Without this an exchange could appoint an AML
 *    officer -- or quietly widen a read-only officer to read-write -- with
 *    the offline key never having been involved.
 *
 * 2) every AML decision in `aml_history` must carry a valid signature by an
 *    officer who, at the time of the decision, was appointed and had
 *    read-write access.
 *
 * 3) every row in `legitimization_outcomes` must have something that accounts
 *    for it.  That table holds the rules currently in force for an account,
 *    so it is what decides whether a customer's transaction is allowed; a row
 *    that nothing produced is precisely how an exchange would quietly exempt
 *    a customer from KYC.  See #check_outcome_cb() for what counts.
 *
 * None of the findings is a loss: neither an AML decision nor a rule change
 * moves money, so a bad signature or an unaccounted-for rule set costs the
 * exchange nothing directly.  What it costs is the point of having officers
 * sign at all, so the findings are reported as row inconsistencies rather
 * than as bad-sig losses.
 *
 * `aml_staff` is append-only -- a status change is a new row -- so the
 * exchange's own database does answer "was this officer allowed to decide at
 * that time?".  The auditor nevertheless keeps its own record of every status
 * change it has seen, in `auditor_aml_staff`, and answers the question from
 * that: append-only is a promise the exchange's code makes, not one the
 * database enforces against the exchange's operator, and an independent
 * record is the point of an auditor.
 *
 * That record is also what makes backdating detectable.  The master key is
 * the exchange operator's own, so a dishonest operator can sign an
 * appointment naming any `last_change` it likes, and a decision naming any
 * `decision_time`.  What it cannot do is change what the auditor already
 * saw: if the auditor recorded an officer as disabled before this round
 * began, a decision by that officer surfacing now is backdated, whatever
 * `decision_time` claims.  This does not stop backdating *within* the set of
 * rows processed in one round -- an auditor running promptly keeps that
 * window small, which is the whole defence.
 */
#include "platform.h"
#include <gnunet/gnunet_util_lib.h>
#include "auditordb_lib.h"
#include "exchangedb_lib.h"
#include "report-lib.h"
#include "taler/taler_dbevents.h"
#include <jansson.h>
#include <inttypes.h>
#include "auditor-database/event_listen.h"
#include "auditor-database/get_auditor_progress.h"
#include "auditor-database/insert_aml_staff.h"
#include "auditor-database/insert_auditor_progress.h"
#include "auditor-database/insert_row_inconsistency.h"
#include "auditor-database/iterate_aml_staff.h"
#include "exchange-database/iterate_aml_history_above_serial_id.h"
#include "exchange-database/iterate_aml_staff_above_serial_id.h"
#include "exchange-database/iterate_legitimization_outcomes_above_serial_id.h"


/**
 * Return value from main().
 */
static int global_ret;

/**
 * Row of `aml_history` up to which we have checked AML decisions.
 */
static TALER_ARL_DEF_PP (aml_history_serial_id);

/**
 * Row of `aml_staff` up to which we have checked status changes.
 */
static TALER_ARL_DEF_PP (aml_staff_uuid);

/**
 * Row of `legitimization_outcomes` up to which we have checked that the
 * rules in force for an account are accounted for.
 */
static TALER_ARL_DEF_PP (legitimization_outcome_serial_id);

/**
 * Run in test mode. Exit when idle instead of
 * going to sleep and waiting for more work.
 */
static int test_mode;

/**
 * Should we run checks that only work for exchange-internal audits?
 * Does nothing for this helper (present only for uniformity).
 */
static int internal_checks;

/**
 * Handle to the database event we wait on when running resident.
 */
static struct GNUNET_DB_EventHandler *eh;

/**
 * The auditors's configuration.
 */
static const struct GNUNET_CONFIGURATION_Handle *cfg;

/**
 * Map from the hash of an officer's public key to a `struct StaffMember`.
 */
static struct GNUNET_CONTAINER_MultiHashMap *staff_map;

/**
 * Status of the DB operations of our callbacks; they cannot return a
 * query status themselves.
 */
static enum GNUNET_DB_QueryStatus global_qs;


/**
 * One observed status of an AML staff member, in force from @e last_change
 * until the next status of the same member.
 */
struct StaffStatus
{
  /**
   * When this status took effect, as claimed by the exchange.
   */
  struct GNUNET_TIME_Timestamp last_change;

  /**
   * Was the member allowed to act?
   */
  bool is_active;

  /**
   * Was the member restricted to read-only access?
   */
  bool read_only;

  /**
   * Did the exchange's offline master key actually sign this status?
   * A status that fails this check confers no authority.
   */
  bool master_sig_valid;
};


/**
 * What we know about one AML staff member.
 */
struct StaffMember
{
  /**
   * Key under which this member is stored in #staff_map.  The map is
   * created with @a do_not_copy_keys, so it keeps this pointer: the key
   * has to live as long as the entry does.
   */
  struct GNUNET_HashCode key;

  /**
   * Observed statuses, oldest first.
   */
  struct StaffStatus *statuses;

  /**
   * Length of @e statuses.
   */
  unsigned int num_statuses;

  /**
   * Did we already know this member when the round began?
   */
  bool known_at_start;

  /**
   * Was the member enabled according to the most recent status we knew
   * when the round began?  Meaningless unless @e known_at_start.
   */
  bool active_at_start;

  /**
   * Did we observe the member being enabled during this round?
   */
  bool enabled_this_round;
};


/**
 * Compute the map key for @a decider_pub.
 *
 * @param decider_pub public key of the staff member
 * @param[out] key set to the key to use
 */
static void
staff_key (const struct TALER_AmlOfficerPublicKeyP *decider_pub,
           struct GNUNET_HashCode *key)
{
  GNUNET_CRYPTO_hash (decider_pub,
                      sizeof (*decider_pub),
                      key);
}


/**
 * Look up @a decider_pub in #staff_map, creating the entry if needed.
 *
 * @param decider_pub public key of the staff member
 * @return the entry, never NULL
 */
static struct StaffMember *
staff_member_get (const struct TALER_AmlOfficerPublicKeyP *decider_pub)
{
  struct GNUNET_HashCode key;
  struct StaffMember *sm;

  staff_key (decider_pub,
             &key);
  sm = GNUNET_CONTAINER_multihashmap_get (staff_map,
                                          &key);
  if (NULL != sm)
    return sm;
  sm = GNUNET_new (struct StaffMember);
  sm->key = key;
  GNUNET_assert (GNUNET_OK ==
                 GNUNET_CONTAINER_multihashmap_put (
                   staff_map,
                   &sm->key,
                   sm,
                   GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY));
  return sm;
}


/**
 * Append @a status to the statuses of @a sm, keeping them ordered by
 * @e last_change.
 *
 * @param[in,out] sm staff member to extend
 * @param status status to add
 */
static void
staff_member_add (struct StaffMember *sm,
                  const struct StaffStatus *status)
{
  unsigned int i;

  GNUNET_array_grow (sm->statuses,
                     sm->num_statuses,
                     sm->num_statuses + 1);
  for (i = sm->num_statuses - 1; i > 0; i--)
  {
    if (GNUNET_TIME_timestamp_cmp (sm->statuses[i - 1].last_change,
                                   <=,
                                   status->last_change))
      break;
    sm->statuses[i] = sm->statuses[i - 1];
  }
  sm->statuses[i] = *status;
}


/**
 * Find the status that was in force for @a sm at @a when, considering only
 * statuses the offline master key really signed.
 *
 * @param sm staff member to look at
 * @param when point in time of interest
 * @return NULL if no signed status covers @a when
 */
static const struct StaffStatus *
staff_member_status_at (const struct StaffMember *sm,
                        struct GNUNET_TIME_Timestamp when)
{
  const struct StaffStatus *ret = NULL;

  for (unsigned int i = 0; i < sm->num_statuses; i++)
  {
    const struct StaffStatus *ss = &sm->statuses[i];

    if (GNUNET_TIME_timestamp_cmp (ss->last_change,
                                   >,
                                   when))
      break; /* sorted, so no later entry can apply either */
    if (ss->master_sig_valid)
      ret = ss;
  }
  return ret;
}


/**
 * Report a row inconsistency.
 *
 * @param table name of the exchange table the bad row is in
 * @param rowid row that is bad
 * @param diagnostic what is wrong with it
 * @return true on success, false if the database failed us
 */
static bool
report_row (const char *table,
            uint64_t rowid,
            const char *diagnostic)
{
  struct TALER_AUDITORDB_RowInconsistency ri = {
    .row_id = rowid,
    .row_table = (char *) table,
    .diagnostic = (char *) diagnostic
  };
  enum GNUNET_DB_QueryStatus qs;

  GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
              "Row %llu of `%s' is bad: %s\n",
              (unsigned long long) rowid,
              table,
              diagnostic);
  qs = TALER_AUDITORDB_insert_row_inconsistency (TALER_ARL_adb,
                                                 &ri);
  if (0 > qs)
  {
    GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
    global_qs = qs;
    return false;
  }
  return true;
}


/**
 * Function called with an AML staff status change we recorded earlier.
 * Rebuilds our view of who was allowed to decide when.
 *
 * @param cls NULL
 * @param decider_pub public key of the staff member
 * @param decider_name legal name of the staff member
 * @param is_active true if the member could act from @a last_change on
 * @param read_only true if the member had read-only access
 * @param master_sig_valid true if the master key really signed this status
 * @param last_change when the status took effect
 * @return #GNUNET_OK to continue to iterate
 */
static enum GNUNET_GenericReturnValue
known_staff_cb (void *cls,
                const struct TALER_AmlOfficerPublicKeyP *decider_pub,
                const char *decider_name,
                bool is_active,
                bool read_only,
                bool master_sig_valid,
                struct GNUNET_TIME_Timestamp last_change)
{
  struct StaffMember *sm = staff_member_get (decider_pub);
  struct StaffStatus ss = {
    .last_change = last_change,
    .is_active = is_active,
    .read_only = read_only,
    .master_sig_valid = master_sig_valid
  };

  (void) cls;
  (void) decider_name;
  staff_member_add (sm,
                    &ss);
  /* We are called oldest-first, so the last call for a member leaves the
     status that was in force when this round began. */
  sm->known_at_start = true;
  sm->active_at_start = is_active && master_sig_valid;
  return GNUNET_OK;
}


/**
 * Function called with a status change of an AML staff member the exchange
 * recorded.  Verifies the offline master key's signature on it and, if this
 * is a status we have not seen before, records it.
 *
 * @param cls NULL
 * @param rowid row in `aml_staff`
 * @param decider_pub public key of the staff member
 * @param master_sig signature by the offline master key, NULL if absent
 * @param decider_name legal name of the staff member
 * @param is_active true if the member may currently act
 * @param read_only true if the member has read-only access
 * @param last_change when the status took effect, as claimed
 * @return #GNUNET_OK to continue to iterate, #GNUNET_SYSERR to stop
 */
static enum GNUNET_GenericReturnValue
check_staff_cb (void *cls,
                uint64_t rowid,
                const struct TALER_AmlOfficerPublicKeyP *decider_pub,
                const struct TALER_MasterSignatureP *master_sig,
                const char *decider_name,
                bool is_active,
                bool read_only,
                struct GNUNET_TIME_Timestamp last_change)
{
  struct StaffMember *sm = staff_member_get (decider_pub);
  struct StaffStatus ss = {
    .last_change = last_change,
    .is_active = is_active,
    .read_only = read_only,
    .master_sig_valid = false
  };
  enum GNUNET_DB_QueryStatus qs;
  bool regression = false;

  (void) cls;
  TALER_ARL_USE_PP (aml_staff_uuid) = rowid + 1;
  for (unsigned int i = 0; i < sm->num_statuses; i++)
  {
    if (GNUNET_TIME_timestamp_cmp (sm->statuses[i].last_change,
                                   ==,
                                   last_change))
      return GNUNET_OK; /* already seen and judged in an earlier round */
    if (GNUNET_TIME_timestamp_cmp (sm->statuses[i].last_change,
                                   >,
                                   last_change))
      regression = true;
  }
  if (NULL == master_sig)
  {
    if (! report_row ("aml_staff",
                      rowid,
                      "staff status without a master key signature"))
      return GNUNET_SYSERR;
  }
  else if (GNUNET_OK !=
           TALER_exchange_offline_aml_officer_status_verify (
             decider_pub,
             decider_name,
             last_change,
             is_active,
             read_only,
             &TALER_ARL_master_pub,
             master_sig))
  {
    if (! report_row ("aml_staff",
                      rowid,
                      "invalid master key signature on staff status")
        )
      return GNUNET_SYSERR;
  }
  else
  {
    ss.master_sig_valid = true;
  }
  if (regression)
  {
    /* exchange_do_insert_aml_officer() refuses to store a status older than
       the one it has, so seeing one means somebody wrote to the table
       behind the exchange's back. */
    if (! report_row ("aml_staff",
                      rowid,
                      "staff status is older than one seen before"))
      return GNUNET_SYSERR;
  }
  if (ss.master_sig_valid && is_active)
    sm->enabled_this_round = true;
  staff_member_add (sm,
                    &ss);
  qs = TALER_AUDITORDB_insert_aml_staff (TALER_ARL_adb,
                                         decider_pub,
                                         decider_name,
                                         is_active,
                                         read_only,
                                         ss.master_sig_valid,
                                         last_change);
  if (0 > qs)
  {
    GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
    global_qs = qs;
    return GNUNET_SYSERR;
  }
  return GNUNET_OK;
}


/**
 * Function called with an AML decision the exchange recorded.  Checks the
 * officer's signature and that the officer was allowed to make it.
 *
 * @param cls NULL
 * @param rowid row in `aml_history`
 * @param h_payto account the decision is about
 * @param justification justification given
 * @param decider_pub officer who decided, NULL if not recorded
 * @param decider_sig the officer's signature, NULL if not recorded
 * @param decision_time when the decision was taken, as claimed
 * @param jproperties new account properties, NULL for none
 * @param jnew_rules new KYC rules, NULL if not recorded
 * @param new_measure_name measure to apply, NULL for none
 * @param to_investigate whether staff should investigate the account
 * @param attributes_expiration when attributes set with the decision expire
 * @param h_attributes hash of the attributes set, NULL if none
 * @return #GNUNET_OK to continue to iterate, #GNUNET_SYSERR to stop
 */
static enum GNUNET_GenericReturnValue
check_decision_cb (void *cls,
                   uint64_t rowid,
                   const struct TALER_NormalizedPaytoHashP *h_payto,
                   const char *justification,
                   const struct TALER_AmlOfficerPublicKeyP *decider_pub,
                   const struct TALER_AmlOfficerSignatureP *decider_sig,
                   struct GNUNET_TIME_Timestamp decision_time,
                   const json_t *jproperties,
                   const json_t *jnew_rules,
                   const char *new_measure_name,
                   bool to_investigate,
                   struct GNUNET_TIME_Timestamp attributes_expiration,
                   const struct GNUNET_HashCode *h_attributes)
{
  struct StaffMember *sm;
  const struct StaffStatus *ss;

  (void) cls;
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Analyzing AML decision %llu taken at %s\n",
              (unsigned long long) rowid,
              GNUNET_TIME_timestamp2s (decision_time));
  TALER_ARL_USE_PP (aml_history_serial_id) = rowid + 1;
  if (NULL == decider_pub)
  {
    /* exchange_do_insert_aml_decision() only writes an aml_history row when
       it has a decider, so this cannot happen without tampering. */
    if (! report_row ("aml_history",
                      rowid,
                      "decision without an officer public key"))
      return GNUNET_SYSERR;
    return GNUNET_OK;
  }
  if (NULL == decider_sig)
  {
    if (! report_row ("aml_history",
                      rowid,
                      "decision without an officer signature"))
      return GNUNET_SYSERR;
  }
  else if (NULL == jnew_rules)
  {
    /* The signature is over the new rules, so without them there is nothing
       we could check it against. */
    if (! report_row ("aml_history",
                      rowid,
                      "decision without the new rules it signed over"))
      return GNUNET_SYSERR;
  }
  else if (GNUNET_OK !=
           TALER_officer_aml_decision_verify_hashed (
             justification,
             decision_time,
             h_payto,
             jnew_rules,
             jproperties,
             new_measure_name,
             to_investigate,
             decider_pub,
             decider_sig,
             attributes_expiration,
             h_attributes))
  {
    if (! report_row ("aml_history",
                      rowid,
                      "invalid officer signature on decision"))
      return GNUNET_SYSERR;
  }
  sm = staff_member_get (decider_pub);
  ss = staff_member_status_at (sm,
                               decision_time);
  if (NULL == ss)
  {
    if (! report_row ("aml_history",
                      rowid,
                      "officer was not appointed when the decision was made"))
      return GNUNET_SYSERR;
  }
  else if (! ss->is_active)
  {
    if (! report_row ("aml_history",
                      rowid,
                      "officer was not active when the decision was made"))
      return GNUNET_SYSERR;
  }
  else if (ss->read_only)
  {
    if (! report_row ("aml_history",
                      rowid,
                      "officer had read-only access when deciding"))
      return GNUNET_SYSERR;
  }
  /* The eligibility check above trusts decision_time, which the exchange
     picks.  This one does not: whatever the decision claims, a record for an
     officer we already knew to be disabled has no business appearing now. */
  if (sm->known_at_start &&
      (! sm->active_at_start) &&
      (! sm->enabled_this_round))
  {
    if (! report_row ("aml_history",
                      rowid,
                      "decision appeared after the officer was disabled"))
      return GNUNET_SYSERR;
  }
  return GNUNET_OK;
}


/**
 * Function called with a row of `legitimization_outcomes`, the table that
 * says which KYC rules currently apply to an account.  Checks that something
 * in the exchange's books accounts for the row existing.
 *
 * The exchange has exactly three ways to create one, and each leaves its own
 * trace:
 *
 * - an AML officer decides, and `exchange_do_insert_aml_decision()` writes an
 *   `aml_history` row pointing at the outcome.  #check_decision_cb() has
 *   already checked that decision's signature and the officer's appointment,
 *   so this is the strongest of the three;
 * - a customer passes (or fails) a KYC check, and the AML program's verdict
 *   on it becomes the outcome.  What is left of the check is a row in
 *   `legitimization_processes` for the same account, started no later than
 *   the decision;
 * - the rules in force expire, and `exchange_do_insert_successor_measure()`
 *   or a re-run of the AML program replaces them.  What is left of that is
 *   the expired predecessor outcome.
 *
 * A row with none of the three was written by something other than the
 * exchange's own code.  Note that the third alternative is the weak one: an
 * account that has ever had an outcome expire can be given further outcomes
 * without any fresh justification, exactly as the exchange legitimately does
 * on expiry.  The check is a floor, not a proof that the rules are right.
 *
 * @param cls NULL
 * @param rowid row in `legitimization_outcomes`
 * @param h_payto account the outcome is about
 * @param decision_time when the outcome was decided, as claimed
 * @param expiration_time when the outcome expires
 * @param has_aml_decision an `aml_history` row points at this outcome
 * @param has_legitimization_process the account had a legitimization process
 *        that had started by @a decision_time
 * @param has_expired_predecessor an earlier outcome for the account had
 *        expired by @a decision_time
 * @return #GNUNET_OK to continue to iterate, #GNUNET_SYSERR to stop
 */
static enum GNUNET_GenericReturnValue
check_outcome_cb (void *cls,
                  uint64_t rowid,
                  const struct TALER_NormalizedPaytoHashP *h_payto,
                  struct GNUNET_TIME_Timestamp decision_time,
                  struct GNUNET_TIME_Timestamp expiration_time,
                  bool has_aml_decision,
                  bool has_legitimization_process,
                  bool has_expired_predecessor)
{
  (void) cls;
  (void) h_payto;
  (void) expiration_time;
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Analyzing legitimization outcome %llu decided at %s\n",
              (unsigned long long) rowid,
              GNUNET_TIME_timestamp2s (decision_time));
  TALER_ARL_USE_PP (legitimization_outcome_serial_id) = rowid + 1;
  if (has_aml_decision ||
      has_legitimization_process ||
      has_expired_predecessor)
    return GNUNET_OK;
  if (! report_row ("legitimization_outcomes",
                    rowid,
                    "KYC rules in force without an AML decision,"
                    " a legitimization process or an expired predecessor"))
    return GNUNET_SYSERR;
  return GNUNET_OK;
}


/**
 * Free a `struct StaffMember`.
 *
 * @param cls NULL
 * @param key unused
 * @param value the `struct StaffMember` to free
 * @return #GNUNET_OK
 */
static enum GNUNET_GenericReturnValue
free_staff_member (void *cls,
                   const struct GNUNET_HashCode *key,
                   void *value)
{
  struct StaffMember *sm = value;

  (void) cls;
  (void) key;
  GNUNET_assert (GNUNET_YES ==
                 GNUNET_CONTAINER_multihashmap_remove (staff_map,
                                                       &sm->key,
                                                       sm));
  GNUNET_array_grow (sm->statuses,
                     sm->num_statuses,
                     0);
  GNUNET_free (sm);
  return GNUNET_OK;
}


/**
 * Drop our in-memory view of the AML staff.
 */
static void
clear_staff_map (void)
{
  if (NULL == staff_map)
    return;
  GNUNET_CONTAINER_multihashmap_iterate (staff_map,
                                         &free_staff_member,
                                         NULL);
  GNUNET_CONTAINER_multihashmap_destroy (staff_map);
  staff_map = NULL;
}


/**
 * Analyze the AML staff appointments and the decisions they justify.
 *
 * @param cls NULL
 * @return transaction status code
 */
static enum GNUNET_DB_QueryStatus
analyze_aml (void *cls)
{
  enum GNUNET_DB_QueryStatus qs;
  bool had_pp;

  (void) cls;
  global_qs = GNUNET_DB_STATUS_SUCCESS_NO_RESULTS;
  clear_staff_map ();
  staff_map = GNUNET_CONTAINER_multihashmap_create (32,
                                                    GNUNET_YES);
  qs = TALER_AUDITORDB_get_auditor_progress (
    TALER_ARL_adb,
    TALER_ARL_GET_PP (aml_history_serial_id),
    TALER_ARL_GET_PP (aml_staff_uuid),
    TALER_ARL_GET_PP (legitimization_outcome_serial_id),
    NULL);
  if (0 > qs)
  {
    GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
    return qs;
  }
  /* Not from @a qs: auditor_do_get_auditor_progress() returns one row per
     key whether or not the key is on file, so the query status says nothing
     about whether we have run before. */
  had_pp = (0 != TALER_ARL_USE_PP (aml_staff_uuid)) ||
           (0 != TALER_ARL_USE_PP (aml_history_serial_id)) ||
           (0 != TALER_ARL_USE_PP (legitimization_outcome_serial_id));
  if (had_pp)
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Resuming AML audit at %llu/%llu/%llu\n",
                (unsigned long long) TALER_ARL_USE_PP (aml_staff_uuid),
                (unsigned long long) TALER_ARL_USE_PP (aml_history_serial_id),
                (unsigned long long) TALER_ARL_USE_PP (
                  legitimization_outcome_serial_id));
  else
    GNUNET_log (GNUNET_ERROR_TYPE_MESSAGE,
                "First analysis using AML auditor, starting audit from scratch\n");

  /* What we knew about the staff when this round began. */
  qs = TALER_AUDITORDB_iterate_aml_staff (TALER_ARL_adb,
                                          &known_staff_cb,
                                          NULL);
  if (0 > qs)
  {
    GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
    return qs;
  }

  /* Status changes the exchange recorded since we last looked.  Must run
     before the decisions below, which are judged against the result. */
  qs = TALER_EXCHANGEDB_iterate_aml_staff_above_serial_id (
    TALER_ARL_edb,
    TALER_ARL_USE_PP (aml_staff_uuid),
    &check_staff_cb,
    NULL);
  if (0 > qs)
  {
    GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
    return qs;
  }
  if (0 > global_qs)
    return global_qs;

  qs = TALER_EXCHANGEDB_iterate_aml_history_above_serial_id (
    TALER_ARL_edb,
    TALER_ARL_USE_PP (aml_history_serial_id),
    &check_decision_cb,
    NULL);
  if (0 > qs)
  {
    GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
    return qs;
  }
  if (0 > global_qs)
    return global_qs;
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Analyzed %d AML decisions\n",
              (int) qs);

  /* Runs after the decisions: an outcome an AML officer signed is accounted
     for by the `aml_history' row we just judged, so leaving that judgement
     for a later round would report the outcome against an incomplete
     picture. */
  qs = TALER_EXCHANGEDB_iterate_legitimization_outcomes_above_serial_id (
    TALER_ARL_edb,
    TALER_ARL_USE_PP (legitimization_outcome_serial_id),
    &check_outcome_cb,
    NULL);
  if (0 > qs)
  {
    GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
    return qs;
  }
  if (0 > global_qs)
    return global_qs;
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Analyzed %d legitimization outcomes\n",
              (int) qs);
  /* Insert first (a no-op once the rows exist), then update: the three
     progress points are written on the very first round as well as on
     every later one.  Branching on @e had_pp instead would leave the
     rows uncreated for ever, and the helper would re-audit the whole
     history every time it woke up. */
  qs = TALER_AUDITORDB_insert_auditor_progress (
    TALER_ARL_adb,
    TALER_ARL_SET_PP (aml_history_serial_id),
    TALER_ARL_SET_PP (aml_staff_uuid),
    TALER_ARL_SET_PP (legitimization_outcome_serial_id),
    NULL);
  if (0 > qs)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Failed to update auditor DB, not recording progress\n");
    GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
    return qs;
  }
  return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
}


/**
 * Function called when the exchange recorded an AML staff status change or
 * an AML decision.  Re-runs the analysis.
 *
 * @param cls NULL
 * @param extra additional event data provided
 * @param extra_size number of bytes in @a extra
 */
static void
db_notify (void *cls,
           const void *extra,
           size_t extra_size)
{
  (void) cls;
  (void) extra;
  (void) extra_size;
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Received notification for new AML data\n");
  if (GNUNET_OK !=
      TALER_ARL_setup_sessions_and_run (&analyze_aml,
                                        NULL))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Audit failed\n");
    GNUNET_SCHEDULER_shutdown ();
    global_ret = EXIT_FAILURE;
    return;
  }
}


/**
 * Function called on shutdown.
 *
 * @param cls NULL
 */
static void
do_shutdown (void *cls)
{
  (void) cls;
  if (NULL != eh)
  {
    TALER_AUDITORDB_event_listen_cancel (eh);
    eh = NULL;
  }
  clear_staff_map ();
  TALER_ARL_done ();
}


/**
 * Main function that will be run.
 *
 * @param cls closure
 * @param args remaining command-line arguments
 * @param cfgfile name of the configuration file used (for saving, can be NULL!)
 * @param c configuration
 */
static void
run (void *cls,
     char *const *args,
     const char *cfgfile,
     const struct GNUNET_CONFIGURATION_Handle *c)
{
  (void) cls;
  (void) args;
  (void) cfgfile;
  cfg = c;
  GNUNET_SCHEDULER_add_shutdown (&do_shutdown,
                                 NULL);
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Launching AML auditor\n");
  if (GNUNET_OK !=
      TALER_ARL_init (c))
  {
    global_ret = EXIT_FAILURE;
    return;
  }
  if (test_mode != 1)
  {
    struct GNUNET_DB_EventHeaderP es = {
      .size = htons (sizeof (es)),
      .type = htons (TALER_DBEVENT_EXCHANGE_AUDITOR_WAKE_HELPER_AML)
    };

    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Running helper indefinitely\n");
    eh = TALER_AUDITORDB_event_listen (TALER_ARL_adb,
                                       &es,
                                       GNUNET_TIME_UNIT_FOREVER_REL,
                                       &db_notify,
                                       NULL);
  }
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Starting audit\n");
  if (GNUNET_OK !=
      TALER_ARL_setup_sessions_and_run (&analyze_aml,
                                        NULL))
  {
    GNUNET_SCHEDULER_shutdown ();
    global_ret = EXIT_FAILURE;
    return;
  }
}


/**
 * The main function of the AML auditing helper tool.
 *
 * @param argc number of arguments from the command line
 * @param argv command line arguments
 * @return 0 ok, 1 on error
 */
int
main (int argc,
      char *const *argv)
{
  const struct GNUNET_GETOPT_CommandLineOption options[] = {
    GNUNET_GETOPT_option_flag ('i',
                               "internal",
                               "perform checks only applicable for exchange-internal audits",
                               &internal_checks),
    GNUNET_GETOPT_option_flag ('t',
                               "test",
                               "run in test mode and exit when idle",
                               &test_mode),
    GNUNET_GETOPT_option_timetravel ('T',
                                     "timetravel"),
    GNUNET_GETOPT_OPTION_END
  };
  enum GNUNET_GenericReturnValue ret;

  ret = GNUNET_PROGRAM_run (
    TALER_AUDITOR_project_data (),
    argc,
    argv,
    "taler-helper-auditor-aml",
    gettext_noop (
      "Audit exchange database for AML decisions made by unauthorised staff"),
    options,
    &run,
    NULL);
  if (GNUNET_SYSERR == ret)
    return EXIT_INVALIDARGUMENT;
  if (GNUNET_NO == ret)
    return EXIT_SUCCESS;
  return global_ret;
}


/* end of taler-helper-auditor-aml.c */
