wallet-core: fix revocation, re-introduce reserves object store

This commit is contained in:
Florian Dold 2022-08-26 01:18:01 +02:00
parent 70d0199572
commit 30e8fd83c2
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
10 changed files with 218 additions and 112 deletions

View File

@ -243,7 +243,7 @@ export async function runLibeufinBasicTest(t: GlobalTestState) {
WalletApiOperation.AcceptManualWithdrawal,
{
exchangeBaseUrl: exchange.baseUrl,
amount: "EUR:10",
amount: "EUR:15",
},
);

View File

@ -17,12 +17,11 @@
/**
* Imports.
*/
import { GlobalTestState, delayMs } from "../harness/harness.js";
import { GlobalTestState } from "../harness/harness.js";
import {
SandboxUserBundle,
NexusUserBundle,
launchLibeufinServices,
LibeufinSandboxApi,
LibeufinNexusApi,
} from "../harness/libeufin";
@ -73,7 +72,7 @@ export async function runLibeufinNexusBalanceTest(t: GlobalTestState) {
user02sandbox.ebicsBankAccount.label, // debit
user01sandbox.ebicsBankAccount.label, // credit
"EUR:10",
"first payment",
"second payment",
);
await LibeufinNexusApi.fetchTransactions(
@ -82,13 +81,13 @@ export async function runLibeufinNexusBalanceTest(t: GlobalTestState) {
"all", // range
"report", // level
);
// Check that user 01 has 20, via Nexus.
let accountInfo = await LibeufinNexusApi.getBankAccount(
libeufinServices.libeufinNexus,
user01nexus.localAccountName
user01nexus.localAccountName,
);
t.assertTrue(accountInfo.data.lastSeenBalance == "EUR:20");
t.assertAmountEquals(accountInfo.data.lastSeenBalance, "EUR:20");
// user 01 gives 30
await libeufinServices.libeufinSandbox.makeTransaction(
@ -107,8 +106,8 @@ export async function runLibeufinNexusBalanceTest(t: GlobalTestState) {
let accountInfoDebit = await LibeufinNexusApi.getBankAccount(
libeufinServices.libeufinNexus,
user01nexus.localAccountName
user01nexus.localAccountName,
);
t.assertTrue(accountInfoDebit.data.lastSeenBalance == "-EUR:10");
t.assertDeepEqual(accountInfoDebit.data.lastSeenBalance, "-EUR:10");
}
runLibeufinNexusBalanceTest.suites = ["libeufin"];

View File

@ -26,71 +26,71 @@ import {
TestRunResult,
} from "../harness/harness.js";
import { runAgeRestrictionsTest } from "./test-age-restrictions.js";
import { runBankApiTest } from "./test-bank-api";
import { runClaimLoopTest } from "./test-claim-loop";
import { runBankApiTest } from "./test-bank-api.js";
import { runClaimLoopTest } from "./test-claim-loop.js";
import { runClauseSchnorrTest } from "./test-clause-schnorr.js";
import { runDenomUnofferedTest } from "./test-denom-unoffered.js";
import { runDepositTest } from "./test-deposit";
import { runExchangeManagementTest } from "./test-exchange-management";
import { runDepositTest } from "./test-deposit.js";
import { runExchangeManagementTest } from "./test-exchange-management.js";
import { runExchangeTimetravelTest } from "./test-exchange-timetravel.js";
import { runFeeRegressionTest } from "./test-fee-regression";
import { runFeeRegressionTest } from "./test-fee-regression.js";
import { runForcedSelectionTest } from "./test-forced-selection.js";
import { runLibeufinApiBankaccountTest } from "./test-libeufin-api-bankaccount";
import { runLibeufinApiBankconnectionTest } from "./test-libeufin-api-bankconnection";
import { runLibeufinApiFacadeTest } from "./test-libeufin-api-facade";
import { runLibeufinApiFacadeBadRequestTest } from "./test-libeufin-api-facade-bad-request";
import { runLibeufinApiPermissionsTest } from "./test-libeufin-api-permissions";
import { runLibeufinApiSandboxCamtTest } from "./test-libeufin-api-sandbox-camt";
import { runLibeufinApiSandboxTransactionsTest } from "./test-libeufin-api-sandbox-transactions";
import { runLibeufinApiSchedulingTest } from "./test-libeufin-api-scheduling";
import { runLibeufinApiUsersTest } from "./test-libeufin-api-users";
import { runLibeufinBadGatewayTest } from "./test-libeufin-bad-gateway";
import { runLibeufinBasicTest } from "./test-libeufin-basic";
import { runLibeufinC5xTest } from "./test-libeufin-c5x";
import { runLibeufinAnastasisFacadeTest } from "./test-libeufin-facade-anastasis";
import { runLibeufinKeyrotationTest } from "./test-libeufin-keyrotation";
import { runLibeufinNexusBalanceTest } from "./test-libeufin-nexus-balance";
import { runLibeufinRefundTest } from "./test-libeufin-refund";
import { runLibeufinRefundMultipleUsersTest } from "./test-libeufin-refund-multiple-users";
import { runLibeufinSandboxWireTransferCliTest } from "./test-libeufin-sandbox-wire-transfer-cli";
import { runLibeufinTutorialTest } from "./test-libeufin-tutorial";
import { runMerchantExchangeConfusionTest } from "./test-merchant-exchange-confusion";
import { runMerchantInstancesTest } from "./test-merchant-instances";
import { runLibeufinApiBankaccountTest } from "./test-libeufin-api-bankaccount.js";
import { runLibeufinApiBankconnectionTest } from "./test-libeufin-api-bankconnection.js";
import { runLibeufinApiFacadeTest } from "./test-libeufin-api-facade.js";
import { runLibeufinApiFacadeBadRequestTest } from "./test-libeufin-api-facade-bad-request.js";
import { runLibeufinApiPermissionsTest } from "./test-libeufin-api-permissions.js";
import { runLibeufinApiSandboxCamtTest } from "./test-libeufin-api-sandbox-camt.js";
import { runLibeufinApiSandboxTransactionsTest } from "./test-libeufin-api-sandbox-transactions.js";
import { runLibeufinApiSchedulingTest } from "./test-libeufin-api-scheduling.js";
import { runLibeufinApiUsersTest } from "./test-libeufin-api-users.js";
import { runLibeufinBadGatewayTest } from "./test-libeufin-bad-gateway.js";
import { runLibeufinBasicTest } from "./test-libeufin-basic.js";
import { runLibeufinC5xTest } from "./test-libeufin-c5x.js";
import { runLibeufinAnastasisFacadeTest } from "./test-libeufin-facade-anastasis.js";
import { runLibeufinKeyrotationTest } from "./test-libeufin-keyrotation.js";
import { runLibeufinNexusBalanceTest } from "./test-libeufin-nexus-balance.js";
import { runLibeufinRefundTest } from "./test-libeufin-refund.js";
import { runLibeufinRefundMultipleUsersTest } from "./test-libeufin-refund-multiple-users.js";
import { runLibeufinSandboxWireTransferCliTest } from "./test-libeufin-sandbox-wire-transfer-cli.js";
import { runLibeufinTutorialTest } from "./test-libeufin-tutorial.js";
import { runMerchantExchangeConfusionTest } from "./test-merchant-exchange-confusion.js";
import { runMerchantInstancesTest } from "./test-merchant-instances.js";
import { runMerchantInstancesDeleteTest } from "./test-merchant-instances-delete";
import { runMerchantInstancesUrlsTest } from "./test-merchant-instances-urls";
import { runMerchantLongpollingTest } from "./test-merchant-longpolling";
import { runMerchantRefundApiTest } from "./test-merchant-refund-api";
import { runMerchantInstancesUrlsTest } from "./test-merchant-instances-urls.js";
import { runMerchantLongpollingTest } from "./test-merchant-longpolling.js";
import { runMerchantRefundApiTest } from "./test-merchant-refund-api.js";
import { runMerchantSpecPublicOrdersTest } from "./test-merchant-spec-public-orders.js";
import { runPayAbortTest } from "./test-pay-abort";
import { runPayPaidTest } from "./test-pay-paid";
import { runPaymentTest } from "./test-payment";
import { runPaymentClaimTest } from "./test-payment-claim";
import { runPaymentFaultTest } from "./test-payment-fault";
import { runPayAbortTest } from "./test-pay-abort.js";
import { runPayPaidTest } from "./test-pay-paid.js";
import { runPaymentTest } from "./test-payment.js";
import { runPaymentClaimTest } from "./test-payment-claim.js";
import { runPaymentFaultTest } from "./test-payment-fault.js";
import { runPaymentForgettableTest } from "./test-payment-forgettable.js";
import { runPaymentIdempotencyTest } from "./test-payment-idempotency";
import { runPaymentMultipleTest } from "./test-payment-multiple";
import { runPaymentDemoTest } from "./test-payment-on-demo";
import { runPaymentTransientTest } from "./test-payment-transient";
import { runPaymentIdempotencyTest } from "./test-payment-idempotency.js";
import { runPaymentMultipleTest } from "./test-payment-multiple.js";
import { runPaymentDemoTest } from "./test-payment-on-demo.js";
import { runPaymentTransientTest } from "./test-payment-transient.js";
import { runPaymentZeroTest } from "./test-payment-zero.js";
import { runPaywallFlowTest } from "./test-paywall-flow";
import { runPaywallFlowTest } from "./test-paywall-flow.js";
import { runPeerToPeerPullTest } from "./test-peer-to-peer-pull.js";
import { runPeerToPeerPushTest } from "./test-peer-to-peer-push.js";
import { runRefundTest } from "./test-refund";
import { runRefundAutoTest } from "./test-refund-auto";
import { runRefundGoneTest } from "./test-refund-gone";
import { runRefundIncrementalTest } from "./test-refund-incremental";
import { runRevocationTest } from "./test-revocation";
import { runTimetravelAutorefreshTest } from "./test-timetravel-autorefresh";
import { runTimetravelWithdrawTest } from "./test-timetravel-withdraw";
import { runTippingTest } from "./test-tipping";
import { runWalletBackupBasicTest } from "./test-wallet-backup-basic";
import { runWalletBackupDoublespendTest } from "./test-wallet-backup-doublespend";
import { runRefundTest } from "./test-refund.js";
import { runRefundAutoTest } from "./test-refund-auto.js";
import { runRefundGoneTest } from "./test-refund-gone.js";
import { runRefundIncrementalTest } from "./test-refund-incremental.js";
import { runRevocationTest } from "./test-revocation.js";
import { runTimetravelAutorefreshTest } from "./test-timetravel-autorefresh.js";
import { runTimetravelWithdrawTest } from "./test-timetravel-withdraw.js";
import { runTippingTest } from "./test-tipping.js";
import { runWalletBackupBasicTest } from "./test-wallet-backup-basic.js";
import { runWalletBackupDoublespendTest } from "./test-wallet-backup-doublespend.js";
import { runWalletDblessTest } from "./test-wallet-dbless.js";
import { runWallettestingTest } from "./test-wallettesting";
import { runWithdrawalAbortBankTest } from "./test-withdrawal-abort-bank";
import { runWithdrawalBankIntegratedTest } from "./test-withdrawal-bank-integrated";
import { runWallettestingTest } from "./test-wallettesting.js";
import { runWithdrawalAbortBankTest } from "./test-withdrawal-abort-bank.js";
import { runWithdrawalBankIntegratedTest } from "./test-withdrawal-bank-integrated.js";
import { runWithdrawalFakebankTest } from "./test-withdrawal-fakebank.js";
import { runTestWithdrawalManualTest } from "./test-withdrawal-manual";
import { runTestWithdrawalManualTest } from "./test-withdrawal-manual.js";
/**
* Test runner.

View File

@ -1224,6 +1224,7 @@ export const enum WithdrawalRecordType {
BankIntegrated = "bank-integrated",
PeerPullCredit = "peer-pull-credit",
PeerPushCredit = "peer-push-credit",
Recoup = "recoup",
}
export interface WgInfoBankIntegrated {
@ -1253,11 +1254,16 @@ export interface WgInfoBankPeerPush {
withdrawalType: WithdrawalRecordType.PeerPushCredit;
}
export interface WgInfoBankRecoup {
withdrawalType: WithdrawalRecordType.Recoup;
}
export type WgInfo =
| WgInfoBankIntegrated
| WgInfoBankManual
| WgInfoBankPeerPull
| WgInfoBankPeerPush;
| WgInfoBankPeerPush
| WgInfoBankRecoup;
/**
* Group of withdrawal operations that need to be executed.
@ -1287,6 +1293,8 @@ export interface WithdrawalGroupRecord {
/**
* The reserve private key.
*
* FIXME: Already in the reserves object store, redundant!
*/
reservePriv: string;
@ -1355,9 +1363,9 @@ export interface WithdrawalGroupRecord {
denomSelUid: string;
/**
* Retry info, always present even on completed operations so that indexing works.
* Retry info.
*/
retryInfo: RetryInfo;
retryInfo?: RetryInfo;
lastError: TalerErrorDetail | undefined;
}
@ -1386,6 +1394,8 @@ export interface RecoupGroupRecord {
*/
recoupGroupId: string;
exchangeBaseUrl: string;
timestampStarted: TalerProtocolTimestamp;
timestampFinished: TalerProtocolTimestamp | undefined;
@ -1724,6 +1734,13 @@ export interface PeerPullPaymentIncomingRecord {
contractPriv: string;
}
// FIXME: give this some smaller "row ID" to
// reference in other records?
export interface ReserveRecord {
reservePub: string;
reservePriv: string;
}
export const WalletStoresV1 = {
coins: describeStore(
describeContents<CoinRecord>("coins", {
@ -1735,6 +1752,12 @@ export const WalletStoresV1 = {
byCoinEvHash: describeIndex("byCoinEvHash", "coinEvHash"),
},
),
reserves: describeStore(
describeContents<ReserveRecord>("reserves", {
keyPath: "reservePub",
}),
{},
),
config: describeStore(
describeContents<ConfigRecord>("config", { keyPath: "key" }),
{},

View File

@ -73,7 +73,6 @@ export interface MerchantOperations {
): Promise<MerchantInfo>;
}
/**
* Interface for exchange-related operations.
*/
@ -113,6 +112,7 @@ export interface RecoupOperations {
refreshGroups: typeof WalletStoresV1.refreshGroups;
coins: typeof WalletStoresV1.coins;
}>,
exchangeBaseUrl: string,
coinPubs: string[],
): Promise<string>;
processRecoupGroup(

View File

@ -743,6 +743,7 @@ async function updateExchangeFromUrlImpl(
recoupGroupId = await ws.recoupOps.createRecoupGroup(
ws,
tx,
exchange.baseUrl,
newlyRevokedCoinPubs,
);
}

View File

@ -126,7 +126,7 @@ async function gatherWithdrawalPending(
resp.pendingOperations.push({
type: PendingTaskType.Withdraw,
givesLifeness: true,
timestampDue: wsr.retryInfo.nextRetry,
timestampDue: wsr.retryInfo?.nextRetry ?? AbsoluteTime.now(),
withdrawalGroupId: wsr.withdrawalGroupId,
lastError: wsr.lastError,
retryInfo: wsr.retryInfo,

View File

@ -36,16 +36,17 @@ import {
TalerErrorDetail,
TalerProtocolTimestamp,
URL,
codecForReserveStatus,
} from "@gnu-taler/taler-util";
import {
CoinRecord,
CoinSourceType,
CoinStatus,
OperationStatus,
RecoupGroupRecord,
RefreshCoinSource,
ReserveRecordStatus,
WalletStoresV1,
WithdrawalRecordType,
WithdrawCoinSource,
} from "../db.js";
import { InternalWalletState } from "../internal-wallet-state.js";
@ -109,6 +110,10 @@ async function reportRecoupError(
ws.notify({ type: NotificationType.RecoupOperationError, error: err });
}
/**
* Store a recoup group record in the database after marking
* a coin in the group as finished.
*/
async function putGroupAsFinished(
ws: InternalWalletState,
tx: GetReadWriteAccess<{
@ -127,29 +132,6 @@ async function putGroupAsFinished(
return;
}
recoupGroup.recoupFinishedPerCoin[coinIdx] = true;
let allFinished = true;
for (const b of recoupGroup.recoupFinishedPerCoin) {
if (!b) {
allFinished = false;
}
}
if (allFinished) {
logger.info("all recoups of recoup group are finished");
recoupGroup.timestampFinished = TalerProtocolTimestamp.now();
recoupGroup.retryInfo = RetryInfo.reset();
recoupGroup.lastError = undefined;
if (recoupGroup.scheduleRefreshCoins.length > 0) {
const refreshGroupId = await createRefreshGroup(
ws,
tx,
recoupGroup.scheduleRefreshCoins.map((x) => ({ coinPub: x })),
RefreshReason.Recoup,
);
processRefreshGroup(ws, refreshGroupId.refreshGroupId).catch((e) => {
logger.error(`error while refreshing after recoup ${e}`);
});
}
}
await tx.recoupGroups.put(recoupGroup);
}
@ -258,8 +240,6 @@ async function recoupWithdrawCoin(
const currency = updatedCoin.currentAmount.currency;
updatedCoin.currentAmount = Amounts.getZero(currency);
await tx.coins.put(updatedCoin);
// FIXME: Actually withdraw here!
// await internalCreateWithdrawalGroup(ws, {...});
await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
});
@ -392,7 +372,7 @@ async function processRecoupGroupImpl(
): Promise<void> {
const forceNow = options.forceNow ?? false;
await setupRecoupRetry(ws, recoupGroupId, { reset: forceNow });
const recoupGroup = await ws.db
let recoupGroup = await ws.db
.mktx((x) => ({
recoupGroups: x.recoupGroups,
}))
@ -416,23 +396,105 @@ async function processRecoupGroupImpl(
});
await Promise.all(ps);
const reserveSet = new Set<string>();
for (let i = 0; i < recoupGroup.coinPubs.length; i++) {
const coinPub = recoupGroup.coinPubs[i];
const coin = await ws.db
.mktx((x) => ({
coins: x.coins,
}))
.runReadOnly(async (tx) => {
return tx.coins.get(coinPub);
});
if (!coin) {
throw Error(`Coin ${coinPub} not found, can't request recoup`);
}
if (coin.coinSource.type === CoinSourceType.Withdraw) {
reserveSet.add(coin.coinSource.reservePub);
recoupGroup = await ws.db
.mktx((x) => ({
recoupGroups: x.recoupGroups,
}))
.runReadOnly(async (tx) => {
return tx.recoupGroups.get(recoupGroupId);
});
if (!recoupGroup) {
return;
}
for (const b of recoupGroup.recoupFinishedPerCoin) {
if (!b) {
return;
}
}
logger.info("all recoups of recoup group are finished");
const reserveSet = new Set<string>();
const reservePrivMap: Record<string, string> = {};
for (let i = 0; i < recoupGroup.coinPubs.length; i++) {
const coinPub = recoupGroup.coinPubs[i];
await ws.db
.mktx((x) => ({
coins: x.coins,
reserves: x.reserves,
}))
.runReadOnly(async (tx) => {
const coin = await tx.coins.get(coinPub);
if (!coin) {
throw Error(`Coin ${coinPub} not found, can't request recoup`);
}
if (coin.coinSource.type === CoinSourceType.Withdraw) {
const reserve = await tx.reserves.get(coin.coinSource.reservePub);
if (!reserve) {
return;
}
reserveSet.add(coin.coinSource.reservePub);
reservePrivMap[coin.coinSource.reservePub] = reserve.reservePriv;
}
});
}
for (const reservePub of reserveSet) {
const reserveUrl = new URL(
`reserves/${reservePub}`,
recoupGroup.exchangeBaseUrl,
);
logger.info(`querying reserve status for recoup via ${reserveUrl}`);
const resp = await ws.http.get(reserveUrl.href);
const result = await readSuccessResponseJsonOrThrow(
resp,
codecForReserveStatus(),
);
await internalCreateWithdrawalGroup(ws, {
amount: Amounts.parseOrThrow(result.balance),
exchangeBaseUrl: recoupGroup.exchangeBaseUrl,
reserveStatus: ReserveRecordStatus.QueryingStatus,
reserveKeyPair: {
pub: reservePub,
priv: reservePrivMap[reservePub],
},
wgInfo: {
withdrawalType: WithdrawalRecordType.Recoup,
},
});
}
await ws.db
.mktx((x) => ({
recoupGroups: x.recoupGroups,
denominations: WalletStoresV1.denominations,
refreshGroups: WalletStoresV1.refreshGroups,
coins: WalletStoresV1.coins,
}))
.runReadWrite(async (tx) => {
const rg2 = await tx.recoupGroups.get(recoupGroupId);
if (!rg2) {
return;
}
rg2.timestampFinished = TalerProtocolTimestamp.now();
rg2.retryInfo = RetryInfo.reset();
rg2.lastError = undefined;
if (rg2.scheduleRefreshCoins.length > 0) {
const refreshGroupId = await createRefreshGroup(
ws,
tx,
rg2.scheduleRefreshCoins.map((x) => ({ coinPub: x })),
RefreshReason.Recoup,
);
processRefreshGroup(ws, refreshGroupId.refreshGroupId).catch((e) => {
logger.error(`error while refreshing after recoup ${e}`);
});
}
await tx.recoupGroups.put(rg2);
});
}
export async function createRecoupGroup(
@ -443,12 +505,14 @@ export async function createRecoupGroup(
refreshGroups: typeof WalletStoresV1.refreshGroups;
coins: typeof WalletStoresV1.coins;
}>,
exchangeBaseUrl: string,
coinPubs: string[],
): Promise<string> {
const recoupGroupId = encodeCrock(getRandomBytes(32));
const recoupGroup: RecoupGroupRecord = {
recoupGroupId,
exchangeBaseUrl: exchangeBaseUrl,
coinPubs: coinPubs,
lastError: undefined,
timestampFinished: undefined,

View File

@ -1135,6 +1135,22 @@ async function processWithdrawGroupImpl(
withdrawalGroup.exchangeBaseUrl,
);
if (withdrawalGroup.denomsSel.selectedDenoms.length === 0) {
await ws.db
.mktx((x) => ({ withdrawalGroups: x.withdrawalGroups }))
.runReadWrite(async (tx) => {
const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
if (!wg) {
return;
}
wg.operationStatus = OperationStatus.Finished;
delete wg.lastError;
delete wg.retryInfo;
await tx.withdrawalGroups.put(wg);
});
return;
}
const numTotalCoins = withdrawalGroup.denomsSel.selectedDenoms
.map((x) => x.count)
.reduce((a, b) => a + b);
@ -1709,7 +1725,6 @@ export async function internalCreateWithdrawalGroup(
args: {
reserveStatus: ReserveRecordStatus;
amount: AmountJson;
bankInfo?: ReserveBankInfo;
exchangeBaseUrl: string;
forcedDenomSel?: ForcedDenomSel;
reserveKeyPair?: EddsaKeypair;
@ -1776,12 +1791,17 @@ export async function internalCreateWithdrawalGroup(
await ws.db
.mktx((x) => ({
withdrawalGroups: x.withdrawalGroups,
reserves: x.reserves,
exchanges: x.exchanges,
exchangeDetails: x.exchangeDetails,
exchangeTrust: x.exchangeTrust,
}))
.runReadWrite(async (tx) => {
await tx.withdrawalGroups.add(withdrawalGroup);
await tx.reserves.put({
reservePub: withdrawalGroup.reservePub,
reservePriv: withdrawalGroup.reservePriv,
});
if (!isAudited && !isTrusted) {
await tx.exchangeTrust.put({
@ -1906,7 +1926,6 @@ export async function createManualWithdrawal(
withdrawalType: WithdrawalRecordType.BankManual,
},
exchangeBaseUrl: req.exchangeBaseUrl,
bankInfo: undefined,
forcedDenomSel: req.forcedDenomSel,
restrictAge: req.restrictAge,
reserveStatus: ReserveRecordStatus.QueryingStatus,

View File

@ -182,7 +182,7 @@ export interface PendingRecoupTask {
export interface PendingWithdrawTask {
type: PendingTaskType.Withdraw;
lastError: TalerErrorDetail | undefined;
retryInfo: RetryInfo;
retryInfo?: RetryInfo;
withdrawalGroupId: string;
}