commit 2f28326170fe9fef8cadafe07c3f975b901dd573
parent 0acdec7b3de4e2019660dbbfa25a19f3c90a2b38
Author: Christian Grothoff <christian@grothoff.org>
Date: Sat, 17 Oct 2026 15:57:57 +0200
add max_p2p_expiration option
Diffstat:
12 files changed, 367 insertions(+), 14 deletions(-)
diff --git a/src/auditor/generate-auditor-basedb.conf b/src/auditor/generate-auditor-basedb.conf
@@ -14,11 +14,11 @@ SIGNKEY_LEGAL_DURATION = 4 weeks
db = postgres
BASE_URL = http://localhost:8081/
IDLE_RESERVE_EXPIRATION_TIME = 4 weeks
-LEGAL_RESERVE_EXPIRATION_TIME = 4 weeks
+LEGAL_RESERVE_EXPIRATION_TIME = 7 years
[exchangedb]
IDLE_RESERVE_EXPIRATION_TIME = 4 weeks
-LEGAL_RESERVE_EXPIRATION_TIME = 4 weeks
+LEGAL_RESERVE_EXPIRATION_TIME = 7 years
AGGREGATOR_SHIFT = 1 s
DEFAULT_PURSE_LIMIT = 1
@@ -45,7 +45,7 @@ CONFIG = postgresql:///auditor-basedb
CONFIG = postgres:///auditor-basedb
SQL_DIR = ${DATADIR}sql/
IDLE_RESERVE_EXPIRATION_TIME = 4 weeks
-LEGAL_RESERVE_EXPIRATION_TIME = 4 weeks
+LEGAL_RESERVE_EXPIRATION_TIME = 7 years
[exchange-account-1]
PAYTO_URI = payto://iban/DE89370400440532013000?receiver-name=Exchange+Company
diff --git a/src/exchange/exchange.conf b/src/exchange/exchange.conf
@@ -82,6 +82,14 @@ ASSET_TYPE = "fiat"
# FIXME: document!
ROUTER_IDLE_SLEEP_INTERVAL = 60 s
+# How far in the future may a purse expire? An expiring purse refunds the
+# coins deposited into it, and the auditor can only account for such a
+# refund while the denomination of the coin is still legally valid, so this
+# must not exceed the shortest DURATION_LEGAL of any denomination. The
+# exchange refuses to start if it does, and defaults to that shortest
+# DURATION_LEGAL (with a warning) if this option is not given.
+# PURSE_EXPIRATION_LIMIT = 3 years
+
# 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/taler-exchange-httpd.c b/src/exchange/taler-exchange-httpd.c
@@ -142,6 +142,8 @@ struct GNUNET_TIME_Relative TEH_reserve_closing_delay;
struct GNUNET_TIME_Relative TEH_default_p2p_expiration;
+struct GNUNET_TIME_Relative TEH_purse_expiration_limit;
+
struct GNUNET_TIME_Relative TEH_aml_program_timeout;
struct TALER_MasterPublicKeyP TEH_master_public_key;
@@ -2190,6 +2192,238 @@ handle_mhd_request (void *cls,
/**
+ * Closure for #denomination_duration_cb().
+ */
+struct LegalDurationContext
+{
+ /**
+ * Shortest DURATION_LEGAL of any denomination seen so far,
+ * #GNUNET_TIME_UNIT_FOREVER_REL if no denomination was seen.
+ */
+ struct GNUNET_TIME_Relative min;
+
+ /**
+ * Name of the section @e min was read from, NULL if none.
+ */
+ char *section;
+
+ /**
+ * Longest lifetime (DURATION_WITHDRAW plus DURATION_SPEND plus
+ * DURATION_LEGAL) of any denomination seen so far.
+ */
+ struct GNUNET_TIME_Relative max_lifetime;
+
+ /**
+ * Name of the section @e max_lifetime was computed from, NULL if none.
+ */
+ char *lifetime_section;
+
+ /**
+ * Set to true if a denomination section lacks one of the durations.
+ */
+ bool invalid;
+};
+
+
+/**
+ * Function called on each configuration section to find the shortest
+ * DURATION_LEGAL and the longest total lifetime configured for any
+ * denomination.
+ *
+ * @param cls a `struct LegalDurationContext *`
+ * @param section name of the section
+ */
+static void
+denomination_duration_cb (void *cls,
+ const char *section)
+{
+ struct LegalDurationContext *ldc = cls;
+ struct GNUNET_TIME_Relative withdraw_duration;
+ struct GNUNET_TIME_Relative deposit_duration;
+ struct GNUNET_TIME_Relative legal_duration;
+ struct GNUNET_TIME_Relative lifetime;
+
+ if (0 != strncasecmp (section,
+ "coin_",
+ strlen ("coin_")))
+ return; /* not a denomination type definition */
+ if (GNUNET_OK !=
+ GNUNET_CONFIGURATION_get_value_time (TEH_cfg,
+ section,
+ "DURATION_LEGAL",
+ &legal_duration))
+ {
+ GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
+ section,
+ "DURATION_LEGAL");
+ ldc->invalid = true;
+ return;
+ }
+ if (GNUNET_OK !=
+ GNUNET_CONFIGURATION_get_value_time (TEH_cfg,
+ section,
+ "DURATION_WITHDRAW",
+ &withdraw_duration))
+ {
+ GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
+ section,
+ "DURATION_WITHDRAW");
+ ldc->invalid = true;
+ return;
+ }
+ if (GNUNET_OK !=
+ GNUNET_CONFIGURATION_get_value_time (TEH_cfg,
+ section,
+ "DURATION_SPEND",
+ &deposit_duration))
+ {
+ GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
+ section,
+ "DURATION_SPEND");
+ ldc->invalid = true;
+ return;
+ }
+ lifetime = GNUNET_TIME_relative_add (
+ GNUNET_TIME_relative_add (withdraw_duration,
+ deposit_duration),
+ legal_duration);
+ if (GNUNET_TIME_relative_cmp (lifetime,
+ >,
+ ldc->max_lifetime))
+ {
+ ldc->max_lifetime = lifetime;
+ GNUNET_free (ldc->lifetime_section);
+ ldc->lifetime_section = GNUNET_strdup (section);
+ }
+ if (GNUNET_TIME_relative_cmp (legal_duration,
+ >=,
+ ldc->min))
+ return;
+ ldc->min = legal_duration;
+ GNUNET_free (ldc->section);
+ ldc->section = GNUNET_strdup (section);
+}
+
+
+/**
+ * Determine #TEH_purse_expiration_limit from the configuration. A purse
+ * that is not decided refunds its coins when it expires, and the auditor
+ * can only account for such a refund while the denomination of the coin is
+ * still legally valid. The limit must therefore not exceed the shortest
+ * DURATION_LEGAL of any denomination: as a deposit into a purse requires
+ * the denomination to be valid for deposits, its legal expiration is then
+ * at least DURATION_LEGAL away, and a purse created now expires at the
+ * latest after #TEH_purse_expiration_limit.
+ *
+ * @return #GNUNET_OK on success,
+ * #GNUNET_SYSERR if the configuration is invalid
+ */
+static enum GNUNET_GenericReturnValue
+load_denomination_duration_limits (void)
+{
+ struct LegalDurationContext ldc = {
+ .min = GNUNET_TIME_UNIT_FOREVER_REL
+ };
+ struct GNUNET_TIME_Relative lret;
+ enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR;
+
+ GNUNET_CONFIGURATION_iterate_sections (TEH_cfg,
+ &denomination_duration_cb,
+ &ldc);
+ if (ldc.invalid)
+ goto cleanup;
+ if (GNUNET_OK !=
+ GNUNET_CONFIGURATION_get_value_time (TEH_cfg,
+ "exchange",
+ "PURSE_EXPIRATION_LIMIT",
+ &TEH_purse_expiration_limit))
+ {
+ TEH_purse_expiration_limit = ldc.min;
+ GNUNET_log (
+ GNUNET_ERROR_TYPE_WARNING,
+ "PURSE_EXPIRATION_LIMIT not configured in section `exchange', using the shortest DURATION_LEGAL of any denomination (%s)\n",
+ GNUNET_TIME_relative2s (TEH_purse_expiration_limit,
+ true));
+ }
+ else if (GNUNET_TIME_relative_cmp (TEH_purse_expiration_limit,
+ >,
+ ldc.min))
+ {
+ GNUNET_log (
+ GNUNET_ERROR_TYPE_ERROR,
+ "PURSE_EXPIRATION_LIMIT (%s) in section `exchange' exceeds DURATION_LEGAL of denomination `%s'; a purse could then expire after the denomination of a coin in it stopped being legally valid\n",
+ GNUNET_TIME_relative2s (TEH_purse_expiration_limit,
+ true),
+ ldc.section);
+ goto cleanup;
+ }
+ if (GNUNET_TIME_relative_cmp (TEH_default_p2p_expiration,
+ >,
+ TEH_purse_expiration_limit))
+ {
+ /* #GNUNET_TIME_relative2s() returns a shared buffer, so the two
+ durations cannot be rendered in one statement. */
+ char *dp2p = GNUNET_strdup (
+ GNUNET_TIME_relative2s (TEH_default_p2p_expiration,
+ true));
+
+ GNUNET_log (
+ GNUNET_ERROR_TYPE_ERROR,
+ "DEFAULT_P2P_EXPIRATION (%s) in section `exchange' exceeds PURSE_EXPIRATION_LIMIT (%s), so the default expiration would be refused\n",
+ dp2p,
+ GNUNET_TIME_relative2s (TEH_purse_expiration_limit,
+ true));
+ GNUNET_free (dp2p);
+ goto cleanup;
+ }
+ /* The auditor books a denomination as finished once its legal period is
+ over, which requires the denomination to still be in the exchange's
+ database at that point. exchange_do_main_gc() keeps a denomination as
+ long as a withdraw row refers to it, and a withdraw row lives until the
+ reserve's gc_date (the time of the withdrawal plus
+ LEGAL_RESERVE_EXPIRATION_TIME) has passed. In the worst case a coin is
+ withdrawn at the very start of the denomination, so the whole lifetime
+ of the denomination must fit into LEGAL_RESERVE_EXPIRATION_TIME. */
+ if (GNUNET_OK !=
+ GNUNET_CONFIGURATION_get_value_time (TEH_cfg,
+ "exchangedb",
+ "LEGAL_RESERVE_EXPIRATION_TIME",
+ &lret))
+ {
+ GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
+ "exchangedb",
+ "LEGAL_RESERVE_EXPIRATION_TIME");
+ goto cleanup;
+ }
+ if (GNUNET_TIME_relative_cmp (lret,
+ <,
+ ldc.max_lifetime))
+ {
+ /* #GNUNET_TIME_relative2s() returns a shared buffer, so the two
+ durations cannot be rendered in one statement. */
+ char *lifetime = GNUNET_strdup (
+ GNUNET_TIME_relative2s (ldc.max_lifetime,
+ true));
+
+ GNUNET_log (
+ GNUNET_ERROR_TYPE_ERROR,
+ "DURATION_WITHDRAW plus DURATION_SPEND plus DURATION_LEGAL of denomination `%s' (%s) exceeds LEGAL_RESERVE_EXPIRATION_TIME (%s) of section `exchangedb'; the exchange could then garbage-collect the denomination before the auditor is allowed to finish accounting for it\n",
+ ldc.lifetime_section,
+ lifetime,
+ GNUNET_TIME_relative2s (lret,
+ true));
+ GNUNET_free (lifetime);
+ goto cleanup;
+ }
+ ret = GNUNET_OK;
+cleanup:
+ GNUNET_free (ldc.section);
+ GNUNET_free (ldc.lifetime_section);
+ return ret;
+}
+
+
+/**
* Load configuration parameters for the exchange
* server into the corresponding global variables.
*
@@ -2283,6 +2517,9 @@ exchange_serve_process_config (const char *cfg_fn)
4);
}
if (GNUNET_OK !=
+ load_denomination_duration_limits ())
+ return EXIT_NOTCONFIGURED;
+ if (GNUNET_OK !=
GNUNET_CONFIGURATION_get_value_time (TEH_cfg,
"exchangedb",
"MAX_AML_PROGRAM_RUNTIME",
diff --git a/src/exchange/taler-exchange-httpd.h b/src/exchange/taler-exchange-httpd.h
@@ -47,6 +47,14 @@ extern struct GNUNET_TIME_Relative TEH_reserve_closing_delay;
extern struct GNUNET_TIME_Relative TEH_default_p2p_expiration;
/**
+ * How far in the future may a purse expire? A purse that expires refunds
+ * the coins deposited into it, and that refund must still be auditable, so
+ * the expiration is bounded by the legal expiration of the denominations
+ * of the coins that can be deposited into the purse.
+ */
+extern struct GNUNET_TIME_Relative TEH_purse_expiration_limit;
+
+/**
* How long do we allow AML programs to run?
*/
extern struct GNUNET_TIME_Relative TEH_aml_program_timeout;
diff --git a/src/exchange/taler-exchange-httpd_common_deposit.c b/src/exchange/taler-exchange-httpd_common_deposit.c
@@ -27,7 +27,8 @@ enum GNUNET_GenericReturnValue
TEH_common_purse_deposit_parse_coin (
struct MHD_Connection *connection,
struct TEH_PurseDepositedCoin *coin,
- const json_t *jcoin)
+ const json_t *jcoin,
+ struct GNUNET_TIME_Timestamp purse_expiration)
{
struct GNUNET_JSON_Specification spec[] = {
TALER_JSON_spec_amount ("amount",
@@ -124,6 +125,25 @@ TEH_common_purse_deposit_parse_coin (
"PURSE CREATE"))
? GNUNET_NO : GNUNET_SYSERR;
}
+ if ( (! GNUNET_TIME_absolute_is_zero (purse_expiration.abs_time)) &&
+ (GNUNET_TIME_timestamp_cmp (purse_expiration,
+ >,
+ dk->meta.expire_legal)) )
+ {
+ /* The purse can expire with the coin still in it and refund it then.
+ After expire_legal the exchange may forget the denomination, so
+ such a refund could no longer be accounted for. TEH_cfg bounds
+ the purse expiration for new purses, but a denomination issued
+ under a shorter DURATION_LEGAL may still be in the key state. */
+ GNUNET_break_op (0);
+ GNUNET_JSON_parse_free (spec);
+ return (MHD_YES ==
+ TALER_MHD_reply_with_ec (
+ connection,
+ TALER_EC_EXCHANGE_PURSE_DENOMINATION_LEGAL_EXPIRES_TOO_SOON,
+ GNUNET_TIME_timestamp2s (dk->meta.expire_legal)))
+ ? GNUNET_NO : GNUNET_SYSERR;
+ }
if (GNUNET_TIME_absolute_is_future (dk->meta.start.abs_time))
{
/* This denomination is not yet valid */
diff --git a/src/exchange/taler-exchange-httpd_common_deposit.h b/src/exchange/taler-exchange-httpd_common_deposit.h
@@ -89,6 +89,10 @@ struct TEH_PurseDepositedCoin
* @param[in,out] connection our HTTP connection
* @param[out] coin coin to initialize
* @param jcoin coin to parse
+ * @param purse_expiration when the purse the coin is deposited into expires,
+ * #GNUNET_TIME_UNIT_ZERO_TS if the coin is not deposited into a purse;
+ * a purse must not outlive the legal expiration of the denomination,
+ * as it refunds the coin when it expires
* @return #GNUNET_OK on success, #GNUNET_NO if an error was returned,
* #GNUNET_SYSERR on failure and no error could be returned
*/
@@ -96,7 +100,8 @@ enum GNUNET_GenericReturnValue
TEH_common_purse_deposit_parse_coin (
struct MHD_Connection *connection,
struct TEH_PurseDepositedCoin *coin,
- const json_t *jcoin);
+ const json_t *jcoin,
+ struct GNUNET_TIME_Timestamp purse_expiration);
/**
diff --git a/src/exchange/taler-exchange-httpd_get-keys.c b/src/exchange/taler-exchange-httpd_get-keys.c
@@ -1831,7 +1831,7 @@ find_withdraw_family_cb (void *cls,
return GNUNET_OK;
}
family->next_start = GNUNET_TIME_timestamp_min (family->next_start,
- dk->meta.start);
+ dk->meta.start);
return GNUNET_OK;
}
@@ -1846,8 +1846,8 @@ find_withdraw_family_cb (void *cls,
*/
static enum GNUNET_GenericReturnValue
select_withdraw_denomination_cb (void *cls,
- const struct GNUNET_HashCode *h_denom_pub,
- void *value)
+ const struct GNUNET_HashCode *h_denom_pub,
+ void *value)
{
struct SelectWithdrawDenominationsContext *ctx = cls;
struct TEH_DenominationKey *dk = value;
@@ -2013,7 +2013,7 @@ build_withdraw_auditors (const struct TEH_KeyStateHandle *ksh)
dk = GNUNET_CONTAINER_multihashmap_get (ksh->denomkey_map,
&h_denom_pub.hash);
if ( (NULL != dk) &&
- dk->withdraw_relevant )
+ dk->withdraw_relevant)
GNUNET_assert (0 ==
json_array_append (filtered_keys,
denom_sig));
@@ -2059,7 +2059,8 @@ setup_response_headers (struct TEH_KeyStateHandle *ksh,
MHD_HTTP_HEADER_CACHE_CONTROL,
withdraw_filtered
? "public,must-revalidate"
- : "public,must-revalidate,max-age=86400")
+ :
+ "public,must-revalidate,max-age=86400")
);
if (! GNUNET_TIME_relative_is_zero (ksh->rekey_frequency))
{
@@ -2272,6 +2273,8 @@ create_krd (struct TEH_KeyStateHandle *ksh,
TEH_reserve_closing_delay),
GNUNET_JSON_pack_time_rel ("default_p2p_push_expiration",
TEH_default_p2p_expiration),
+ GNUNET_JSON_pack_time_rel ("max_p2p_expiration",
+ TEH_purse_expiration_limit),
GNUNET_JSON_pack_array_incref ("signkeys",
signkeys),
GNUNET_JSON_pack_array_incref ("recoup",
@@ -2933,7 +2936,8 @@ finish_keys_response (struct TEH_KeyStateHandle *ksh)
true))
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Failed to generate withdrawal-focused key response data\n");
+ "Failed to generate withdrawal-focused key response data\n")
+ ;
goto CLEANUP;
}
GNUNET_assert (1 == ksh->withdraw_krd_array_length);
diff --git a/src/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-create.c b/src/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-create.c
@@ -28,6 +28,7 @@
#include "taler/taler_mhd_lib.h"
#include "taler-exchange-httpd_common_deposit.h"
#include "taler-exchange-httpd_post-purses-PURSE_PUB-create.h"
+#include "taler-exchange-httpd.h"
#include "taler-exchange-httpd_responses.h"
#include "exchangedb_lib.h"
#include "taler-exchange-httpd_get-keys.h"
@@ -417,7 +418,8 @@ parse_coin (struct MHD_Connection *connection,
if (GNUNET_OK !=
(iret = TEH_common_purse_deposit_parse_coin (connection,
coin,
- jcoin)))
+ jcoin,
+ pcc->pd.purse_expiration)))
return iret;
if (GNUNET_OK !=
(iret = TEH_common_deposit_check_purse_deposit (
@@ -522,6 +524,24 @@ TEH_handler_purses_create (
TALER_EC_EXCHANGE_PURSE_CREATE_EXPIRATION_IS_NEVER,
NULL);
}
+ {
+ struct GNUNET_TIME_Timestamp limit
+ = GNUNET_TIME_relative_to_timestamp (TEH_purse_expiration_limit);
+
+ if (GNUNET_TIME_timestamp_cmp (pcc.pd.purse_expiration,
+ >,
+ limit))
+ {
+ /* An expiring purse refunds its coins, and that refund must remain
+ auditable; see TEH_purse_expiration_limit. */
+ GNUNET_break_op (0);
+ GNUNET_JSON_parse_free (spec);
+ return TALER_MHD_reply_with_ec (
+ connection,
+ TALER_EC_EXCHANGE_PURSE_CREATE_EXPIRATION_TOO_LATE,
+ GNUNET_TIME_timestamp2s (limit));
+ }
+ }
pcc.num_coins = json_array_size (deposits);
if ( (0 == pcc.num_coins) ||
(pcc.num_coins > TALER_MAX_COINS) )
diff --git a/src/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-deposit.c b/src/exchange/taler-exchange-httpd_post-purses-PURSE_PUB-deposit.c
@@ -320,7 +320,8 @@ parse_coin (struct MHD_Connection *connection,
if (GNUNET_OK !=
(iret = TEH_common_purse_deposit_parse_coin (connection,
coin,
- jcoin)))
+ jcoin,
+ pcc->purse_expiration)))
return iret;
if (GNUNET_OK !=
(iret = TEH_common_deposit_check_purse_deposit (
diff --git a/src/exchange/taler-exchange-httpd_post-reserves-RESERVE_PUB-open.c b/src/exchange/taler-exchange-httpd_post-reserves-RESERVE_PUB-open.c
@@ -393,7 +393,8 @@ TEH_handler_reserves_open (struct TEH_RequestContext *rc,
rc->connection,
coin,
json_array_get (payments,
- i));
+ i),
+ GNUNET_TIME_UNIT_ZERO_TS);
if (GNUNET_SYSERR == res)
{
GNUNET_break (0);
diff --git a/src/exchange/taler-exchange-httpd_post-reserves-RESERVE_PUB-purse.c b/src/exchange/taler-exchange-httpd_post-reserves-RESERVE_PUB-purse.c
@@ -30,6 +30,7 @@
#include "taler/taler_mhd_lib.h"
#include "taler-exchange-httpd_common_kyc.h"
#include "taler-exchange-httpd_post-reserves-RESERVE_PUB-purse.h"
+#include "taler-exchange-httpd.h"
#include "taler-exchange-httpd_responses.h"
#include "exchangedb_lib.h"
#include "taler-exchange-httpd_get-keys.h"
@@ -720,6 +721,24 @@ TEH_handler_reserves_purse (
TALER_EC_EXCHANGE_RESERVES_PURSE_EXPIRATION_IS_NEVER,
NULL);
}
+ {
+ struct GNUNET_TIME_Timestamp limit
+ = GNUNET_TIME_relative_to_timestamp (TEH_purse_expiration_limit);
+
+ if (GNUNET_TIME_timestamp_cmp (rpc->pd.purse_expiration,
+ >,
+ limit))
+ {
+ /* A purse that is not paid in full refunds the coins that were
+ deposited into it when it expires, and that refund must remain
+ auditable; see TEH_purse_expiration_limit. */
+ GNUNET_break_op (0);
+ return TALER_MHD_reply_with_ec (
+ connection,
+ TALER_EC_EXCHANGE_RESERVES_PURSE_EXPIRATION_TOO_LATE,
+ GNUNET_TIME_timestamp2s (limit));
+ }
+ }
{
struct TEH_KeyStateHandle *keys;
diff --git a/src/testing/test_exchange_p2p.c b/src/testing/test_exchange_p2p.c
@@ -217,9 +217,39 @@ run (void *cls,
"push-get-contract",
"create-reserve-2"),
+ /**
+ * A purse expiring after PURSE_EXPIRATION_LIMIT is refused: it would
+ * refund its coins at a point where the auditor can no longer account
+ * for the refund. The limit defaults to the shortest DURATION_LEGAL
+ * of any denomination, which is 3 years in the test configuration.
+ */
+ TALER_TESTING_cmd_purse_create_with_deposit (
+ "purse-with-deposit-expiring-after-limit",
+ MHD_HTTP_BAD_REQUEST,
+ "{\"amount\":\"EUR:1\",\"summary\":\"ice cream\"}",
+ true, /* upload contract */
+ GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_YEARS,
+ 4), /* expiration */
+ "withdraw-coin-1",
+ "EUR:1.01",
+ NULL),
TALER_TESTING_cmd_end ()
};
struct TALER_TESTING_Command pull[] = {
+ /**
+ * The same limit applies to a purse created together with its merge:
+ * such a purse also refunds the coins deposited into it if it is not
+ * paid in full by the time it expires.
+ */
+ TALER_TESTING_cmd_purse_create_with_reserve (
+ "purse-create-with-reserve-expiring-after-limit",
+ MHD_HTTP_BAD_REQUEST,
+ "{\"amount\":\"EUR:1\",\"summary\":\"ice cream\"}",
+ true /* upload contract */,
+ true /* pay purse fee */,
+ GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_YEARS,
+ 4), /* expiration */
+ "create-reserve-1"),
TALER_TESTING_cmd_purse_create_with_reserve (
"purse-create-with-reserve",
MHD_HTTP_OK,