gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated: wallet-core: implement coin s


From: gnunet
Subject: [taler-wallet-core] branch master updated: wallet-core: implement coin selection repair for p2p payments
Date: Mon, 19 Jun 2023 12:02:46 +0200

This is an automated email from the git hooks/post-receive script.

dold pushed a commit to branch master
in repository wallet-core.

The following commit(s) were added to refs/heads/master by this push:
     new bcff03949 wallet-core: implement coin selection repair for p2p payments
bcff03949 is described below

commit bcff03949b40d0d37069bdb7af941061e367a093
Author: Florian Dold <florian@dold.me>
AuthorDate: Mon Jun 19 12:02:43 2023 +0200

    wallet-core: implement coin selection repair for p2p payments
---
 .../src/operations/pay-peer-common.ts              |  12 +-
 .../src/operations/pay-peer-pull-debit.ts          | 253 ++++++++++++++-------
 .../src/operations/pay-peer-push-debit.ts          | 104 ++++++++-
 3 files changed, 283 insertions(+), 86 deletions(-)

diff --git a/packages/taler-wallet-core/src/operations/pay-peer-common.ts 
b/packages/taler-wallet-core/src/operations/pay-peer-common.ts
index 72e48cb03..4856fbe36 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-common.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-common.ts
@@ -158,6 +158,12 @@ export async function queryCoinInfosForSelection(
   return infos;
 }
 
