commit 4f219f7a878b23d1b38c52cfff783b220911400d
parent d42b291f6e2f460de80b03d2e544d5635a864f80
Author: Özgür Kesim <oec@codeblau.de>
Date: Mon, 14 Sep 2026 19:00:55 +0200
exchange API: one batch signature per recoup response
Replace the per-coin confirmations in the /recoup-withdraw and
/recoup-refresh responses by a single exchange signature over the
whole response: new signed structs TALER_RecoupWithdrawBatchConfirmationPS
and TALER_RecoupRefreshBatchConfirmationPS bind the timestamp, the
credited reserve resp. old coin, the operation's commitment, the total
amount and a hash over the (coin_pub, amount) pairs of all recouped
coins. The response lists the recouped coins with their amounts next
to exchange_sig and exchange_pub.
The per-coin history entries keep their existing per-coin confirmation
purposes, which the exchange creates on the fly when serving a history.
Diffstat:
3 files changed, 110 insertions(+), 38 deletions(-)
diff --git a/core/api-common.rst b/core/api-common.rst
@@ -1907,6 +1907,58 @@ within the
union GNUNET_CRYPTO_BlindingSecretP coin_blind;
};
+.. _TALER_RecoupWithdrawBatchConfirmationPS:
+.. sourcecode:: c
+
+ /**
+ * Signed by the exchange in the response to /recoup-withdraw,
+ * affirming that it credited the reserve for all recouped coins
+ * of one request.
+ */
+ struct TALER_RecoupWithdrawBatchConfirmationPS {
+ /**
+ * purpose.purpose = TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP_WITHDRAW_BATCH
+ */
+ struct GNUNET_CRYPTO_EccSignaturePurpose purpose;
+ struct GNUNET_TIME_TimestampNBO timestamp;
+ struct TALER_ReservePublicKeyP reserve_pub;
+ struct TALER_HashBlindedPlanchetsP planchets_h;
+ struct TALER_AmountNBO total_amount;
+ /**
+ * SHA-512 hash over the concatenation, in the order of the
+ * ``recoups`` array of the response, of the coin public key
+ * (32 bytes) followed by the recouped amount (as TALER_AmountNBO)
+ * of every recouped coin.
+ */
+ struct GNUNET_HashCode h_recoups;
+ };
+
+.. _TALER_RecoupRefreshBatchConfirmationPS:
+.. sourcecode:: c
+
+ /**
+ * Signed by the exchange in the response to /recoup-refresh,
+ * affirming that it credited the old coin for all recouped coins
+ * of one request.
+ */
+ struct TALER_RecoupRefreshBatchConfirmationPS {
+ /**
+ * purpose.purpose = TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP_REFRESH_BATCH
+ */
+ struct GNUNET_CRYPTO_EccSignaturePurpose purpose;
+ struct GNUNET_TIME_TimestampNBO timestamp;
+ struct TALER_CoinSpendPublicKeyP old_coin_pub;
+ struct TALER_RefreshCommitmentP rc;
+ struct TALER_AmountNBO total_amount;
+ /**
+ * SHA-512 hash over the concatenation, in the order of the
+ * ``recoups`` array of the response, of the coin public key
+ * (32 bytes) followed by the recouped amount (as TALER_AmountNBO)
+ * of every recouped coin.
+ */
+ struct GNUNET_HashCode h_recoups;
+ };
+
.. _TALER_RecoupRefreshConfirmationPS:
.. sourcecode:: c
diff --git a/core/exchange/post-recoup-refresh.rst b/core/exchange/post-recoup-refresh.rst
@@ -39,9 +39,10 @@
:http:statuscode:`200 OK`:
The request was successful and the response is a
`RecoupRefreshResponse`. Repeating exactly the same request
- yields the same response, including the same timestamps and
- signatures, so if the network goes down before the client can commit the
- result to disk, the recoup is not lost.
+ 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.
:http:statuscode:`400 Bad Request`:
The request body is malformed or a parameter is invalid.
This response comes with a standard `ErrorDetail` response.
@@ -164,29 +165,33 @@
// Public key of the old coin that was credited.
old_coin_pub: CoinPublicKey;
- // One confirmation per `RecoupedCoin` in the request,
- // in the same order.
- recoups: RecoupRefreshConfirmation[];
- }
-
- .. ts:def:: RecoupRefreshConfirmation
+ // The refresh commitment of the refresh operation, as in the request.
+ rc: HashCode;
- interface RecoupRefreshConfirmation {
- // Public key of the coin that was recouped.
- coin_pub: CoinPublicKey;
+ // Time when the exchange accepted the recoup. All coins
+ // of one request share this timestamp.
+ timestamp: Timestamp;
- // Amount credited to the old coin for this coin, that is
- // the coin's residual value at the time of the recoup.
- amount: Amount;
+ // Total amount credited to the old coin, that is the sum
+ // of the ``amount`` values in ``recoups``.
+ total_amount: Amount;
- // Time when the exchange accepted the recoup of this coin.
- timestamp: Timestamp;
+ // One entry per `RecoupedCoin` in the request, in the same order.
+ recoups: RecoupedCoinResult[];
- // Signature over `TALER_RecoupRefreshConfirmationPS` with purpose
- // ``TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP_REFRESH``.
+ // Signature over `TALER_RecoupRefreshBatchConfirmationPS` with
+ // purpose ``TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP_REFRESH_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;
}
+
+ The coin histories report each recouped coin individually, see
+ `CoinRecoupRefreshTransaction` for the recouped coin and
+ `CoinRecoupRefreshReceiverTransaction` for the old coin. Those entries
+ carry a signature per coin over :ref:`TALER_RecoupRefreshConfirmationPS <TALER_RecoupRefreshConfirmationPS>`,
+ which the exchange creates when a history is requested; the batch
+ signature above is only part of the response to this request.
diff --git a/core/exchange/post-recoup-withdraw.rst b/core/exchange/post-recoup-withdraw.rst
@@ -35,9 +35,10 @@
:http:statuscode:`200 OK`:
The request was successful and the response is a
`RecoupWithdrawResponse`. Repeating exactly the same request
- yields the same response, including the same timestamps and
- signatures, so if the network goes down before the client can commit the
- result to disk, the recoup is not lost.
+ 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.
:http:statuscode:`400 Bad Request`:
The request body is malformed or a parameter is invalid.
This response comes with a standard `ErrorDetail` response.
@@ -242,29 +243,43 @@
// Public key of the reserve that was credited.
reserve_pub: EddsaPublicKey;
- // One confirmation per `RecoupedCoin` in the request,
- // in the same order.
- recoups: RecoupWithdrawConfirmation[];
- }
-
- .. ts:def:: RecoupWithdrawConfirmation
+ // The commitment of the withdraw operation, as in the request.
+ planchets_h: HashCode;
- interface RecoupWithdrawConfirmation {
- // Public key of the coin that was recouped.
- coin_pub: CoinPublicKey;
+ // Time when the exchange accepted the recoup. All coins
+ // of one request share this timestamp.
+ timestamp: Timestamp;
- // Amount credited to the reserve for this coin, that is
- // the coin's residual value at the time of the recoup.
- amount: Amount;
+ // Total amount credited to the reserve, that is the sum
+ // of the ``amount`` values in ``recoups``.
+ total_amount: Amount;
- // Time when the exchange accepted the recoup of this coin.
- timestamp: Timestamp;
+ // One entry per `RecoupedCoin` in the request, in the same order.
+ recoups: RecoupedCoinResult[];
- // Signature over `TALER_RecoupConfirmationPS` with purpose
- // ``TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP``.
+ // 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;
}
+
+ .. ts:def:: RecoupedCoinResult
+
+ 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 :ref:`TALER_RecoupConfirmationPS <TALER_RecoupConfirmationPS>`,
+ which the exchange creates when a history is requested; the batch
+ signature above is only part of the response to this request.