gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: towards recovering from accid


From: gnunet
Subject: [taler-wallet-core] branch master updated: towards recovering from accidental double spends
Date: Thu, 11 Mar 2021 13:08:55 +0100

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 fb3da3a2 towards recovering from accidental double spends
fb3da3a2 is described below

commit fb3da3a28d6ed6a16ca7d0fa8ec775de51c7df6b
Author: Florian Dold <florian@dold.me>
AuthorDate: Thu Mar 11 13:08:41 2021 +0100

    towards recovering from accidental double spends
---
 .../test-wallet-backup-doublespend.ts              | 140 +++++++++++++++++++++
 .../src/integrationtests/testrunner.ts             |   2 +
 packages/taler-wallet-core/src/operations/pay.ts   |  44 +++++++
 .../taler-wallet-core/src/types/backupTypes.ts     |   2 +-
 packages/taler-wallet-core/src/types/dbTypes.ts    |  35 +++++-
 5 files changed, 220 insertions(+), 3 deletions(-)

diff --git 
a/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts
 
b/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts
new file mode 100644
index 00000000..94cad751
--- /dev/null
+++ 
b/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts
@@ -0,0 +1,140 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ 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/>
+ */
+
+/**
+ * Imports.
+ */
+import { PreparePayResultType } from "@gnu-taler/taler-wallet-core";
+import { testPay } from "@gnu-taler/taler-wallet-core/src/operations/testing";
+import {
+  GlobalTestState,
+  BankApi,
+  BankAccessApi,
+  WalletCli,
+  MerchantPrivateApi,
+} from "./harness";
+import {
+  createSimpleTestkudosEnvironment,
+  makeTestPayment,
+  withdrawViaBank,
+} from "./helpers";
+import { SyncService } from "./sync";
+
+/**
+ * Run test for basic, bank-integrated withdrawal.
+ */
+export async function runWalletBackupDoublespendTest(t: GlobalTestState) {
+  // Set up test environment
+
+  const {
+    commonDb,
+    merchant,
+    wallet,
+    bank,
+    exchange,
+  } = await createSimpleTestkudosEnvironment(t);
+
+  const sync = await SyncService.create(t, {
+    currency: "TESTKUDOS",
+    annualFee: "TESTKUDOS:0.5",
+    database: commonDb.connStr,
+    fulfillmentUrl: "taler://fulfillment-success",
+    httpPort: 8089,
+    name: "sync1",
+    paymentBackendUrl: merchant.makeInstanceBaseUrl(),
+    uploadLimitMb: 10,
+  });
+
+  await sync.start();
+  await sync.pingUntilAvailable();
+
+  await wallet.addBackupProvider({
+    backupProviderBaseUrl: sync.baseUrl,
+    activate: true,
+  });
+
+  await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:10" });
+
+  await wallet.runBackupCycle();
+  await wallet.runUntilDone();
+  await wallet.runBackupCycle();
+
+  const backupRecovery = await wallet.exportBackupRecovery();
+
+  const wallet2 = new WalletCli(t, "wallet2");
+
+  await wallet2.importBackupRecovery({ recovery: backupRecovery });
+
+  await wallet2.runBackupCycle();
+
+  console.log("wallet1 balance before spend:", await wallet.getBalances());
+
+  await makeTestPayment(t, {
+    merchant,
+    wallet,
+    order: {
+      summary: "foo",
+      amount: "TESTKUDOS:7",
+    },
+  });
+
+  await wallet.runUntilDone();
+
+  console.log("wallet1 balance after spend:", await wallet.getBalances());
+
+  {
+    console.log("wallet2 balance:", await wallet2.getBalances());
+  }
+
+  // Now we double-spend with the second wallet
+
+  {
+    const instance = "default";
+
+    const orderResp = await MerchantPrivateApi.createOrder(merchant, instance, 
{
+      order: {
+        amount: "TESTKUDOS:8",
+        summary: "bla",
+        fulfillment_url: "taler://fulfillment-success",
+      },
+    });
+
+    let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(
+      merchant,
+      {
+        orderId: orderResp.order_id,
+      },
+    );
+
+    t.assertTrue(orderStatus.order_status === "unpaid");
+
+    // Make wallet pay for the order
+
+    const preparePayResult = await wallet2.preparePay({
+      talerPayUri: orderStatus.taler_pay_uri,
+    });
+
+    t.assertTrue(
+      preparePayResult.status === PreparePayResultType.PaymentPossible,
+    );
+
+    const res = await wallet2.confirmPay({
+      proposalId: preparePayResult.proposalId,
+    });
+
+    console.log(res);
+  }
+}
diff --git a/packages/taler-wallet-cli/src/integrationtests/testrunner.ts 
b/packages/taler-wallet-cli/src/integrationtests/testrunner.ts
index 50850d6d..9f1edbd6 100644
--- a/packages/taler-wallet-cli/src/integrationtests/testrunner.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/testrunner.ts
@@ -63,6 +63,7 @@ import { runMerchantInstancesTest } from 
"./test-merchant-instances";
 import { runMerchantInstancesUrlsTest } from "./test-merchant-instances-urls";
 import { runWalletBackupBasicTest } from "./test-wallet-backup-basic";
 import { runMerchantInstancesDeleteTest } from 
