taler-docs

Documentation for GNU Taler components, APIs and protocols
Log | Files | Refs | README | LICENSE

098-token-fountains.rst (11902B)


      1 .. _dd-98:
      2 
      3 DD 98: Token Fountains for Promotions
      4 #####################################
      5 
      6 :Design status: Draft
      7 :Implementation status: Partial
      8 :DD shepherd: TBD
      9 :Historical contributors: Bohdan Potuzhnyi
     10 :First published: 2026-07-23
     11 :Last substantive change: 2026-09-06
     12 :Normative references: ``core/api-merchant.rst`` (v43 token-fountain API)
     13 
     14 Summary
     15 =======
     16 
     17 This document proposes token fountains as a simple way for institutions to
     18 distribute promotional ``discount`` and ``subscription`` tokens.  A wallet
     19 imports a bearer credential, periodically retrieves the available grants and
     20 withdraws blind-signed tokens within limits configured by the institution.
     21 
     22 .. note::
     23 
     24   The merchant backend implements the fountain management and wallet APIs
     25   under protocol **v43** (commit ``97439d8f``), replacing the provisional
     26   ``vTokenFountains`` name.
     27 
     28 Motivation
     29 ==========
     30 
     31 Institutions sometimes want to distribute promotional benefits to a group
     32 without creating and paying a separate order for every recipient.  For
     33 example, a university may give students discount tokens accepted by a campus
     34 merchant, or periodically provide subscription tokens for a continuing
     35 promotion.
     36 
     37 GNU Taler already defines blind-signed merchant tokens.  What is missing is a
     38 simple distribution channel through which a wallet can obtain an authorized
     39 set of those tokens over time.  A **token fountain** is a bearer credential
     40 that authorizes such withdrawals.
     41 
     42 The institution can distribute a fountain as a QR code or deep link.  The
     43 wallet imports the link, learns which token families are available and
     44 withdraws blind-signed tokens subject to the configured limits.  Blind
     45 signatures keep a later use of a token unlinkable from its withdrawal.
     46 
     47 User Flow
     48 =========
     49 
     50 The recipient scans the promotion once.  The wallet obtains promotional
     51 tokens and offers an applicable token later at checkout.
     52 
     53 .. uml:: images/098/promotions-user-flow.puml
     54    :align: center
     55    :width: 90%
     56    :caption: Joining and using a token-fountain promotion
     57    :alt: Scan the promotion, receive promotional tokens in the wallet, use a
     58          token at checkout and receive the discount or benefit.
     59 
     60 Requirements
     61 ============
     62 
     63 * Institutions must be able to distribute promotional tokens without
     64   creating a separate order for every withdrawal.
     65 
     66 * A fountain must support grants for existing ``discount`` and
     67   ``subscription`` token families.
     68 
     69 * Token withdrawals must use blind signatures so that later token use is not
     70   linkable to withdrawal.
     71 
     72 * The merchant backend must enforce per-period withdrawal limits atomically.
     73 
     74 * Wallets must be able to import a fountain using a QR code or Taler URI.
     75 
     76 * The bearer secret must not appear in HTTP request URLs after onboarding.
     77 
     78 * Deployments must be able to update or revoke future withdrawals without
     79   invalidating tokens that were already issued.
     80 
     81 Proposed Solution
     82 =================
     83 
     84 Terminology
     85 -----------
     86 
     87 In protocol descriptions, this document uses the existing token-family
     88 ``kind`` values:
     89 
     90 * A ``discount`` token is useful for a one-time or otherwise limited
     91   promotion.  It may be consumed without the merchant issuing a replacement.
     92 
     93 * A ``subscription`` token represents a continuing promotion.  When an order
     94   consumes it, the existing merchant token mechanism can return a replacement
     95   token envelope as an output of the selected choice.
     96 
     97 User-facing applications may call either benefit a **pass**.  The protocol
     98 continues to distinguish them by token-family kind.
     99 
    100 Fountain Credential
    101 -------------------
    102 
    103 A fountain has:
    104 
    105 * a public ``fountain_id`` identifying the fountain within a merchant
    106   instance;
    107 
    108 * a random ``fountain_secret`` used as its bearer credential;
    109 
    110 * a polling frequency; and
    111 
    112 * zero or more grants describing which token families may be withdrawn and
    113   at what rate.
    114 
    115 The onboarding URI is:
    116 
    117 ::
    118 
    119   taler://fountain/$MERCHANT_HOST[/$INSTANCE_PATH]/$FOUNTAIN_ID/$FOUNTAIN_SECRET
    120 
    121 The URI may be displayed as a QR code or opened as a wallet deep link.
    122 ``fountain_id`` tells the wallet which fountain it is importing, while
    123 ``fountain_secret`` authenticates the import.  The wallet must treat the full
    124 URI as sensitive.
    125 
    126 After import, the secret must not be placed in subsequent HTTP URLs.  It is
    127 sent in the ``Authorization`` header when reading fountain information and in
    128 the request body when withdrawing tokens, as specified by the merchant API.
    129 
    130 Deployment Models
    131 -----------------
    132 
    133 An institution may use one fountain for an entire campaign.  This is simple
    134 to distribute, but every recipient shares one bearer credential and the same
    135 withdrawal limits.  A leaked link cannot be revoked for only one recipient.
    136 
    137 Alternatively, the institution may create one fountain per recipient and
    138 distribute each link through its existing student communication system.  This
    139 allows independent limits and revocation without requiring the merchant
    140 backend to store the student's real identity.  The fountain description can
    141 contain an opaque reference meaningful only to the institution.
    142 
    143 The choice is operational rather than cryptographic.  Blind token issuance
    144 prevents the merchant from linking a withdrawn token to its later use in
    145 either model.
    146 
    147 Grant Model
    148 -----------
    149 
    150 Each :ts:type:`FountainGrant` names a token family and defines:
    151 
    152 * ``tokens_per_period_limit``, the maximum number of tokens signed for one
    153   issue-key validity period;
    154 
    155 * ``tokens_per_period_stash``, the number of tokens the wallet should try to
    156   keep available for that period; and
    157 
    158 * ``key_window_size``, the number of issue-key slots into the future for
    159   which the wallet may prepare tokens.
    160 
    161 The merchant may update grants without replacing the bearer credential.
    162 Wallets periodically retrieve the current grants according to ``poll_freq``.
    163 Removing a grant prevents future withdrawals from that family, but tokens
    164 already withdrawn remain valid according to the token family's issue keys.
    165 
    166 An empty grants array means no grants are currently available, not that the
    167 credential is revoked.  Wallets continue polling for updates.
    168 
    169 Protocol Flow
    170 -------------
    171 
    172 1. The institution configures suitable ``discount`` or ``subscription`` token
    173    families on the merchant backend.
    174 
    175 2. It creates a fountain with grants for those token families.
    176 
    177 3. The backend returns ``fountain_id`` and ``fountain_secret`` once.  The
    178    institution forms the Taler fountain URI and gives it to the intended
    179    recipients.
    180 
    181 4. A wallet imports the URI and calls ``GET /fountain/info`` using the bearer
    182    credential.
    183 
    184 5. The wallet prepares blinded token envelopes for any grants it needs to
    185    replenish and submits them to ``POST /fountain/withdraw``.
    186 
    187 6. The backend checks the grant, issue-key window and withdrawal limit, then
    188    blind-signs the accepted envelopes.
    189 
    190 7. The wallet unblinds and stores the tokens.  Their later use follows the
    191    existing merchant token protocol.
    192 
    193 Merchant API
    194 ------------
    195 
    196 Private Management Operations
    197 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    198 
    199 The merchant API adds private operations to create, inspect, update, list and
    200 delete fountains:
    201 
    202 * ``POST /private/fountains``
    203 
    204 * ``GET /private/fountains`` (since protocol **v44**, paginated listing ordered
    205   by ``fountain_serial`` with ``limit``/``offset`` parameters; defaults to 20
    206   entries, with a hard cap of 100 per request)
    207 
    208 * ``GET /private/fountains/$FOUNTAIN_ID``
    209 
    210 * ``PATCH /private/fountains/$FOUNTAIN_ID``
    211 
    212 * ``DELETE /private/fountains/$FOUNTAIN_ID``
    213 
    214 Creation returns:
    215 
    216 .. ts:def:: FountainCreateResponse
    217 
    218   interface FountainCreateResponse {
    219     // Public identifier included in the wallet onboarding URI.
    220     fountain_id: Slug;
    221 
    222     // 256-bit bearer credential, Crockford Base32 encoded.
    223     // Returned only at creation; the backend stores only its hash.
    224     fountain_secret: string;
    225   }
    226 
    227 Wallet Operations
    228 ~~~~~~~~~~~~~~~~~
    229 
    230 ``GET /fountain/info`` returns the current grants, token-family metadata and
    231 issue public keys needed to prepare blinded envelopes.
    232 
    233 ``POST /fountain/withdraw`` accepts the bearer credential and blinded
    234 envelopes.  The request is grouped by token family and issue-key slot so that
    235 the backend can enforce each grant's limits.
    236 
    237 Each grant result returns ``token_sigs`` using the same
    238 :ts:type:`SignedTokenEnvelope` format as the order payment response.
    239 
    240 The backend records completed withdrawals by fountain and canonical request
    241 hash. Retrying with the same ``grants`` array returns the original result
    242 without consuming quota or signing again, even if the grants or key window
    243 have changed. Quota consumption, issued-token records and replay data are
    244 committed in one transaction so a failed operation does not consume quota.
    245 Replay data is retained as a complete response until the latest issue-key
    246 expiry in the withdrawal, even if some entries expire earlier.  Deleting a
    247 fountain drops its replay data at once.
    248 
    249 Deletion invalidates future requests using the credential.  It does not
    250 invalidate tokens that the wallet already withdrew.
    251 
    252 Security Considerations
    253 =======================
    254 
    255 Anyone possessing a fountain URI can withdraw under its grants.  Distribution
    256 systems must therefore protect the URI like any bearer credential.  Wallets
    257 must avoid exposing it in history, telemetry, screenshots or backups that are
    258 not intended to contain secrets.
    259 
    260 The backend stores only a hash of ``fountain_secret``.  Rate limiting of failed
    261 authentication may be handled by the deployment's reverse proxy.  The backend
    262 must enforce withdrawal limits atomically to prevent concurrent requests from
    263 exceeding a grant.
    264 
    265 Fountains are not proof of student identity.  Eligibility is decided by the
    266 institution when it distributes the link.  A shared campaign link can be
    267 forwarded, while a per-recipient link can only be individually disabled after
    268 the institution learns that it was compromised.
    269 
    270 Privacy Considerations
    271 ======================
    272 
    273 Fountain descriptions should contain opaque administrative references rather
    274 than names or student identifiers.  The merchant backend does not need a
    275 recipient's identity to issue blind-signed tokens.
    276 
    277 Network metadata can still correlate a wallet's requests.  Deployments that
    278 require stronger network-level privacy need an appropriate transport or proxy;
    279 blind signatures alone do not hide IP addresses or request timing.
    280 
    281 Drawbacks
    282 =========
    283 
    284 The fountain API adds persistent bearer credentials, polling and quota state
    285 to the merchant backend.
    286 
    287 Shared campaign fountains trade administrative simplicity for coarse
    288 revocation and shared quotas.  Per-recipient fountains provide better control
    289 but require the institution to create and distribute many distinct links.
    290 
    291 Test Plan
    292 =========
    293 
    294 * Test creation, inspection, update, listing and deletion of fountains.
    295 
    296 * Test importing a URI containing both ``fountain_id`` and
    297   ``fountain_secret``.
    298 
    299 * Test withdrawals for both ``discount`` and ``subscription`` token families.
    300 
    301 * Test atomic enforcement of per-period limits under concurrent requests.
    302 
    303 * Test rejection of unknown grants, unsupported issue-key slots and invalid
    304   credentials.
    305 
    306 * Test that deleting a fountain prevents new withdrawals without invalidating
    307   tokens already held by a wallet.
    308 
    309 * Test that token use cannot be linked to its fountain withdrawal from the
    310   blind-signature transcript.
    311 
    312 Definition of Done
    313 ==================
    314 
    315 * The merchant backend implements fountain management, information and
    316   withdrawal operations.
    317 
    318 * The onboarding URI includes the merchant location, ``fountain_id`` and
    319   ``fountain_secret``.
    320 
    321 * Wallet Core can import a fountain, poll its grants, replenish tokens within
    322   the configured limits and stop when the credential is revoked.
    323 
    324 * Merchant applications can create and distribute promotional fountains
    325   without supplying recipient identities to the merchant backend.
    326 
    327 * Discount and subscription token families retain their existing consumption
    328   and replacement semantics.