diff options
author | Florian Dold <florian@dold.me> | 2023-12-18 19:25:26 +0100 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2023-12-18 19:25:26 +0100 |
commit | 12a9b08c6f8c31f684239a30fc39acc9189c6571 (patch) | |
tree | 3755eb1a6b7659d44059ddb4cf85c832a8f88a8e /packages/taler-wallet-core | |
parent | a488ce70d6dbfe08845eccaeb2375b367f7c307a (diff) | |
download | wallet-core-12a9b08c6f8c31f684239a30fc39acc9189c6571.tar.xz |
wallet-core: towards properly handling peer-pull-debit expiry
Diffstat (limited to 'packages/taler-wallet-core')
6 files changed, 214 insertions, 29 deletions
diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts index 36ca128ae..7c6b142fb 100644 --- a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts +++ b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts @@ -103,6 +103,8 @@ import { EncryptContractForDepositResponse, EncryptContractRequest, EncryptContractResponse, + SignCoinHistoryRequest, + SignCoinHistoryResponse, SignDeletePurseRequest, SignDeletePurseResponse, SignPurseMergeRequest, @@ -243,6 +245,10 @@ export interface TalerCryptoInterface { signDeletePurse( req: SignDeletePurseRequest, ): Promise<SignDeletePurseResponse>; + + signCoinHistoryRequest( + req: SignCoinHistoryRequest, + ): Promise<SignCoinHistoryResponse>; } /** @@ -427,6 +433,11 @@ export const nullCrypto: TalerCryptoInterface = { ): Promise<SignDeletePurseResponse> { throw new Error("Function not implemented."); }, + signCoinHistoryRequest: function ( + req: SignCoinHistoryRequest, + ): Promise<SignCoinHistoryResponse> { + throw new Error("Function not implemented."); + }, }; export type WithArg<X> = X extends (req: infer T) => infer R @@ -1705,6 +1716,23 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { sig: sigResp.sig, }; }, + async signCoinHistoryRequest( + tci: TalerCryptoInterfaceR, + req: SignCoinHistoryRequest, + ): Promise<SignCoinHistoryResponse> { + const coinHistorySigBlob = buildSigPS( + TalerSignaturePurpose.WALLET_COIN_HISTORY, + ) + .put(bufferForUint64(req.startOffset)) + .build(); + const sigResp = await tci.eddsaSign(tci, { + msg: encodeCrock(coinHistorySigBlob), + priv: req.coinPriv, + }); + return { + sig: sigResp.sig, + }; + }, }; export interface EddsaSignRequest { diff --git a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts index 2204fac71..df25b87e4 100644 --- a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts +++ b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts @@ -279,6 +279,16 @@ export interface SignDeletePurseResponse { sig: EddsaSignatureString; } +export interface SignCoinHistoryRequest { + coinPub: string; + coinPriv: string; + startOffset: number; +} + +export interface SignCoinHistoryResponse { + sig: EddsaSignatureString; +} + export interface SignReservePurseCreateRequest { mergeTimestamp: TalerProtocolTimestamp; diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index d2c6b8368..6f6aad256 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -1799,12 +1799,20 @@ export enum PeerPushDebitStatus { PendingCreatePurse = 0x0100_0000 /* ACTIVE_START */, PendingReady = 0x0100_0001, AbortingDeletePurse = 0x0103_0000, - AbortingRefresh = 0x0103_0001, + /** + * Refresh after the purse got deleted by the wallet. + */ + AbortingRefreshDeleted = 0x0103_0001, + /** + * Refresh after the purse expired. + */ + AbortingRefreshExpired = 0x0103_0002, SuspendedCreatePurse = 0x0110_0000, SuspendedReady = 0x0110_0001, SuspendedAbortingDeletePurse = 0x0113_0000, - SuspendedAbortingRefresh = 0x0113_0001, + SuspendedAbortingRefreshDeleted = 0x0113_0001, + SuspendedAbortingRefreshExpired = 0x0113_0002, Done = 0x0500_0000, Aborted = 0x0503_0000, diff --git a/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts index da779a07d..c79aca1ad 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts @@ -365,7 +365,7 @@ async function processPeerPushDebitAbortingDeletePurse( coinPubs, RefreshReason.AbortPeerPushDebit, ); - ppiRec.status = PeerPushDebitStatus.AbortingRefresh; + ppiRec.status = PeerPushDebitStatus.AbortingRefreshDeleted; ppiRec.abortRefreshGroupId = refresh.refreshGroupId; await tx.peerPushDebit.put(ppiRec); const newTxState = computePeerPushDebitTransactionState(ppiRec); @@ -415,7 +415,7 @@ async function transitionPeerPushDebitTransaction( notifyTransition(ws, transactionId, transitionInfo); } -async function processPeerPushDebitAbortingRefresh( +async function processPeerPushDebitAbortingRefreshDeleted( ws: InternalWalletState, peerPushInitiation: PeerPushDebitRecord, ): Promise<TaskRunResult> { @@ -463,6 +463,54 @@ async function processPeerPushDebitAbortingRefresh( return TaskRunResult.pending(); } +async function processPeerPushDebitAbortingRefreshExpired( + ws: InternalWalletState, + peerPushInitiation: PeerPushDebitRecord, +): Promise<TaskRunResult> { + const pursePub = peerPushInitiation.pursePub; + const abortRefreshGroupId = peerPushInitiation.abortRefreshGroupId; + checkLogicInvariant(!!abortRefreshGroupId); + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPushDebit, + pursePub: peerPushInitiation.pursePub, + }); + const transitionInfo = await ws.db + .mktx((x) => [x.refreshGroups, x.peerPushDebit]) + .runReadWrite(async (tx) => { + const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId); + let newOpState: PeerPushDebitStatus | undefined; + if (!refreshGroup) { + // Maybe it got manually deleted? Means that we should + // just go into failed. + logger.warn("no aborting refresh group found for deposit group"); + newOpState = PeerPushDebitStatus.Failed; + } else { + if (refreshGroup.operationStatus === RefreshOperationStatus.Finished) { + newOpState = PeerPushDebitStatus.Expired; + } else if ( + refreshGroup.operationStatus === RefreshOperationStatus.Failed + ) { + newOpState = PeerPushDebitStatus.Failed; + } + } + if (newOpState) { + const newDg = await tx.peerPushDebit.get(pursePub); + if (!newDg) { + return; + } + const oldTxState = computePeerPushDebitTransactionState(newDg); + newDg.status = newOpState; + const newTxState = computePeerPushDebitTransactionState(newDg); + await tx.peerPushDebit.put(newDg); + return { oldTxState, newTxState }; + } + return undefined; + }); + notifyTransition(ws, transactionId, transitionInfo); + // FIXME: Shouldn't this be finished in some cases?! + return TaskRunResult.pending(); +} + /** * Process the "pending(ready)" state of a peer-push-debit transaction. */ @@ -476,6 +524,10 @@ async function processPeerPushDebitReady( tag: PendingTaskType.PeerPushDebit, pursePub, }); + const transactionId = constructTaskIdentifier({ + tag: PendingTaskType.PeerPushDebit, + pursePub, + }); runLongpollAsync(ws, retryTag, async (ct) => { const mergeUrl = new URL( `purses/${pursePub}/merge`, @@ -510,14 +562,50 @@ async function processPeerPushDebitReady( }; } } else if (resp.status === HttpStatusCode.Gone) { - await transitionPeerPushDebitTransaction( - ws, - peerPushInitiation.pursePub, - { - stFrom: PeerPushDebitStatus.PendingReady, - stTo: PeerPushDebitStatus.Expired, - }, - ); + const transitionInfo = await ws.db + .mktx((x) => [ + x.peerPushDebit, + x.refreshGroups, + x.denominations, + x.coinAvailability, + x.coins, + ]) + .runReadWrite(async (tx) => { + const ppiRec = await tx.peerPushDebit.get(pursePub); + if (!ppiRec) { + return undefined; + } + if (ppiRec.status !== PeerPushDebitStatus.PendingReady) { + return undefined; + } + const currency = Amounts.currencyOf(ppiRec.amount); + const oldTxState = computePeerPushDebitTransactionState(ppiRec); + const coinPubs: CoinRefreshRequest[] = []; + + for (let i = 0; i < ppiRec.coinSel.coinPubs.length; i++) { + coinPubs.push({ + amount: ppiRec.coinSel.contributions[i], + coinPub: ppiRec.coinSel.coinPubs[i], + }); + } + + const refresh = await createRefreshGroup( + ws, + tx, + currency, + coinPubs, + RefreshReason.AbortPeerPushDebit, + ); + ppiRec.status = PeerPushDebitStatus.AbortingRefreshExpired; + ppiRec.abortRefreshGroupId = refresh.refreshGroupId; + await tx.peerPushDebit.put(ppiRec); + const newTxState = computePeerPushDebitTransactionState(ppiRec); + return { + oldTxState, + newTxState, + }; + }); + notifyTransition(ws, transactionId, transitionInfo); return { ready: true, }; @@ -569,8 +657,10 @@ export async function processPeerPushDebit( return processPeerPushDebitReady(ws, peerPushInitiation); case PeerPushDebitStatus.AbortingDeletePurse: return processPeerPushDebitAbortingDeletePurse(ws, peerPushInitiation); - case PeerPushDebitStatus.AbortingRefresh: - return processPeerPushDebitAbortingRefresh(ws, peerPushInitiation); + case PeerPushDebitStatus.AbortingRefreshDeleted: + return processPeerPushDebitAbortingRefreshDeleted(ws, peerPushInitiation); + case PeerPushDebitStatus.AbortingRefreshExpired: + return processPeerPushDebitAbortingRefreshExpired(ws, peerPushInitiation); default: { const txState = computePeerPushDebitTransactionState(peerPushInitiation); logger.warn( @@ -722,11 +812,15 @@ export function computePeerPushDebitTransactionActions( return [TransactionAction.Delete]; case PeerPushDebitStatus.AbortingDeletePurse: return [TransactionAction.Suspend, TransactionAction.Fail]; - case PeerPushDebitStatus.AbortingRefresh: + case PeerPushDebitStatus.AbortingRefreshDeleted: return [TransactionAction.Suspend, TransactionAction.Fail]; + case PeerPushDebitStatus.AbortingRefreshExpired: + return [TransactionAction.Resume, TransactionAction.Fail]; + case PeerPushDebitStatus.SuspendedAbortingRefreshExpired: + return [TransactionAction.Resume, TransactionAction.Fail]; case PeerPushDebitStatus.SuspendedAbortingDeletePurse: return [TransactionAction.Resume, TransactionAction.Fail]; - case PeerPushDebitStatus.SuspendedAbortingRefresh: + case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted: return [TransactionAction.Resume, TransactionAction.Fail]; case PeerPushDebitStatus.SuspendedCreatePurse: return [TransactionAction.Resume, TransactionAction.Abort]; @@ -773,9 +867,11 @@ export async function abortPeerPushDebitTransaction( // Network request might already be in-flight! newStatus = PeerPushDebitStatus.AbortingDeletePurse; break; - case PeerPushDebitStatus.SuspendedAbortingRefresh: + case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted: case PeerPushDebitStatus.SuspendedAbortingDeletePurse: - case PeerPushDebitStatus.AbortingRefresh: + case PeerPushDebitStatus.SuspendedAbortingRefreshExpired: + case PeerPushDebitStatus.AbortingRefreshDeleted: + case PeerPushDebitStatus.AbortingRefreshExpired: case PeerPushDebitStatus.Done: case PeerPushDebitStatus.AbortingDeletePurse: case PeerPushDebitStatus.Aborted: @@ -824,13 +920,15 @@ export async function failPeerPushDebitTransaction( } let newStatus: PeerPushDebitStatus | undefined = undefined; switch (pushDebitRec.status) { - case PeerPushDebitStatus.AbortingRefresh: - case PeerPushDebitStatus.SuspendedAbortingRefresh: + case PeerPushDebitStatus.AbortingRefreshDeleted: + case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted: // FIXME: What to do about the refresh group? newStatus = PeerPushDebitStatus.Failed; break; case PeerPushDebitStatus.AbortingDeletePurse: case PeerPushDebitStatus.SuspendedAbortingDeletePurse: + case PeerPushDebitStatus.AbortingRefreshExpired: + case PeerPushDebitStatus.SuspendedAbortingRefreshExpired: case PeerPushDebitStatus.PendingReady: case PeerPushDebitStatus.SuspendedReady: case PeerPushDebitStatus.SuspendedCreatePurse: @@ -887,8 +985,11 @@ export async function suspendPeerPushDebitTransaction( case PeerPushDebitStatus.PendingCreatePurse: newStatus = PeerPushDebitStatus.SuspendedCreatePurse; break; - case PeerPushDebitStatus.AbortingRefresh: - newStatus = PeerPushDebitStatus.SuspendedAbortingRefresh; + case PeerPushDebitStatus.AbortingRefreshDeleted: + newStatus = PeerPushDebitStatus.SuspendedAbortingRefreshDeleted; + break; + case PeerPushDebitStatus.AbortingRefreshExpired: + newStatus = PeerPushDebitStatus.SuspendedAbortingRefreshExpired; break; case PeerPushDebitStatus.AbortingDeletePurse: newStatus = PeerPushDebitStatus.SuspendedAbortingDeletePurse; @@ -897,7 +998,8 @@ export async function suspendPeerPushDebitTransaction( newStatus = PeerPushDebitStatus.SuspendedReady; break; case PeerPushDebitStatus.SuspendedAbortingDeletePurse: - case PeerPushDebitStatus.SuspendedAbortingRefresh: + case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted: + case PeerPushDebitStatus.SuspendedAbortingRefreshExpired: case PeerPushDebitStatus.SuspendedReady: case PeerPushDebitStatus.SuspendedCreatePurse: case PeerPushDebitStatus.Done: @@ -950,8 +1052,11 @@ export async function resumePeerPushDebitTransaction( case PeerPushDebitStatus.SuspendedAbortingDeletePurse: newStatus = PeerPushDebitStatus.AbortingDeletePurse; break; - case PeerPushDebitStatus.SuspendedAbortingRefresh: - newStatus = PeerPushDebitStatus.AbortingRefresh; + case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted: + newStatus = PeerPushDebitStatus.AbortingRefreshDeleted; + break; + case PeerPushDebitStatus.SuspendedAbortingRefreshExpired: + newStatus = PeerPushDebitStatus.AbortingRefreshExpired; break; case PeerPushDebitStatus.SuspendedReady: newStatus = PeerPushDebitStatus.PendingReady; @@ -960,7 +1065,8 @@ export async function resumePeerPushDebitTransaction( newStatus = PeerPushDebitStatus.PendingCreatePurse; break; case PeerPushDebitStatus.PendingCreatePurse: - case PeerPushDebitStatus.AbortingRefresh: + case PeerPushDebitStatus.AbortingRefreshDeleted: + case PeerPushDebitStatus.AbortingRefreshExpired: case PeerPushDebitStatus.AbortingDeletePurse: case PeerPushDebitStatus.PendingReady: case PeerPushDebitStatus.Done: @@ -1011,17 +1117,27 @@ export function computePeerPushDebitTransactionState( major: TransactionMajorState.Aborting, minor: TransactionMinorState.DeletePurse, }; - case PeerPushDebitStatus.AbortingRefresh: + case PeerPushDebitStatus.AbortingRefreshDeleted: return { major: TransactionMajorState.Aborting, minor: TransactionMinorState.Refresh, }; + case PeerPushDebitStatus.AbortingRefreshExpired: + return { + major: TransactionMajorState.Aborting, + minor: TransactionMinorState.RefreshExpired, + }; case PeerPushDebitStatus.SuspendedAbortingDeletePurse: return { major: TransactionMajorState.SuspendedAborting, minor: TransactionMinorState.DeletePurse, }; - case PeerPushDebitStatus.SuspendedAbortingRefresh: + case PeerPushDebitStatus.SuspendedAbortingRefreshExpired: + return { + major: TransactionMajorState.SuspendedAborting, + minor: TransactionMinorState.RefreshExpired, + }; + case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted: return { major: TransactionMajorState.SuspendedAborting, minor: TransactionMinorState.Refresh, diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts index a9d6c5595..233ca3fa4 100644 --- a/packages/taler-wallet-core/src/operations/pending.ts +++ b/packages/taler-wallet-core/src/operations/pending.ts @@ -640,7 +640,7 @@ export async function iterRecordsForPeerPushInitiation( if (filter.onlyState === "nonfinal") { const keyRange = GlobalIDB.KeyRange.bound( PeerPushDebitStatus.PendingCreatePurse, - PeerPushDebitStatus.AbortingRefresh, + PeerPushDebitStatus.AbortingRefreshDeleted, ); await tx.peerPushDebit.indexes.byStatus.iter(keyRange).forEachAsync(f); } else { diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index c58f03e05..3bbbc2a4b 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -528,6 +528,29 @@ async function refreshMelt( derived.meltValueWithFee, )} failed in refresh group ${refreshGroupId} due to conflict`, ); + + const historySig = await ws.cryptoApi.signCoinHistoryRequest({ + coinPriv: oldCoin.coinPriv, + coinPub: oldCoin.coinPub, + startOffset: 0, + }); + + const historyUrl = new URL( + `coins/${oldCoin.coinPub}/history`, + oldCoin.exchangeBaseUrl, + ); + + const historyResp = await ws.http.fetch(historyUrl.href, { + method: "GET", + headers: { + "Taler-Coin-History-Signature": historySig.sig, + }, + }); + + const historyJson = await historyResp.json(); + logger.info(`coin history: ${j2s(historyJson)}`); + + // FIXME: Before failing and re-trying, analyse response and adjust amount } const meltResponse = await readSuccessResponseJsonOrThrow( |