taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

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:
Mpackages/taler-wallet-core/src/denominations.ts | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mpackages/taler-wallet-core/src/withdraw.ts | 105+++++++++++++------------------------------------------------------------------
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); } /**