taler-docs

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

commit eda8312b713df9e471d0b8d91a29a4632b90a772
parent 72c3a1380e8a126dad95eae430b97e48cea72e0e
Author: Özgür Kesim <oec@codeblau.de>
Date:   Tue, 15 Sep 2026 21:05:25 +0200

exchange API: specify the coin conflict proofs (vCONFLICT)

Define CoinDenominationConflictError and CoinAgeCommitmentConflictError
for the 409 responses with error codes
TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY and
TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH.  Both carry the
denomination signature the exchange holds for the coin, so that
clients can verify the conflict.  Reference them from the endpoints
that make coins known: batch deposit, melt, purse create, purse
deposit, reserve open and both recoup endpoints.

See #9422

Diffstat:
Mcore/api-exchange.rst | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/exchange/post-batch-deposit.rst | 7++++++-
Mcore/exchange/post-melt.rst | 18+++++++++++-------
Mcore/exchange/post-purses-PURSE_PUB-create.rst | 5++++-
Mcore/exchange/post-purses-PURSE_PUB-deposit.rst | 5+++--
Mcore/exchange/post-recoup-refresh.rst | 14+++++++++-----
Mcore/exchange/post-recoup-withdraw.rst | 14+++++++++-----
Mcore/exchange/post-reserves-RESERVE_PUB-open.rst | 4+++-
8 files changed, 125 insertions(+), 22 deletions(-)

