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:
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.