backup schema WIP

This commit is contained in:
Florian Dold 2020-12-07 20:24:16 +01:00
parent bbd65fc4b7
commit bd88f3f443
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
5 changed files with 813 additions and 108 deletions

View File

@ -29,7 +29,9 @@ import {
BackupCoin,
BackupCoinSource,
BackupCoinSourceType,
BackupDenomination,
BackupExchangeData,
BackupExchangeWireFee,
WalletBackupContentV1,
} from "../types/backupTypes";
import { TransactionHandle } from "../util/query";
@ -128,21 +130,88 @@ export async function exportBackup(
): Promise<WalletBackupContentV1> {
await provideBackupState(ws);
return ws.db.runWithWriteTransaction(
[Stores.config, Stores.exchanges, Stores.coins],
[Stores.config, Stores.exchanges, Stores.coins, Stores.denominations],
async (tx) => {
const bs = await getWalletBackupState(ws, tx);
const exchanges: BackupExchangeData[] = [];
const coins: BackupCoin[] = [];
const denominations: BackupDenomination[] = [];
await tx.iter(Stores.exchanges).forEach((ex) => {
// Only back up permanently added exchanges.
if (!ex.details) {
return;
}
if (!ex.wireInfo) {
return;
}
if (!ex.addComplete) {
return;
}
if (!ex.permanent) {
return;
}
const wi = ex.wireInfo;
const wireFees: BackupExchangeWireFee[] = [];
Object.keys(wi.feesForType).forEach((x) => {
for (const f of wi.feesForType[x]) {
wireFees.push({
wireType: x,
closingFee: Amounts.stringify(f.closingFee),
endStamp: f.endStamp,
sig: f.sig,
startStamp: f.startStamp,
wireFee: Amounts.stringify(f.wireFee),
});
}
});
exchanges.push({
exchangeBaseUrl: ex.baseUrl,
exchangeMasterPub: ex.details?.masterPublicKey,
baseUrl: ex.baseUrl,
accounts: ex.wireInfo.accounts.map((x) => ({
paytoUri: x.payto_uri,
})),
auditors: ex.details.auditors.map((x) => ({
auditorPub: x.auditor_pub,
auditorUrl: x.auditor_url,
denominationKeys: x.denomination_keys,
})),
masterPublicKey: ex.details.masterPublicKey,
currency: ex.details.currency,
protocolVersion: ex.details.protocolVersion,
wireFees,
signingKeys: ex.details.signingKeys.map((x) => ({
key: x.key,
masterSig: x.master_sig,
stampEnd: x.stamp_end,
stampExpire: x.stamp_expire,
stampStart: x.stamp_start,
})),
termsOfServiceAcceptedEtag: ex.termsOfServiceAcceptedEtag,
termsOfServiceLastEtag: ex.termsOfServiceLastEtag,
});
});
await tx.iter(Stores.denominations).forEach((denom) => {
denominations.push({
denomPub: denom.denomPub,
denomPubHash: denom.denomPubHash,
exchangeBaseUrl: canonicalizeBaseUrl(denom.exchangeBaseUrl),
feeDeposit: Amounts.stringify(denom.feeDeposit),
feeRefresh: Amounts.stringify(denom.feeRefresh),
feeRefund: Amounts.stringify(denom.feeRefund),
feeWithdraw: Amounts.stringify(denom.feeWithdraw),
isOffered: denom.isOffered,
isRevoked: denom.isRevoked,
masterSig: denom.masterSig,
stampExpireDeposit: denom.stampExpireDeposit,
stampExpireLegal: denom.stampExpireLegal,
stampExpireWithdraw: denom.stampExpireWithdraw,
stampStart: denom.stampStart,
value: Amounts.stringify(denom.value),
});
});
@ -192,6 +261,7 @@ export async function exportBackup(
planchets: [],
refreshSessions: [],
reserves: [],
denominations: [],
walletRootPub: bs.walletRootPub,
};

View File

@ -359,7 +359,6 @@ export async function acceptExchangeTermsOfService(
return;
}
r.termsOfServiceAcceptedEtag = etag;
r.termsOfServiceAcceptedTimestamp = getTimestampNow();
await tx.put(Stores.exchanges, r);
});
}
@ -490,9 +489,7 @@ async function updateExchangeFromUrlImpl(
updateStatus: ExchangeUpdateStatus.FetchKeys,
updateStarted: now,
updateReason: ExchangeUpdateReason.Initial,
timestampAdded: getTimestampNow(),
termsOfServiceAcceptedEtag: undefined,
termsOfServiceAcceptedTimestamp: undefined,
termsOfServiceLastEtag: undefined,
termsOfServiceText: undefined,
retryInfo: initRetryInfo(false),

View File

@ -793,9 +793,9 @@ export async function getExchangeWithdrawalInfo(
let tosAccepted = false;
if (exchangeInfo.termsOfServiceAcceptedTimestamp) {
if (exchangeInfo.termsOfServiceLastEtag) {
if (
exchangeInfo.termsOfServiceAcceptedEtag ==
exchangeInfo.termsOfServiceAcceptedEtag ===
exchangeInfo.termsOfServiceLastEtag
) {
tosAccepted = true;

View File

@ -14,6 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { Timestamp } from "../util/time";
/**
* Type declarations for backup.
@ -21,6 +22,23 @@
* Contains some redundancy with the other type declarations,
* as the backup schema must be very stable.
*
* Current limitations:
* 1. Contracts that are claimed but not accepted aren't
* exported yet.
* 2. There is no garbage collection mechanism for the export yet.
* (But this should actually become the main GC mechanism!)
* 3. Reserve history isn't backed up yet.
* 4. Recoup metadata isn't exported yet.
*
* General considerations / decisions:
* 1. Information about previously occurring errors and
* retries is never backed up.
* 2. The ToS text of an exchange is never backed up.
* 3. Public keys are always exported in the backup
* and never recomputed (this allows the import to
* complete within a DB transaction that can't access
* the crypto worker).
*
* @author Florian Dold <dold@taler.net>
*/
@ -51,88 +69,19 @@ export interface WalletBackupContentV1 {
*/
exchanges: BackupExchangeData[];
reserves: ReserveBackupData[];
denominations: BackupDenomination[];
reserves: BackupReserveData[];
withdrawalGroups: BackupWithdrawalGroup[];
refreshGroups: BackupRefreshGroup[];
coins: BackupCoin[];
planchets: BackupWithdrawalPlanchet[];
refreshSessions: BackupRefreshSession[];
purchases: BackupPurchase[];
}
export interface BackupRefreshSession {
}
export interface BackupReserve {
reservePub: string;
reservePriv: string;
/**
* The exchange base URL.
*/
exchangeBaseUrl: string;
bankConfirmUrl?: string;
/**
* Wire information (as payto URI) for the bank account that
* transfered funds for this reserve.
*/
senderWire?: string;
}
export interface ReserveBackupData {
/**
* The reserve public key.
*/
reservePub: string;
/**
* The reserve private key.
*/
reservePriv: string;
/**
* The exchange base URL.
*/
exchangeBaseUrl: string;
instructedAmount: string;
/**
* Wire information (as payto URI) for the bank account that
* transfered funds for this reserve.
*/
senderWire?: string;
}
export interface BackupExchangeData {
exchangeBaseUrl: string;
exchangeMasterPub: string;
/**
* ETag for last terms of service download.
*/
termsOfServiceAcceptedEtag: string | undefined;
}
export interface BackupWithdrawalPlanchet {
coinSource: BackupWithdrawCoinSource | BackupTipCoinSource;
blindingKey: string;
coinPriv: string;
coinPub: string;
denomPubHash: string;
/**
* Base URL that identifies the exchange from which we are getting the
* coin.
*/
exchangeBaseUrl: string;
}
export enum BackupCoinSourceType {
Withdraw = "withdraw",
Refresh = "refresh",
@ -141,6 +90,10 @@ export enum BackupCoinSourceType {
export interface BackupWithdrawCoinSource {
type: BackupCoinSourceType.Withdraw;
/**
* Can be the empty string for orphaned coins.
*/
withdrawalGroupId: string;
/**
@ -170,37 +123,36 @@ export type BackupCoinSource =
| BackupRefreshCoinSource
| BackupTipCoinSource;
/**
* Coin that has been withdrawn and might have been
* (partially) spent.
*/
export interface BackupCoin {
/**
* Where did the coin come from? Used for recouping coins.
*/
coinSource: BackupCoinSource;
/**
* Public key of the coin.
*/
coinPub: string;
/**
* Private key of the coin.
* Private key to authorize operations on the coin.
*/
coinPriv: string;
/**
* Where did the coin come from (withdrawal/refresh/tip)?
* Used for recouping coins.
* Key used by the exchange used to sign the coin.
*/
coinSource: BackupCoinSource;
denomPub: string;
/**
* Is the coin still fresh
* Hash of the public key that signs the coin.
*/
fresh: boolean;
denomPubHash: string;
/**
* Blinding key used when withdrawing the coin.
* Potentionally used again during payback.
* Unblinded signature by the exchange.
*/
blindingKey: string;
denomSig: string;
/**
* Amount that's left on the coin.
@ -212,4 +164,698 @@ export interface BackupCoin {
* coin.
*/
exchangeBaseUrl: string;
/**
* Blinding key used when withdrawing the coin.
* Potentionally used again during payback.
*/
blindingKey: string;
fresh: boolean;
}
/**
* Status of a tip we got from a merchant.
*/
export interface BackupTip {
/**
* Tip ID chosen by the wallet.
*/
walletTipId: string;
/**
* The merchant's identifier for this tip.
*/
merchantTipId: string;
/**
* Has the user accepted the tip? Only after the tip has been accepted coins
* withdrawn from the tip may be used.
*/
acceptedTimestamp: Timestamp | undefined;
createdTimestamp: Timestamp;
/**
* Timestamp for when the wallet finished picking up the tip
* from the merchant.
*/
pickedUpTimestamp: Timestamp | undefined;
/**
* The tipped amount.
*/
tipAmountRaw: BackupAmountString;
tipAmountEffective: BackupAmountString;
/**
* Timestamp, the tip can't be picked up anymore after this deadline.
*/
tipExpiration: Timestamp;
/**
* The exchange that will sign our coins, chosen by the merchant.
*/
exchangeBaseUrl: string;
/**
* Base URL of the merchant that is giving us the tip.
*/
merchantBaseUrl: string;
/**
* Planchets, the members included in TipPlanchetDetail will be sent to the
* merchant.
*/
planchets?: {
blindingKey: string;
coinEv: string;
coinPriv: string;
coinPub: string;
}[];
totalCoinValue: BackupAmountString;
totalWithdrawCost: BackupAmountString;
selectedDenoms: {
denomPubHash: string;
count: number;
}[];
}
/**
* Reasons for why a coin is being refreshed.
*/
export enum BackupRefreshReason {
Manual = "manual",
Pay = "pay",
Refund = "refund",
AbortPay = "abort-pay",
Recoup = "recoup",
BackupRestored = "backup-restored",
Scheduled = "scheduled",
}
/**
* Planchet for a coin during refrehs.
*/
export interface BackupRefreshPlanchet {
/**
* Public key for the coin.
*/
publicKey: string;
/**
* Private key for the coin.
*/
privateKey: string;
/**
* Blinded public key.
*/
coinEv: string;
/**
* Blinding key used.
*/
blindingKey: string;
}
export interface BackupRefreshSessionRecord {
/**
* Public key that's being melted in this session.
*/
meltCoinPub: string;
/**
* How much of the coin's value is melted away
* with this refresh session?
*/
amountRefreshInput: BackupAmountString;
/**
* Sum of the value of denominations we want
* to withdraw in this session, without fees.
*/
amountRefreshOutput: BackupAmountString;
/**
* Signature to confirm the melting.
*/
confirmSig: string;
/**
* Hased denominations of the newly requested coins.
*/
newDenomHashes: string[];
/**
* Denominations of the newly requested coins.
*/
newDenoms: string[];
/**
* Planchets for each cut-and-choose instance.
*/
planchetsForGammas: BackupRefreshPlanchet[][];
/**
* The transfer keys, kappa of them.
*/
transferPubs: string[];
/**
* Private keys for the transfer public keys.
*/
transferPrivs: string[];
/**
* The no-reveal-index after we've done the melting.
*/
norevealIndex?: number;
/**
* Hash of the session.
*/
hash: string;
/**
* Timestamp when the refresh session finished.
*/
finishedTimestamp: Timestamp | undefined;
/**
* When has this refresh session been created?
*/
timestampCreated: Timestamp;
/**
* Base URL for the exchange we're doing the refresh with.
*/
exchangeBaseUrl: string;
}
export interface BackupRefreshGroup {
refreshGroupId: string;
reason: BackupRefreshReason;
oldCoinPubs: string[];
refreshSessionPerCoin: (BackupRefreshSessionRecord | undefined)[];
inputPerCoin: BackupAmountString[];
estimatedOutputPerCoin: BackupAmountString[];
/**
* Timestamp when the refresh session finished.
*/
timestampFinished: Timestamp | undefined;
}
export interface BackupWithdrawalGroup {
withdrawalGroupId: string;
reservePub: string;
/**
* When was the withdrawal operation started started?
* Timestamp in milliseconds.
*/
timestampStart: Timestamp;
/**
* When was the withdrawal operation completed?
*/
timestampFinish?: Timestamp;
/**
* Amount including fees (i.e. the amount subtracted from the
* reserve to withdraw all coins in this withdrawal session).
*/
rawWithdrawalAmount: BackupAmountString;
totalCoinValue: BackupAmountString;
totalWithdrawCost: BackupAmountString;
selectedDenoms: {
denomPubHash: string;
count: number;
}[];
/**
* One planchet/coin for each selected denomination.
*/
planchets: {
blindingKey: string;
coinPriv: string;
coinPub: string;
}[];
}
export enum BackupRefundState {
Failed = "failed",
Applied = "applied",
Pending = "pending",
}
export interface BackupRefundItemCommon {
// Execution time as claimed by the merchant
executionTime: Timestamp;
/**
* Time when the wallet became aware of the refund.
*/
obtainedTime: Timestamp;
refundAmount: BackupAmountString;
refundFee: BackupAmountString;
/**
* Upper bound on the refresh cost incurred by
* applying this refund.
*
* Might be lower in practice when two refunds on the same
* coin are refreshed in the same refresh operation.
*/
totalRefreshCostBound: BackupAmountString;
}
/**
* Failed refund, either because the merchant did
* something wrong or it expired.
*/
export interface BackupRefundFailedItem extends BackupRefundItemCommon {
type: BackupRefundState.Failed;
}
export interface BackupRefundPendingItem extends BackupRefundItemCommon {
type: BackupRefundState.Pending;
}
export interface BackupRefundAppliedItem extends BackupRefundItemCommon {
type: BackupRefundState.Applied;
}
/**
* State of one refund from the merchant, maintained by the wallet.
*/
export type BackupRefundItem =
| BackupRefundFailedItem
| BackupRefundPendingItem
| BackupRefundAppliedItem;
export interface BackupPurchase {
/**
* Proposal ID for this purchase. Uniquely identifies the
* purchase and the proposal.
*/
proposalId: string;
/**
* Contract terms we got from the merchant.
*/
contractTermsRaw: string;
/**
* Amount requested by the merchant.
*/
paymentAmount: BackupAmountString;
/**
* Public keys of the coins that were selected.
*/
coinPubs: string[];
/**
* Deposit permission signature of each coin.
*/
coinSigs: string[];
/**
* Amount that each coin contributes.
*/
coinContributions: BackupAmountString[];
/**
* How much of the wire fees is the customer paying?
*/
customerWireFees: BackupAmountString;
/**
* How much of the deposit fees is the customer paying?
*/
customerDepositFees: BackupAmountString;
totalPayCost: BackupAmountString;
/**
* Timestamp of the first time that sending a payment to the merchant
* for this purchase was successful.
*/
timestampFirstSuccessfulPay: Timestamp | undefined;
merchantPaySig: string | undefined;
/**
* When was the purchase made?
* Refers to the time that the user accepted.
*/
timestampAccept: Timestamp;
/**
* Pending refunds for the purchase. A refund is pending
* when the merchant reports a transient error from the exchange.
*/
refunds: { [refundKey: string]: BackupRefundItem };
/**
* When was the last refund made?
* Set to 0 if no refund was made on the purchase.
*/
timestampLastRefundStatus: Timestamp | undefined;
abortStatus?: "abort-refund" | "abort-finished";
/**
* Continue querying the refund status until this deadline has expired.
*/
autoRefundDeadline: Timestamp | undefined;
}
/**
* Info about one denomination in the backup.
*
* Note that the wallet only backs up validated denominations.
*/
export interface BackupDenomination {
/**
* Value of one coin of the denomination.
*/
value: BackupAmountString;
/**
* The denomination public key.
*/
denomPub: string;
/**
* Hash of the denomination public key.
* Stored in the database for faster lookups.
*/
denomPubHash: string;
/**
* Fee for withdrawing.
*/
feeWithdraw: BackupAmountString;
/**
* Fee for depositing.
*/
feeDeposit: BackupAmountString;
/**
* Fee for refreshing.
*/
feeRefresh: BackupAmountString;
/**
* Fee for refunding.
*/
feeRefund: BackupAmountString;
/**
* Validity start date of the denomination.
*/
stampStart: Timestamp;
/**
* Date after which the currency can't be withdrawn anymore.
*/
stampExpireWithdraw: Timestamp;
/**
* Date after the denomination officially doesn't exist anymore.
*/
stampExpireLegal: Timestamp;
/**
* Data after which coins of this denomination can't be deposited anymore.
*/
stampExpireDeposit: Timestamp;
/**
* Signature by the exchange's master key over the denomination
* information.
*/
masterSig: string;
/**
* Was this denomination still offered by the exchange the last time
* we checked?
* Only false when the exchange redacts a previously published denomination.
*/
isOffered: boolean;
/**
* Did the exchange revoke the denomination?
* When this field is set to true in the database, the same transaction
* should also mark all affected coins as revoked.
*/
isRevoked: boolean;
/**
* Base URL of the exchange.
*/
exchangeBaseUrl: string;
}
export interface BackupReserve {
reservePub: string;
reservePriv: string;
/**
* The exchange base URL.
*/
exchangeBaseUrl: string;
bankConfirmUrl?: string;
/**
* Wire information (as payto URI) for the bank account that
* transfered funds for this reserve.
*/
senderWire?: string;
}
export interface BackupReserveData {
/**
* The reserve public key.
*/
reservePub: string;
/**
* The reserve private key.
*/
reservePriv: string;
/**
* The exchange base URL.
*/
exchangeBaseUrl: string;
/**
* Time when the reserve was created.
*/
timestampCreated: Timestamp;
/**
* Time when the information about this reserve was posted to the bank.
*
* Only applies if bankWithdrawStatusUrl is defined.
*
* Set to 0 if that hasn't happened yet.
*/
timestampReserveInfoPosted: Timestamp | undefined;
/**
* Time when the reserve was confirmed by the bank.
*
* Set to undefined if not confirmed yet.
*/
timestampBankConfirmed: Timestamp | undefined;
/**
* Wire information (as payto URI) for the bank account that
* transfered funds for this reserve.
*/
senderWire?: string;
/**
* Amount that was sent by the user to fund the reserve.
*/
instructedAmount: BackupAmountString;
/**
* Extra state for when this is a withdrawal involving
* a Taler-integrated bank.
*/
bankInfo?: {
/**
* Status URL that the wallet will use to query the status
* of the Taler withdrawal operation on the bank's side.
*/
statusUrl: string;
confirmUrl?: string;
/**
* Exchange payto URI that the bank will use to fund the reserve.
*/
exchangePaytoUri: string;
/**
* Do we still need to register the reserve with the bank?
*/
registerPending: boolean;
};
initialWithdrawalGroupId: string;
initialTotalCoinValue: BackupAmountString;
initialTotalWithdrawCost: BackupAmountString;
initialSelectedDenoms: {
denomPubHash: string;
count: number;
}[];
}
export interface ExchangeBankAccount {
paytoUri: string;
}
/**
* Wire fee for one wire method as stored in the
* wallet's database.
*/
export interface BackupExchangeWireFee {
wireType: string;
/**
* Fee for wire transfers.
*/
wireFee: string;
/**
* Fees to close and refund a reserve.
*/
closingFee: string;
/**
* Start date of the fee.
*/
startStamp: Timestamp;
/**
* End date of the fee.
*/
endStamp: Timestamp;
/**
* Signature made by the exchange master key.
*/
sig: string;
}
/**
* Structure of one exchange signing key in the /keys response.
*/
export class BackupExchangeSignKey {
stampStart: Timestamp;
stampExpire: Timestamp;
stampEnd: Timestamp;
key: string;
masterSig: string;
}
/**
* Signature by the auditor that a particular denomination key is audited.
*/
export class AuditorDenomSig {
/**
* Denomination public key's hash.
*/
denom_pub_h: string;
/**
* The signature.
*/
auditor_sig: string;
}
/**
* Auditor information as given by the exchange in /keys.
*/
export class BackupExchangeAuditor {
/**
* Auditor's public key.
*/
auditorPub: string;
/**
* Base URL of the auditor.
*/
auditorUrl: string;
/**
* List of signatures for denominations by the auditor.
*/
denominationKeys: AuditorDenomSig[];
}
export interface BackupExchangeData {
/**
* Base url of the exchange.
*/
baseUrl: string;
/**
* Master public key of the exchange.
*/
masterPublicKey: string;
/**
* Auditors (partially) auditing the exchange.
*/
auditors: BackupExchangeAuditor[];
/**
* Currency that the exchange offers.
*/
currency: string;
/**
* Last observed protocol version.
*/
protocolVersion: string;
/**
* Signing keys we got from the exchange, can also contain
* older signing keys that are not returned by /keys anymore.
*/
signingKeys: BackupExchangeSignKey[];
wireFees: BackupExchangeWireFee[];
accounts: ExchangeBankAccount[];
/**
* ETag for last terms of service download.
*/
termsOfServiceLastEtag: string | undefined;
/**
* ETag for last terms of service download.
*/
termsOfServiceAcceptedEtag: string | undefined;
}

View File

@ -554,13 +554,10 @@ export interface ExchangeRecord {
*/
wireInfo: ExchangeWireInfo | undefined;
/**
* When was the exchange added to the wallet?
*/
timestampAdded: Timestamp;
/**
* Terms of service text or undefined if not downloaded yet.
*
* This is just used as a cache of the last downloaded ToS.
*/
termsOfServiceText: string | undefined;
@ -574,11 +571,6 @@ export interface ExchangeRecord {
*/
termsOfServiceAcceptedEtag: string | undefined;
/**
* ETag for last terms of service download.
*/
termsOfServiceAcceptedTimestamp: Timestamp | undefined;
/**
* Time when the update to the exchange has been started or
* undefined if no update is in progress.