commit 87bf2728030373e7086d4878220a0737c2d40c9c parent 03b5285dd6f7f5a23a2c53b99f3ab3721bf033b1 Author: Christian Grothoff <christian@grothoff.org> Date: Sat, 17 Oct 2026 17:13:38 +0200 add PURSE_EXPIRATION_LIMIT to fix auditor issue with denominations changing after legal expiration Diffstat:
20 files changed, 565 insertions(+), 100 deletions(-)
diff --git a/contrib/packages/fedora/etc-taler-exchange/taler/conf.d/exchange-business.conf b/contrib/packages/fedora/etc-taler-exchange/taler/conf.d/exchange-business.conf @@ -28,6 +28,14 @@ ATTRIBUTE_ENCRYPTION_KEY = SET_ME_PLEASE # TERMS_ETAG = # PRIVACY_ETAG = +# How far in the future may a purse expire? This must not exceed the +# shortest DURATION_LEGAL of any denomination in exchange-coins.conf, as +# an expiring purse refunds the coins deposited into it and the auditor +# can only account for such a refund while their denomination is still +# legally valid. Without this option the exchange uses that shortest +# DURATION_LEGAL and warns about it. +# PURSE_EXPIRATION_LIMIT = 3 years + SERVE = unix UNIXPATH_MODE = 666 diff --git a/debian/etc-taler-exchange/taler-exchange/conf.d/exchange-business.conf b/debian/etc-taler-exchange/taler-exchange/conf.d/exchange-business.conf @@ -24,6 +24,14 @@ ATTRIBUTE_ENCRYPTION_KEY = SET_ME_PLEASE # TERMS_ETAG = # PRIVACY_ETAG = +# How far in the future may a purse expire? This must not exceed the +# shortest DURATION_LEGAL of any denomination in exchange-coins.conf, as +# an expiring purse refunds the coins deposited into it and the auditor +# can only account for such a refund while their denomination is still +# legally valid. Without this option the exchange uses that shortest +# DURATION_LEGAL and warns about it. +# PURSE_EXPIRATION_LIMIT = 3 years + # Bank accounts used by the exchange should be specified here: [exchange-account-1] diff --git a/src/auditor/batch.conf b/src/auditor/batch.conf @@ -133,6 +133,9 @@ NAME = Merchant Inc. KEYFILE = ${TALER_DATA_HOME}/merchant/default.priv [exchange] +# A purse must not outlive the denominations of the coins in it, see the +# comment on PURSE_EXPIRATION_LIMIT in exchange.conf. +PURSE_EXPIRATION_LIMIT = 3 years CURRENCY_ROUND_UNIT = TESTKUDOS:0.01 CURRENCY = TESTKUDOS diff --git a/src/auditor/generate-auditor-basedb.conf b/src/auditor/generate-auditor-basedb.conf @@ -5,6 +5,9 @@ TALER_DATA_HOME = $TALER_HOME/.local/share/taler/ TALER_HOME = ${PWD}/generate_auditordb_home/ [exchange] +# A purse must not outlive the denominations of the coins in it, see the +# comment on PURSE_EXPIRATION_LIMIT in exchange.conf. +PURSE_EXPIRATION_LIMIT = 3 years CURRENCY = TESTKUDOS CURRENCY_ROUND_UNIT = TESTKUDOS:0.01 MASTER_PUBLIC_KEY = M4FGP18EQFXFGGFQ1AWXHACN2JX0SMVK9CNF6459Z1WG18JSN0BG diff --git a/src/auditor/generate-kyc-basedb.conf b/src/auditor/generate-kyc-basedb.conf @@ -8,6 +8,9 @@ @INLINE@ generate-auditor-basedb.conf [exchange] +# A purse must not outlive the denominations of the coins in it, see the +# comment on PURSE_EXPIRATION_LIMIT in exchange.conf. +PURSE_EXPIRATION_LIMIT = 3 years ENABLE_KYC = YES diff --git a/src/auditor/taler-helper-auditor-coins.c b/src/auditor/taler-helper-auditor-coins.c @@ -28,6 +28,7 @@ #include "auditor-database/get_auditor_progress.h" #include "auditor-database/get_balance.h" #include "auditor-database/get_denomination_balance.h" +#include "auditor-database/get_exists_historic_denom_revenue.h" #include "auditor-database/insert_amount_arithmetic_inconsistency.h" #include "auditor-database/insert_auditor_progress.h" #include "auditor-database/insert_bad_sig_losses.h" @@ -44,6 +45,9 @@ #include "exchange-database/get_denomination_revocation.h" #include "exchange-database/get_known_coin.h" struct CoinContext; +struct SweepContext; +#define TALER_AUDITORDB_DENOMINATION_PENDING_RESULT_CLOSURE struct SweepContext +#include "auditor-database/iterate_denomination_pending.h" #define TALER_EXCHANGEDB_DENOMINATION_RESULT_CLOSURE enum GNUNET_DB_QueryStatus #define TALER_EXCHANGEDB_DEPOSIT_RESULT_CLOSURE struct CoinContext #define TALER_EXCHANGEDB_PURSE_DECISION_RESULT_CLOSURE struct CoinContext @@ -80,6 +84,12 @@ struct CoinContext; #define DEPOSIT_GRACE_PERIOD GNUNET_TIME_UNIT_DAYS /** + * How many pending denominations do we fetch per query when looking for + * those that no transaction of this pass touched? + */ +#define SWEEP_BATCH_SIZE 1024 + +/** * Return value from main(). */ static int global_ret; @@ -726,6 +736,14 @@ struct DenominationSummary * True if this denomination was revoked. */ bool was_revoked; + + /** + * True if the remaining balance of this denomination was already booked + * as revenue in an earlier pass, which left a historic revenue row + * behind. Then @e dcd only holds what happened to its coins in this + * pass. + */ + bool finalized; }; @@ -789,6 +807,21 @@ init_denomination (const struct TALER_DenominationHashP *denom_hash, GNUNET_assert (GNUNET_OK == TALER_amount_set_zero (TALER_ARL_currency, &ds->dcd.recoup_loss)); + /* No pending balance: either we never saw this denomination before, or + #sync_denomination() already booked its remaining balance as + revenue, which left a historic revenue row behind. */ + qs = TALER_AUDITORDB_get_exists_historic_denom_revenue (TALER_ARL_adb, + denom_hash); + if (0 > qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + ds->finalized = (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs); + if (ds->finalized) + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Denomination `%s' was already booked as finished\n", + GNUNET_h2s (&denom_hash->hash)); } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Starting balance for denomination `%s' is %s (%llu)\n", @@ -872,6 +905,241 @@ get_denomination_summary ( /** + * Check whether the deposit period of a denomination is over, so that no + * coin of it can be spent any more and the risk of a recoup on it is gone. + * Allows for a grace period of #DEPOSIT_GRACE_PERIOD, as clocks are not + * perfectly synchronized. + * + * @param issue denomination to check + * @return true if the deposit period of @a issue (plus grace) is over + */ +static bool +is_past_deposit_grace ( + const struct TALER_EXCHANGEDB_DenominationKeyInformation *issue) +{ + return GNUNET_TIME_absolute_cmp ( + GNUNET_TIME_absolute_get (), + >, + GNUNET_TIME_absolute_add (issue->expire_deposit.abs_time, + DEPOSIT_GRACE_PERIOD)); +} + + +/** + * Check whether the legal period of a denomination is over. From then on + * the exchange may forget the denomination, so nothing can move value on + * it any anymore and its remaining balance can be booked as revenue for + * good. + * + * @param issue denomination to check + * @return true if the legal period of @a issue (plus grace) is over + */ +static bool +is_past_legal_grace ( + const struct TALER_EXCHANGEDB_DenominationKeyInformation *issue) +{ + return GNUNET_TIME_absolute_cmp ( + GNUNET_TIME_absolute_get (), + >, + GNUNET_TIME_absolute_add (issue->expire_legal.abs_time, + DEPOSIT_GRACE_PERIOD)); +} + + +/** + * Take the risk of a denomination out of ``coin_balance_risk`` and zero it in @a + * ds. Called once the deposit period of the denomination is over as from + * then on there is nothing left to lose. + * + * @param[in,out] ds summary of the denomination + */ +static void +release_denomination_risk (struct DenominationSummary *ds) +{ + if (TALER_amount_is_zero (&ds->dcd.denom_risk)) + return; + TALER_ARL_amount_subtract (&TALER_ARL_USE_AB (coin_balance_risk), + &TALER_ARL_USE_AB (coin_balance_risk), + &ds->dcd.denom_risk); + /* If the above fails, our risk assessment is inconsistent! This is + really, really bad (auditor-internal invariant would be violated). + Hence we can "safely" assert. If this assertion fails, well, good + luck: there is a bug in the auditor _or_ the auditor's database is + corrupt. */ + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TALER_ARL_currency, + &ds->dcd.denom_risk)); +} + + +/** + * Book the remaining balance of a denomination as revenue: the coins can + * no longer be spent, so the exchange keeps their value and no longer + * holds it in escrow. Appends a row to the historic revenue table, which + * also marks the denomination as finished for the passes that follow. + * + * @param[in,out] ds summary of the denomination + * @param denom_h hash of the denomination public key + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +book_denomination_revenue (struct DenominationSummary *ds, + const struct TALER_DenominationHashP *denom_h) +{ + enum GNUNET_DB_QueryStatus qs; + + release_denomination_risk (ds); + if (! TALER_amount_is_zero (&ds->dcd.denom_balance)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Denomination `%s' is finished, booking %s in expiration profits\n", + GNUNET_h2s (&denom_h->hash), + TALER_amount2s (&ds->dcd.denom_balance)); + if (-1 == TALER_amount_cmp (&TALER_ARL_USE_AB (total_escrowed), + &ds->dcd.denom_balance)) + { + /* We never held that much in escrow? Can happen if the exchange + accepted coins it never issued (an emergency). */ + qs = report_amount_arithmetic_inconsistency ( + "subtracting expired coins from escrow balance", + 0, + &TALER_ARL_USE_AB (total_escrowed), + &ds->dcd.denom_balance, + 0); + if (0 > qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } + } + else + { + TALER_ARL_amount_subtract (&TALER_ARL_USE_AB (total_escrowed), + &TALER_ARL_USE_AB (total_escrowed), + &ds->dcd.denom_balance); + } + } + /* Written unconditionally: the row is what tells the next pass that this + denomination was already finished. */ + return TALER_AUDITORDB_insert_historic_denom_revenue ( + TALER_ARL_adb, + denom_h, + ds->issue->expire_deposit, + &ds->dcd.denom_balance, + &ds->dcd.recoup_loss); +} + + +/** + * Settle the state of a denomination at the end of a pass: release the + * risk once its deposit period is over, book its remaining balance as + * revenue once its legal period is over, and otherwise write the pending + * balance back to the database. + * + * @param cc our execution context + * @param denom_h hash of the denomination public key + * @param[in,out] ds summary of the denomination + * @return #GNUNET_OK on success, + * #GNUNET_SYSERR on failure (@e qs of @a cc is set) + */ +static enum GNUNET_GenericReturnValue +settle_denomination (struct CoinContext *cc, + const struct TALER_DenominationHashP *denom_h, + struct DenominationSummary *ds) +{ + const struct TALER_EXCHANGEDB_DenominationKeyInformation *issue = ds->issue; + enum GNUNET_DB_QueryStatus qs; + + if (ds->finalized) + { + /* The denomination was booked as finished in an earlier pass, but the + exchange moved value on it again. The bound on the purse expiration + is supposed to prevent that, so report it -- and append the credit + as another final row instead of rewriting the row that was already + published. */ + qs = report_row_inconsistency ( + "historic denomination revenue", + 0, + "denomination was credited after it was booked as finished"); + if (0 > qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + cc->qs = qs; + return GNUNET_SYSERR; + } + qs = book_denomination_revenue (ds, + denom_h); + if (0 > qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + cc->qs = qs; + return GNUNET_SYSERR; + } + return GNUNET_OK; + } + if (is_past_deposit_grace (issue)) + { + /* The coins of this denomination can no longer be deposited, so the + risk of a recoup on it is gone (stage one). What was not spent only + becomes revenue once the legal period is over as well: until then + the exchange still has to answer for those coins, and until then it + can still move value on the denomination, which the pending row has + to be around to record. */ + release_denomination_risk (ds); + } + if (is_past_legal_grace (issue)) + { + /* Nothing can happen to this denomination any more: book what is left + as revenue (stage two) and drop the pending row. The historic row + is final, and finalization happens exactly once. */ + if (ds->in_db) + { + qs = TALER_AUDITORDB_delete_denomination_balance (TALER_ARL_adb, + denom_h); + if (0 >= qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + cc->qs = qs; + return GNUNET_SYSERR; + } + } + qs = book_denomination_revenue (ds, + denom_h); + if (0 > qs) + { + /* Failed to store profits? Bad database */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + cc->qs = qs; + return GNUNET_SYSERR; + } + return GNUNET_OK; + } + /* Not finished yet, just store the current denomination summary to the + auditor database for the next iteration */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Final balance for denomination `%s' is %s (%llu)\n", + GNUNET_h2s (&denom_h->hash), + TALER_amount2s (&ds->dcd.denom_balance), + (unsigned long long) ds->dcd.num_issued); + if (ds->in_db) + qs = TALER_AUDITORDB_update_denomination_balance (TALER_ARL_adb, + denom_h, + &ds->dcd); + else + qs = TALER_AUDITORDB_insert_denomination_balance (TALER_ARL_adb, + denom_h, + &ds->dcd); + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + cc->qs = qs; + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** * Write information about the current knowledge about a denomination key * back to the database and update our global reporting data about the * denomination. @@ -893,25 +1161,23 @@ sync_denomination (void *cls, }; struct DenominationSummary *ds = value; const struct TALER_EXCHANGEDB_DenominationKeyInformation *issue = ds->issue; - struct GNUNET_TIME_Absolute now; - struct GNUNET_TIME_Timestamp expire_deposit; - struct GNUNET_TIME_Absolute expire_deposit_grace; enum GNUNET_DB_QueryStatus qs; - now = GNUNET_TIME_absolute_get (); - expire_deposit = issue->expire_deposit; - /* add day grace period to deal with clocks not being perfectly synchronized */ - expire_deposit_grace = GNUNET_TIME_absolute_add (expire_deposit.abs_time, - DEPOSIT_GRACE_PERIOD); /* Check for emergencies first: this must happen regardless of whether the denomination has expired in the meantime, as a denomination that was over-issued or over-spent still signals a key compromise (and rows are routinely first analyzed after the deposit expiration). */ { - long long cnt; + long long cnt = 0; - cnt = TALER_EXCHANGEDB_get_count_known_coins (TALER_ARL_edb, - &denom_h); + /* For a denomination that was already booked as finished, ds->dcd only + covers this pass and its num_issued is zero, so comparing it with + the number of known coins would report a false emergency. The count + was checked when the denomination was finished, and no coin can be + issued in it any more. */ + if (! ds->finalized) + cnt = TALER_EXCHANGEDB_get_count_known_coins (TALER_ARL_edb, + &denom_h); if (0 > cnt) { /* Failed to obtain count? Bad database */ @@ -949,87 +1215,153 @@ sync_denomination (void *cls, } } } - if (GNUNET_TIME_absolute_cmp (now, - >, - expire_deposit_grace)) + return settle_denomination (cc, + &denom_h, + ds); +} + + +/** + * Closure for #sweep_denomination_cb(). + */ +struct SweepContext +{ + /** + * Our execution context. + */ + struct CoinContext *cc; + + /** + * Row ID of the last pending denomination we looked at. + */ + uint64_t last_row; + + /** + * Number of pending denominations we looked at. + */ + unsigned int count; + + /** + * Transaction status code. + */ + enum GNUNET_DB_QueryStatus qs; +}; + + +/** + * Function called on each denomination that still has a pending balance. + * Settles those that no transaction of this pass touched; the ones that + * were touched are settled by #sync_denomination() and are skipped here. + * + * @param sc context for the sweep + * @param serial_id row of the pending balance + * @param dp the pending balance + * @return #GNUNET_OK to continue to iterate + */ +static enum GNUNET_GenericReturnValue +sweep_denomination_cb (struct SweepContext *sc, + uint64_t serial_id, + const struct TALER_AUDITORDB_DenominationPending *dp) +{ + struct CoinContext *cc = sc->cc; + const struct TALER_EXCHANGEDB_DenominationKeyInformation *issue; + struct DenominationSummary *ds; + enum GNUNET_DB_QueryStatus qs; + enum GNUNET_GenericReturnValue ret; + + sc->last_row = serial_id; + sc->count++; + if (NULL != + GNUNET_CONTAINER_multihashmap_get (cc->denom_summaries, + &dp->denom_pub_hash.hash)) + return GNUNET_OK; /* settled by #sync_denomination() */ + qs = TALER_ARL_get_denomination_info_by_hash (&dp->denom_pub_hash, + &issue); + if (0 > qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + sc->qs = qs; + return GNUNET_SYSERR; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { - /* Denomination key has expired, book remaining balance of - outstanding coins as revenue; and reduce cc->risk exposure. */ - if (ds->in_db) - qs = TALER_AUDITORDB_delete_denomination_balance (TALER_ARL_adb, - &denom_h); - else - qs = GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; - if (qs < 0) + /* The exchange no longer knows the denomination, so we cannot tell + when it ends and can never settle this balance. exchange_do_main_gc + only drops a denomination once its legal period is over, so this + means that the exchange forgot it before we were done with it. */ + qs = report_row_inconsistency ( + "denomination pending", + serial_id, + "denomination with a pending balance is unknown to the exchange"); + if (0 > qs) { GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); - cc->qs = qs; + sc->qs = qs; return GNUNET_SYSERR; } - if ( (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) && - (! TALER_amount_is_zero (&ds->dcd.denom_risk)) ) - { - /* The denomination expired and carried a balance; we can now - book the remaining balance as profit, and reduce our risk - exposure by the accumulated risk of the denomination. */ - TALER_ARL_amount_subtract (&TALER_ARL_USE_AB (coin_balance_risk), - &TALER_ARL_USE_AB (coin_balance_risk), - &ds->dcd.denom_risk); - /* If the above fails, our risk assessment is inconsistent! - This is really, really bad (auditor-internal invariant - would be violated). Hence we can "safely" assert. If - this assertion fails, well, good luck: there is a bug - in the auditor _or_ the auditor's database is corrupt. */ - } - if ( (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) && - (! TALER_amount_is_zero (&ds->dcd.denom_balance)) ) - { - /* book denom_balance coin expiration profits! */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Denomination `%s' expired, booking %s in expiration profits\n", - GNUNET_h2s (denom_hash), - TALER_amount2s (&ds->dcd.denom_balance)); - qs = TALER_AUDITORDB_insert_historic_denom_revenue ( - TALER_ARL_adb, - &denom_h, - expire_deposit, - &ds->dcd.denom_balance, - &ds->dcd.recoup_loss); - if (qs < 0) - { - /* Failed to store profits? Bad database */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); - cc->qs = qs; - return GNUNET_SYSERR; - } - } + return GNUNET_OK; } - else + if (! is_past_deposit_grace (issue)) + return GNUNET_OK; /* nothing to settle yet */ + /* No coin of this denomination was created or spent in this pass, so + neither the number of known coins nor the balance can have changed + since the pass that last touched it: the emergency checks of + #sync_denomination() would only repeat themselves here. */ + ds = GNUNET_new (struct DenominationSummary); + ds->issue = issue; + ds->in_db = true; + ds->dcd.denom_balance = dp->denom_balance; + ds->dcd.denom_loss = dp->denom_loss; + ds->dcd.denom_risk = dp->denom_risk; + ds->dcd.recoup_loss = dp->recoup_loss; + ds->dcd.num_issued = dp->num_issued; + ret = settle_denomination (cc, + &dp->denom_pub_hash, + ds); + GNUNET_free (ds); + if (GNUNET_OK != ret) + sc->qs = cc->qs; + return ret; +} + + +/** + * Settle the denominations that still have a pending balance but that no + * transaction of this pass touched. Without this, a denomination whose + * coins see no activity after its deposit period would keep its risk and + * its outstanding balance on the auditor's books forever. + * + * @param cc our execution context + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +sweep_denominations (struct CoinContext *cc) +{ + struct SweepContext sc = { + .cc = cc + }; + + while (true) { - /* Not expired, just store current denomination summary - to auditor database for next iteration */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Final balance for denomination `%s' is %s (%llu)\n", - GNUNET_h2s (denom_hash), - TALER_amount2s (&ds->dcd.denom_balance), - (unsigned long long) ds->dcd.num_issued); - if (ds->in_db) - qs = TALER_AUDITORDB_update_denomination_balance (TALER_ARL_adb, - &denom_h, - &ds->dcd); - else - qs = TALER_AUDITORDB_insert_denomination_balance (TALER_ARL_adb, - &denom_h, - &ds->dcd); + enum GNUNET_DB_QueryStatus qs; - if (qs < 0) + sc.count = 0; + qs = TALER_AUDITORDB_iterate_denomination_pending ( + TALER_ARL_adb, + SWEEP_BATCH_SIZE, + sc.last_row, + &sweep_denomination_cb, + &sc); + if (0 > qs) { GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); - cc->qs = qs; - return GNUNET_SYSERR; + return qs; } + if (0 > sc.qs) + return sc.qs; + if (SWEEP_BATCH_SIZE > sc.count) + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; } - return GNUNET_OK; } @@ -1976,15 +2308,22 @@ refund_cb (struct CoinContext *cc, TALER_ARL_amount_add (&ds->dcd.denom_balance, &ds->dcd.denom_balance, &amount_without_fee); - TALER_ARL_amount_add (&ds->dcd.denom_risk, - &ds->dcd.denom_risk, - &amount_without_fee); TALER_ARL_amount_add (&TALER_ARL_USE_AB (total_escrowed), &TALER_ARL_USE_AB (total_escrowed), &amount_without_fee); - TALER_ARL_amount_add (&TALER_ARL_USE_AB (coin_balance_risk), - &TALER_ARL_USE_AB (coin_balance_risk), - &amount_without_fee); + /* A coin of a denomination whose deposit period is over cannot be + recouped (#TEH_recoup_check_keys() refuses it), so crediting it back + does not put anything at risk again; see + #release_denomination_risk(). */ + if (! is_past_deposit_grace (issue)) + { + TALER_ARL_amount_add (&ds->dcd.denom_risk, + &ds->dcd.denom_risk, + &amount_without_fee); + TALER_ARL_amount_add (&TALER_ARL_USE_AB (coin_balance_risk), + &TALER_ARL_USE_AB (coin_balance_risk), + &amount_without_fee); + } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "New balance of denomination `%s' after refund is %s\n", GNUNET_h2s (&issue->denom_hash.hash), @@ -2075,15 +2414,22 @@ purse_refund_coin_cb ( TALER_ARL_amount_add (&ds->dcd.denom_balance, &ds->dcd.denom_balance, amount_with_fee); - TALER_ARL_amount_add (&ds->dcd.denom_risk, - &ds->dcd.denom_risk, - amount_with_fee); TALER_ARL_amount_add (&TALER_ARL_USE_AB (total_escrowed), &TALER_ARL_USE_AB (total_escrowed), amount_with_fee); - TALER_ARL_amount_add (&TALER_ARL_USE_AB (coin_balance_risk), - &TALER_ARL_USE_AB (coin_balance_risk), - amount_with_fee); + /* A coin of a denomination whose deposit period is over cannot be + recouped (#TEH_recoup_check_keys() refuses it), so crediting it back + does not put anything at risk again; see + #release_denomination_risk(). */ + if (! is_past_deposit_grace (issue)) + { + TALER_ARL_amount_add (&ds->dcd.denom_risk, + &ds->dcd.denom_risk, + amount_with_fee); + TALER_ARL_amount_add (&TALER_ARL_USE_AB (coin_balance_risk), + &TALER_ARL_USE_AB (coin_balance_risk), + amount_with_fee); + } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "New balance of denomination `%s' after purse-refund is %s\n", GNUNET_h2s (&issue->denom_hash.hash), @@ -2978,6 +3324,14 @@ analyze_coins (void *cls) goto cleanup; } + /* settle the denominations that nothing touched in this pass */ + qs = sweep_denominations (&cc); + if (0 > qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + goto cleanup; + } + qs = TALER_AUDITORDB_insert_balance ( TALER_ARL_adb, TALER_ARL_SET_AB (coin_balance_risk), diff --git a/src/benchmark/benchmark-common.conf b/src/benchmark/benchmark-common.conf @@ -7,6 +7,9 @@ CURRENCY=EUR CURRENCY_ROUND_UNIT=EUR:0.01 [exchange] +# A purse must not outlive the denominations of the coins in it, see the +# comment on PURSE_EXPIRATION_LIMIT in exchange.conf. +PURSE_EXPIRATION_LIMIT = 3 years SIGNKEY_LEGAL_DURATION=2 years PORT=8081 MASTER_PUBLIC_KEY=98NJW3CQHZQGQXTY3K85K531XKPAPAVV4Q5V8PYYRR00NJGZWNVG diff --git a/src/exchange/exchange.conf b/src/exchange/exchange.conf @@ -90,6 +90,12 @@ ROUTER_IDLE_SLEEP_INTERVAL = 60 s # DURATION_LEGAL (with a warning) if this option is not given. # PURSE_EXPIRATION_LIMIT = 3 years +# How long does a purse last if the client does not specify an expiration +# of its own? Must not exceed PURSE_EXPIRATION_LIMIT, as the exchange +# would otherwise refuse every purse that relies on this default; the +# exchange refuses to start if it does. +DEFAULT_P2P_EXPIRATION = 4 weeks + # How big is an individual shard to be processed # by taler-exchange-expire (in time). It may take # this much time for an expired purse to be really diff --git a/src/exchange/test_taler_exchange_httpd.conf b/src/exchange/test_taler_exchange_httpd.conf @@ -12,6 +12,9 @@ CURRENCY_ROUND_UNIT = EUR:0.01 TINY_AMOUNT = EUR:0.01 [exchange] +# A purse must not outlive the denominations of the coins in it, see the +# comment on PURSE_EXPIRATION_LIMIT in exchange.conf. +PURSE_EXPIRATION_LIMIT = 3 years # Directory with our terms of service. TERMS_DIR = ../../contrib/tos diff --git a/src/exchange/test_taler_exchange_unix.conf b/src/exchange/test_taler_exchange_unix.conf @@ -9,6 +9,9 @@ CURRENCY = EUR CURRENCY_ROUND_UNIT = EUR:0.01 [exchange] +# A purse must not outlive the denominations of the coins in it, see the +# comment on PURSE_EXPIRATION_LIMIT in exchange.conf. +PURSE_EXPIRATION_LIMIT = 3 years # Directory with our terms of service. TERMS_DIR = ../../contrib/tos diff --git a/src/exchangedb/exchangedb.conf b/src/exchangedb/exchangedb.conf @@ -13,9 +13,13 @@ IDLE_RESERVE_EXPIRATION_TIME = 4 weeks -# After how long do we forget about reserves? Should be above -# the legal expiration timeframe of withdrawn coins. -LEGAL_RESERVE_EXPIRATION_TIME = 7 years +# After how long do we forget about reserves? Must be above the legal +# expiration timeframe of withdrawn coins: a withdrawal has to stay in the +# database until the auditor is done accounting for the denomination it +# was made with, so this has to be at least DURATION_WITHDRAW plus +# DURATION_SPEND plus DURATION_LEGAL of every denomination. The exchange +# refuses to start if it is not. +LEGAL_RESERVE_EXPIRATION_TIME = 10 years # What is the desired delay between a transaction being ready and the # aggregator triggering on it? diff --git a/src/include/taler/taler_error_codes.h b/src/include/taler/taler_error_codes.h @@ -1736,6 +1736,14 @@ enum TALER_ErrorCode /** + * The purse expiration time is further in the future than the exchange allows. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + TALER_EC_EXCHANGE_RESERVES_PURSE_EXPIRATION_TOO_LATE = 1779, + + + /** * The signature by the reserve affirming the open operation is invalid. * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). * (A value of 0 indicates that the error is generated client-side). @@ -2096,6 +2104,22 @@ enum TALER_ErrorCode /** + * The purse expiration time is further in the future than the exchange allows. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + TALER_EC_EXCHANGE_PURSE_CREATE_EXPIRATION_TOO_LATE = 1862, + + + /** + * The denomination of a coin deposited into the purse stops being legally valid before the purse expiration time. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + TALER_EC_EXCHANGE_PURSE_DENOMINATION_LEGAL_EXPIRES_TOO_SOON = 1863, + + + /** * The signature using the merge key is invalid. * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). * (A value of 0 indicates that the error is generated client-side). diff --git a/src/testing/test-taler-exchange-aggregator-postgres.conf b/src/testing/test-taler-exchange-aggregator-postgres.conf @@ -13,6 +13,9 @@ LOOKAHEAD_SIGN = 24 days DURATION = 14 days [exchange] +# A purse must not outlive the denominations of the coins in it, see the +# comment on PURSE_EXPIRATION_LIMIT in exchange.conf. +PURSE_EXPIRATION_LIMIT = 3 years CURRENCY = EUR CURRENCY_ROUND_UNIT = EUR:0.01 TINY_AMOUNT = EUR:0.01 diff --git a/src/testing/test-taler-exchange-wirewatch-postgres.conf b/src/testing/test-taler-exchange-wirewatch-postgres.conf @@ -12,6 +12,9 @@ LOOKAHEAD_SIGN = 24 days DURATION = 14 days [exchange] +# A purse must not outlive the denominations of the coins in it, see the +# comment on PURSE_EXPIRATION_LIMIT in exchange.conf. +PURSE_EXPIRATION_LIMIT = 3 years CURRENCY = EUR CURRENCY_ROUND_UNIT = EUR:0.01 TINY_AMOUNT = EUR:0.01 diff --git a/src/testing/test_exchange_api.conf b/src/testing/test_exchange_api.conf @@ -51,6 +51,9 @@ SQL_DIR = ${DATADIR}sql/ HTTP_PORT = 8082 [exchange] +# A purse must not outlive the denominations of the coins in it, see the +# comment on PURSE_EXPIRATION_LIMIT in exchange.conf. +PURSE_EXPIRATION_LIMIT = 3 years CURRENCY = EUR CURRENCY_ROUND_UNIT = EUR:0.01 TINY_AMOUNT = EUR:0.01 diff --git a/src/testing/test_exchange_api_age_restriction.conf b/src/testing/test_exchange_api_age_restriction.conf @@ -5,6 +5,9 @@ TALER_TEST_HOME = test_exchange_api_home/ [exchange] +# A purse must not outlive the denominations of the coins in it, see the +# comment on PURSE_EXPIRATION_LIMIT in exchange.conf. +PURSE_EXPIRATION_LIMIT = 3 years CURRENCY = EUR CURRENCY_ROUND_UNIT = EUR:0.01 diff --git a/src/testing/test_exchange_api_keys_cherry_picking.conf b/src/testing/test_exchange_api_keys_cherry_picking.conf @@ -15,6 +15,10 @@ DURATION = 30 s LOOKAHEAD_SIGN = 20 s [exchange] +# The denominations of this test are legally valid for 7 s only, so +# neither a purse nor the default expiration of a purse may outlive them. +PURSE_EXPIRATION_LIMIT = 7 s +DEFAULT_P2P_EXPIRATION = 7 s CURRENCY = EUR CURRENCY_ROUND_UNIT = EUR:0.01 TINY_AMOUNT = EUR:0.01 diff --git a/src/testing/test_sanctions.conf b/src/testing/test_sanctions.conf @@ -14,6 +14,9 @@ DURATION = 14 days HTTP_PORT = 8082 [exchange] +# A purse must not outlive the denominations of the coins in it, see the +# comment on PURSE_EXPIRATION_LIMIT in exchange.conf. +PURSE_EXPIRATION_LIMIT = 3 years CURRENCY = EUR CURRENCY_ROUND_UNIT = EUR:0.01 TINY_AMOUNT = EUR:0.01 diff --git a/src/util/taler_error_codes.c b/src/util/taler_error_codes.c @@ -1690,6 +1690,14 @@ static const struct ErrorCodeAndHint code_hint_pairs[] = { }, { + /* 1779 */ + .ec = TALER_EC_EXCHANGE_RESERVES_PURSE_EXPIRATION_TOO_LATE, + .hint = + "The purse expiration time is further in the future than the exchange allows.", + .http_code = MHD_HTTP_BAD_REQUEST + }, + + { /* 1785 */ .ec = TALER_EC_EXCHANGE_RESERVES_OPEN_BAD_SIGNATURE, .hint = @@ -2025,6 +2033,22 @@ static const struct ErrorCodeAndHint code_hint_pairs[] = { }, { + /* 1862 */ + .ec = TALER_EC_EXCHANGE_PURSE_CREATE_EXPIRATION_TOO_LATE, + .hint = + "The purse expiration time is further in the future than the exchange allows.", + .http_code = MHD_HTTP_BAD_REQUEST + }, + + { + /* 1863 */ + .ec = TALER_EC_EXCHANGE_PURSE_DENOMINATION_LEGAL_EXPIRES_TOO_SOON, + .hint = + "The denomination of a coin deposited into the purse stops being legally valid before the purse expiration time.", + .http_code = MHD_HTTP_BAD_REQUEST + }, + + { /* 1876 */ .ec = TALER_EC_EXCHANGE_PURSE_MERGE_INVALID_MERGE_SIGNATURE, .hint = "The signature using the merge key is invalid.", @@ -6274,7 +6298,7 @@ static const struct ErrorCodeAndHint code_hint_pairs[] = { /** * The length of @e code_hint_pairs. */ -static const unsigned int code_hint_pairs_length = 808; +static const unsigned int code_hint_pairs_length = 811; const char * diff --git a/src/util/test_crypto.c b/src/util/test_crypto.c @@ -234,13 +234,13 @@ test_planchets_cs (uint8_t age) if (0 < age) { struct TALER_AgeCommitmentProof acp; - struct GNUNET_HashCode seed; + struct GNUNET_HashCode aseed; - GNUNET_CRYPTO_random_block (&seed, - sizeof(seed)); + GNUNET_CRYPTO_random_block (&aseed, + sizeof(aseed)); TALER_age_restriction_commit (&age_mask, age, - &seed, + &aseed, &acp); TALER_age_commitment_hash (&acp.commitment, &ah);