commit 41abea61fdd060024e7871bc4a851062b6791554
parent 509412474d1a296afa83e54fd10d6af7c2630ebc
Author: Florian Dold <florian@dold.me>
Date: Tue, 22 Apr 2025 16:08:48 +0200
wallet-core: scaffolding for new refresh protocol
Diffstat:
10 files changed, 587 insertions(+), 288 deletions(-)
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-refresh.ts b/packages/taler-harness/src/integrationtests/test-wallet-refresh.ts
@@ -41,7 +41,7 @@ import {
} from "../harness/harness.js";
/**
- * Run test for refreshe after a payment.
+ * Run test for refresh after a payment.
*/
export async function runWalletRefreshTest(t: GlobalTestState) {
// Set up test environment
diff --git a/packages/taler-util/src/types-taler-exchange.ts b/packages/taler-util/src/types-taler-exchange.ts
@@ -27,6 +27,7 @@ import {
codecOptional,
} from "./codec.js";
import {
+ TalerFormAttributes,
buildCodecForUnion,
codecForAmountString,
codecForBoolean,
@@ -38,7 +39,6 @@ import {
codecForURN,
codecOptionalDefault,
strcmp,
- TalerFormAttributes,
} from "./index.js";
import { Edx25519PublicKeyEnc } from "./taler-crypto.js";
import {
@@ -274,7 +274,7 @@ export interface RecoupRequest {
*
* The string variant is for the legacy exchange protocol.
*/
- denom_sig: UnblindedSignature;
+ denom_sig: UnblindedDenominationSignature;
/**
* Blinding key that was used during withdraw,
@@ -302,7 +302,7 @@ export interface RecoupRefreshRequest {
*
* The string variant is for the legacy exchange protocol.
*/
- denom_sig: UnblindedSignature;
+ denom_sig: UnblindedDenominationSignature;
/**
* Coin's blinding factor.
@@ -334,7 +334,7 @@ export interface RecoupConfirmation {
old_coin_pub?: string;
}
-export type UnblindedSignature = RsaUnblindedSignature;
+export type UnblindedDenominationSignature = RsaUnblindedSignature;
export interface RsaUnblindedSignature {
cipher: DenomKeyType.Rsa;
@@ -361,7 +361,7 @@ export interface CoinDepositPermission {
* The string variant is for legacy protocol support.
*/
- ub_sig: UnblindedSignature;
+ ub_sig: UnblindedDenominationSignature;
/**
* The denomination public key associated with this coin.
@@ -549,12 +549,59 @@ export interface ExchangeMeltRequest {
coin_pub: CoinPublicKeyString;
confirm_sig: EddsaSignatureString;
denom_pub_hash: HashCodeString;
- denom_sig: UnblindedSignature;
+ denom_sig: UnblindedDenominationSignature;
rc: string;
value_with_fee: AmountString;
age_commitment_hash?: HashCodeString;
}
+/**
+ * Docs name: NewMeltRequest
+ */
+export interface ExchangeMeltRequestV2 {
+ // The old coin's public key
+ old_coin_pub: CoinPublicKeyString;
+
+ // Hash of the denomination public key of the old coin, to determine total coin value.
+ old_denom_pub_h: HashCodeString;
+
+ // The hash of the age-commitment for the old coin. Only present
+ // if the denomination has support for age restriction.
+ old_age_commitment_h?: string;
+
+ // Signature over the old coin public key by the denomination.
+ old_denom_sig: UnblindedDenominationSignature;
+
+ // Amount of the value of the old coin that should be melted as part of
+ // this refresh operation, including melting fee.
+ value_with_fee: Amount;
+
+ // Array of n new hash codes of denomination public keys
+ // for the new coins to order.
+ denoms_h: HashCode[];
+
+ // Seed from which the nonces for the n*κ coin candidates are derived
+ // from.
+ refresh_seed: HashCode;
+
+ // Master seed for the Clause-Schnorr R-value
+ // creation. Must match the /blinding-prepare request.
+ // Must not have been used in any prior melt request.
+ // Must be present if one of the fresh coin's
+ // denominations is of type Clause-Schnorr.
+ blinding_seed?: string;
+
+ // kappa arrays of n entries for blinded coin candidates,
+ // each matching the respective entries in denoms_h.
+ //
+ // Note: These are essentially the m_i values in the RefreshDerivePQ
+ // function.
+ coin_evs: CoinEnvelope[][];
+
+ // Signature by the coin over TALER_RefreshMeltCoinAffirmationPS.
+ confirm_sig: EddsaSignatureString;
+}
+
export interface ExchangeMeltResponse {
/**
* Which of the kappa indices does the client not have to reveal.
@@ -919,6 +966,30 @@ export interface ExchangeRefreshRevealRequest {
old_age_commitment?: Edx25519PublicKeyEnc[];
}
+export interface ExchangeRefreshRevealRequestV2 {
+ // The commitment from the /melt/ step,
+ // i.e. the SHA512 value of
+ // 1. refresh_seed
+ // 2. blinding_seed, if applicable, skip otherwise
+ // 4. amount with fee (NBO)
+ // 5. kappa*n blinded planchet hashes (which include denomination information),
+ // depths first: [0..n)[0..n)[0..n)
+ rc: string;
+
+ // The disclosed kappa-1 signatures by the old coin's private key,
+ // over Hash1a("Refresh", Cp, r, i), where Cp is the melted coin's public key,
+ // r is the public refresh nonce from the metling step and i runs over the
+ // _disclosed_ kappa-1 indices.
+ signatures: EddsaSignature[];
+
+ // IFF the denomination of the old coin had support for age restriction,
+ // the client MUST provide the original age commitment, i. e. the
+ // vector of public keys, or omitted otherwise.
+ // The size of the vector MUST be the number of age groups as defined by the
+ // Exchange in the field .age_groups of the extension age_restriction.
+ age_commitment?: Edx25519PublicKeyEnc[];
+}
+
export const codecForRecoup = (): Codec<Recoup> =>
buildCodecForObject<Recoup>()
.property("h_denom_pub", codecForString())
@@ -1044,6 +1115,12 @@ export const codecForExchangeWithdrawResponse =
.property("ev_sigs", codecForList(codecForBlindedDenominationSignature()))
.build("WithdrawResponse");
+export const codecForExchangeRevealMeltResponseV2 =
+ (): Codec<ExchangeWithdrawResponse> =>
+ buildCodecForObject<ExchangeWithdrawResponse>()
+ .property("ev_sigs", codecForList(codecForBlindedDenominationSignature()))
+ .build("ExchangeRevealMeltResponseV2");
+
export const codecForExchangeMeltResponse = (): Codec<ExchangeMeltResponse> =>
buildCodecForObject<ExchangeMeltResponse>()
.property("exchange_pub", codecForString())
@@ -1100,7 +1177,7 @@ export interface PurseDeposit {
/**
* Exchange's unblinded RSA signature of the coin.
*/
- ub_sig: UnblindedSignature;
+ ub_sig: UnblindedDenominationSignature;
/**
* Age commitment for the coin, if the denomination is age-restricted.
@@ -1269,7 +1346,7 @@ export interface ExchangeDepositRequest {
denom_pub_hash: HashCodeString;
// Exchange's unblinded RSA signature of the coin.
- ub_sig: UnblindedSignature;
+ ub_sig: UnblindedDenominationSignature;
// Timestamp when the contract was finalized.
timestamp: TalerProtocolTimestamp;
@@ -1481,7 +1558,7 @@ export interface BatchDepositRequestCoin {
denom_pub_hash: HashCodeString;
// Exchange's unblinded RSA signature of the coin.
- ub_sig: UnblindedSignature;
+ ub_sig: UnblindedDenominationSignature;
// Amount to be deposited, can be a fraction of the
// coin's total value.
@@ -1864,9 +1941,9 @@ export type ExchangeKycUploadFormRequest = {
// Which form is being submitted. Further details depend on the form.
// @since protocol v26.
// form_id: string;
- [TalerFormAttributes.FORM_ID] : string;
- [TalerFormAttributes.FORM_VERSION] : number;
-}
+ [TalerFormAttributes.FORM_ID]: string;
+ [TalerFormAttributes.FORM_VERSION]: number;
+};
// Since **vATTEST**.
export interface KycCheckPublicInformation {
diff --git a/packages/taler-util/src/types-taler-merchant.ts b/packages/taler-util/src/types-taler-merchant.ts
@@ -35,7 +35,7 @@ import {
ObjectCodec,
PaytoString,
TalerPreciseTimestamp,
- UnblindedSignature,
+ UnblindedDenominationSignature,
assertUnreachable,
buildCodecForUnion,
codecForAccountLimit,
@@ -135,7 +135,7 @@ export interface CSTokenIssueBlindSig {
export interface TokenUseSig {
token_sig: EddsaSignatureString;
token_pub: EddsaPublicKeyString;
- ub_sig: UnblindedSignature;
+ ub_sig: UnblindedDenominationSignature;
h_issue: string;
}
diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts
@@ -79,12 +79,11 @@ import {
ExchangeRefundRequest,
ExchangeWireAccount,
PeerContractTerms,
- UnblindedSignature,
+ UnblindedDenominationSignature,
codecForExchangeWireAccount,
codecForPeerContractTerms,
} from "./types-taler-exchange.js";
import {
- MerchantContractChoice,
MerchantContractTerms,
MerchantContractTermsV0,
MerchantContractTermsV1,
@@ -1256,7 +1255,7 @@ export interface DepositInfo {
wireInfoHash: string;
denomKeyType: DenomKeyType;
denomPubHash: string;
- denomSig: UnblindedSignature;
+ denomSig: UnblindedDenominationSignature;
requiredMinimumAge?: number;
@@ -2390,7 +2389,7 @@ export type GetChoicesForPaymentResult = {
* are payable.
*/
automaticExecution?: boolean;
-}
+};
export interface SharePaymentRequest {
merchantBaseUrl: string;
@@ -2562,6 +2561,7 @@ export interface RefreshPlanchetInfo {
blindingKey: string;
maxAge: number;
+
ageCommitmentProof?: AgeCommitmentProof;
}
diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
@@ -83,7 +83,6 @@ import {
rsaBlind,
rsaUnblind,
rsaVerify,
- setupTipPlanchet,
SignTokenUseRequest,
Slate,
SlateCreationRequest,
@@ -95,7 +94,7 @@ import {
TokenEnvelope,
TokenIssueBlindSig,
TokenIssuePublicKey,
- UnblindedSignature,
+ UnblindedDenominationSignature,
WireFee,
WithdrawalPlanchet,
} from "@gnu-taler/taler-util";
@@ -107,9 +106,9 @@ import {
DecryptContractRequest,
DecryptContractResponse,
DerivedRefreshSession,
- DerivedTipPlanchet,
+ DerivedRefreshSessionV2,
DeriveRefreshSessionRequest,
- DeriveTipRequest,
+ DeriveRefreshSessionRequestV2,
EncryptContractForDepositRequest,
EncryptContractForDepositResponse,
EncryptContractRequest,
@@ -151,11 +150,6 @@ export interface TalerCryptoInterface {
eddsaSign(req: EddsaSignRequest): Promise<EddsaSignResponse>;
/**
- * Create a planchet used for tipping, including the private keys.
- */
- createTipPlanchet(req: DeriveTipRequest): Promise<DerivedTipPlanchet>;
-
- /**
* Create a pre-token (a.k.a. slate) of a given token family.
*/
createSlate(req: SlateCreationRequest): Promise<Slate>;
@@ -202,11 +196,11 @@ export interface TalerCryptoInterface {
unblindDenominationSignature(
req: UnblindDenominationSignatureRequest,
- ): Promise<UnblindedSignature>;
+ ): Promise<UnblindedDenominationSignature>;
unblindTokenIssueSignature(
req: UnblindTokenIssueSignatureRequest,
- ): Promise<UnblindedSignature>;
+ ): Promise<UnblindedDenominationSignature>;
rsaUnblind(req: RsaUnblindRequest): Promise<RsaUnblindResponse>;
@@ -222,6 +216,10 @@ export interface TalerCryptoInterface {
req: DeriveRefreshSessionRequest,
): Promise<DerivedRefreshSession>;
+ deriveRefreshSessionV2(
+ req: DeriveRefreshSessionRequestV2,
+ ): Promise<DerivedRefreshSessionV2>;
+
hashString(req: HashStringRequest): Promise<HashStringResult>;
signCoinLink(req: SignCoinLinkRequest): Promise<EddsaSigningResult>;
@@ -317,11 +315,6 @@ export const nullCrypto: TalerCryptoInterface = {
eddsaSign: function (req: EddsaSignRequest): Promise<EddsaSignResponse> {
throw new Error("Function not implemented.");
},
- createTipPlanchet: function (
- req: DeriveTipRequest,
- ): Promise<DerivedTipPlanchet> {
- throw new Error("Function not implemented.");
- },
createSlate: function (req: SlateCreationRequest): Promise<Slate> {
throw new Error("Function not implemented.");
},
@@ -390,12 +383,12 @@ export const nullCrypto: TalerCryptoInterface = {
},
unblindDenominationSignature: function (
req: UnblindDenominationSignatureRequest,
- ): Promise<UnblindedSignature> {
+ ): Promise<UnblindedDenominationSignature> {
throw new Error("Function not implemented.");
},
unblindTokenIssueSignature: function (
req: UnblindTokenIssueSignatureRequest,
- ): Promise<UnblindedSignature> {
+ ): Promise<UnblindedDenominationSignature> {
throw new Error("Function not implemented.");
},
rsaUnblind: function (req: RsaUnblindRequest): Promise<RsaUnblindResponse> {
@@ -533,6 +526,11 @@ export const nullCrypto: TalerCryptoInterface = {
): Promise<SignWithdrawalResponse> {
throw new Error("Function not implemented.");
},
+ deriveRefreshSessionV2: function (
+ req: DeriveRefreshSessionRequestV2,
+ ): Promise<DerivedRefreshSessionV2> {
+ throw new Error("Function not implemented.");
+ },
};
export type WithArg<X> = X extends (req: infer T) => infer R
@@ -608,7 +606,7 @@ export interface SpendCoinDetails {
contribution: AmountString;
feeDeposit: AmountString;
denomPubHash: string;
- denomSig: UnblindedSignature;
+ denomSig: UnblindedDenominationSignature;
ageCommitmentProof: AgeCommitmentProof | undefined;
}
@@ -685,7 +683,7 @@ export interface PaymentSignatureValidationRequest {
export interface TokenSignatureValidationRequest {
tokenUsePub: string;
tokenIssuePub: TokenIssuePublicKey;
- sig: UnblindedSignature;
+ sig: UnblindedDenominationSignature;
}
export interface ContractTermsValidationRequest {
@@ -939,45 +937,6 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
}
},
- async createTipPlanchet(
- tci: TalerCryptoInterfaceR,
- req: DeriveTipRequest,
- ): Promise<DerivedTipPlanchet> {
- if (req.denomPub.cipher !== DenomKeyType.Rsa) {
- throw Error(`unsupported cipher (${req.denomPub.cipher})`);
- }
- const fc = await setupTipPlanchet(
- decodeCrock(req.secretSeed),
- req.denomPub,
- req.planchetIndex,
- );
- const maybeAch = fc.ageCommitmentProof
- ? AgeRestriction.hashCommitment(fc.ageCommitmentProof.commitment)
- : undefined;
- const denomPub = decodeCrock(req.denomPub.rsa_public_key);
- const coinPubHash = hashCoinPub(encodeCrock(fc.coinPub), maybeAch);
- const blindResp = await tci.rsaBlind(tci, {
- bks: encodeCrock(fc.bks),
- hm: encodeCrock(coinPubHash),
- pub: encodeCrock(denomPub),
- });
- const coinEv = {
- cipher: DenomKeyType.Rsa,
- rsa_blinded_planchet: blindResp.blinded,
- };
- const tipPlanchet: DerivedTipPlanchet = {
- blindingKey: encodeCrock(fc.bks),
- coinEv,
- coinEvHash: encodeCrock(
- hashCoinEv(coinEv, encodeCrock(hashDenomPub(req.denomPub))),
- ),
- coinPriv: encodeCrock(fc.coinPriv),
- coinPub: encodeCrock(fc.coinPub),
- ageCommitmentProof: fc.ageCommitmentProof,
- };
- return tipPlanchet;
- },
-
async createSlate(
tci: TalerCryptoInterfaceR,
req: SlateCreationRequest,
@@ -1328,7 +1287,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
async unblindDenominationSignature(
tci: TalerCryptoInterfaceR,
req: UnblindDenominationSignatureRequest,
- ): Promise<UnblindedSignature> {
+ ): Promise<UnblindedDenominationSignature> {
if (req.evSig.cipher === DenomKeyType.Rsa) {
if (req.planchet.denomPub.cipher !== DenomKeyType.Rsa) {
throw new Error(
@@ -1352,7 +1311,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
async unblindTokenIssueSignature(
tci: TalerCryptoInterfaceR,
req: UnblindTokenIssueSignatureRequest,
- ): Promise<UnblindedSignature> {
+ ): Promise<UnblindedDenominationSignature> {
if (req.evSig.cipher === DenomKeyType.Rsa) {
if (req.slate.tokenIssuePub.cipher !== DenomKeyType.Rsa) {
throw new Error("slate cipher does not match blind signature cipher");
@@ -1488,6 +1447,13 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
}
},
+ async deriveRefreshSessionV2(
+ tci: TalerCryptoInterfaceR,
+ req: DeriveRefreshSessionRequestV2,
+ ): Promise<DerivedRefreshSessionV2> {
+ throw new Error("Function not implemented.");
+ },
+
async deriveRefreshSession(
tci: TalerCryptoInterfaceR,
req: DeriveRefreshSessionRequest,
@@ -1638,7 +1604,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
confirmSig: confirmSigResp.sig,
hash: encodeCrock(sessionHash),
meltCoinPub: meltCoinPub,
- planchetsForGammas: planchetsForGammas,
+ planchets: planchetsForGammas,
transferPrivs,
transferPubs,
meltValueWithFee: valueWithFee,
diff --git a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts
@@ -31,7 +31,6 @@ import {
AgeCommitmentProof,
AmountJson,
AmountString,
- CoinEnvelope,
DenominationPubKey,
EddsaPrivateKeyString,
EddsaPublicKeyString,
@@ -40,7 +39,7 @@ import {
HashCodeString,
RefreshPlanchetInfo,
TalerProtocolTimestamp,
- UnblindedSignature,
+ UnblindedDenominationSignature,
WalletAccountMergeFlags,
} from "@gnu-taler/taler-util";
@@ -70,8 +69,23 @@ export interface DeriveRefreshSessionRequest {
}
/**
+ * Request to derive a refresh session from the refresh session
+ * public seed.
*
+ * This version is for the improved, PQC-compatible refresh protocol.
*/
+export interface DeriveRefreshSessionRequestV2 {
+ sessionPublicSeed: string;
+ kappa: number;
+ meltCoinPub: string;
+ meltCoinPriv: string;
+ meltCoinDenomPubHash: string;
+ meltCoinMaxAge: number;
+ meltCoinAgeCommitmentProof?: AgeCommitmentProof;
+ newCoinDenoms: RefreshNewDenomInfo[];
+ feeRefresh: AmountJson;
+}
+
export interface DerivedRefreshSession {
/**
* Public key that's being melted in this session.
@@ -86,7 +100,7 @@ export interface DerivedRefreshSession {
/**
* Planchets for each cut-and-choose instance.
*/
- planchetsForGammas: RefreshPlanchetInfo[][];
+ planchets: RefreshPlanchetInfo[][];
/**
* The transfer keys, kappa of them.
@@ -109,22 +123,36 @@ export interface DerivedRefreshSession {
meltValueWithFee: AmountJson;
}
-export interface DeriveTipRequest {
- secretSeed: string;
- denomPub: DenominationPubKey;
- planchetIndex: number;
-}
+export interface DerivedRefreshSessionV2 {
+ /**
+ * Public key that's being melted in this session.
+ */
+ meltCoinPub: string;
-/**
- * Tipping planchet stored in the database.
- */
-export interface DerivedTipPlanchet {
- blindingKey: string;
- coinEv: CoinEnvelope;
- coinEvHash: string;
- coinPriv: string;
- coinPub: string;
- ageCommitmentProof: AgeCommitmentProof | undefined;
+ /**
+ * Signature to confirm the melting.
+ */
+ confirmSig: string;
+
+ /**
+ * Planchets for each cut-and-choose instance.
+ */
+ planchets: RefreshPlanchetInfo[][];
+
+ /**
+ * kappa signatures by the old coin private key.
+ */
+ signatures: EddsaSignatureString[];
+
+ /**
+ * Hash of the session.
+ */
+ hash: string;
+
+ /**
+ * Exact value that is being melted.
+ */
+ meltValueWithFee: AmountJson;
}
export interface SignTrackTransactionRequest {
@@ -144,7 +172,7 @@ export interface CreateRecoupReqRequest {
blindingKey: string;
denomPub: DenominationPubKey;
denomPubHash: string;
- denomSig: UnblindedSignature;
+ denomSig: UnblindedDenominationSignature;
}
/**
@@ -156,7 +184,7 @@ export interface CreateRecoupRefreshReqRequest {
blindingKey: string;
denomPub: DenominationPubKey;
denomPubHash: string;
- denomSig: UnblindedSignature;
+ denomSig: UnblindedDenominationSignature;
}
export interface EncryptedContract {
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
@@ -65,7 +65,7 @@ import {
TokenIssuePublicKey,
TokenUseSig,
TransactionIdStr,
- UnblindedSignature,
+ UnblindedDenominationSignature,
WireInfo,
WithdrawalExchangeAccountDetails,
ZeroLimitedOperation,
@@ -902,7 +902,7 @@ export interface CoinRecord {
/**
* Unblinded signature by the exchange.
*/
- denomSig: UnblindedSignature;
+ denomSig: UnblindedDenominationSignature;
/**
* Base URL that identifies the exchange from which we got the
@@ -1026,13 +1026,13 @@ export interface TokenRecord {
/**
* End time of the token family's validity period.
- */
+ */
validBefore: DbProtocolTimestamp;
/**
* Unblinded token issue signature made by the merchant.
*/
- tokenIssueSig: UnblindedSignature;
+ tokenIssueSig: UnblindedDenominationSignature;
/**
* Token use public key used to confirm usage of tokens.
@@ -1070,7 +1070,7 @@ export interface TokenRecord {
* also the database representation of a token before being
* signed by the merchant, as stored in the `slates' data store.
*/
-export type SlateRecord = Omit<TokenRecord, 'tokenIssueSig'>;
+export type SlateRecord = Omit<TokenRecord, "tokenIssueSig">;
/**
* History item for a coin.
@@ -1233,8 +1233,15 @@ export interface RefreshSessionRecord {
/**
* 512-bit secret that can be used to derive
* the other cryptographic material for the refresh session.
+ *
+ * If this field is set, it's a legacy V1 refresh session.
+ */
+ sessionSecretSeed?: string;
+
+ /**
+ * If this field is set, it's a V2 refresh session.
*/
- sessionSecretSeed: string;
+ sessionPublicSeed?: string;
/**
* Sum of the value of denominations we want
@@ -2857,12 +2864,15 @@ export const WalletStoresV1 = {
keyPath: "tokenUsePub",
}),
{
- byTokenIssuePubHash: describeIndex("byTokenIssuePubHash", "tokenIssuePubHash"),
- byPurchaseIdAndChoiceIndex: describeIndex(
- "byPurchaseIdAndChoiceIndex",
- ["purchaseId", "choiceIndex"],
+ byTokenIssuePubHash: describeIndex(
+ "byTokenIssuePubHash",
+ "tokenIssuePubHash",
),
- }
+ byPurchaseIdAndChoiceIndex: describeIndex("byPurchaseIdAndChoiceIndex", [
+ "purchaseId",
+ "choiceIndex",
+ ]),
+ },
),
slates: describeStore(
"slates",
@@ -2870,13 +2880,13 @@ export const WalletStoresV1 = {
keyPath: "tokenUsePub",
}),
{
- byPurchaseIdAndChoiceIndex: describeIndex(
- "byPurchaseIdAndChoiceIndex",
- ["purchaseId", "choiceIndex"],
- ),
+ byPurchaseIdAndChoiceIndex: describeIndex("byPurchaseIdAndChoiceIndex", [
+ "purchaseId",
+ "choiceIndex",
+ ]),
byPurchaseIdAndChoiceIndexAndOutputIndex: describeIndex(
"byPurchaseIdAndChoiceIndexAndOutputIndex",
- ["purchaseId", "choiceIndex", "outputIndex"]
+ ["purchaseId", "choiceIndex", "outputIndex"],
),
},
),
diff --git a/packages/taler-wallet-core/src/dbless.ts b/packages/taler-wallet-core/src/dbless.ts
@@ -40,7 +40,7 @@ import {
ExchangeProtocolVersion,
Logger,
TalerCorebankApiClient,
- UnblindedSignature,
+ UnblindedDenominationSignature,
codecForAny,
codecForBankWithdrawalOperationPostResponse,
codecForBatchDepositSuccess,
@@ -79,7 +79,7 @@ export interface CoinInfo {
coinPub: string;
coinPriv: string;
exchangeBaseUrl: string;
- denomSig: UnblindedSignature;
+ denomSig: UnblindedDenominationSignature;
denomPub: DenominationPubKey;
denomPubHash: string;
feeDeposit: string;
diff --git a/packages/taler-wallet-core/src/refresh.ts b/packages/taler-wallet-core/src/refresh.ts
@@ -26,12 +26,15 @@ import {
AgeCommitment,
AgeRestriction,
AmountJson,
+ AmountLike,
Amounts,
amountToPretty,
assertUnreachable,
+ BlindedDenominationSignature,
checkDbInvariant,
codecForCoinHistoryResponse,
codecForExchangeMeltResponse,
+ codecForExchangeRevealMeltResponseV2,
codecForExchangeRevealResponse,
CoinPublicKeyString,
CoinRefreshRequest,
@@ -41,8 +44,10 @@ import {
Duration,
encodeCrock,
ExchangeMeltRequest,
+ ExchangeMeltRequestV2,
ExchangeProtocolVersion,
ExchangeRefreshRevealRequest,
+ ExchangeRefreshRevealRequestV2,
ExchangeRefundRequest,
fnutil,
ForceRefreshRequest,
@@ -51,9 +56,11 @@ import {
HashCodeString,
HttpStatusCode,
j2s,
+ LibtoolVersion,
Logger,
makeErrorDetail,
NotificationType,
+ RefreshPlanchetInfo,
RefreshReason,
TalerErrorCode,
TalerErrorDetail,
@@ -111,7 +118,7 @@ import {
WalletDbStoresArr,
} from "./db.js";
import { selectWithdrawalDenominations } from "./denomSelection.js";
-import { getScopeForAllExchanges } from "./exchanges.js";
+import { fetchFreshExchange, getScopeForAllExchanges } from "./exchanges.js";
import {
BalanceEffect,
constructTransactionIdentifier,
@@ -523,8 +530,6 @@ async function initRefreshSession(
const exchangeBaseUrl = oldCoin.exchangeBaseUrl;
- const sessionSecretSeed = encodeCrock(getRandomBytes(64));
-
const oldDenom = await getDenomInfo(
wex,
tx,
@@ -588,7 +593,6 @@ async function initRefreshSession(
coinIndex,
refreshGroupId,
norevealIndex: undefined,
- sessionSecretSeed: sessionSecretSeed,
newDenoms: newCoinDenoms.selectedDenoms.map((x) => ({
count: x.count,
denomPubHash: x.denomPubHash,
@@ -727,35 +731,53 @@ async function refreshMelt(
return;
}
- const { newCoinDenoms, oldCoin, oldDenom, refreshGroup, refreshSession } = d;
-
- let exchangeProtocolVersion: ExchangeProtocolVersion;
- switch (d.oldDenom.denomPub.cipher) {
- case DenomKeyType.Rsa: {
- exchangeProtocolVersion = ExchangeProtocolVersion.V12;
- break;
+ // Make sure that we have a seed.
+ if (
+ d.refreshSession.sessionPublicSeed == null &&
+ d.refreshSession.sessionSecretSeed == null
+ ) {
+ const exchange = await fetchFreshExchange(wex, d.oldCoin.exchangeBaseUrl);
+ const exchangeVer = LibtoolVersion.parseVersion(
+ exchange.protocolVersionRange,
+ );
+ checkDbInvariant(!!exchangeVer, "bad exchange version string");
+ const seed = encodeCrock(getRandomBytes(64));
+ const updatedSession = await wex.db.runReadWriteTx(
+ {
+ storeNames: ["refreshSessions"],
+ },
+ async (tx) => {
+ const refreshSession = await tx.refreshSessions.get([
+ refreshGroupId,
+ coinIndex,
+ ]);
+ if (!refreshSession) {
+ return undefined;
+ }
+ if (
+ refreshSession.sessionPublicSeed != null ||
+ refreshSession.sessionSecretSeed != null
+ ) {
+ return refreshSession;
+ }
+ if (exchangeVer.current >= 27) {
+ refreshSession.sessionPublicSeed = seed;
+ } else {
+ refreshSession.sessionSecretSeed = seed;
+ }
+ await tx.refreshSessions.put(refreshSession);
+ return refreshSession;
+ },
+ );
+ if (!updatedSession) {
+ throw Error(
+ "Could not update refresh session (concurrent deletion?).",
+ );
}
- default:
- throw Error("unsupported key type");
+ d.refreshSession = updatedSession;
}
- const derived = await wex.cryptoApi.deriveRefreshSession({
- exchangeProtocolVersion,
- kappa: 3,
- meltCoinDenomPubHash: oldCoin.denomPubHash,
- meltCoinPriv: oldCoin.coinPriv,
- meltCoinPub: oldCoin.coinPub,
- feeRefresh: Amounts.parseOrThrow(oldDenom.feeRefresh),
- meltCoinMaxAge: oldCoin.maxAge,
- meltCoinAgeCommitmentProof: oldCoin.ageCommitmentProof,
- newCoinDenoms,
- sessionSecretSeed: refreshSession.sessionSecretSeed,
- });
-
- const reqUrl = new URL(
- `coins/${oldCoin.coinPub}/melt`,
- oldCoin.exchangeBaseUrl,
- );
+ const { newCoinDenoms, oldCoin, oldDenom, refreshGroup, refreshSession } = d;
let maybeAch: HashCodeString | undefined;
if (oldCoin.ageCommitmentProof) {
@@ -764,88 +786,221 @@ async function refreshMelt(
);
}
- const meltReqBody: ExchangeMeltRequest = {
- coin_pub: oldCoin.coinPub,
- confirm_sig: derived.confirmSig,
- denom_pub_hash: oldCoin.denomPubHash,
- denom_sig: oldCoin.denomSig,
- rc: derived.hash,
- value_with_fee: Amounts.stringify(derived.meltValueWithFee),
- age_commitment_hash: maybeAch,
- };
+ if (refreshSession.sessionSecretSeed) {
+ // Old legacy melt protocol.
+ let exchangeProtocolVersion: ExchangeProtocolVersion;
+ switch (d.oldDenom.denomPub.cipher) {
+ case DenomKeyType.Rsa: {
+ exchangeProtocolVersion = ExchangeProtocolVersion.V12;
+ break;
+ }
+ default:
+ throw Error("unsupported key type");
+ }
- const resp = await wex.ws.runSequentialized(
- [EXCHANGE_COINS_LOCK],
- async () => {
- return await cancelableFetch(wex, reqUrl, {
- method: "POST",
- body: meltReqBody,
- timeout: getRefreshRequestTimeout(refreshGroup)
- });
- },
- );
+ const derived = await wex.cryptoApi.deriveRefreshSession({
+ exchangeProtocolVersion,
+ kappa: 3,
+ meltCoinDenomPubHash: oldCoin.denomPubHash,
+ meltCoinPriv: oldCoin.coinPriv,
+ meltCoinPub: oldCoin.coinPub,
+ feeRefresh: Amounts.parseOrThrow(oldDenom.feeRefresh),
+ meltCoinMaxAge: oldCoin.maxAge,
+ meltCoinAgeCommitmentProof: oldCoin.ageCommitmentProof,
+ newCoinDenoms,
+ sessionSecretSeed: refreshSession.sessionSecretSeed,
+ });
- switch (resp.status) {
- case HttpStatusCode.NotFound: {
- const errDetail = await readTalerErrorResponse(resp);
- await handleRefreshMeltNotFound(ctx, coinIndex, resp, errDetail);
- return;
- }
- case HttpStatusCode.Gone: {
- const errDetail = await readTalerErrorResponse(resp);
- await handleRefreshMeltGone(ctx, coinIndex, errDetail);
- return;
- }
- case HttpStatusCode.Conflict: {
- const errDetail = await readTalerErrorResponse(resp);
- await handleRefreshMeltConflict(
- ctx,
- refreshGroup,
- coinIndex,
- errDetail,
- derived,
- oldCoin,
- );
- return;
- }
- case HttpStatusCode.Ok:
- break;
- default: {
- const errDetail = await readTalerErrorResponse(resp);
- throwUnexpectedRequestError(resp, errDetail);
+ const reqUrl = new URL(
+ `coins/${oldCoin.coinPub}/melt`,
+ oldCoin.exchangeBaseUrl,
+ );
+
+ const meltReqBody: ExchangeMeltRequest = {
+ coin_pub: oldCoin.coinPub,
+ confirm_sig: derived.confirmSig,
+ denom_pub_hash: oldCoin.denomPubHash,
+ denom_sig: oldCoin.denomSig,
+ rc: derived.hash,
+ value_with_fee: Amounts.stringify(derived.meltValueWithFee),
+ age_commitment_hash: maybeAch,
+ };
+
+ const resp = await wex.ws.runSequentialized(
+ [EXCHANGE_COINS_LOCK],
+ async () => {
+ return await cancelableFetch(wex, reqUrl, {
+ method: "POST",
+ body: meltReqBody,
+ timeout: getRefreshRequestTimeout(refreshGroup),
+ });
+ },
+ );
+
+ switch (resp.status) {
+ case HttpStatusCode.NotFound: {
+ const errDetail = await readTalerErrorResponse(resp);
+ await handleRefreshMeltNotFound(ctx, coinIndex, resp, errDetail);
+ return;
+ }
+ case HttpStatusCode.Gone: {
+ const errDetail = await readTalerErrorResponse(resp);
+ await handleRefreshMeltGone(ctx, coinIndex, errDetail);
+ return;
+ }
+ case HttpStatusCode.Conflict: {
+ const errDetail = await readTalerErrorResponse(resp);
+ await handleRefreshMeltConflict(
+ ctx,
+ refreshGroup,
+ coinIndex,
+ errDetail,
+ derived.meltValueWithFee,
+ oldCoin,
+ );
+ return;
+ }
+ case HttpStatusCode.Ok:
+ break;
+ default: {
+ const errDetail = await readTalerErrorResponse(resp);
+ throwUnexpectedRequestError(resp, errDetail);
+ }
}
- }
- const meltResponse = await readSuccessResponseJsonOrThrow(
- resp,
- codecForExchangeMeltResponse(),
- );
+ const meltResponse = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForExchangeMeltResponse(),
+ );
- const norevealIndex = meltResponse.noreveal_index;
+ const norevealIndex = meltResponse.noreveal_index;
- refreshSession.norevealIndex = norevealIndex;
+ refreshSession.norevealIndex = norevealIndex;
- await wex.db.runReadWriteTx(
- { storeNames: ["refreshGroups", "refreshSessions"] },
- async (tx) => {
- const rg = await tx.refreshGroups.get(refreshGroupId);
- if (!rg) {
+ await wex.db.runReadWriteTx(
+ { storeNames: ["refreshGroups", "refreshSessions"] },
+ async (tx) => {
+ const rg = await tx.refreshGroups.get(refreshGroupId);
+ if (!rg) {
+ return;
+ }
+ if (rg.timestampFinished) {
+ return;
+ }
+ const rs = await tx.refreshSessions.get([refreshGroupId, coinIndex]);
+ if (!rs) {
+ return;
+ }
+ if (rs.norevealIndex !== undefined) {
+ return;
+ }
+ rs.norevealIndex = norevealIndex;
+ await tx.refreshSessions.put(rs);
+ },
+ );
+ } else if (refreshSession.sessionPublicSeed) {
+ // New melt protocol.
+ const derived = await wex.cryptoApi.deriveRefreshSessionV2({
+ kappa: 3,
+ meltCoinDenomPubHash: oldCoin.denomPubHash,
+ meltCoinPriv: oldCoin.coinPriv,
+ meltCoinPub: oldCoin.coinPub,
+ feeRefresh: Amounts.parseOrThrow(oldDenom.feeRefresh),
+ meltCoinMaxAge: oldCoin.maxAge,
+ meltCoinAgeCommitmentProof: oldCoin.ageCommitmentProof,
+ newCoinDenoms,
+ sessionPublicSeed: refreshSession.sessionPublicSeed,
+ });
+
+ const reqUrl = new URL(`melt`, oldCoin.exchangeBaseUrl);
+ const meltReqBody: ExchangeMeltRequestV2 = {
+ old_coin_pub: oldCoin.coinPub,
+ old_denom_pub_h: oldCoin.denomPubHash,
+ old_denom_sig: oldCoin.denomSig,
+ old_age_commitment_h: maybeAch,
+ refresh_seed: refreshSession.sessionPublicSeed,
+ confirm_sig: derived.confirmSig,
+ coin_evs: derived.planchets.map((x) => x.map((y) => y.coinEv)),
+ denoms_h: newCoinDenoms.map((x) => x.denomPubHash),
+ value_with_fee: Amounts.stringify(derived.meltValueWithFee),
+ };
+ const resp = await wex.ws.runSequentialized(
+ [EXCHANGE_COINS_LOCK],
+ async () => {
+ return await cancelableFetch(wex, reqUrl, {
+ method: "POST",
+ body: meltReqBody,
+ timeout: getRefreshRequestTimeout(refreshGroup),
+ });
+ },
+ );
+
+ switch (resp.status) {
+ case HttpStatusCode.NotFound: {
+ const errDetail = await readTalerErrorResponse(resp);
+ await handleRefreshMeltNotFound(ctx, coinIndex, resp, errDetail);
return;
}
- if (rg.timestampFinished) {
+ case HttpStatusCode.Gone: {
+ const errDetail = await readTalerErrorResponse(resp);
+ await handleRefreshMeltGone(ctx, coinIndex, errDetail);
return;
}
- const rs = await tx.refreshSessions.get([refreshGroupId, coinIndex]);
- if (!rs) {
+ case HttpStatusCode.Conflict: {
+ const errDetail = await readTalerErrorResponse(resp);
+ await handleRefreshMeltConflict(
+ ctx,
+ refreshGroup,
+ coinIndex,
+ errDetail,
+ derived.meltValueWithFee,
+ oldCoin,
+ );
return;
}
- if (rs.norevealIndex !== undefined) {
- return;
+ case HttpStatusCode.Ok:
+ break;
+ default: {
+ const errDetail = await readTalerErrorResponse(resp);
+ throwUnexpectedRequestError(resp, errDetail);
}
- rs.norevealIndex = norevealIndex;
- await tx.refreshSessions.put(rs);
- },
- );
+ }
+
+ const meltResponse = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForExchangeMeltResponse(),
+ );
+
+ // FIXME: Check exchange's signature.
+
+ const norevealIndex = meltResponse.noreveal_index;
+
+ refreshSession.norevealIndex = norevealIndex;
+
+ await wex.db.runReadWriteTx(
+ { storeNames: ["refreshGroups", "refreshSessions"] },
+ async (tx) => {
+ const rg = await tx.refreshGroups.get(refreshGroupId);
+ if (!rg) {
+ return;
+ }
+ if (rg.timestampFinished) {
+ return;
+ }
+ const rs = await tx.refreshSessions.get([refreshGroupId, coinIndex]);
+ if (!rs) {
+ return;
+ }
+ if (rs.norevealIndex !== undefined) {
+ return;
+ }
+ rs.norevealIndex = norevealIndex;
+ await tx.refreshSessions.put(rs);
+ },
+ );
+ } else {
+ throw Error("unsupported refresh session (neither secret nor public seed)");
+ }
}
async function handleRefreshMeltGone(
@@ -901,14 +1056,14 @@ async function handleRefreshMeltConflict(
refreshGroup: RefreshGroupRecord,
coinIndex: number,
errDetails: TalerErrorDetail,
- derived: DerivedRefreshSession,
+ meltValueWithFee: AmountLike,
oldCoin: CoinRecord,
): Promise<void> {
// Just log for better diagnostics here, error status
// will be handled later.
logger.error(
`melt request for ${Amounts.stringify(
- derived.meltValueWithFee,
+ meltValueWithFee,
)} failed in refresh group ${ctx.refreshGroupId} due to conflict`,
);
@@ -1110,7 +1265,7 @@ export async function assembleRefreshRevealRequest(args: {
const privs = Array.from(derived.transferPrivs);
privs.splice(norevealIndex, 1);
- const planchets = derived.planchetsForGammas[norevealIndex];
+ const planchets = derived.planchets[norevealIndex];
if (!planchets) {
throw Error("refresh index error");
}
@@ -1238,89 +1393,146 @@ async function refreshReveal(
norevealIndex,
} = d;
- let exchangeProtocolVersion: ExchangeProtocolVersion;
- switch (d.oldDenom.denomPub.cipher) {
- case DenomKeyType.Rsa: {
- exchangeProtocolVersion = ExchangeProtocolVersion.V12;
- break;
+ // Blinded signatures, either from the old or the new reveal protocol.
+ let resEvSigs: BlindedDenominationSignature[];
+ let planchets: RefreshPlanchetInfo[][];
+
+ if (refreshSession.sessionSecretSeed != null) {
+ // Legacy refresh session.
+
+ let exchangeProtocolVersion: ExchangeProtocolVersion;
+ switch (d.oldDenom.denomPub.cipher) {
+ case DenomKeyType.Rsa: {
+ exchangeProtocolVersion = ExchangeProtocolVersion.V12;
+ break;
+ }
+ default:
+ throw Error("unsupported key type");
}
- default:
- throw Error("unsupported key type");
- }
- const derived = await wex.cryptoApi.deriveRefreshSession({
- exchangeProtocolVersion,
- kappa: 3,
- meltCoinDenomPubHash: oldCoin.denomPubHash,
- meltCoinPriv: oldCoin.coinPriv,
- meltCoinPub: oldCoin.coinPub,
- feeRefresh: Amounts.parseOrThrow(oldDenom.feeRefresh),
- newCoinDenoms,
- meltCoinMaxAge: oldCoin.maxAge,
- meltCoinAgeCommitmentProof: oldCoin.ageCommitmentProof,
- sessionSecretSeed: refreshSession.sessionSecretSeed,
- });
+ const derived = await wex.cryptoApi.deriveRefreshSession({
+ exchangeProtocolVersion,
+ kappa: 3,
+ meltCoinDenomPubHash: oldCoin.denomPubHash,
+ meltCoinPriv: oldCoin.coinPriv,
+ meltCoinPub: oldCoin.coinPub,
+ feeRefresh: Amounts.parseOrThrow(oldDenom.feeRefresh),
+ newCoinDenoms,
+ meltCoinMaxAge: oldCoin.maxAge,
+ meltCoinAgeCommitmentProof: oldCoin.ageCommitmentProof,
+ sessionSecretSeed: refreshSession.sessionSecretSeed,
+ });
- const reqUrl = new URL(
- `refreshes/${derived.hash}/reveal`,
- oldCoin.exchangeBaseUrl,
- );
+ const reqUrl = new URL(
+ `refreshes/${derived.hash}/reveal`,
+ oldCoin.exchangeBaseUrl,
+ );
- const req = await assembleRefreshRevealRequest({
- cryptoApi: wex.cryptoApi,
- derived,
- newDenoms: newCoinDenoms,
- norevealIndex: norevealIndex,
- oldCoinPriv: oldCoin.coinPriv,
- oldCoinPub: oldCoin.coinPub,
- oldAgeCommitment: oldCoin.ageCommitmentProof?.commitment,
- });
+ const req = await assembleRefreshRevealRequest({
+ cryptoApi: wex.cryptoApi,
+ derived,
+ newDenoms: newCoinDenoms,
+ norevealIndex: norevealIndex,
+ oldCoinPriv: oldCoin.coinPriv,
+ oldCoinPub: oldCoin.coinPub,
+ oldAgeCommitment: oldCoin.ageCommitmentProof?.commitment,
+ });
- const resp = await wex.ws.runSequentialized(
- [EXCHANGE_COINS_LOCK],
- async () => cancelableFetch(wex, reqUrl, {
- body: req,
- method: "POST",
- timeout: getRefreshRequestTimeout(refreshGroup)
- }),
- );
+ const resp = await wex.ws.runSequentialized(
+ [EXCHANGE_COINS_LOCK],
+ async () =>
+ cancelableFetch(wex, reqUrl, {
+ body: req,
+ method: "POST",
+ timeout: getRefreshRequestTimeout(refreshGroup),
+ }),
+ );
- switch (resp.status) {
- case HttpStatusCode.Ok:
- break;
- case HttpStatusCode.Conflict:
- case HttpStatusCode.Gone: {
- const errDetail = await readTalerErrorResponse(resp);
- await handleRefreshRevealError(ctx, coinIndex, errDetail);
- return;
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ break;
+ case HttpStatusCode.Conflict:
+ case HttpStatusCode.Gone: {
+ const errDetail = await readTalerErrorResponse(resp);
+ await handleRefreshRevealError(ctx, coinIndex, errDetail);
+ return;
+ }
+ default: {
+ const errDetail = await readTalerErrorResponse(resp);
+ throwUnexpectedRequestError(resp, errDetail);
+ }
}
- default: {
- const errDetail = await readTalerErrorResponse(resp);
- throwUnexpectedRequestError(resp, errDetail);
+
+ const reveal = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForExchangeRevealResponse(),
+ );
+ planchets = derived.planchets;
+ resEvSigs = reveal.ev_sigs.map((x) => x.ev_sig);
+ } else if (refreshSession.sessionPublicSeed != null) {
+ const derived = await wex.cryptoApi.deriveRefreshSessionV2({
+ kappa: 3,
+ meltCoinDenomPubHash: oldCoin.denomPubHash,
+ meltCoinPriv: oldCoin.coinPriv,
+ meltCoinPub: oldCoin.coinPub,
+ sessionPublicSeed: refreshSession.sessionPublicSeed,
+ feeRefresh: Amounts.parseOrThrow(oldDenom.feeRefresh),
+ newCoinDenoms,
+ meltCoinMaxAge: oldCoin.maxAge,
+ meltCoinAgeCommitmentProof: oldCoin.ageCommitmentProof,
+ });
+ const req: ExchangeRefreshRevealRequestV2 = {
+ rc: derived.hash,
+ signatures: derived.signatures.filter((v, i) => i != norevealIndex),
+ age_commitment: oldCoin.ageCommitmentProof?.commitment?.publicKeys,
+ };
+ const reqUrl = new URL(`reveal-melt`, oldCoin.exchangeBaseUrl);
+ const resp = await wex.ws.runSequentialized(
+ [EXCHANGE_COINS_LOCK],
+ async () =>
+ cancelableFetch(wex, reqUrl, {
+ body: req,
+ method: "POST",
+ timeout: getRefreshRequestTimeout(refreshGroup),
+ }),
+ );
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ break;
+ case HttpStatusCode.Conflict:
+ case HttpStatusCode.Gone: {
+ const errDetail = await readTalerErrorResponse(resp);
+ await handleRefreshRevealError(ctx, coinIndex, errDetail);
+ return;
+ }
+ default: {
+ const errDetail = await readTalerErrorResponse(resp);
+ throwUnexpectedRequestError(resp, errDetail);
+ }
}
- }
- const reveal = await readSuccessResponseJsonOrThrow(
- resp,
- codecForExchangeRevealResponse(),
- );
+ const reveal = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForExchangeRevealMeltResponseV2(),
+ );
+ resEvSigs = reveal.ev_sigs;
+ planchets = derived.planchets;
+ } else {
+ throw Error("refresh session not supported");
+ }
const coins: CoinRecord[] = [];
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Refresh,
- refreshGroupId,
- });
-
for (let i = 0; i < refreshSession.newDenoms.length; i++) {
const ncd = newCoinDenoms[i];
for (let j = 0; j < refreshSession.newDenoms[i].count; j++) {
const newCoinIndex = coins.length;
- const pc = derived.planchetsForGammas[norevealIndex][newCoinIndex];
+ const pc = planchets[norevealIndex][newCoinIndex];
if (ncd.denomPub.cipher !== DenomKeyType.Rsa) {
throw Error("cipher unsupported");
}
- const evSig = reveal.ev_sigs[newCoinIndex].ev_sig;
+ const evSig = resEvSigs[newCoinIndex];
const denomSig = await wex.cryptoApi.unblindDenominationSignature({
planchet: {
blindingKey: pc.blindingKey,
@@ -1341,7 +1553,7 @@ async function refreshReveal(
refreshGroupId,
oldCoinPub: refreshGroup.oldCoinPubs[coinIndex],
},
- sourceTransactionId: transactionId,
+ sourceTransactionId: ctx.transactionId,
coinEvHash: pc.coinEvHash,
maxAge: pc.maxAge,
ageCommitmentProof: pc.ageCommitmentProof,
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts
@@ -78,7 +78,7 @@ import {
TransactionType,
TransactionWithdrawal,
URL,
- UnblindedSignature,
+ UnblindedDenominationSignature,
WalletNotification,
WithdrawUriInfoResponse,
WithdrawalDetailsForAmount,
@@ -1916,7 +1916,7 @@ async function processPlanchetVerifyAndStoreCoin(
return;
}
- let denomSig: UnblindedSignature;
+ let denomSig: UnblindedDenominationSignature;
if (planchetDenomPub.cipher === DenomKeyType.Rsa) {
denomSig = {
cipher: planchetDenomPub.cipher,
@@ -2629,7 +2629,13 @@ async function processWithdrawalGroupPendingReady(
for (let i = 0; i < numTotalCoins; i += maxBatchSize) {
let resp: WithdrawalBatchResult;
if (exchangeVer.current >= 26) {
- resp = await processPlanchetExchangeBatchRequest(wex, wgContext, {
+ logger.warn("new exchange version, but still using old batch request");
+ // resp = await processPlanchetExchangeBatchRequest(wex, wgContext, {
+ // batchSize: maxBatchSize,
+ // coinStartIndex: i,
+ // });
+ // FIXME: Use new batch request here!
+ resp = await processPlanchetExchangeLegacyBatchRequest(wex, wgContext, {
batchSize: maxBatchSize,
coinStartIndex: i,
});