commit 48764f27bc67764112665b792668099c9d175eb5
parent adf02fabb020fb26c1767f61822619a05293e212
Author: Iván Ávalos <avalos@disroot.org>
Date: Wed, 11 Mar 2026 14:10:27 +0100
wallet-core: deduplicate denom validation
Diffstat:
2 files changed, 91 insertions(+), 119 deletions(-)
diff --git a/packages/taler-wallet-core/src/denominations.ts b/packages/taler-wallet-core/src/denominations.ts
@@ -568,49 +568,92 @@ export async function isValidDenomRecord(
const denomBatchSize = 70;
+export async function validateDenoms(
+ wex: WalletExecutionContext,
+ denoms: DenominationRecord[],
+): Promise<void> {
+ if (denoms.length == 0) {
+ logger.info(`no denominations need to be validated`);
+ return;
+ }
+
+ logger.info(
+ `need to validate ${denoms.length} denominations`,
+ );
+
+ let current = 0;
+ while (current < denoms.length) {
+ const updatedDenoms: DenominationRecord[] = [];
+ for (
+ let batchIdx = 0;
+ batchIdx < denomBatchSize && current < denoms.length;
+ batchIdx++, current++
+ ) {
+ const denom = denoms[current];
+ if (denom.verificationStatus ===
+ DenominationVerificationStatus.Unverified
+ ) {
+ logger.trace(
+ `Validating denomination ${current + 1}/${denoms.length}`
+ + ` signature of ${denom.denomPubHash}`,
+ );
+
+ let valid = false;
+
+ if (wex.ws.config.testing.insecureTrustExchange) {
+ valid = true;
+ } else {
+ valid = await isValidDenomRecord(wex, denom.exchangeMasterPub, denom);
+ }
+
+ logger.trace(`Done validating ${denom.denomPubHash}`);
+
+ if (!valid) {
+ logger.warn(`Signature check for denomination h=${denom.denomPubHash} failed`);
+ denom.verificationStatus = DenominationVerificationStatus.VerifiedBad;
+ } else {
+ denom.verificationStatus = DenominationVerificationStatus.VerifiedGood;
+ }
+
+ updatedDenoms.push(denom);
+ }
+ }
+
+ if (updatedDenoms.length > 0) {
+ logger.trace("writing denomination batch to db");
+ await wex.db.runReadWriteTx(
+ { storeNames: ["denominations"] },
+ async (tx) => {
+ for (let i = 0; i < updatedDenoms.length; i++) {
+ const denom = updatedDenoms[i];
+ await tx.denominations.put(denom);
+ }
+ }
+ );
+
+ wex.ws.denomInfoCache.clear();
+ logger.trace("done with DB write");
+ }
+ }
+}
+
export async function processValidateDenoms(
wex: WalletExecutionContext,
): Promise<TaskRunResult> {
logger.trace("validating denoms in background task");
+
const denoms = await wex.db.runAllStoresReadOnlyTx({}, async (tx) => {
return tx.denominations.indexes.byVerificationStatus.getAll(
DenominationVerificationStatus.Unverified,
- denomBatchSize,
);
});
+
if (denoms.length === 0) {
return TaskRunResult.finished();
}
- logger.trace(`validating batch of ${denoms.length} denoms`);
- const dv: boolean[] = [];
- for (const denom of denoms) {
- const valid = await isValidDenomRecord(wex, denom.exchangeMasterPub, denom);
- dv.push(valid);
- }
- await wex.db.runAllStoresReadWriteTx({}, async (tx) => {
- for (let i = 0; i < denoms.length; i++) {
- const d = denoms[i];
- const rec = await tx.denominations.get([
- d.exchangeBaseUrl,
- d.denomPubHash,
- ]);
- if (!rec) {
- continue;
- }
- if (
- rec.verificationStatus !== DenominationVerificationStatus.Unverified
- ) {
- continue;
- }
- if (dv[i]) {
- rec.verificationStatus = DenominationVerificationStatus.VerifiedGood;
- } else {
- rec.verificationStatus = DenominationVerificationStatus.VerifiedBad;
- }
- await tx.denominations.put(rec);
- }
- });
- logger.trace(`denom validation batch done`);
+
+ await validateDenoms(wex, denoms);
+
return TaskRunResult.runAgainAfter({
seconds: 1,
});
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts
@@ -161,8 +161,8 @@ import {
} from "./denomSelection.js";
import {
isCandidateWithdrawableDenomRec,
- isValidDenomRecord,
isWithdrawableDenom,
+ validateDenoms,
} from "./denominations.js";
import {
BalanceThresholdCheckResult,
@@ -2160,23 +2160,18 @@ export async function updateWithdrawalDenomsForExchange(
logger.trace(
`updating denominations used for withdrawal for ${exchangeBaseUrl}`,
);
+
const dbNow = timestampProtocolToDb(TalerProtocolTimestamp.now());
- const res = await wex.db.runReadOnlyTx(
+ const denoms = await wex.db.runReadOnlyTx(
{
storeNames: [
- "exchanges",
- "exchangeDetails",
"denominations",
"denominationFamilies",
],
},
async (tx) => {
- const exchangeDetails = await getExchangeDetailsInTx(tx, exchangeBaseUrl);
- const allFamilies =
- await tx.denominationFamilies.indexes.byExchangeBaseUrl.getAll(
- exchangeBaseUrl,
- );
+ const allFamilies = await tx.denominationFamilies.indexes.byExchangeBaseUrl.getAll();
const denominations: DenominationRecord[] | undefined = [];
for (const fam of allFamilies) {
const fpSerial = fam.denominationFamilySerial;
@@ -2184,118 +2179,52 @@ export async function updateWithdrawalDenomsForExchange(
typeof fpSerial === "number",
"denominationFamilySerial",
);
- const denomCursor =
- tx.denominations.indexes.byDenominationFamilySerialAndStampExpireWithdraw.iter();
+ const denomCursor = tx.denominations.indexes
+ .byDenominationFamilySerialAndStampExpireWithdraw.iter();
// Need to wait for cursor to be positioned before we can move it.
const dr0 = await denomCursor.current();
+
if (!dr0.hasValue) {
logger.warn(`no current denom for family ${fpSerial}`);
continue;
}
- if (
- dr0.value.denominationFamilySerial < fpSerial ||
+
+ if (dr0.value.denominationFamilySerial < fpSerial ||
(dr0.value.denominationFamilySerial === fpSerial &&
dr0.value.stampExpireWithdraw < dbNow)
) {
denomCursor.continue([fpSerial, dbNow]);
}
+
while (1) {
const dr = await denomCursor.current();
if (!dr.hasValue) {
break;
}
+
if (dr.value.denominationFamilySerial != fpSerial) {
// Cursor went to next serial, we need to stop.
break;
}
+
if (isCandidateWithdrawableDenomRec(dr.value)) {
- if (
- dr.value.verificationStatus ===
+ if (dr.value.verificationStatus ===
DenominationVerificationStatus.Unverified
) {
denominations.push(dr.value);
}
break;
}
+
denomCursor.continue();
}
}
- return { exchangeDetails, denominations };
- },
- );
- logger.info(
- `need to validate ${res.denominations.length} withdrawal candidate denominations`,
+ return denominations;
+ },
);
- const exchangeDetails = res.exchangeDetails;
-
- if (!exchangeDetails) {
- logger.error("exchange details not available");
- return;
- }
- // First do a pass where the validity of candidate denominations
- // is checked and the result is stored in the database.
- const denominations = res.denominations;
- logger.trace(`got ${denominations.length} candidate denominations`);
- const batchSize = 50;
- let current = 0;
-
- while (current < denominations.length) {
- const updatedDenominations: DenominationRecord[] = [];
- // Do a batch of batchSize
- for (
- let batchIdx = 0;
- batchIdx < batchSize && current < denominations.length;
- batchIdx++, current++
- ) {
- const denom = denominations[current];
- if (
- denom.verificationStatus === DenominationVerificationStatus.Unverified
- ) {
- logger.trace(
- `Validating denomination (${current + 1}/${
- denominations.length
- }) signature of ${denom.denomPubHash}`,
- );
- let valid = false;
- if (wex.ws.config.testing.insecureTrustExchange) {
- valid = true;
- } else {
- valid = await isValidDenomRecord(
- wex,
- exchangeDetails.masterPublicKey,
- denom,
- );
- }
- logger.trace(`Done validating ${denom.denomPubHash}`);
- if (!valid) {
- logger.warn(
- `Signature check for denomination h=${denom.denomPubHash} failed`,
- );
- denom.verificationStatus = DenominationVerificationStatus.VerifiedBad;
- } else {
- denom.verificationStatus =
- DenominationVerificationStatus.VerifiedGood;
- }
- updatedDenominations.push(denom);
- }
- }
- if (updatedDenominations.length > 0) {
- logger.trace("writing denomination batch to db");
- await wex.db.runReadWriteTx(
- { storeNames: ["denominations"] },
- async (tx) => {
- for (let i = 0; i < updatedDenominations.length; i++) {
- const denom = updatedDenominations[i];
- await tx.denominations.put(denom);
- }
- },
- );
- wex.ws.denomInfoCache.clear();
- logger.trace("done with DB write");
- }
- }
+ await validateDenoms(wex, denoms);
}
/**