diff options
Diffstat (limited to 'src/operations/refresh.ts')
-rw-r--r-- | src/operations/refresh.ts | 572 |
1 files changed, 0 insertions, 572 deletions
diff --git a/src/operations/refresh.ts b/src/operations/refresh.ts deleted file mode 100644 index 74b032b91..000000000 --- a/src/operations/refresh.ts +++ /dev/null @@ -1,572 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2019 GNUnet e.V. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -import { Amounts, AmountJson } from "../util/amounts"; -import { - DenominationRecord, - Stores, - CoinStatus, - RefreshPlanchetRecord, - CoinRecord, - RefreshSessionRecord, - initRetryInfo, - updateRetryInfoTimeout, - RefreshGroupRecord, - CoinSourceType, -} from "../types/dbTypes"; -import { amountToPretty } from "../util/helpers"; -import { TransactionHandle } from "../util/query"; -import { InternalWalletState } from "./state"; -import { Logger } from "../util/logging"; -import { getWithdrawDenomList } from "./withdraw"; -import { updateExchangeFromUrl } from "./exchanges"; -import { - OperationErrorDetails, - CoinPublicKey, - RefreshReason, - RefreshGroupId, -} from "../types/walletTypes"; -import { guardOperationException } from "./errors"; -import { NotificationType } from "../types/notifications"; -import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto"; -import { getTimestampNow } from "../util/time"; -import { readSuccessResponseJsonOrThrow } from "../util/http"; -import { - codecForExchangeMeltResponse, - codecForExchangeRevealResponse, -} from "../types/talerTypes"; - -const logger = new Logger("refresh.ts"); - -/** - * Get the amount that we lose when refreshing a coin of the given denomination - * with a certain amount left. - * - * If the amount left is zero, then the refresh cost - * is also considered to be zero. If a refresh isn't possible (e.g. due to lack of - * the right denominations), then the cost is the full amount left. - * - * Considers refresh fees, withdrawal fees after refresh and amounts too small - * to refresh. - */ -export function getTotalRefreshCost( - denoms: DenominationRecord[], - refreshedDenom: DenominationRecord, - amountLeft: AmountJson, -): AmountJson { - const withdrawAmount = Amounts.sub(amountLeft, refreshedDenom.feeRefresh) - .amount; - const withdrawDenoms = getWithdrawDenomList(withdrawAmount, denoms); - const resultingAmount = Amounts.add( - Amounts.getZero(withdrawAmount.currency), - ...withdrawDenoms.selectedDenoms.map( - (d) => Amounts.mult(d.denom.value, d.count).amount, - ), - ).amount; - const totalCost = Amounts.sub(amountLeft, resultingAmount).amount; - logger.trace( - `total refresh cost for ${amountToPretty(amountLeft)} is ${amountToPretty( - totalCost, - )}`, - ); - return totalCost; -} - -/** - * Create a refresh session inside a refresh group. - */ -async function refreshCreateSession( - ws: InternalWalletState, - refreshGroupId: string, - coinIndex: number, -): Promise<void> { - logger.trace( - `creating refresh session for coin ${coinIndex} in refresh group ${refreshGroupId}`, - ); - const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId); - if (!refreshGroup) { - return; - } - if (refreshGroup.finishedPerCoin[coinIndex]) { - return; - } - const existingRefreshSession = refreshGroup.refreshSessionPerCoin[coinIndex]; - if (existingRefreshSession) { - return; - } - const oldCoinPub = refreshGroup.oldCoinPubs[coinIndex]; - const coin = await ws.db.get(Stores.coins, oldCoinPub); - if (!coin) { - throw Error("Can't refresh, coin not found"); - } - - const exchange = await updateExchangeFromUrl(ws, coin.exchangeBaseUrl); - if (!exchange) { - throw Error("db inconsistent: exchange of coin not found"); - } - - const oldDenom = await ws.db.get(Stores.denominations, [ - exchange.baseUrl, - coin.denomPub, - ]); - - if (!oldDenom) { - throw Error("db inconsistent: denomination for coin not found"); - } - - const availableDenoms: DenominationRecord[] = await ws.db - .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl) - .toArray(); - - const availableAmount = Amounts.sub(coin.currentAmount, oldDenom.feeRefresh) - .amount; - - const newCoinDenoms = getWithdrawDenomList(availableAmount, availableDenoms); - - if (newCoinDenoms.selectedDenoms.length === 0) { - logger.trace( - `not refreshing, available amount ${amountToPretty( - availableAmount, - )} too small`, - ); - await ws.db.runWithWriteTransaction( - [Stores.coins, Stores.refreshGroups], - async (tx) => { - const rg = await tx.get(Stores.refreshGroups, refreshGroupId); - if (!rg) { - return; - } - rg.finishedPerCoin[coinIndex] = true; - let allDone = true; - for (const f of rg.finishedPerCoin) { - if (!f) { - allDone = false; - break; - } - } - if (allDone) { - rg.timestampFinished = getTimestampNow(); - rg.retryInfo = initRetryInfo(false); - } - await tx.put(Stores.refreshGroups, rg); - }, - ); - ws.notify({ type: NotificationType.RefreshUnwarranted }); - return; - } - - const refreshSession: RefreshSessionRecord = await ws.cryptoApi.createRefreshSession( - exchange.baseUrl, - 3, - coin, - newCoinDenoms, - oldDenom.feeRefresh, - ); - - // Store refresh session and subtract refreshed amount from - // coin in the same transaction. - await ws.db.runWithWriteTransaction( - [Stores.refreshGroups, Stores.coins], - async (tx) => { - const c = await tx.get(Stores.coins, coin.coinPub); - if (!c) { - throw Error("coin not found, but marked for refresh"); - } - const r = Amounts.sub(c.currentAmount, refreshSession.amountRefreshInput); - if (r.saturated) { - console.log("can't refresh coin, no amount left"); - return; - } - c.currentAmount = r.amount; - c.status = CoinStatus.Dormant; - const rg = await tx.get(Stores.refreshGroups, refreshGroupId); - if (!rg) { - return; - } - if (rg.refreshSessionPerCoin[coinIndex]) { - return; - } - rg.refreshSessionPerCoin[coinIndex] = refreshSession; - await tx.put(Stores.refreshGroups, rg); - await tx.put(Stores.coins, c); - }, - ); - logger.info( - `created refresh session for coin #${coinIndex} in ${refreshGroupId}`, - ); - ws.notify({ type: NotificationType.RefreshStarted }); -} - -async function refreshMelt( - ws: InternalWalletState, - refreshGroupId: string, - coinIndex: number, -): Promise<void> { - const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId); - if (!refreshGroup) { - return; - } - const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex]; - if (!refreshSession) { - return; - } - if (refreshSession.norevealIndex !== undefined) { - return; - } - - const coin = await ws.db.get(Stores.coins, refreshSession.meltCoinPub); - - if (!coin) { - console.error("can't melt coin, it does not exist"); - return; - } - - const reqUrl = new URL( - `coins/${coin.coinPub}/melt`, - refreshSession.exchangeBaseUrl, - ); - const meltReq = { - coin_pub: coin.coinPub, - confirm_sig: refreshSession.confirmSig, - denom_pub_hash: coin.denomPubHash, - denom_sig: coin.denomSig, - rc: refreshSession.hash, - value_with_fee: Amounts.stringify(refreshSession.amountRefreshInput), - }; - logger.trace(`melt request for coin:`, meltReq); - const resp = await ws.http.postJson(reqUrl.href, meltReq); - const meltResponse = await readSuccessResponseJsonOrThrow( - resp, - codecForExchangeMeltResponse(), - ); - - const norevealIndex = meltResponse.noreveal_index; - - refreshSession.norevealIndex = norevealIndex; - - await ws.db.mutate(Stores.refreshGroups, refreshGroupId, (rg) => { - const rs = rg.refreshSessionPerCoin[coinIndex]; - if (!rs) { - return; - } - if (rs.norevealIndex !== undefined) { - return; - } - if (rs.finishedTimestamp) { - return; - } - rs.norevealIndex = norevealIndex; - return rg; - }); - - ws.notify({ - type: NotificationType.RefreshMelted, - }); -} - -async function refreshReveal( - ws: InternalWalletState, - refreshGroupId: string, - coinIndex: number, -): Promise<void> { - const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId); - if (!refreshGroup) { - return; - } - const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex]; - if (!refreshSession) { - return; - } - const norevealIndex = refreshSession.norevealIndex; - if (norevealIndex === undefined) { - throw Error("can't reveal without melting first"); - } - const privs = Array.from(refreshSession.transferPrivs); - privs.splice(norevealIndex, 1); - - const planchets = refreshSession.planchetsForGammas[norevealIndex]; - if (!planchets) { - throw Error("refresh index error"); - } - - const meltCoinRecord = await ws.db.get( - Stores.coins, - refreshSession.meltCoinPub, - ); - if (!meltCoinRecord) { - throw Error("inconsistent database"); - } - - const evs = planchets.map((x: RefreshPlanchetRecord) => x.coinEv); - - const linkSigs: string[] = []; - for (let i = 0; i < refreshSession.newDenoms.length; i++) { - const linkSig = await ws.cryptoApi.signCoinLink( - meltCoinRecord.coinPriv, - refreshSession.newDenomHashes[i], - refreshSession.meltCoinPub, - refreshSession.transferPubs[norevealIndex], - planchets[i].coinEv, - ); - linkSigs.push(linkSig); - } - - const req = { - coin_evs: evs, - new_denoms_h: refreshSession.newDenomHashes, - rc: refreshSession.hash, - transfer_privs: privs, - transfer_pub: refreshSession.transferPubs[norevealIndex], - link_sigs: linkSigs, - }; - - const reqUrl = new URL( - `refreshes/${refreshSession.hash}/reveal`, - refreshSession.exchangeBaseUrl, - ); - - const resp = await ws.http.postJson(reqUrl.href, req); - const reveal = await readSuccessResponseJsonOrThrow( - resp, - codecForExchangeRevealResponse(), - ); - - const coins: CoinRecord[] = []; - - for (let i = 0; i < reveal.ev_sigs.length; i++) { - const denom = await ws.db.get(Stores.denominations, [ - refreshSession.exchangeBaseUrl, - refreshSession.newDenoms[i], - ]); - if (!denom) { - console.error("denom not found"); - continue; - } - const pc = refreshSession.planchetsForGammas[norevealIndex][i]; - const denomSig = await ws.cryptoApi.rsaUnblind( - reveal.ev_sigs[i].ev_sig, - pc.blindingKey, - denom.denomPub, - ); - const coin: CoinRecord = { - blindingKey: pc.blindingKey, - coinPriv: pc.privateKey, - coinPub: pc.publicKey, - currentAmount: denom.value, - denomPub: denom.denomPub, - denomPubHash: denom.denomPubHash, - denomSig, - exchangeBaseUrl: refreshSession.exchangeBaseUrl, - status: CoinStatus.Fresh, - coinSource: { - type: CoinSourceType.Refresh, - oldCoinPub: refreshSession.meltCoinPub, - }, - suspended: false, - }; - - coins.push(coin); - } - - await ws.db.runWithWriteTransaction( - [Stores.coins, Stores.refreshGroups], - async (tx) => { - const rg = await tx.get(Stores.refreshGroups, refreshGroupId); - if (!rg) { - console.log("no refresh session found"); - return; - } - const rs = rg.refreshSessionPerCoin[coinIndex]; - if (!rs) { - return; - } - if (rs.finishedTimestamp) { - console.log("refresh session already finished"); - return; - } - rs.finishedTimestamp = getTimestampNow(); - rg.finishedPerCoin[coinIndex] = true; - let allDone = true; - for (const f of rg.finishedPerCoin) { - if (!f) { - allDone = false; - break; - } - } - if (allDone) { - rg.timestampFinished = getTimestampNow(); - rg.retryInfo = initRetryInfo(false); - } - for (const coin of coins) { - await tx.put(Stores.coins, coin); - } - await tx.put(Stores.refreshGroups, rg); - }, - ); - console.log("refresh finished (end of reveal)"); - ws.notify({ - type: NotificationType.RefreshRevealed, - }); -} - -async function incrementRefreshRetry( - ws: InternalWalletState, - refreshGroupId: string, - err: OperationErrorDetails | undefined, -): Promise<void> { - await ws.db.runWithWriteTransaction([Stores.refreshGroups], async (tx) => { - const r = await tx.get(Stores.refreshGroups, refreshGroupId); - if (!r) { - return; - } - if (!r.retryInfo) { - return; - } - r.retryInfo.retryCounter++; - updateRetryInfoTimeout(r.retryInfo); - r.lastError = err; - await tx.put(Stores.refreshGroups, r); - }); - if (err) { - ws.notify({ type: NotificationType.RefreshOperationError, error: err }); - } -} - -export async function processRefreshGroup( - ws: InternalWalletState, - refreshGroupId: string, - forceNow = false, -): Promise<void> { - await ws.memoProcessRefresh.memo(refreshGroupId, async () => { - const onOpErr = (e: OperationErrorDetails): Promise<void> => - incrementRefreshRetry(ws, refreshGroupId, e); - return await guardOperationException( - async () => await processRefreshGroupImpl(ws, refreshGroupId, forceNow), - onOpErr, - ); - }); -} - -async function resetRefreshGroupRetry( - ws: InternalWalletState, - refreshSessionId: string, -): Promise<void> { - await ws.db.mutate(Stores.refreshGroups, refreshSessionId, (x) => { - if (x.retryInfo.active) { - x.retryInfo = initRetryInfo(); - } - return x; - }); -} - -async function processRefreshGroupImpl( - ws: InternalWalletState, - refreshGroupId: string, - forceNow: boolean, -): Promise<void> { - if (forceNow) { - await resetRefreshGroupRetry(ws, refreshGroupId); - } - const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId); - if (!refreshGroup) { - return; - } - if (refreshGroup.timestampFinished) { - return; - } - const ps = refreshGroup.oldCoinPubs.map((x, i) => - processRefreshSession(ws, refreshGroupId, i), - ); - await Promise.all(ps); - logger.trace("refresh finished"); -} - -async function processRefreshSession( - ws: InternalWalletState, - refreshGroupId: string, - coinIndex: number, -): Promise<void> { - logger.trace( - `processing refresh session for coin ${coinIndex} of group ${refreshGroupId}`, - ); - let refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId); - if (!refreshGroup) { - return; - } - if (refreshGroup.finishedPerCoin[coinIndex]) { - return; - } - if (!refreshGroup.refreshSessionPerCoin[coinIndex]) { - await refreshCreateSession(ws, refreshGroupId, coinIndex); - refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId); - if (!refreshGroup) { - return; - } - } - const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex]; - if (!refreshSession) { - if (!refreshGroup.finishedPerCoin[coinIndex]) { - throw Error( - "BUG: refresh session was not created and coin not marked as finished", - ); - } - return; - } - if (refreshSession.norevealIndex === undefined) { - await refreshMelt(ws, refreshGroupId, coinIndex); - } - await refreshReveal(ws, refreshGroupId, coinIndex); -} - -/** - * Create a refresh group for a list of coins. - */ -export async function createRefreshGroup( - ws: InternalWalletState, - tx: TransactionHandle, - oldCoinPubs: CoinPublicKey[], - reason: RefreshReason, -): Promise<RefreshGroupId> { - const refreshGroupId = encodeCrock(getRandomBytes(32)); - - const refreshGroup: RefreshGroupRecord = { - timestampFinished: undefined, - finishedPerCoin: oldCoinPubs.map((x) => false), - lastError: undefined, - lastErrorPerCoin: {}, - oldCoinPubs: oldCoinPubs.map((x) => x.coinPub), - reason, - refreshGroupId, - refreshSessionPerCoin: oldCoinPubs.map((x) => undefined), - retryInfo: initRetryInfo(), - }; - - await tx.put(Stores.refreshGroups, refreshGroup); - - const processAsync = async (): Promise<void> => { - try { - await processRefreshGroup(ws, refreshGroupId); - } catch (e) { - logger.trace(`Error during refresh: ${e}`) - } - }; - - processAsync(); - - return { - refreshGroupId, - }; -} |