commit e0639bf7621f4f65e5b773ec15b651ba2eeb436f
parent b1c49f631179bc4a39b00ece8713b3ebdb624162
Author: Christian Grothoff <christian@grothoff.org>
Date: Sat, 17 Oct 2026 17:06:41 +0200
add new purse expiration limit option
Diffstat:
2 files changed, 89 insertions(+), 0 deletions(-)
diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts
@@ -862,6 +862,88 @@ function setTalerPaths(config: Configuration, home: string, name?: string) {
config.setString("paths", "taler_cache_home", "$TALER_HOME/.config/taler/");
}
+/**
+ * Units of GNUnet's relative time syntax, in microseconds. Only the ones a
+ * denomination duration is plausibly written in.
+ */
+const relativeTimeUnits: { [unit: string]: number } = {
+ us: 1,
+ ms: 1000,
+ s: 1000 * 1000,
+ second: 1000 * 1000,
+ seconds: 1000 * 1000,
+ minute: 60 * 1000 * 1000,
+ minutes: 60 * 1000 * 1000,
+ hour: 60 * 60 * 1000 * 1000,
+ hours: 60 * 60 * 1000 * 1000,
+ day: 24 * 60 * 60 * 1000 * 1000,
+ days: 24 * 60 * 60 * 1000 * 1000,
+ week: 7 * 24 * 60 * 60 * 1000 * 1000,
+ weeks: 7 * 24 * 60 * 60 * 1000 * 1000,
+ month: 30 * 24 * 60 * 60 * 1000 * 1000,
+ months: 30 * 24 * 60 * 60 * 1000 * 1000,
+ year: 365 * 24 * 60 * 60 * 1000 * 1000,
+ years: 365 * 24 * 60 * 60 * 1000 * 1000,
+};
+
+/**
+ * Parse a duration as the exchange's configuration parser reads it.
+ */
+function parseRelativeTime(s: string): number {
+ let total = 0;
+ const re = /([0-9]+)\s*([a-zA-Z]+)/g;
+ let m: RegExpExecArray | null;
+ while ((m = re.exec(s)) !== null) {
+ const unit = relativeTimeUnits[m[2].toLowerCase()];
+ if (unit == null) {
+ throw new Error(`unsupported duration unit "${m[2]}" in "${s}"`);
+ }
+ total += Number.parseInt(m[1], 10) * unit;
+ }
+ if (total === 0) {
+ throw new Error(`cannot parse duration "${s}"`);
+ }
+ return total;
+}
+
+/**
+ * A purse refunds the coins deposited into it when it expires, and the
+ * auditor can only account for that refund while the denomination of the
+ * coin is still legally valid. The exchange therefore refuses a purse that
+ * expires after PURSE_EXPIRATION_LIMIT, and refuses to start when that limit
+ * exceeds the shortest DURATION_LEGAL of any denomination or falls below
+ * DEFAULT_P2P_EXPIRATION. Derive both from the denominations the test
+ * configured instead of leaving the exchange to warn about the default.
+ */
+function setPurseExpirationLimit(config: Configuration): void {
+ let limit: string | undefined;
+ let limitUs = Number.MAX_SAFE_INTEGER;
+ for (const section of config.getSectionNames()) {
+ if (!section.toLowerCase().startsWith("coin_")) {
+ continue;
+ }
+ const dl = config.getString(section, "duration_legal").getValue();
+ if (dl == null) {
+ continue;
+ }
+ const us = parseRelativeTime(dl);
+ if (us < limitUs) {
+ limitUs = us;
+ limit = dl;
+ }
+ }
+ if (limit == null) {
+ return;
+ }
+ config.setString("exchange", "purse_expiration_limit", limit);
+ const dp2p = config
+ .getString("exchange", "default_p2p_expiration")
+ .getValue();
+ if (dp2p != null && parseRelativeTime(dp2p) > limitUs) {
+ config.setString("exchange", "default_p2p_expiration", limit);
+ }
+}
+
function setExchangeCoin(config: Configuration, c: CoinConfig) {
const s = `coin_${c.name}`;
config.setString(s, "value", c.value);
@@ -1950,6 +2032,7 @@ export class ExchangeService implements ExchangeServiceInterface {
offeredCoins.forEach((cc) =>
setExchangeCoin(config, cc(this.exchangeConfig.currency)),
);
+ setPurseExpirationLimit(config);
config.writeTo(this.configFilename, { excludeDefaults: true });
}
@@ -1959,6 +2042,7 @@ export class ExchangeService implements ExchangeServiceInterface {
ConfigSources["taler-exchange"],
);
ccs.forEach((cc) => setExchangeCoin(config, cc));
+ setPurseExpirationLimit(config);
config.writeTo(this.configFilename, { excludeDefaults: true });
}
diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts
@@ -1875,6 +1875,11 @@ deploymentCli
out += "# Coin configuration for the exchange.\n";
out += '# Should be placed in "/etc/taler/config.d/exchange-coins.conf".\n';
+ out += "#\n";
+ out += "# The DURATION_LEGAL below bounds how far in the future a purse\n";
+ out +=
+ "# may expire: set PURSE_EXPIRATION_LIMIT in section [exchange] to\n";
+ out += "# at most that, or the exchange warns and uses it as the limit.\n";
out += "\n";
while (Amounts.cmp(x, coinMax) < 0) {