diff --git a/core/api-exchange.rst b/core/api-exchange.rst @@ -65,6 +65,12 @@ The currently implemented protocol version is **v41**. ``/coins/$COIN_PUB/recoup`` and ``/coins/$COIN_PUB/recoup-refresh``; adds ``planchets_h``, ``rc`` and ``coin_index`` to the recoup entries of coin and reserve histories +* ``vCONFLICT``: coin conflict proofs: specifies `CoinDenominationConflictError` + and `CoinAgeCommitmentConflictError` for the 409 responses with + error codes ``TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY`` + and ``TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH``, which + now carry the stored denomination signature (and age commitment + hash) of the coin so that clients can verify the conflict * ``vATTEST``: KYC attestation support **Ideas for future version:** @@ -299,6 +305,80 @@ exchange. } +.. ts:def:: CoinDenominationConflictError + + // Response to an operation on a coin whose public key the exchange + // already knows under a *different* denomination. Since protocol + // **vCONFLICT** the response is a proof: it carries the denomination + // signature the exchange holds for the coin, which clients verify + // against ``prev_denom_pub`` (see ``TALER_EXCHANGE_check_coin_denomination_conflict_``). + interface CoinDenominationConflictError { + + // Must be ``TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY``. + code: Integer; + + // A string explaining the conflict. + hint: string; + + // Public key of the coin that was already used with another + // denomination. + coin_pub: EddsaPublicKey; + + // Denomination public key under which the exchange knows the coin. + // Its hash differs from the denomination hash used in the request. + prev_denom_pub: DenominationKey; + + // Signature by ``prev_denom_pub`` over ``coin_pub`` (and + // ``prev_h_age_commitment`` if present), i.e. the denomination + // signature of the coin as issued by the exchange. + prev_denom_sig: DenominationSignature; + + // Age commitment hash stored for the coin. Present exactly + // if ``prev_denom_pub`` is an age-restricted denomination. + // @since **vCONFLICT** + prev_h_age_commitment?: AgeCommitmentHash; + + } + +.. ts:def:: CoinAgeCommitmentConflictError + + // Response to an operation on a coin whose public key the exchange + // already knows under the same denomination, but with a different + // age commitment hash. Since protocol **vCONFLICT** the response is + // a proof: it carries the denomination signature the exchange holds + // for the coin, which covers ``coin_pub`` and + // ``expected_age_commitment_hash``. + interface CoinAgeCommitmentConflictError { + + // Must be ``TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH``. + code: Integer; + + // A string explaining the conflict. + hint: string; + + // Public key of the coin. + coin_pub: EddsaPublicKey; + + // Hash of the denomination public key of the coin + // (as stored by the exchange, which equals the one + // used in the request). + h_denom_pub: HashCode; + + // Age commitment hash stored for the coin, ``null`` if + // the exchange knows the coin without one. + expected_age_commitment_hash: AgeCommitmentHash | null; + + // Human-readable description of the mismatch. + conflict_detail: string; + + // Signature by the denomination over ``coin_pub`` and + // ``expected_age_commitment_hash``, i.e. the denomination + // signature of the coin as issued by the exchange. + // @since **vCONFLICT** + prev_denom_sig: DenominationSignature; + + } + .. ts:def:: WithdrawError diff --git a/core/exchange/post-batch-deposit.rst b/core/exchange/post-batch-deposit.rst @@ -45,6 +45,9 @@ 3. ``TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY`` (same coin public key, but different denomination). 4. ``TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH`` (same coin public key, but different age commitment). + The response is a `DepositDoubleSpendError` for cases 1 and 2, a + `CoinDenominationConflictError` for case 3 and a + `CoinAgeCommitmentConflictError` for case 4. The request should not be repeated again with this coin. Instead, the client can get from the exchange via the ``/coin/$COIN_PUB/history`` endpoint the record of the transactions known for this coin's public key. @@ -225,7 +228,9 @@ interface DepositDoubleSpendError { // Must be TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS - // or TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY + // or TALER_EC_EXCHANGE_DEPOSIT_CONFLICTING_CONTRACT. + // (Since **vCONFLICT**, a conflicting denomination is + // reported with a `CoinDenominationConflictError` instead.) code: Integer; // A string explaining that the user tried to diff --git a/core/exchange/post-melt.rst b/core/exchange/post-melt.rst @@ -40,11 +40,14 @@ :http:statuscode:`409 Conflict`: The operation is not allowed as the coin has insufficient residual value, or because the same public key of the coin has been - previously used with a different denomination. Which case it is - can be decided by looking at the error code - (``TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS`` or - ``TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY``). - The response is `MeltForbiddenResponse` in both cases. + previously used with a different denomination or a different age + commitment. Which case it is can be decided by looking at the error + code: ``TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS`` (response is a + `MeltForbiddenResponse`), + ``TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY`` + (response is a `CoinDenominationConflictError`) or + ``TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH`` + (response is a `CoinAgeCommitmentConflictError`). :http:statuscode:`410 Gone`: The requested denomination key is no longer valid. It is past the expiration or was revoked. The response is a @@ -191,8 +194,9 @@ interface MeltForbiddenResponse { - // Must be TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS - // or TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY + // Must be TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS. + // (Since **vCONFLICT**, a conflicting denomination is + // reported with a `CoinDenominationConflictError` instead.) code: Integer; // A string explaining that the user tried to diff --git a/core/exchange/post-purses-PURSE_PUB-create.rst b/core/exchange/post-purses-PURSE_PUB-create.rst @@ -36,12 +36,13 @@ can be decided by looking at the error code (``TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS`` or ``TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY`` or + ``TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH`` or ``TALER_EC_EXCHANGE_PURSE_CREATE_CONFLICTING_META_DATA`` or ``TALER_EC_EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA`` or ``TALER_EC_EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA``). The specific fields of the response depend on the error code and include the signatures (and what was signed over) proving the - conflict. + conflict. The response is a `PurseConflict`. :http:statuscode:`410 Gone`: The requested denomination key is not yet or no longer valid. It either before the validity start, past the expiration or was revoked. @@ -156,6 +157,8 @@ // Union discriminated by the "code" field. type PurseConflict = | DepositDoubleSpendError + | CoinDenominationConflictError + | CoinAgeCommitmentConflictError | PurseCreateConflict | PurseDepositConflict | PurseContractConflict; diff --git a/core/exchange/post-purses-PURSE_PUB-deposit.rst b/core/exchange/post-purses-PURSE_PUB-deposit.rst @@ -26,10 +26,11 @@ :http:statuscode:`409 Conflict`: The deposit operation has either failed because a coin has insufficient residual value, or because the same public key of the coin has been - previously used with a different denomination. Which case it is - can be decided by looking at the error code + previously used with a different denomination or age commitment. + Which case it is can be decided by looking at the error code (``TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS`` or ``TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY`` or + ``TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH`` or ``TALER_EC_EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA``). This response comes with a standard `PurseConflict` response (alas some cases are impossible). diff --git a/core/exchange/post-recoup-refresh.rst b/core/exchange/post-recoup-refresh.rst @@ -81,11 +81,15 @@ recorded for that position of the refresh operation. This response comes with a standard `ErrorDetail` response with error code ``TALER_EC_EXCHANGE_RECOUP_REFRESH_DENOMINATION_MISMATCH``. - 3. A disclosed coin has no residual value left, or the same coin - public key was previously used with a different denomination. - The response is a `DepositDoubleSpendError` with error code - ``TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS`` or - ``TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY``. + 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. diff --git a/core/exchange/post-recoup-withdraw.rst b/core/exchange/post-recoup-withdraw.rst @@ -76,11 +76,15 @@ 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, or the same coin - public key was previously used with a different denomination. - The response is a `DepositDoubleSpendError` with error code - ``TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS`` or - ``TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY``. + 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. diff --git a/core/exchange/post-reserves-RESERVE_PUB-open.rst b/core/exchange/post-reserves-RESERVE_PUB-open.rst @@ -30,11 +30,13 @@ Which case it is can be decided by looking at the error code (``TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS`` or ``TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY`` or + ``TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH`` or ``TALER_EC_EXCHANGE_RESERVES_OPEN_INSUFFICIENT_FUNDS``). The specific fields of the response depend on the error code and include the signatures (and what was signed over) proving the conflict. - The response is `WithdrawError` object or a `DepositDoubleSpendError` + The response is a `WithdrawError` object, a `DepositDoubleSpendError`, + a `CoinDenominationConflictError` or a `CoinAgeCommitmentConflictError` depending on the error type. :http:statuscode:`413 Request entity too large`: The uploaded body is to long, it exceeds the size limit.