/*
This file is part of TALER
Copyright (C) 2016-2024 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
*/
/**
* @file auditor/taler-helper-auditor-purses.c
* @brief audits the purses of an exchange database
* @author Christian Grothoff
*/
#include "platform.h"
#include
#include "taler_auditordb_plugin.h"
#include "taler_exchangedb_lib.h"
#include "taler_bank_service.h"
#include "taler_signatures.h"
#include "report-lib.h"
#include "taler_dbevents.h"
/**
* Use a 1 day grace period to deal with clocks not being perfectly synchronized.
*/
#define EXPIRATION_GRACE_PERIOD GNUNET_TIME_UNIT_DAYS
/**
* Return value from main().
*/
static int global_ret;
/**
* Run in test mode. Exit when idle instead of
* going to sleep and waiting for more work.
*/
static int test_mode;
/**
* Checkpointing our progress for purses.
*/
static TALER_ARL_DEF_PP (purse_account_merge_serial_id);
static TALER_ARL_DEF_PP (purse_decision_serial_id);
static TALER_ARL_DEF_PP (purse_deposits_serial_id);
static TALER_ARL_DEF_PP (purse_merges_serial_id);
static TALER_ARL_DEF_PP (purse_request_serial_id);
static TALER_ARL_DEF_PP (purse_open_counter);
static TALER_ARL_DEF_AB (purse_global_balance);
/**
* Total amount purses were merged with insufficient balance.
*/
static TALER_ARL_DEF_AB (purse_total_balance_insufficient_loss);
/**
* Total amount purse decisions are delayed past deadline.
*/
static TALER_ARL_DEF_AB (purse_total_delayed_decisions);
/**
* Total amount affected by purses not having been closed on time.
*/
static TALER_ARL_DEF_AB (purse_total_balance_purse_not_closed);
/**
* Profits the exchange made by bad amount calculations.
*/
static TALER_ARL_DEF_AB (purse_total_arithmetic_delta_plus);
/**
* Losses the exchange made by bad amount calculations.
*/
static TALER_ARL_DEF_AB (purse_total_arithmetic_delta_minus);
/**
* Total amount lost by operations for which signatures were invalid.
*/
static TALER_ARL_DEF_AB (purse_total_bad_sig_loss);
/**
* Should we run checks that only work for exchange-internal audits?
*/
static int internal_checks;
static struct GNUNET_DB_EventHandler *eh;
/**
* The auditors's configuration.
*/
static const struct GNUNET_CONFIGURATION_Handle *cfg;
/* ***************************** Report logic **************************** */
/**
* Report a (serious) inconsistency in the exchange's database with
* respect to calculations involving amounts.
*
* @param operation what operation had the inconsistency
* @param rowid affected row, 0 if row is missing
* @param exchange amount calculated by exchange
* @param auditor amount calculated by auditor
* @param profitable 1 if @a exchange being larger than @a auditor is
* profitable for the exchange for this operation,
* -1 if @a exchange being smaller than @a auditor is
* profitable for the exchange, and 0 if it is unclear
* @return transaction status
*/
static enum GNUNET_DB_QueryStatus
report_amount_arithmetic_inconsistency (
const char *operation,
uint64_t rowid,
const struct TALER_Amount *exchange,
const struct TALER_Amount *auditor,
int profitable)
{
struct TALER_Amount delta;
struct TALER_Amount *target;
enum GNUNET_DB_QueryStatus qs;
if (0 < TALER_amount_cmp (exchange,
auditor))
{
/* exchange > auditor */
TALER_ARL_amount_subtract (&delta,
exchange,
auditor);
}
else
{
/* auditor < exchange */
profitable = -profitable;
TALER_ARL_amount_subtract (&delta,
auditor,
exchange);
}
{
struct TALER_AUDITORDB_AmountArithmeticInconsistency aai = {
.profitable = profitable,
.operation = (char *) operation,
.exchange_amount = *exchange,
.auditor_amount = *auditor
};
qs = TALER_ARL_adb->insert_amount_arithmetic_inconsistency (
TALER_ARL_adb->cls,
&aai);
if (qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
return qs;
}
}
if (0 != profitable)
{
target = (1 == profitable)
? &TALER_ARL_USE_AB (purse_total_arithmetic_delta_plus)
: &TALER_ARL_USE_AB (purse_total_arithmetic_delta_minus);
TALER_ARL_amount_add (target,
target,
&delta);
}
return qs;
}
/**
* Report a (serious) inconsistency in the exchange's database.
*
* @param table affected table
* @param rowid affected row, 0 if row is missing
* @param diagnostic message explaining the problem
* @return transaction status
*/
static enum GNUNET_DB_QueryStatus
report_row_inconsistency (const char *table,
uint64_t rowid,
const char *diagnostic)
{
enum GNUNET_DB_QueryStatus qs;
struct TALER_AUDITORDB_RowInconsistency ri = {
.diagnostic = (char *) diagnostic,
.row_table = (char *) table,
.row_id = rowid
};
qs = TALER_ARL_adb->insert_row_inconsistency (
TALER_ARL_adb->cls,
&ri);
if (qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
return qs;
}
return qs;
}
/**
* Obtain the purse fee for a purse created at @a time.
*
* @param atime when was the purse created
* @param[out] fee set to the purse fee
* @return #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT on success
*/
static enum GNUNET_DB_QueryStatus
get_purse_fee (struct GNUNET_TIME_Timestamp atime,
struct TALER_Amount *fee)
{
enum GNUNET_DB_QueryStatus qs;
struct TALER_MasterSignatureP master_sig;
struct GNUNET_TIME_Timestamp start_date;
struct GNUNET_TIME_Timestamp end_date;
struct TALER_GlobalFeeSet fees;
struct GNUNET_TIME_Relative ptimeout;
struct GNUNET_TIME_Relative hexp;
uint32_t pacl;
qs = TALER_ARL_edb->get_global_fee (TALER_ARL_edb->cls,
atime,
&start_date,
&end_date,
&fees,
&ptimeout,
&hexp,
&pacl,
&master_sig);
if (0 > qs)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
return qs;
}
if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
{
char *diag;
GNUNET_asprintf (&diag,
"purse fee unavailable at %s\n",
GNUNET_TIME_timestamp2s (atime));
qs = report_row_inconsistency ("purse-fee",
atime.abs_time.abs_value_us,
diag);
GNUNET_free (diag);
if (0 > qs)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
return qs;
}
return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS;
}
*fee = fees.purse;
return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
}
/* ***************************** Analyze purses ************************ */
/* This logic checks the purses_requests, purse_deposits,
purse_refunds, purse_merges and account_merges */
/**
* Summary data we keep per purse.
*/
struct PurseSummary
{
/**
* Public key of the purse.
* Always set when the struct is first initialized.
*/
struct TALER_PurseContractPublicKeyP purse_pub;
/**
* Balance of the purse from deposits (includes purse fee, excludes deposit
* fees), as calculated by auditor.
*/
struct TALER_Amount balance;
/**
* Expected value of the purse, excludes purse fee.
*/
struct TALER_Amount total_value;
/**
* Purse balance according to exchange DB.
*/
struct TALER_Amount exchange_balance;
/**
* Contract terms of the purse.
*/
struct TALER_PrivateContractHashP h_contract_terms;
/**
* Merge timestamp (as per exchange DB).
*/
struct GNUNET_TIME_Timestamp merge_timestamp;
/**
* Purse creation date. This is when the merge
* fee is applied.
*/
struct GNUNET_TIME_Timestamp creation_date;
/**
* Purse expiration date.
*/
struct GNUNET_TIME_Timestamp expiration_date;
/**
* Did we have a previous purse info? Used to decide between UPDATE and
* INSERT later. Initialized in #load_auditor_purse_summary().
*/
bool had_pi;
/**
* Was the purse deleted? FIXME: Not yet handled (do we need to? purse
* might just appear as expired eventually; but in the meantime, exchange
* may seem to have refunded the coins for no good reason...), also we do
* not yet check the deletion signature.
*/
bool purse_deleted;
/**
* Was the purse refunded? FIXME: Not yet handled (do we need to?)
*/
bool purse_refunded;
};
/**
* Load the auditor's remembered state about the purse into @a ps.
*
* @param[in,out] ps purse summary to (fully) initialize
* @return transaction status code
*/
static enum GNUNET_DB_QueryStatus
load_auditor_purse_summary (struct PurseSummary *ps)
{
enum GNUNET_DB_QueryStatus qs;
uint64_t rowid;
qs = TALER_ARL_adb->get_purse_info (TALER_ARL_adb->cls,
&ps->purse_pub,
&rowid,
&ps->balance,
&ps->expiration_date);
if (0 > qs)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
return qs;
}
if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
{
ps->had_pi = false;
GNUNET_assert (GNUNET_OK ==
TALER_amount_set_zero (TALER_ARL_currency,
&ps->balance));
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Creating fresh purse `%s'\n",
TALER_B2S (&ps->purse_pub));
return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS;
}
ps->had_pi = true;
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Auditor remembers purse `%s' has balance %s\n",
TALER_B2S (&ps->purse_pub),
TALER_amount2s (&ps->balance));
return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
}
/**
* Closure to the various callbacks we make while checking a purse.
*/
struct PurseContext
{
/**
* Map from hash of purse's public key to a `struct PurseSummary`.
*/
struct GNUNET_CONTAINER_MultiHashMap *purses;
/**
* Transaction status code, set to error codes if applicable.
*/
enum GNUNET_DB_QueryStatus qs;
};
/**
* Create a new reserve for @a reserve_pub in @a rc.
*
* @param[in,out] pc context to update
* @param purse_pub key for which to create a purse
* @return NULL on error
*/
static struct PurseSummary *
setup_purse (struct PurseContext *pc,
const struct TALER_PurseContractPublicKeyP *purse_pub)
{
struct PurseSummary *ps;
struct GNUNET_HashCode key;
enum GNUNET_DB_QueryStatus qs;
GNUNET_CRYPTO_hash (purse_pub,
sizeof (*purse_pub),
&key);
ps = GNUNET_CONTAINER_multihashmap_get (pc->purses,
&key);
if (NULL != ps)
return ps;
ps = GNUNET_new (struct PurseSummary);
ps->purse_pub = *purse_pub;
GNUNET_assert (GNUNET_OK ==
TALER_amount_set_zero (TALER_ARL_currency,
&ps->balance));
/* get purse meta-data from exchange DB */
qs = TALER_ARL_edb->select_purse (TALER_ARL_edb->cls,
purse_pub,
&ps->creation_date,
&ps->expiration_date,
&ps->total_value,
&ps->exchange_balance,
&ps->h_contract_terms,
&ps->merge_timestamp,
&ps->purse_deleted,
&ps->purse_refunded);
if (0 >= qs)
{
GNUNET_free (ps);
pc->qs = qs;
return NULL;
}
GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs);
qs = load_auditor_purse_summary (ps);
if (0 > qs)
{
GNUNET_free (ps);
pc->qs = qs;
return NULL;
}
GNUNET_assert (GNUNET_OK ==
GNUNET_CONTAINER_multihashmap_put (pc->purses,
&key,
ps,
GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY));
return ps;
}
/**
* Function called on purse requests.
*
* @param cls closure
* @param rowid which row in the database was the request stored in
* @param purse_pub public key of the purse
* @param merge_pub public key representing the merge capability
* @param purse_creation when was the purse created
* @param purse_expiration when would an unmerged purse expire
* @param h_contract_terms contract associated with the purse
* @param age_limit the age limit for deposits into the purse
* @param target_amount amount to be put into the purse
* @param purse_sig signature of the purse over the initialization data
* @return #GNUNET_OK to continue to iterate
*/
static enum GNUNET_GenericReturnValue
handle_purse_requested (
void *cls,
uint64_t rowid,
const struct TALER_PurseContractPublicKeyP *purse_pub,
const struct TALER_PurseMergePublicKeyP *merge_pub,
struct GNUNET_TIME_Timestamp purse_creation,
struct GNUNET_TIME_Timestamp purse_expiration,
const struct TALER_PrivateContractHashP *h_contract_terms,
uint32_t age_limit,
const struct TALER_Amount *target_amount,
const struct TALER_PurseContractSignatureP *purse_sig)
{
struct PurseContext *pc = cls;
struct PurseSummary *ps;
struct GNUNET_HashCode key;
enum GNUNET_DB_QueryStatus qs;
TALER_ARL_USE_PP (purse_request_serial_id) = rowid;
if (GNUNET_OK !=
TALER_wallet_purse_create_verify (purse_expiration,
h_contract_terms,
merge_pub,
age_limit,
target_amount,
purse_pub,
purse_sig))
{
struct TALER_AUDITORDB_BadSigLosses bsl = {
.row_id = rowid,
.operation = "purse-request",
.loss = *target_amount,
.operation_specific_pub = purse_pub->eddsa_pub
};
qs = TALER_ARL_adb->insert_bad_sig_losses (
TALER_ARL_adb->cls,
&bsl);
if (qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
pc->qs = qs;
return GNUNET_SYSERR;
}
TALER_ARL_amount_add (&TALER_ARL_USE_AB (purse_total_bad_sig_loss),
&TALER_ARL_USE_AB (purse_total_bad_sig_loss),
target_amount);
}
GNUNET_CRYPTO_hash (purse_pub,
sizeof (*purse_pub),
&key);
ps = GNUNET_new (struct PurseSummary);
ps->purse_pub = *purse_pub;
GNUNET_assert (GNUNET_OK ==
TALER_amount_set_zero (TALER_ARL_currency,
&ps->balance));
ps->creation_date = purse_creation;
ps->expiration_date = purse_expiration;
ps->total_value = *target_amount;
ps->h_contract_terms = *h_contract_terms;
GNUNET_assert (GNUNET_OK ==
GNUNET_CONTAINER_multihashmap_put (pc->purses,
&key,
ps,
GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY));
return GNUNET_OK;
}
/**
* Function called with details about purse deposits that have been made, with
* the goal of auditing the deposit's execution.
*
* @param cls closure
* @param rowid unique serial ID for the deposit in our DB
* @param deposit deposit details
* @param reserve_pub which reserve is the purse merged into, NULL if unknown
* @param flags purse flags
* @param auditor_balance purse balance (according to the
* auditor during auditing)
* @param purse_total target amount the purse should reach
* @param denom_pub denomination public key of @a coin_pub
* @return #GNUNET_OK to continue to iterate, #GNUNET_SYSERR to stop
*/
static enum GNUNET_GenericReturnValue
handle_purse_deposits (
void *cls,
uint64_t rowid,
const struct TALER_EXCHANGEDB_PurseDeposit *deposit,
const struct TALER_ReservePublicKeyP *reserve_pub,
enum TALER_WalletAccountMergeFlags flags,
const struct TALER_Amount *auditor_balance,
const struct TALER_Amount *purse_total,
const struct TALER_DenominationPublicKey *denom_pub)
{
struct PurseContext *pc = cls;
struct TALER_Amount amount_minus_fee;
struct PurseSummary *ps;
const char *base_url
= (NULL == deposit->exchange_base_url)
? TALER_ARL_exchange_url
: deposit->exchange_base_url;
struct TALER_DenominationHashP h_denom_pub;
enum GNUNET_DB_QueryStatus qs;
/* should be monotonically increasing */
GNUNET_assert (rowid >= TALER_ARL_USE_PP (purse_deposits_serial_id));
TALER_ARL_USE_PP (purse_deposits_serial_id) = rowid + 1;
{
const struct TALER_EXCHANGEDB_DenominationKeyInformation *issue;
qs = TALER_ARL_get_denomination_info (denom_pub,
&issue,
&h_denom_pub);
if (0 > qs)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
if (GNUNET_DB_STATUS_HARD_ERROR == qs)
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Hard database error trying to get denomination %s from database!\n",
TALER_B2S (denom_pub));
pc->qs = qs;
return GNUNET_SYSERR;
}
if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
{
qs = report_row_inconsistency ("purse-deposit",
rowid,
"denomination key not found");
if (0 > qs)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
pc->qs = qs;
return GNUNET_SYSERR;
}
return GNUNET_OK;
}
TALER_ARL_amount_subtract (&amount_minus_fee,
&deposit->amount,
&issue->fees.deposit);
}
if (GNUNET_OK !=
TALER_wallet_purse_deposit_verify (base_url,
&deposit->purse_pub,
&deposit->amount,
&h_denom_pub,
&deposit->h_age_commitment,
&deposit->coin_pub,
&deposit->coin_sig))
{
struct TALER_AUDITORDB_BadSigLosses bsl = {
.row_id = rowid,
.operation = "purse-deposit",
.loss = deposit->amount,
.operation_specific_pub = deposit->coin_pub.eddsa_pub
};
qs = TALER_ARL_adb->insert_bad_sig_losses (
TALER_ARL_adb->cls,
&bsl);
if (qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
pc->qs = qs;
return GNUNET_SYSERR;
}
TALER_ARL_amount_add (&TALER_ARL_USE_AB (purse_total_bad_sig_loss),
&TALER_ARL_USE_AB (purse_total_bad_sig_loss),
&deposit->amount);
return GNUNET_OK;
}
ps = setup_purse (pc,
&deposit->purse_pub);
if (NULL == ps)
{
if (0 > pc->qs)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == pc->qs);
return GNUNET_SYSERR;
}
GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == pc->qs);
qs = report_row_inconsistency ("purse_deposit",
rowid,
"purse not found");
if (0 > qs)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
pc->qs = qs;
return GNUNET_SYSERR;
}
return GNUNET_OK;
}
TALER_ARL_amount_add (&ps->balance,
&ps->balance,
&amount_minus_fee);
TALER_ARL_amount_add (&TALER_ARL_USE_AB (purse_global_balance),
&TALER_ARL_USE_AB (purse_global_balance),
&amount_minus_fee);
return GNUNET_OK;
}
/**
* Function called with details about purse merges that have been made, with
* the goal of auditing the purse merge execution.
*
* @param cls closure
* @param rowid unique serial ID for the deposit in our DB
* @param partner_base_url where is the reserve, NULL for this exchange
* @param amount total amount expected in the purse
* @param balance current balance in the purse
* @param flags purse flags
* @param merge_pub merge capability key
* @param reserve_pub reserve the merge affects
* @param merge_sig signature affirming the merge
* @param purse_pub purse key
* @param merge_timestamp when did the merge happen
* @return #GNUNET_OK to continue to iterate, #GNUNET_SYSERR to stop
*/
static enum GNUNET_GenericReturnValue
handle_purse_merged (
void *cls,
uint64_t rowid,
const char *partner_base_url,
const struct TALER_Amount *amount,
const struct TALER_Amount *balance,
enum TALER_WalletAccountMergeFlags flags,
const struct TALER_PurseMergePublicKeyP *merge_pub,
const struct TALER_ReservePublicKeyP *reserve_pub,
const struct TALER_PurseMergeSignatureP *merge_sig,
const struct TALER_PurseContractPublicKeyP *purse_pub,
struct GNUNET_TIME_Timestamp merge_timestamp)
{
struct PurseContext *pc = cls;
struct PurseSummary *ps;
enum GNUNET_DB_QueryStatus qs;
/* should be monotonically increasing */
GNUNET_assert (rowid >= TALER_ARL_USE_PP (purse_merges_serial_id));
TALER_ARL_USE_PP (purse_merges_serial_id) = rowid + 1;
{
char *reserve_url;
reserve_url
= TALER_reserve_make_payto (NULL == partner_base_url
? TALER_ARL_exchange_url
: partner_base_url,
reserve_pub);
if (GNUNET_OK !=
TALER_wallet_purse_merge_verify (reserve_url,
merge_timestamp,
purse_pub,
merge_pub,
merge_sig))
{
struct TALER_AUDITORDB_BadSigLosses bsl = {
.row_id = rowid,
.operation = "merge-purse",
.loss = *amount,
.operation_specific_pub = merge_pub->eddsa_pub
};
GNUNET_free (reserve_url);
qs = TALER_ARL_adb->insert_bad_sig_losses (
TALER_ARL_adb->cls,
&bsl);
if (qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
pc->qs = qs;
return GNUNET_SYSERR;
}
TALER_ARL_amount_add (&TALER_ARL_USE_AB (purse_total_bad_sig_loss),
&TALER_ARL_USE_AB (purse_total_bad_sig_loss),
amount);
return GNUNET_OK;
}
GNUNET_free (reserve_url);
}
ps = setup_purse (pc,
purse_pub);
if (NULL == ps)
{
if (0 < pc->qs)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == pc->qs);
return GNUNET_SYSERR;
}
GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == pc->qs);
qs = report_row_inconsistency ("purse-merge",
rowid,
"purse not found");
if (qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
pc->qs = qs;
return GNUNET_SYSERR;
}
return GNUNET_OK;
}
GNUNET_break (0 ==
GNUNET_TIME_timestamp_cmp (merge_timestamp,
==,
ps->merge_timestamp));
TALER_ARL_amount_add (&ps->balance,
&ps->balance,
amount);
return GNUNET_OK;
}
/**
* Function called with details about account merge requests that have been
* made, with the goal of auditing the account merge execution.
*
* @param cls closure
* @param rowid unique serial ID for the deposit in our DB
* @param reserve_pub reserve affected by the merge
* @param purse_pub purse being merged
* @param h_contract_terms hash over contract of the purse
* @param purse_expiration when would the purse expire
* @param amount total amount in the purse
* @param min_age minimum age of all coins deposited into the purse
* @param flags how was the purse created
* @param purse_fee if a purse fee was paid, how high is it
* @param merge_timestamp when was the merge approved
* @param reserve_sig signature by reserve approving the merge
* @return #GNUNET_OK to continue to iterate, #GNUNET_SYSERR to stop
*/
static enum GNUNET_GenericReturnValue
handle_account_merged (
void *cls,
uint64_t rowid,
const struct TALER_ReservePublicKeyP *reserve_pub,
const struct TALER_PurseContractPublicKeyP *purse_pub,
const struct TALER_PrivateContractHashP *h_contract_terms,
struct GNUNET_TIME_Timestamp purse_expiration,
const struct TALER_Amount *amount,
uint32_t min_age,
enum TALER_WalletAccountMergeFlags flags,
const struct TALER_Amount *purse_fee,
struct GNUNET_TIME_Timestamp merge_timestamp,
const struct TALER_ReserveSignatureP *reserve_sig)
{
struct PurseContext *pc = cls;
struct PurseSummary *ps;
enum GNUNET_DB_QueryStatus qs;
/* should be monotonically increasing */
GNUNET_assert (rowid >= TALER_ARL_USE_PP (purse_account_merge_serial_id));
TALER_ARL_USE_PP (purse_account_merge_serial_id) = rowid + 1;
if (GNUNET_OK !=
TALER_wallet_account_merge_verify (merge_timestamp,
purse_pub,
purse_expiration,
h_contract_terms,
amount,
purse_fee,
min_age,
flags,
reserve_pub,
reserve_sig))
{
struct TALER_AUDITORDB_BadSigLosses bsl = {
.row_id = rowid,
.operation = "account-merge",
.loss = *purse_fee,
.operation_specific_pub = reserve_pub->eddsa_pub
};
qs = TALER_ARL_adb->insert_bad_sig_losses (
TALER_ARL_adb->cls,
&bsl);
if (qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
pc->qs = qs;
return GNUNET_SYSERR;
}
TALER_ARL_amount_add (&TALER_ARL_USE_AB (purse_total_bad_sig_loss),
&TALER_ARL_USE_AB (purse_total_bad_sig_loss),
purse_fee);
return GNUNET_OK;
}
ps = setup_purse (pc,
purse_pub);
if (NULL == ps)
{
if (0 > pc->qs)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == pc->qs);
return GNUNET_SYSERR;
}
GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == pc->qs);
qs = report_row_inconsistency ("account-merge",
rowid,
"purse not found");
if (0 > qs)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
pc->qs = qs;
return GNUNET_SYSERR;
}
return GNUNET_OK;
}
TALER_ARL_amount_add (&TALER_ARL_USE_AB (purse_global_balance),
&TALER_ARL_USE_AB (purse_global_balance),
purse_fee);
TALER_ARL_amount_add (&ps->balance,
&ps->balance,
purse_fee);
return GNUNET_OK;
}
/**
* Function called with details about purse decisions that have been made.
*
* @param cls closure
* @param rowid unique serial ID for the deposit in our DB
* @param purse_pub which purse was the decision made on
* @param refunded true if decision was to refund
* @return #GNUNET_OK to continue to iterate, #GNUNET_SYSERR to stop
*/
static enum GNUNET_GenericReturnValue
handle_purse_decision (
void *cls,
uint64_t rowid,
const struct TALER_PurseContractPublicKeyP *purse_pub,
bool refunded)
{
struct PurseContext *pc = cls;
struct PurseSummary *ps;
struct GNUNET_HashCode key;
enum GNUNET_DB_QueryStatus qs;
struct TALER_Amount purse_fee;
struct TALER_Amount balance_without_purse_fee;
TALER_ARL_USE_PP (purse_decision_serial_id) = rowid;
ps = setup_purse (pc,
purse_pub);
if (NULL == ps)
{
if (0 > pc->qs)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == pc->qs);
return GNUNET_SYSERR;
}
qs = report_row_inconsistency ("purse-decision",
rowid,
"purse not found");
if (0 > qs)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
pc->qs = qs;
return GNUNET_SYSERR;
}
return GNUNET_OK;
}
qs = get_purse_fee (ps->creation_date,
&purse_fee);
if (0 > qs)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
pc->qs = qs;
return GNUNET_SYSERR;
}
if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
return GNUNET_OK; /* already reported */
if (0 >
TALER_amount_subtract (&balance_without_purse_fee,
&ps->balance,
&purse_fee))
{
qs = report_row_inconsistency ("purse-request",
rowid,
"purse fee higher than balance");
if (0 > qs)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
pc->qs = qs;
return GNUNET_SYSERR;
}
GNUNET_assert (GNUNET_OK ==
TALER_amount_set_zero (TALER_ARL_currency,
&balance_without_purse_fee));
}
if (refunded)
{
if (-1 != TALER_amount_cmp (&balance_without_purse_fee,
&ps->total_value))
{
qs = report_amount_arithmetic_inconsistency ("purse-decision: refund",
rowid,
&balance_without_purse_fee,
&ps->total_value,
0);
if (0 > qs)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
pc->qs = qs;
return GNUNET_SYSERR;
}
}
}
else
{
if (-1 == TALER_amount_cmp (&balance_without_purse_fee,
&ps->total_value))
{
qs = report_amount_arithmetic_inconsistency ("purse-decision: merge",
rowid,
&ps->total_value,
&balance_without_purse_fee,
0);
if (0 > qs)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
pc->qs = qs;
return GNUNET_SYSERR;
}
TALER_ARL_amount_add (&TALER_ARL_USE_AB (
purse_total_balance_insufficient_loss),
&TALER_ARL_USE_AB (
purse_total_balance_insufficient_loss),
&ps->total_value);
}
}
qs = TALER_ARL_adb->delete_purse_info (TALER_ARL_adb->cls,
purse_pub);
if (qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
pc->qs = qs;
return GNUNET_SYSERR;
}
GNUNET_CRYPTO_hash (purse_pub,
sizeof (*purse_pub),
&key);
GNUNET_assert (GNUNET_YES ==
GNUNET_CONTAINER_multihashmap_remove (pc->purses,
&key,
ps));
GNUNET_free (ps);
return GNUNET_OK;
}
/**
* Function called on expired purses.
*
* @param cls closure
* @param purse_pub public key of the purse
* @param balance amount of money in the purse
* @param expiration_date when did the purse expire?
* @return #GNUNET_OK to continue to iterate
*/
static enum GNUNET_GenericReturnValue
handle_purse_expired (
void *cls,
const struct TALER_PurseContractPublicKeyP *purse_pub,
const struct TALER_Amount *balance,
struct GNUNET_TIME_Timestamp expiration_date)
{
struct PurseContext *pc = cls;
enum GNUNET_DB_QueryStatus qs;
struct TALER_AUDITORDB_PurseNotClosedInconsistencies pnci = {
.amount = *balance,
.expiration_date = expiration_date.abs_time,
.purse_pub = purse_pub->eddsa_pub
};
(void) pc;
qs = TALER_ARL_adb->insert_purse_not_closed_inconsistencies (
TALER_ARL_adb->cls,
&pnci);
if (qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
pc->qs = qs;
return GNUNET_SYSERR;
}
TALER_ARL_amount_add (&TALER_ARL_USE_AB (purse_total_delayed_decisions),
&TALER_ARL_USE_AB (purse_total_delayed_decisions),
balance);
return GNUNET_OK;
}
/**
* Check that the purse summary matches what the exchange database
* thinks about the purse, and update our own state of the purse.
*
* Remove all purses that we are happy with from the DB.
*
* @param cls our `struct PurseContext`
* @param key hash of the purse public key
* @param value a `struct PurseSummary`
* @return #GNUNET_OK to process more entries
*/
static enum GNUNET_GenericReturnValue
verify_purse_balance (void *cls,
const struct GNUNET_HashCode *key,
void *value)
{
struct PurseContext *pc = cls;
struct PurseSummary *ps = value;
enum GNUNET_DB_QueryStatus qs;
if (internal_checks)
{
struct TALER_Amount pf;
struct TALER_Amount balance_without_purse_fee;
/* subtract purse fee from ps->balance to get actual balance we expect, as
we track the balance including purse fee, while the exchange subtracts
the purse fee early on. */
qs = get_purse_fee (ps->creation_date,
&pf);
if (qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
pc->qs = qs;
return GNUNET_SYSERR;
}
if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
return GNUNET_OK; /* error already reported */
if (0 >
TALER_amount_subtract (&balance_without_purse_fee,
&ps->balance,
&pf))
{
qs = report_row_inconsistency ("purse",
0,
"purse fee higher than balance");
if (qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
pc->qs = qs;
return GNUNET_SYSERR;
}
GNUNET_assert (GNUNET_OK ==
TALER_amount_set_zero (TALER_ARL_currency,
&balance_without_purse_fee));
}
if (0 != TALER_amount_cmp (&ps->exchange_balance,
&balance_without_purse_fee))
{
qs = report_amount_arithmetic_inconsistency ("purse-balance",
0,
&ps->exchange_balance,
&balance_without_purse_fee,
0);
if (qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
pc->qs = qs;
return GNUNET_SYSERR;
}
}
}
if (ps->had_pi)
qs = TALER_ARL_adb->update_purse_info (TALER_ARL_adb->cls,
&ps->purse_pub,
&ps->balance);
else
qs = TALER_ARL_adb->insert_purse_info (TALER_ARL_adb->cls,
&ps->purse_pub,
&ps->balance,
ps->expiration_date);
if (qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
pc->qs = qs;
return GNUNET_SYSERR;
}
GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs);
return GNUNET_OK;
}
/**
* Clear memory from the purses hash map.
*
* @param cls our `struct PurseContext`
* @param key hash of the purse public key
* @param value a `struct PurseSummary`
* @return #GNUNET_OK to process more entries
*/
static enum GNUNET_GenericReturnValue
release_purse_balance (void *cls,
const struct GNUNET_HashCode *key,
void *value)
{
struct PurseContext *pc = cls;
struct PurseSummary *ps = value;
GNUNET_assert (GNUNET_YES ==
GNUNET_CONTAINER_multihashmap_remove (pc->purses,
key,
ps));
GNUNET_free (ps);
return GNUNET_OK;
}
/**
* Analyze purses for being well-formed.
*
* @param cls NULL
* @return transaction status code
*/
static enum GNUNET_DB_QueryStatus
analyze_purses (void *cls)
{
struct PurseContext pc;
enum GNUNET_DB_QueryStatus qs;
(void) cls;
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Analyzing purses\n");
qs = TALER_ARL_adb->get_auditor_progress (
TALER_ARL_adb->cls,
TALER_ARL_GET_PP (purse_account_merge_serial_id),
TALER_ARL_GET_PP (purse_decision_serial_id),
TALER_ARL_GET_PP (purse_deposits_serial_id),
TALER_ARL_GET_PP (purse_merges_serial_id),
TALER_ARL_GET_PP (purse_request_serial_id),
TALER_ARL_GET_PP (purse_open_counter),
NULL);
if (0 > qs)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
return qs;
}
if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
{
GNUNET_log (GNUNET_ERROR_TYPE_MESSAGE,
"First analysis using this auditor, starting audit from scratch\n");
}
else
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Resuming purse audit at %llu/%llu/%llu/%llu/%llu\n",
(unsigned long long) TALER_ARL_USE_PP (
purse_request_serial_id),
(unsigned long long) TALER_ARL_USE_PP (
purse_decision_serial_id),
(unsigned long long) TALER_ARL_USE_PP (
purse_merges_serial_id),
(unsigned long long) TALER_ARL_USE_PP (
purse_deposits_serial_id),
(unsigned long long) TALER_ARL_USE_PP (
purse_account_merge_serial_id));
}
pc.qs = GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
qs = TALER_ARL_adb->get_balance (
TALER_ARL_adb->cls,
TALER_ARL_GET_AB (purse_global_balance),
TALER_ARL_GET_AB (purse_total_balance_insufficient_loss),
TALER_ARL_GET_AB (purse_total_delayed_decisions),
TALER_ARL_GET_AB (purse_total_balance_purse_not_closed),
TALER_ARL_GET_AB (purse_total_arithmetic_delta_plus),
TALER_ARL_GET_AB (purse_total_arithmetic_delta_minus),
TALER_ARL_GET_AB (purse_total_bad_sig_loss),
NULL);
if (qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
return qs;
}
pc.purses = GNUNET_CONTAINER_multihashmap_create (512,
GNUNET_NO);
qs = TALER_ARL_edb->select_purse_requests_above_serial_id (
TALER_ARL_edb->cls,
TALER_ARL_USE_PP (purse_request_serial_id),
&handle_purse_requested,
&pc);
if (qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
return qs;
}
if (pc.qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == pc.qs);
return pc.qs;
}
qs = TALER_ARL_edb->select_purse_merges_above_serial_id (
TALER_ARL_edb->cls,
TALER_ARL_USE_PP (purse_merges_serial_id),
&handle_purse_merged,
&pc);
if (qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
return qs;
}
if (pc.qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == pc.qs);
return pc.qs;
}
qs = TALER_ARL_edb->select_purse_deposits_above_serial_id (
TALER_ARL_edb->cls,
TALER_ARL_USE_PP (purse_deposits_serial_id),
&handle_purse_deposits,
&pc);
if (qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
return qs;
}
if (pc.qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == pc.qs);
return pc.qs;
}
/* Charge purse fee! */
qs = TALER_ARL_edb->select_account_merges_above_serial_id (
TALER_ARL_edb->cls,
TALER_ARL_USE_PP (purse_account_merge_serial_id),
&handle_account_merged,
&pc);
if (qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
return qs;
}
if (pc.qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == pc.qs);
return pc.qs;
}
qs = TALER_ARL_edb->select_all_purse_decisions_above_serial_id (
TALER_ARL_edb->cls,
TALER_ARL_USE_PP (purse_decision_serial_id),
&handle_purse_decision,
&pc);
if (qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
return qs;
}
if (pc.qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == pc.qs);
return pc.qs;
}
qs = TALER_ARL_adb->select_purse_expired (
TALER_ARL_adb->cls,
&handle_purse_expired,
&pc);
if (qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
return qs;
}
if (pc.qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == pc.qs);
return pc.qs;
}
GNUNET_CONTAINER_multihashmap_iterate (pc.purses,
&verify_purse_balance,
&pc);
GNUNET_CONTAINER_multihashmap_iterate (pc.purses,
&release_purse_balance,
&pc);
GNUNET_break (0 ==
GNUNET_CONTAINER_multihashmap_size (pc.purses));
GNUNET_CONTAINER_multihashmap_destroy (pc.purses);
if (pc.qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == pc.qs);
return pc.qs;
}
qs = TALER_ARL_adb->insert_balance (
TALER_ARL_adb->cls,
TALER_ARL_SET_AB (purse_global_balance),
TALER_ARL_SET_AB (purse_total_balance_insufficient_loss),
TALER_ARL_SET_AB (purse_total_delayed_decisions),
TALER_ARL_SET_AB (purse_total_balance_purse_not_closed),
TALER_ARL_SET_AB (purse_total_arithmetic_delta_plus),
TALER_ARL_SET_AB (purse_total_arithmetic_delta_minus),
TALER_ARL_SET_AB (purse_total_bad_sig_loss),
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;
}
qs = TALER_ARL_adb->update_balance (
TALER_ARL_adb->cls,
TALER_ARL_SET_AB (purse_global_balance),
TALER_ARL_SET_AB (purse_total_balance_insufficient_loss),
TALER_ARL_SET_AB (purse_total_delayed_decisions),
TALER_ARL_SET_AB (purse_total_balance_purse_not_closed),
TALER_ARL_SET_AB (purse_total_arithmetic_delta_plus),
TALER_ARL_SET_AB (purse_total_arithmetic_delta_minus),
TALER_ARL_SET_AB (purse_total_bad_sig_loss),
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;
}
qs = TALER_ARL_adb->insert_auditor_progress (
TALER_ARL_adb->cls,
TALER_ARL_SET_PP (purse_account_merge_serial_id),
TALER_ARL_SET_PP (purse_decision_serial_id),
TALER_ARL_SET_PP (purse_deposits_serial_id),
TALER_ARL_SET_PP (purse_merges_serial_id),
TALER_ARL_SET_PP (purse_request_serial_id),
TALER_ARL_SET_PP (purse_open_counter),
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;
}
qs = TALER_ARL_adb->update_auditor_progress (
TALER_ARL_adb->cls,
TALER_ARL_SET_PP (purse_account_merge_serial_id),
TALER_ARL_SET_PP (purse_decision_serial_id),
TALER_ARL_SET_PP (purse_deposits_serial_id),
TALER_ARL_SET_PP (purse_merges_serial_id),
TALER_ARL_SET_PP (purse_request_serial_id),
TALER_ARL_SET_PP (purse_open_counter),
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;
}
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Concluded purse audit step at %llu/%llu/%llu/%llu/%llu\n",
(unsigned long long) TALER_ARL_USE_PP (
purse_request_serial_id),
(unsigned long long) TALER_ARL_USE_PP (
purse_decision_serial_id),
(unsigned long long) TALER_ARL_USE_PP (
purse_merges_serial_id),
(unsigned long long) TALER_ARL_USE_PP (
purse_deposits_serial_id),
(unsigned long long) TALER_ARL_USE_PP (
purse_account_merge_serial_id));
return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
}
/**
* Function called on events received from Postgres.
*
* @param cls closure, 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 to wake purses\n");
if (GNUNET_OK !=
TALER_ARL_setup_sessions_and_run (&analyze_purses,
NULL))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Audit failed\n");
GNUNET_SCHEDULER_shutdown ();
global_ret = EXIT_FAILURE;
return;
}
}
/**
* Function called on shutdown.
*/
static void
do_shutdown (void *cls)
{
(void) cls;
if (NULL != eh)
{
TALER_ARL_adb->event_listen_cancel (eh);
eh = NULL;
}
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 purses 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_PURSES)
};
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Running helper indefinitely\n");
eh = TALER_ARL_adb->event_listen (TALER_ARL_adb->cls,
&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_purses,
NULL))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Audit failed\n");
GNUNET_SCHEDULER_shutdown ();
global_ret = EXIT_FAILURE;
return;
}
}
/**
* The main function to check the database's handling of purses.
*
* @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;
/* force linker to link against libtalerutil; if we do
not do this, the linker may "optimize" libtalerutil
away and skip #TALER_OS_init(), which we do need */
(void) TALER_project_data_default ();
if (GNUNET_OK !=
GNUNET_STRINGS_get_utf8_args (argc, argv,
&argc, &argv))
return EXIT_INVALIDARGUMENT;
ret = GNUNET_PROGRAM_run (
argc,
argv,
"taler-helper-auditor-purses",
gettext_noop ("Audit Taler exchange purse handling"),
options,
&run,
NULL);
GNUNET_free_nz ((void *) argv);
if (GNUNET_SYSERR == ret)
return EXIT_INVALIDARGUMENT;
if (GNUNET_NO == ret)
return EXIT_SUCCESS;
return global_ret;
}
/* end of taler-helper-auditor-purses.c */