+export interface PeerCoinRepair {
+  exchangeBaseUrl: string;
+  coinPubs: CoinPublicKeyString[];
+  contribs: AmountJson[];
+}
+
 export interface PeerCoinSelectionRequest {
   instructedAmount: AmountJson;
 
@@ -165,11 +171,7 @@ export interface PeerCoinSelectionRequest {
    * Instruct the coin selection to repair this coin
    * selection instead of selecting completely new coins.
    */
-  repair?: {
-    exchangeBaseUrl: string;
-    coinPubs: CoinPublicKeyString[];
-    contribs: AmountJson[];
-  };
+  repair?: PeerCoinRepair;
 }
 
 export async function selectPeerCoins(
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts 
b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts
index 2be21c68d..280ad567f 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts
@@ -29,6 +29,7 @@ import {
   TalerError,
   TalerErrorCode,
   TalerPreciseTimestamp,
+  TalerProtocolViolationError,
   TransactionAction,
   TransactionMajorState,
   TransactionMinorState,
@@ -44,7 +45,11 @@ import {
   j2s,
   parsePayPullUri,
 } from "@gnu-taler/taler-util";
-import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
+import {
+  HttpResponse,
+  readSuccessResponseJsonOrThrow,
+  readTalerErrorResponse,
+} from "@gnu-taler/taler-util/http";
 import {
   InternalWalletState,
   PeerPullDebitRecordStatus,
@@ -62,6 +67,7 @@ import {
 } from "../util/retries.js";
 import { runOperationWithErrorReporting, spendCoins } from "./common.js";
 import {
+  PeerCoinRepair,
   codecForExchangePurseStatus,
   getTotalPeerPaymentCost,
   queryCoinInfosForSelection,
@@ -76,6 +82,84 @@ import { checkLogicInvariant } from "../util/invariants.js";
 
 const logger = new Logger("pay-peer-pull-debit.ts");
 
+async function handlePurseCreationConflict(
+  ws: InternalWalletState,
+  peerPullInc: PeerPullPaymentIncomingRecord,
+  resp: HttpResponse,
+): Promise<OperationAttemptResult> {
+  const pursePub = peerPullInc.pursePub;
+  const errResp = await readTalerErrorResponse(resp);
+  if (errResp.code !== TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS) {
+    await failPeerPullDebitTransaction(ws, pursePub);
+    return OperationAttemptResult.finishedEmpty();
+  }
+
+  // FIXME: Properly parse!
+  const brokenCoinPub = (errResp as any).coin_pub;
+  logger.trace(`excluded broken coin pub=${brokenCoinPub}`);
+
+  if (!brokenCoinPub) {
+    // FIXME: Details!
+    throw new TalerProtocolViolationError();
+  }
+
+  const instructedAmount = Amounts.parseOrThrow(
+    peerPullInc.contractTerms.amount,
+  );
+
+  const sel = peerPullInc.coinSel;
+  if (!sel) {
+    throw Error("invalid state (coin selection expected)");
+  }
+
+  const repair: PeerCoinRepair = {
+    coinPubs: sel.coinPubs,
+    contribs: sel.contributions.map((x) => Amounts.parseOrThrow(x)),
+    exchangeBaseUrl: peerPullInc.exchangeBaseUrl,
+  };
+
+  const coinSelRes = await selectPeerCoins(ws, { instructedAmount, repair });
+
+  if (coinSelRes.type == "failure") {
+    // FIXME: Details!
+    throw Error(
+      "insufficient balance to re-select coins to repair double spending",
+    );
+  }
+
+  const totalAmount = await getTotalPeerPaymentCost(
+    ws,
+    coinSelRes.result.coins,
+  );
+
+  await ws.db
+    .mktx((x) => [x.peerPullPaymentIncoming])
+    .runReadWrite(async (tx) => {
+      const myPpi = await tx.peerPullPaymentIncoming.get(
+        peerPullInc.peerPullPaymentIncomingId,
+      );
+      if (!myPpi) {
+        return;
+      }
+      switch (myPpi.status) {
+        case PeerPullDebitRecordStatus.PendingDeposit:
+        case PeerPullDebitRecordStatus.SuspendedDeposit: {
+          const sel = coinSelRes.result;
+          myPpi.coinSel = {
+            coinPubs: sel.coins.map((x) => x.coinPub),
+            contributions: sel.coins.map((x) => x.contribution),
+            totalCost: Amounts.stringify(totalAmount),
+          };
+          break;
+        }
+        default:
+          return;
+      }
+      await tx.peerPullPaymentIncoming.put(myPpi);
+    });
+  return OperationAttemptResult.finishedEmpty();
+}
+
 async function processPeerPullDebitPendingDeposit(
   ws: InternalWalletState,
   peerPullInc: PeerPullPaymentIncomingRecord,
@@ -118,81 +202,98 @@ async function processPeerPullDebitPendingDeposit(
     method: "POST",
     body: depositPayload,
   });
-  if (httpResp.status === HttpStatusCode.Gone) {
-    const transitionInfo = await ws.db
-      .mktx((x) => [
-        x.peerPullPaymentIncoming,
-        x.refreshGroups,
-        x.denominations,
-        x.coinAvailability,
-        x.coins,
-      ])
-      .runReadWrite(async (tx) => {
-        const pi = await tx.peerPullPaymentIncoming.get(
-          peerPullPaymentIncomingId,
-        );
-        if (!pi) {
-          throw Error("peer pull payment not found anymore");
-        }
-        if (pi.status !== PeerPullDebitRecordStatus.PendingDeposit) {
-          return;
-        }
-        const oldTxState = computePeerPullDebitTransactionState(pi);
-
-        const currency = Amounts.currencyOf(pi.totalCostEstimated);
-        const coinPubs: CoinRefreshRequest[] = [];
-
-        if (!pi.coinSel) {
-          throw Error("invalid db state");
-        }
-
-        for (let i = 0; i < pi.coinSel.coinPubs.length; i++) {
-          coinPubs.push({
-            amount: pi.coinSel.contributions[i],
-            coinPub: pi.coinSel.coinPubs[i],
-          });
-        }
-
-        const refresh = await createRefreshGroup(
-          ws,
-          tx,
-          currency,
-          coinPubs,
-          RefreshReason.AbortPeerPushDebit,
-        );
-
-        pi.status = PeerPullDebitRecordStatus.AbortingRefresh;
-        pi.abortRefreshGroupId = refresh.refreshGroupId;
-        const newTxState = computePeerPullDebitTransactionState(pi);
-        await tx.peerPullPaymentIncoming.put(pi);
-        return { oldTxState, newTxState };
-      });
-    notifyTransition(ws, transactionId, transitionInfo);
-  } else {
-    const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny());
-    logger.trace(`purse deposit response: ${j2s(resp)}`);
-
-    const transitionInfo = await ws.db
-      .mktx((x) => [x.peerPullPaymentIncoming])
-      .runReadWrite(async (tx) => {
-        const pi = await tx.peerPullPaymentIncoming.get(
-          peerPullPaymentIncomingId,
-        );
-        if (!pi) {
-          throw Error("peer pull payment not found anymore");
-        }
-        if (pi.status !== PeerPullDebitRecordStatus.PendingDeposit) {
-          return;
-        }
-        const oldTxState = computePeerPullDebitTransactionState(pi);
-        pi.status = PeerPullDebitRecordStatus.DonePaid;
-        const newTxState = computePeerPullDebitTransactionState(pi);
-        await tx.peerPullPaymentIncoming.put(pi);
-        return { oldTxState, newTxState };
-      });
-    notifyTransition(ws, transactionId, transitionInfo);
+  switch (httpResp.status) {
+    case HttpStatusCode.Ok: {
+      const resp = await readSuccessResponseJsonOrThrow(
+        httpResp,
+        codecForAny(),
+      );
+      logger.trace(`purse deposit response: ${j2s(resp)}`);
+
+      const transitionInfo = await ws.db
+        .mktx((x) => [x.peerPullPaymentIncoming])
+        .runReadWrite(async (tx) => {
+          const pi = await tx.peerPullPaymentIncoming.get(
+            peerPullPaymentIncomingId,
+          );
+          if (!pi) {
+            throw Error("peer pull payment not found anymore");
+          }
+          if (pi.status !== PeerPullDebitRecordStatus.PendingDeposit) {
+            return;
+          }
+          const oldTxState = computePeerPullDebitTransactionState(pi);
+          pi.status = PeerPullDebitRecordStatus.DonePaid;
+          const newTxState = computePeerPullDebitTransactionState(pi);
+          await tx.peerPullPaymentIncoming.put(pi);
+          return { oldTxState, newTxState };
+        });
+      notifyTransition(ws, transactionId, transitionInfo);
+      break;
+    }
+    case HttpStatusCode.Gone: {
+      const transitionInfo = await ws.db
+        .mktx((x) => [
+          x.peerPullPaymentIncoming,
+          x.refreshGroups,
+          x.denominations,
+          x.coinAvailability,
+          x.coins,
+        ])
+        .runReadWrite(async (tx) => {
+          const pi = await tx.peerPullPaymentIncoming.get(
+            peerPullPaymentIncomingId,
+          );
+          if (!pi) {
+            throw Error("peer pull payment not found anymore");
+          }
+          if (pi.status !== PeerPullDebitRecordStatus.PendingDeposit) {
+            return;
+          }
+          const oldTxState = computePeerPullDebitTransactionState(pi);
+
+          const currency = Amounts.currencyOf(pi.totalCostEstimated);
+          const coinPubs: CoinRefreshRequest[] = [];
+
+          if (!pi.coinSel) {
+            throw Error("invalid db state");
+          }
+
+          for (let i = 0; i < pi.coinSel.coinPubs.length; i++) {
+            coinPubs.push({
+              amount: pi.coinSel.contributions[i],
+              coinPub: pi.coinSel.coinPubs[i],
+            });
+          }
+
+          const refresh = await createRefreshGroup(
+            ws,
+            tx,
+            currency,
+            coinPubs,
+            RefreshReason.AbortPeerPushDebit,
+          );
+
+          pi.status = PeerPullDebitRecordStatus.AbortingRefresh;
+          pi.abortRefreshGroupId = refresh.refreshGroupId;
+          const newTxState = computePeerPullDebitTransactionState(pi);
+          await tx.peerPullPaymentIncoming.put(pi);
+          return { oldTxState, newTxState };
+        });
+      notifyTransition(ws, transactionId, transitionInfo);
+      break;
+    }
+    case HttpStatusCode.Conflict: {
+      return handlePurseCreationConflict(ws, peerPullInc, httpResp);
+    }
+    default: {
+      const errResp = await readTalerErrorResponse(httpResp);
+      return {
+        type: OperationAttemptResultType.Error,
+        errorDetail: errResp,
+      };
+    }
   }
-
   return {
     type: OperationAttemptResultType.Finished,
     result: undefined,
@@ -434,7 +535,7 @@ export async function preparePeerPullDebit(
 
   const getPurseUrl = new URL(`purses/${pursePub}/merge`, exchangeBaseUrl);
 
-  const purseHttpResp = await ws.http.get(getPurseUrl.href);
+  const purseHttpResp = await ws.http.fetch(getPurseUrl.href);
 
   const purseStatus = await readSuccessResponseJsonOrThrow(
     purseHttpResp,
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 2835a1f64..33d317c6f 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
@@ -28,6 +28,7 @@ import {
   TalerError,
   TalerErrorCode,
   TalerPreciseTimestamp,
+  TalerProtocolViolationError,
   TalerUriAction,
   TransactionAction,
   TransactionMajorState,
@@ -47,8 +48,13 @@ import {
   getTotalPeerPaymentCost,
   codecForExchangePurseStatus,
   queryCoinInfosForSelection,
+  PeerCoinRepair,
 } from "./pay-peer-common.js";
-import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
+import {
+  HttpResponse,
+  readSuccessResponseJsonOrThrow,
+  readTalerErrorResponse,
+} from "@gnu-taler/taler-util/http";
 import {
   PeerPushPaymentInitiationRecord,
   PeerPushPaymentInitiationStatus,
@@ -97,6 +103,73 @@ export async function checkPeerPushDebit(
   };
 }
 
+async function handlePurseCreationConflict(
+  ws: InternalWalletState,
+  peerPushInitiation: PeerPushPaymentInitiationRecord,
+  resp: HttpResponse,
+): Promise<OperationAttemptResult> {
+  const pursePub = peerPushInitiation.pursePub;
+  const errResp = await readTalerErrorResponse(resp);
+  if (errResp.code !== TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS) {
+    await failPeerPushDebitTransaction(ws, pursePub);
+    return OperationAttemptResult.finishedEmpty();
+  }
+
+  // FIXME: Properly parse!
+  const brokenCoinPub = (errResp as any).coin_pub;
+  logger.trace(`excluded broken coin pub=${brokenCoinPub}`);
+
+  if (!brokenCoinPub) {
+    // FIXME: Details!
+    throw new TalerProtocolViolationError();
+  }
+
+  const instructedAmount = Amounts.parseOrThrow(peerPushInitiation.amount);
+
+  const repair: PeerCoinRepair = {
+    coinPubs: peerPushInitiation.coinSel.coinPubs,
+    contribs: peerPushInitiation.coinSel.contributions.map((x) =>
+      Amounts.parseOrThrow(x),
+    ),
+    exchangeBaseUrl: peerPushInitiation.exchangeBaseUrl,
+  };
+
+  const coinSelRes = await selectPeerCoins(ws, { instructedAmount, repair });
+
+  if (coinSelRes.type == "failure") {
+    // FIXME: Details!
+    throw Error(
+      "insufficient balance to re-select coins to repair double spending",
+    );
+  }
+
+  await ws.db
+    .mktx((x) => [x.peerPushPaymentInitiations])
+    .runReadWrite(async (tx) => {
+      const myPpi = await tx.peerPushPaymentInitiations.get(
+        peerPushInitiation.pursePub,
+      );
+      if (!myPpi) {
+        return;
+      }
+      switch (myPpi.status) {
+        case PeerPushPaymentInitiationStatus.PendingCreatePurse:
+        case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: {
+          const sel = coinSelRes.result;
+          myPpi.coinSel =  {
+            coinPubs: sel.coins.map((x) => x.coinPub),
+            contributions: sel.coins.map((x) => x.contribution),
+          }
+          break;
+        }
+        default:
+          return;
+      }
+      await tx.peerPushPaymentInitiations.put(myPpi);
+    });
+  return OperationAttemptResult.finishedEmpty();
+}
+
 async function processPeerPushDebitCreateReserve(
   ws: InternalWalletState,
   peerPushInitiation: PeerPushPaymentInitiationRecord,
@@ -175,6 +248,27 @@ async function processPeerPushDebitCreateReserve(
 
   logger.info(`resp: ${j2s(resp)}`);
 
+  switch (httpResp.status) {
+    case HttpStatusCode.Ok:
+      break;
+    case HttpStatusCode.Forbidden: {
+      // FIXME: Store this error!
+      await failPeerPushDebitTransaction(ws, pursePub);
+      return OperationAttemptResult.finishedEmpty();
+    }
+    case HttpStatusCode.Conflict: {
+      // Handle double-spending
+      return handlePurseCreationConflict(ws, peerPushInitiation, resp);
+    }
+    default: {
+      const errResp = await readTalerErrorResponse(resp);
+      return {
+        type: OperationAttemptResultType.Error,
+        errorDetail: errResp,
+      };
+    }
+  }
+
   if (httpResp.status !== HttpStatusCode.Ok) {
     // FIXME: do proper error reporting
     throw Error("got error response from exchange");
@@ -710,17 +804,17 @@ export async function failPeerPushDebitTransaction(
       switch (pushDebitRec.status) {
         case PeerPushPaymentInitiationStatus.AbortingRefresh:
         case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh:
-          // FIXME: We also need to abort the refresh group!
-          newStatus = PeerPushPaymentInitiationStatus.Aborted;
+          // FIXME: What to do about the refresh group?
+          newStatus = PeerPushPaymentInitiationStatus.Failed;
           break;
         case PeerPushPaymentInitiationStatus.AbortingDeletePurse:
         case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse:
-          newStatus = PeerPushPaymentInitiationStatus.Aborted;
-          break;
         case PeerPushPaymentInitiationStatus.PendingReady:
         case PeerPushPaymentInitiationStatus.SuspendedReady:
         case PeerPushPaymentInitiationStatus.SuspendedCreatePurse:
         case PeerPushPaymentInitiationStatus.PendingCreatePurse:
+          newStatus = PeerPushPaymentInitiationStatus.Failed;
+          break;
         case PeerPushPaymentInitiationStatus.Done:
         case PeerPushPaymentInitiationStatus.Aborted:
         case PeerPushPaymentInitiationStatus.Failed:

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]