"./test-merchant-instances-delete";
+import { runWalletBackupDoublespendTest } from 
"./test-wallet-backup-doublespend";
 
 /**
  * Test runner.
@@ -111,6 +112,7 @@ const allTests: TestMainFunction[] = [
   runTimetravelWithdrawTest,
   runTippingTest,
   runWalletBackupBasicTest,
+  runWalletBackupDoublespendTest,
   runWallettestingTest,
   runWithdrawalAbortBankTest,
   runWithdrawalBankIntegratedTest,
diff --git a/packages/taler-wallet-core/src/operations/pay.ts 
b/packages/taler-wallet-core/src/operations/pay.ts
index 03bf9e11..3add9bbb 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -84,6 +84,8 @@ import {
   throwUnexpectedRequestError,
   getHttpResponseErrorDetails,
   readSuccessResponseJsonOrErrorCode,
+  HttpResponseStatus,
+  readTalerErrorResponse,
 } from "../util/http";
 import { TalerErrorCode } from "../TalerErrorCode";
 import { URL } from "../util/url";
@@ -1001,6 +1003,22 @@ async function storePayReplaySuccess(
   });
 }
 
+/**
+ * Handle a 409 Conflict response from the merchant.
+ *
+ * We do this by going through the coin history provided by the exchange and
+ * (1) verifying the signatures from the exchange
+ * (2) adjusting the remaining coin value
+ * (3) re-do coin selection.
+ */
+async function handleInsufficientFunds(
+  ws: InternalWalletState,
+  proposalId: string,
+  err: TalerErrorDetails,
+): Promise<void> {
+  throw Error("payment re-denomination not implemented yet");
+}
+
 /**
  * Submit a payment to the merchant.
  *
@@ -1078,6 +1096,32 @@ async function submitPay(
       };
     }
 
+    if (resp.status === HttpResponseStatus.Conflict) {
+      const err = await readTalerErrorResponse(resp);
+      if (
+        err.code ===
+        TalerErrorCode.MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_FUNDS
+      ) {
+        // Do this in the background, as it might take some time
+        handleInsufficientFunds(ws, proposalId, err).catch(async (e) => {
+          await incrementProposalRetry(ws, proposalId, {
+            code: TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+            message: "unexpected exception",
+            hint: "unexpected exception",
+            details: {
+              exception: e,
+            },
+          });
+        });
+
+        return {
+          type: ConfirmPayResultType.Pending,
+          // FIXME: should we return something better here?
+          lastError: err,
+        };
+      }
+    }
+
     const merchantResp = await readSuccessResponseJsonOrThrow(
       resp,
       codecForMerchantPayResponse(),
diff --git a/packages/taler-wallet-core/src/types/backupTypes.ts 
b/packages/taler-wallet-core/src/types/backupTypes.ts
index d4b1625f..7e6ceb04 100644
--- a/packages/taler-wallet-core/src/types/backupTypes.ts
+++ b/packages/taler-wallet-core/src/types/backupTypes.ts
@@ -21,7 +21,7 @@
  * as the backup schema must remain very stable and should be self-contained.
  *
  * Future:
- * 1. Ghost spends (coin unexpectedly spend by a wallet with shared data)
+ * 1. Ghost spends (coin unexpectedly spent by a wallet with shared data)
  * 2. Ghost withdrawals (reserve unexpectedly emptied by another wallet with 
shared data)
  * 3. Track losses through re-denomination of payments/refreshes
  * 4. (Feature:) Payments to own bank account and P2P-payments need to be 
backed up
diff --git a/packages/taler-wallet-core/src/types/dbTypes.ts 
b/packages/taler-wallet-core/src/types/dbTypes.ts
index 6972744a..6c37971a 100644
--- a/packages/taler-wallet-core/src/types/dbTypes.ts
+++ b/packages/taler-wallet-core/src/types/dbTypes.ts
@@ -1464,14 +1464,14 @@ export interface BackupProviderRecord {
 
   /**
    * Proposal that we're currently trying to pay for.
-   * 
+   *
    * (Also included in paymentProposalIds.)
    */
   currentPaymentProposalId?: string;
 
   /**
    * Proposals that were used to pay (or attempt to pay) the provider.
-   * 
+   *
    * Stored to display a history of payments to the provider, and
    * to make sure that the wallet isn't overpaying.
    */
@@ -1541,6 +1541,31 @@ export interface DepositGroupRecord {
   retryInfo: RetryInfo;
 }
 
+/**
+ * Record for a deposits that the wallet observed
+ * as a result of double spending, but which is not
+ * present in the wallet's own database otherwise.
+ */
+export interface GhostDepositGroupRecord {
+  /**
+   * When multiple deposits for the same contract terms hash
+   * have a different timestamp, we choose the earliest one.
+   */
+  timestamp: Timestamp;
+
+  contractTermsHash: string;
+
+  deposits: {
+    coinPub: string;
+    amount: AmountString;
+    timestamp: Timestamp;
+    depositFee: AmountString;
+    merchantPub: string;
+    coinSig: string;
+    wireHash: string;
+  }[];
+}
+
 class ExchangesStore extends Store<"exchanges", ExchangeRecord> {
   constructor() {
     super("exchanges", { keyPath: "baseUrl" });
@@ -1750,6 +1775,12 @@ export const Stores = {
   bankWithdrawUris: new BankWithdrawUrisStore(),
   backupProviders: new BackupProvidersStore(),
   depositGroups: new DepositGroupsStore(),
+  ghostDepositGroups: new Store<"ghostDepositGroups", GhostDepositGroupRecord>(
+    "ghostDepositGroups",
+    {
+      keyPath: "contractTermsHash",
+    },
+  ),
 };
 
 export class MetaConfigStore extends Store<"metaConfig", ConfigRecord<any>> {

-- 
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]