commit eb6c37a897206bedb244d7f205c22299a228f953
parent 89c2b5f4044b140695dcb75dd0567457308d4afb
Author: Florian Dold <florian@dold.me>
Date: Fri, 13 Feb 2026 18:44:56 +0100
wallet-core: remove pre-v27 refresh
Diffstat:
2 files changed, 149 insertions(+), 353 deletions(-)
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
@@ -1320,14 +1320,6 @@ export interface RefreshSessionRecord {
coinIndex: number;
/**
- * 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.
*/
sessionPublicSeed?: string;
@@ -1352,6 +1344,10 @@ export interface RefreshSessionRecord {
norevealIndex?: number;
lastError?: TalerErrorDetail;
+
+ // Reserved legacy fields:
+ // * sessionSecretSeed: string
+ // (legacy v1 refresh)
}
export enum RefundReason {
diff --git a/packages/taler-wallet-core/src/refresh.ts b/packages/taler-wallet-core/src/refresh.ts
@@ -31,10 +31,10 @@ import {
assertUnreachable,
BlindedDenominationSignature,
checkDbInvariant,
+ checkLogicInvariant,
codecForCoinHistoryResponse,
codecForExchangeMeltResponse,
codecForExchangeRevealMeltResponseV2,
- codecForExchangeRevealResponse,
CoinPublicKeyString,
CoinRefreshRequest,
CoinStatus,
@@ -42,9 +42,7 @@ import {
DenomKeyType,
Duration,
encodeCrock,
- ExchangeMeltRequest,
ExchangeMeltRequestV2,
- ExchangeProtocolVersion,
ExchangeRefreshRevealRequest,
ExchangeRefreshRevealRequestV2,
ExchangeRefundRequest,
@@ -706,15 +704,15 @@ async function refreshMelt(
}
// Make sure that we have a seed.
- if (
- d.refreshSession.sessionPublicSeed == null &&
- d.refreshSession.sessionSecretSeed == null
- ) {
+ if (d.refreshSession.sessionPublicSeed == null) {
const exchange = await fetchFreshExchange(wex, d.oldCoin.exchangeBaseUrl);
const exchangeVer = LibtoolVersion.parseVersion(
exchange.protocolVersionRange,
);
checkDbInvariant(!!exchangeVer, "bad exchange version string");
+ if (exchangeVer.current < 27) {
+ throw Error("unsupported exchange version");
+ }
const seed = encodeCrock(getRandomBytes(64));
const updatedSession = await wex.db.runReadWriteTx(
{
@@ -728,17 +726,10 @@ async function refreshMelt(
if (!refreshSession) {
return undefined;
}
- if (
- refreshSession.sessionPublicSeed != null ||
- refreshSession.sessionSecretSeed != null
- ) {
+ if (refreshSession.sessionPublicSeed != null) {
return refreshSession;
}
- if (exchangeVer.current >= 27) {
- refreshSession.sessionPublicSeed = seed;
- } else {
- refreshSession.sessionSecretSeed = seed;
- }
+ refreshSession.sessionPublicSeed = seed;
await tx.refreshSessions.put(refreshSession);
return refreshSession;
},
@@ -751,239 +742,124 @@ async function refreshMelt(
const { newCoinDenoms, oldCoin, oldDenom, refreshGroup, refreshSession } = d;
+ checkLogicInvariant(refreshSession.sessionPublicSeed != null);
+
let maybeAch: HashCodeString | undefined;
if (oldCoin.ageCommitmentProof) {
maybeAch = AgeRestriction.hashCommitment(
oldCoin.ageCommitmentProof.commitment,
);
}
+ // 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,
+ });
- 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");
+ // Wallet stores new denoms run-length encoded,
+ // we need to expand the list of denominations
+ // for the exchange.
+ const newDenomsFlat: string[] = [];
+ for (let i = 0; i < newCoinDenoms.length; i++) {
+ const dsel = newCoinDenoms[i];
+ for (let j = 0; j < dsel.count; j++) {
+ newDenomsFlat.push(dsel.denomPubHash);
}
+ }
- 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 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),
- });
- },
- );
+ 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: newDenomsFlat,
+ 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;
- }
- 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);
- }
+ 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 norevealIndex = meltResponse.noreveal_index;
-
- refreshSession.norevealIndex = norevealIndex;
+ const meltResponse = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForExchangeMeltResponse(),
+ );
- 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,
- });
+ // FIXME: Check exchange's signature.
- // Wallet stores new denoms run-length encoded,
- // we need to expand the list of denominations
- // for the exchange.
- const newDenomsFlat: string[] = [];
- for (let i = 0; i < newCoinDenoms.length; i++) {
- const dsel = newCoinDenoms[i];
- for (let j = 0; j < dsel.count; j++) {
- newDenomsFlat.push(dsel.denomPubHash);
- }
- }
+ const norevealIndex = meltResponse.noreveal_index;
- 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: newDenomsFlat,
- 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),
- });
- },
- );
+ refreshSession.norevealIndex = norevealIndex;
- switch (resp.status) {
- case HttpStatusCode.NotFound: {
- const errDetail = await readTalerErrorResponse(resp);
- await handleRefreshMeltNotFound(ctx, coinIndex, resp, errDetail);
+ await wex.db.runReadWriteTx(
+ { storeNames: ["refreshGroups", "refreshSessions"] },
+ async (tx) => {
+ const rg = await tx.refreshGroups.get(refreshGroupId);
+ if (!rg) {
return;
}
- case HttpStatusCode.Gone: {
- const errDetail = await readTalerErrorResponse(resp);
- await handleRefreshMeltGone(ctx, coinIndex, errDetail);
+ if (rg.timestampFinished) {
return;
}
- case HttpStatusCode.Conflict: {
- const errDetail = await readTalerErrorResponse(resp);
- await handleRefreshMeltConflict(
- ctx,
- refreshGroup,
- coinIndex,
- errDetail,
- derived.meltValueWithFee,
- oldCoin,
- );
+ const rs = await tx.refreshSessions.get([refreshGroupId, coinIndex]);
+ if (!rs) {
return;
}
- case HttpStatusCode.Ok:
- break;
- default: {
- const errDetail = await readTalerErrorResponse(resp);
- throwUnexpectedRequestError(resp, errDetail);
+ if (rs.norevealIndex !== undefined) {
+ return;
}
- }
-
- 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)");
- }
+ rs.norevealIndex = norevealIndex;
+ await tx.refreshSessions.put(rs);
+ },
+ );
}
async function handleRefreshMeltGone(
@@ -1381,131 +1257,55 @@ async function refreshReveal(
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");
- }
-
- 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,
- );
+ checkLogicInvariant(refreshSession.sessionPublicSeed != null);
- 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 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);
- }
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ break;
+ case HttpStatusCode.Conflict:
+ case HttpStatusCode.Gone: {
+ const errDetail = await readTalerErrorResponse(resp);
+ await handleRefreshRevealError(ctx, coinIndex, errDetail);
+ return;
}
-
- 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);
- }
+ default: {
+ const errDetail = await readTalerErrorResponse(resp);
+ throwUnexpectedRequestError(resp, errDetail);
}
-
- const reveal = await readSuccessResponseJsonOrThrow(
- resp,
- codecForExchangeRevealMeltResponseV2(),
- );
- resEvSigs = reveal.ev_sigs;
- planchets = derived.planchets;
- } else {
- throw Error("refresh session not supported");
}
+ const reveal = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForExchangeRevealMeltResponseV2(),
+ );
+ resEvSigs = reveal.ev_sigs;
+ planchets = derived.planchets;
+
const coins: CoinRecord[] = [];
for (let i = 0; i < refreshSession.newDenoms.length; i++) {