Contents

POST /recoup-withdraw#

Demand that coins of revoked denominations, which originated from one particular call to /withdraw, be paid back to the reserve they were withdrawn from.

All coins in one request MUST originate from the same withdraw operation. The client identifies that operation by the public key of the reserve and the commitment planchets_h of the original withdraw request. For each coin that is to be recouped, the client discloses the secret material that allows the exchange to recompute the coin’s blinded envelope. For every other coin of the same operation, the client only provides the hash of its blinded envelope. From these values the exchange recomputes the hash over the batch of coins it signed in the original operation and compares it with the commitment it stored. This proves that each disclosed coin was part of that withdraw operation without the exchange having to store individual envelopes.

The residual value of each disclosed coin, that is its denomination’s value minus what has already been spent, is credited to the reserve. The withdraw fee paid in the original operation is not recouped.

The exchange only accepts this request for coins of denominations that are listed in the recoup field of /keys.

This endpoint was introduced in protocol vRECOUP and replaces the former /coins/$COIN_PUB/recoup endpoint.

Request:

The request body must be a RecoupWithdrawRequest object.

Response:

200 OK:

The request was successful and the response is a RecoupWithdrawResponse. Repeating exactly the same request yields the same response (the signature may differ if the exchange has rotated its signing key in the meantime), so if the network goes down before the client can commit the result to disk, the recoup is not lost.

400 Bad Request:

The request body is malformed or a parameter is invalid. This response comes with a standard ErrorDetail response. Possible error codes include TALER_EC_GENERIC_PARAMETER_MALFORMED (also used if coin_data does not contain at least one disclosed coin) and TALER_EC_EXCHANGE_RECOUP_WITHDRAW_BATCH_SIZE_MISMATCH (the number of entries in coin_data differs from the number of coins the exchange signed in the original withdraw operation).

403 Forbidden:

A signature is invalid. This response comes with a standard ErrorDetail response. Possible error codes include TALER_EC_EXCHANGE_RECOUP_SIGNATURE_INVALID (a coin_sig) or TALER_EC_EXCHANGE_DENOMINATION_SIGNATURE_INVALID (a denom_sig).

404 Not found:

One of the following reasons occurred:

  1. A denomination key is unknown. The response is a DenominationUnknownMessage with error code TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN.

  2. The exchange has no record of a withdraw operation for the given reserve_pub and planchets_h. This response comes with a standard ErrorDetail response with error code TALER_EC_EXCHANGE_RECOUP_WITHDRAW_NOT_FOUND.

409 Conflict:

One of the following reasons occurred:

  1. The hash the exchange recomputed over the coin_data array does not match the commitment of the withdraw operation, so at least one disclosed coin was not part of it. This response comes with a standard ErrorDetail response with error code TALER_EC_EXCHANGE_RECOUP_WITHDRAW_COMMITMENT_MISMATCH.

  2. The denomination of a disclosed coin differs from the denomination recorded for that position of the withdraw operation. This response comes with a standard ErrorDetail response with error code TALER_EC_EXCHANGE_RECOUP_WITHDRAW_DENOMINATION_MISMATCH.

  3. A disclosed coin has no residual value left. The response is a DepositDoubleSpendError with error code TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS.

  4. The same coin public key was previously used with a different denomination or age commitment. The response is a CoinDenominationConflictError with error code TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY or a CoinAgeCommitmentConflictError with error code TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH.

In all of these cases nothing is credited, not even for the other coins in the request.

410 Gone:

The denomination of a disclosed coin is not eligible for recoup. The response is a DenominationGoneMessage with error code TALER_EC_EXCHANGE_GENERIC_DENOMINATION_EXPIRED (past the deposit expiration) or TALER_EC_EXCHANGE_RECOUP_NOT_ELIGIBLE (the denomination was not revoked).

412 Precondition Failed:

The denomination of a disclosed coin is not yet valid. The response is a DenominationGoneMessage with error code TALER_EC_EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE.

413 Request entity too large:

The uploaded body is to long, it exceeds the size limit. Returned with an error code of TALER_EC_GENERIC_UPLOAD_EXCEEDS_LIMIT.

500 Internal Server Error:

The exchange encountered an internal error. This response comes with a standard ErrorDetail response. Possible error codes include TALER_EC_EXCHANGE_RECOUP_BLINDING_FAILED, TALER_EC_GENERIC_DB_FETCH_FAILED, TALER_EC_GENERIC_DB_COMMIT_FAILED, or TALER_EC_GENERIC_DB_INVARIANT_FAILURE.

502 Bad gateway:

The exchange could not reach its signing helper to create the confirmation signatures. Returned with an error code of TALER_EC_EXCHANGE_SIGNKEY_HELPER_UNAVAILABLE. Wallets should retry the request (with some delay) at a later time.

503 Service Unavailable:

The exchange currently has no signing keys available. This response comes with a standard ErrorDetail response with a code of TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING.

Details:

interface RecoupWithdrawRequest {
  // Public key of the reserve from which the coins were withdrawn.
  // The recouped value is credited to this reserve.
  reserve_pub: EddsaPublicKey;

  // The commitment of the original withdraw request, as
  // signed in the h_planchets field of TALER_WithdrawRequestPS.
  // Together with reserve_pub, this identifies the
  // withdraw operation.
  planchets_h: HashCode;

