commit 03f3b96138999431f117c4fec5001c43c8ae5665
parent 9bf6ea3483c8e7426141fe1f0d979e70e4e043b3
Author: bohdan-potuzhnyi <bohdan.potuzhnyi@gmail.com>
Date: Mon, 7 Sep 2026 01:11:07 +0200
dd98 related updates
Diffstat:
5 files changed, 51 insertions(+), 10 deletions(-)
diff --git a/core/merchant/get-fountain-info.rst b/core/merchant/get-fountain-info.rst
@@ -20,6 +20,9 @@
:http:statuscode:`401 Unauthorized`:
The fountain secret is unknown. This includes deleted
fountains: the wallet should report the access as revoked.
+ :http:statuscode:`404 Not found`:
+ A token family disappeared during issue-key resolution, returned with
+ ``TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN``.
:http:statuscode:`500 Internal Server Error`:
The server experienced an internal failure.
@@ -34,6 +37,7 @@
// Grants of this fountain, with the token family metadata
// and issue keys needed to prepare blinded envelopes.
+ // May be empty; this does not revoke the credential.
grants: FountainWalletGrant[];
}
diff --git a/core/merchant/patch-private-fountains-FOUNTAIN_ID.rst b/core/merchant/patch-private-fountains-FOUNTAIN_ID.rst
@@ -20,10 +20,10 @@
The request body is malformed.
Returned with ``TALER_EC_GENERIC_PARAMETER_MALFORMED``.
:http:statuscode:`404 Not found`:
- The fountain or merchant instance is unknown, or a
- ``token_family_slug`` in the grants does not refer to a known
- token family.
- Returned with ``TALER_EC_MERCHANT_GENERIC_FOUNTAIN_UNKNOWN``.
+ The fountain or merchant instance is unknown. An unknown fountain is
+ returned with ``TALER_EC_MERCHANT_GENERIC_FOUNTAIN_UNKNOWN``.
+ An unknown ``token_family_slug`` in the grants is returned with
+ ``TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN``.
:http:statuscode:`500 Internal Server Error`:
The server experienced an internal failure.
diff --git a/core/merchant/post-fountain-withdraw.rst b/core/merchant/post-fountain-withdraw.rst
@@ -10,6 +10,23 @@
The fountain secret travels in the request body, never in the
URL.
+ A wallet may retry a withdrawal with the same ``grants`` array and
+ fountain credential. The backend identifies a completed withdrawal
+ by its canonical JSON hash within that fountain and returns the
+ original grant results, without consuming quota again. Keep the
+ envelopes, array order, and ``valid_at`` values (including omission)
+ unchanged when retrying; JSON whitespace and object member order do
+ not matter.
+
+ A completed result stays available for as long as the tokens it covers
+ can still be used, that is until the issue keys it was signed with stop
+ being valid. This holds across changes to the grants or the key window
+ of the fountain. Afterwards the result is garbage collected along with
+ the withdrawal counters it accounts for, and a repeated request is
+ treated as a new one; deleting the fountain drops its results at once.
+ Quota consumption, issued-token records and the replay result are
+ committed together before a successful response is sent.
+
**Request:**
The request must be a `FountainWithdrawRequest`.
@@ -25,6 +42,10 @@
:http:statuscode:`401 Unauthorized`:
The fountain secret is unknown. This includes deleted
fountains: the wallet should report the access as revoked.
+ :http:statuscode:`404 Not found`:
+ A token family or issue key disappeared during processing, returned with
+ ``TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN``
+ or ``TALER_EC_MERCHANT_GENERIC_TOKEN_KEY_UNKNOWN``.
:http:statuscode:`409 Conflict`:
An entry refers to a token family without a matching grant,
or its ``valid_at`` lies outside the grant's key window.
diff --git a/core/merchant/post-private-fountains.rst b/core/merchant/post-private-fountains.rst
@@ -23,6 +23,8 @@
:http:statuscode:`404 Not found`:
The merchant instance is unknown, or a ``token_family_slug``
in the grants does not refer to a known token family.
+ The latter is returned with
+ ``TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN``.
:http:statuscode:`500 Internal Server Error`:
The server experienced an internal failure.
@@ -43,7 +45,7 @@
poll_freq: RelativeTime;
// One grant per token family the user may withdraw
- // tokens for.
+ // tokens for. May be empty; grants can be added later.
grants: FountainGrant[];
}
diff --git a/design-documents/098-token-fountains.rst b/design-documents/098-token-fountains.rst
@@ -4,7 +4,7 @@ DD 98: Token Fountains for Promotions
#####################################
:Design status: Draft
-:Implementation status: Not started
+:Implementation status: Partial
:DD shepherd: TBD
:Historical contributors: Bohdan Potuzhnyi
:First published: 2026-07-23
@@ -108,7 +108,7 @@ A fountain has:
* a polling frequency; and
-* one or more grants describing which token families may be withdrawn and
+* zero or more grants describing which token families may be withdrawn and
at what rate.
The onboarding URI is:
@@ -162,6 +162,9 @@ Wallets periodically retrieve the current grants according to ``poll_freq``.
Removing a grant prevents future withdrawals from that family, but tokens
already withdrawn remain valid according to the token family's issue keys.
+An empty grants array means no grants are currently available, not that the
+credential is revoked. Wallets continue polling for updates.
+
Protocol Flow
-------------
@@ -231,6 +234,16 @@ the backend can enforce each grant's limits.
Each grant result returns ``token_sigs`` using the same
:ts:type:`SignedTokenEnvelope` format as the order payment response.
+The backend records completed withdrawals by fountain and canonical request
+hash. Retrying with the same ``grants`` array returns the original result
+without consuming quota or signing again, even if the grants or key window
+have changed. Quota consumption, issued-token records and replay data are
+committed in one transaction so a failed operation does not consume quota.
+Replay data is retained until the issue keys of the stored signatures expire:
+past that point the tokens it would hand back are worthless and the quota it
+protects has been collected as well, so it is garbage collected together with
+the withdrawal counters. Deleting a fountain drops its replay data at once.
+
Deletion invalidates future requests using the credential. It does not
invalidate tokens that the wallet already withdrew.
@@ -242,9 +255,10 @@ systems must therefore protect the URI like any bearer credential. Wallets
must avoid exposing it in history, telemetry, screenshots or backups that are
not intended to contain secrets.
-The backend stores only a hash of ``fountain_secret``. It must rate-limit
-failed authentication and enforce withdrawal limits atomically to prevent
-concurrent requests from exceeding a grant.
+The backend stores only a hash of ``fountain_secret``. Rate limiting of failed
+authentication may be handled by the deployment's reverse proxy. The backend
+must enforce withdrawal limits atomically to prevent concurrent requests from
+exceeding a grant.
Fountains are not proof of student identity. Eligibility is decided by the
institution when it distributes the link. A shared campaign link can be