  // Exactly one entry per coin the exchange signed in the
  // withdraw operation, in the order of the original request.
  // If the withdraw request had max_age set, these are the
  // n coins of the batch at the noreveal_index returned
  // by the exchange, not all n*kappa candidates.
  // At least one entry MUST be a RecoupedCoin.
  coin_data: RecoupCoinData[];
}
// Union discriminated by the type field.
type RecoupCoinData =
   | NonRecoupedCoin
   | RecoupedCoin;
// A coin of the original operation that is not to be recouped
// in this request.  The client only provides the hash of its
// blinded envelope, which is all the exchange needs to
// recompute the commitment.
interface NonRecoupedCoin {
  type: "hash";

  // Hash of the coin's blinded envelope,
  // see BlindedCoinEnvelopeHash.
  h_coin_ev: BlindedCoinEnvelopeHash;
}
// SHA-512 hash over the hash of the denomination public key followed
// by the canonical serialization of the blinded envelope (the cipher
// identifier in network byte order followed by the blinded message).
// This is the value computed by TALER_coin_ev_hash in
// libtalerutil.  The hash the exchange compares against a
// withdraw or refresh commitment is the SHA-512 hash over the
// concatenation of these values for all coins of a batch, in order
// (TALER_wallet_blinded_planchets_hash in libtalerutil).
type BlindedCoinEnvelopeHash = HashCode;
// A coin of the original operation that is to be recouped.
// The client discloses the secrets from which the exchange can
// recompute the coin's blinded envelope, proves ownership of the
// coin and authorizes the recoup.
interface RecoupedCoin {
  type: "recoup";

  // The coin's public key.
  coin_pub: CoinPublicKey;

  // Hash of the public key of the coin's denomination.
  // It MUST be listed in the recoup field of /keys
  // and MUST match the denomination at this position of the
  // original withdraw request.
  denom_pub_h: HashCode;

  // Unblinded signature of the denomination key over the coin.
  denom_sig: DenominationSignature;

  // The blinding secret that was used to blind the coin
  // in the original withdraw request.
  coin_blinding_secret: DenominationBlindingKeySecret;

  // The hash of the coin's age commitment.  MUST be present
  // if and only if the denomination supports age restriction.
  h_age_commitment?: AgeCommitmentHash;

  // Signature over TALER_RecoupRequestPS with purpose
  // TALER_SIGNATURE_WALLET_COIN_RECOUP, created with the
  // coin's private key.
  coin_sig: EddsaSignature;
}

Note that for denominations of cipher type Clause-Schnorr the client does not need to provide the session nonce or the R-value pair: the exchange derives the nonce from the blinding_seed of the original withdraw request and the coin’s position in coin_data (exactly as the wallet does), and it stored the R-values it returned in /blinding-prepare.

The exchange processes the request as follows:

  1. It looks up the withdraw operation under reserve_pub and planchets_h, which yields the number n of signed coins, their denominations and, where applicable, the blinding seed, the R-values and the noreveal_index. If coin_data does not have n entries, the request is rejected.

  2. For each RecoupedCoin at position i, it checks that the denomination is eligible for recoup and equals the one recorded at position i, verifies denom_sig and coin_sig, recomputes the blinded envelope from coin_pub, coin_blinding_secret, h_age_commitment and the stored cipher-specific values, and hashes it into a BlindedCoinEnvelopeHash. For each NonRecoupedCoin it uses the h_coin_ev provided.

  3. It computes the hash over the resulting n values and compares it to the hash of the batch it signed: planchets_h itself if the withdraw request had no max_age, and otherwise the hash of the batch at the noreveal_index.

  4. In one database transaction, it credits the residual value of every disclosed coin to the reserve and records the recoup. If any coin cannot be recouped, the whole request fails and nothing is credited.

interface RecoupWithdrawResponse {
  // Public key of the reserve that was credited.
  reserve_pub: EddsaPublicKey;

  // The commitment of the withdraw operation, as in the request.
  planchets_h: HashCode;

  // Time when the exchange accepted the recoup.  All coins
  // of one request share this timestamp.
  timestamp: Timestamp;

  // Total amount credited to the reserve, that is the sum
  // of the amount values in recoups.
  total_amount: Amount;

  // One entry per RecoupedCoin in the request, in the same order.
  recoups: RecoupedCoinResult[];

  // Signature over TALER_RecoupWithdrawBatchConfirmationPS with
  // purpose TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP_WITHDRAW_BATCH,
  // affirming the recoup of all coins in recoups.
  exchange_sig: EddsaSignature;

  // Public key used to create exchange_sig.
  // Should match one of the exchange's signing keys from /keys.
  exchange_pub: EddsaPublicKey;
}
interface RecoupedCoinResult {
  // Public key of the coin that was recouped.
  coin_pub: CoinPublicKey;

  // Amount credited for this coin, that is the coin's
  // residual value at the time of the recoup.
  amount: Amount;
}

The coin and reserve histories report each recouped coin individually, see CoinRecoupWithdrawTransaction and ReserveRecoupTransaction. Those entries carry a signature per coin over TALER_RecoupConfirmationPS, which the exchange creates when a history is requested; the batch signature above is only part of the response to this request.