derive tipping planchets from seed, implement backup further

This commit is contained in:
Florian Dold 2020-12-15 17:12:22 +01:00
parent f332d61fb6
commit c09c5bbe62
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
12 changed files with 427 additions and 179 deletions

View File

@ -390,6 +390,25 @@ export function setupRefreshPlanchet(
}; };
} }
export function setupTipPlanchet(
secretSeed: Uint8Array,
coinNumber: number,
): FreshCoin {
const info = stringToBytes("taler-tip-coin-derivation");
const saltArrBuf = new ArrayBuffer(4);
const salt = new Uint8Array(saltArrBuf);
const saltDataView = new DataView(saltArrBuf);
saltDataView.setUint32(0, coinNumber);
const out = kdf(64, secretSeed, salt, info);
const coinPriv = out.slice(0, 32);
const bks = out.slice(32, 64);
return {
bks,
coinPriv,
coinPub: eddsaGetPublic(coinPriv),
};
}
export function setupRefreshTransferPub( export function setupRefreshTransferPub(
secretSeed: Uint8Array, secretSeed: Uint8Array,
transferPubIndex: number, transferPubIndex: number,

View File

@ -22,16 +22,7 @@
/** /**
* Imports. * Imports.
*/ */
import { AmountJson } from "../../util/amounts"; import { CoinRecord, DenominationRecord, WireFee } from "../../types/dbTypes";
import {
CoinRecord,
DenominationRecord,
RefreshSessionRecord,
TipPlanchet,
WireFee,
DenominationSelectionInfo,
} from "../../types/dbTypes";
import { CryptoWorker } from "./cryptoWorker"; import { CryptoWorker } from "./cryptoWorker";
@ -49,7 +40,9 @@ import * as timer from "../../util/timer";
import { Logger } from "../../util/logging"; import { Logger } from "../../util/logging";
import { import {
DerivedRefreshSession, DerivedRefreshSession,
DerivedTipPlanchet,
DeriveRefreshSessionRequest, DeriveRefreshSessionRequest,
DeriveTipRequest,
} from "../../types/cryptoTypes"; } from "../../types/cryptoTypes";
const logger = new Logger("cryptoApi.ts"); const logger = new Logger("cryptoApi.ts");
@ -329,8 +322,8 @@ export class CryptoApi {
return this.doRpc<PlanchetCreationResult>("createPlanchet", 1, req); return this.doRpc<PlanchetCreationResult>("createPlanchet", 1, req);
} }
createTipPlanchet(denom: DenominationRecord): Promise<TipPlanchet> { createTipPlanchet(req: DeriveTipRequest): Promise<DerivedTipPlanchet> {
return this.doRpc<TipPlanchet>("createTipPlanchet", 1, denom); return this.doRpc<DerivedTipPlanchet>("createTipPlanchet", 1, req);
} }
hashString(str: string): Promise<string> { hashString(str: string): Promise<string> {

View File

@ -30,11 +30,8 @@ import {
CoinRecord, CoinRecord,
DenominationRecord, DenominationRecord,
RefreshPlanchet, RefreshPlanchet,
RefreshSessionRecord,
TipPlanchet,
WireFee, WireFee,
CoinSourceType, CoinSourceType,
DenominationSelectionInfo,
} from "../../types/dbTypes"; } from "../../types/dbTypes";
import { CoinDepositPermission, RecoupRequest } from "../../types/talerTypes"; import { CoinDepositPermission, RecoupRequest } from "../../types/talerTypes";
@ -59,25 +56,25 @@ import {
rsaUnblind, rsaUnblind,
stringToBytes, stringToBytes,
createHashContext, createHashContext,
createEcdheKeyPair,
keyExchangeEcdheEddsa, keyExchangeEcdheEddsa,
setupRefreshPlanchet, setupRefreshPlanchet,
rsaVerify, rsaVerify,
getRandomBytes,
setupRefreshTransferPub, setupRefreshTransferPub,
setupTipPlanchet,
} from "../talerCrypto"; } from "../talerCrypto";
import { randomBytes } from "../primitives/nacl-fast"; import { randomBytes } from "../primitives/nacl-fast";
import { kdf } from "../primitives/kdf"; import { kdf } from "../primitives/kdf";
import { import {
Timestamp, Timestamp,
getTimestampNow,
timestampTruncateToSecond, timestampTruncateToSecond,
} from "../../util/time"; } from "../../util/time";
import { Logger } from "../../util/logging"; import { Logger } from "../../util/logging";
import { import {
DerivedRefreshSession, DerivedRefreshSession,
DerivedTipPlanchet,
DeriveRefreshSessionRequest, DeriveRefreshSessionRequest,
DeriveTipRequest,
} from "../../types/cryptoTypes"; } from "../../types/cryptoTypes";
const logger = new Logger("cryptoImplementation.ts"); const logger = new Logger("cryptoImplementation.ts");
@ -199,21 +196,18 @@ export class CryptoImplementation {
/** /**
* Create a planchet used for tipping, including the private keys. * Create a planchet used for tipping, including the private keys.
*/ */
createTipPlanchet(denom: DenominationRecord): TipPlanchet { createTipPlanchet(req: DeriveTipRequest): DerivedTipPlanchet {
const denomPub = decodeCrock(denom.denomPub); const fc = setupTipPlanchet(decodeCrock(req.secretSeed), req.planchetIndex);
const coinKeyPair = createEddsaKeyPair(); const denomPub = decodeCrock(req.denomPub);
const blindingFactor = createBlindingKeySecret(); const blindingFactor = createBlindingKeySecret();
const coinPubHash = hash(coinKeyPair.eddsaPub); const coinPubHash = hash(fc.coinPub);
const ev = rsaBlind(coinPubHash, blindingFactor, denomPub); const ev = rsaBlind(coinPubHash, blindingFactor, denomPub);
const tipPlanchet: TipPlanchet = { const tipPlanchet: DerivedTipPlanchet = {
blindingKey: encodeCrock(blindingFactor), blindingKey: encodeCrock(blindingFactor),
coinEv: encodeCrock(ev), coinEv: encodeCrock(ev),
coinPriv: encodeCrock(coinKeyPair.eddsaPriv), coinPriv: encodeCrock(fc.coinPriv),
coinPub: encodeCrock(coinKeyPair.eddsaPub), coinPub: encodeCrock(fc.coinPub),
coinValue: denom.value,
denomPub: encodeCrock(denomPub),
denomPubHash: encodeCrock(hash(denomPub)),
}; };
return tipPlanchet; return tipPlanchet;
} }

View File

@ -26,20 +26,34 @@
*/ */
import { InternalWalletState } from "./state"; import { InternalWalletState } from "./state";
import { import {
BackupBackupProvider,
BackupCoin, BackupCoin,
BackupCoinSource, BackupCoinSource,
BackupCoinSourceType, BackupCoinSourceType,
BackupDenomination, BackupDenomination,
BackupExchange, BackupExchange,
BackupExchangeWireFee, BackupExchangeWireFee,
BackupProposal,
BackupProposalStatus,
BackupPurchase,
BackupRecoupGroup,
BackupRefreshGroup,
BackupRefreshOldCoin,
BackupRefreshSession,
BackupRefundItem,
BackupRefundState,
BackupReserve, BackupReserve,
BackupTip,
WalletBackupContentV1, WalletBackupContentV1,
} from "../types/backupTypes"; } from "../types/backupTypes";
import { TransactionHandle } from "../util/query"; import { TransactionHandle } from "../util/query";
import { import {
AbortStatus,
CoinSourceType, CoinSourceType,
CoinStatus, CoinStatus,
ConfigRecord, ConfigRecord,
ProposalStatus,
RefundState,
Stores, Stores,
} from "../types/dbTypes"; } from "../types/dbTypes";
import { checkDbInvariant } from "../util/invariants"; import { checkDbInvariant } from "../util/invariants";
@ -56,7 +70,7 @@ import {
import { canonicalizeBaseUrl, canonicalJson, j2s } from "../util/helpers"; import { canonicalizeBaseUrl, canonicalJson, j2s } from "../util/helpers";
import { getTimestampNow, Timestamp } from "../util/time"; import { getTimestampNow, Timestamp } from "../util/time";
import { URL } from "../util/url"; import { URL } from "../util/url";
import { AmountString } from "../types/talerTypes"; import { AmountString, TipResponse } from "../types/talerTypes";
import { import {
buildCodecForObject, buildCodecForObject,
Codec, Codec,
@ -146,16 +160,80 @@ export async function exportBackup(
): Promise<WalletBackupContentV1> { ): Promise<WalletBackupContentV1> {
await provideBackupState(ws); await provideBackupState(ws);
return ws.db.runWithWriteTransaction( return ws.db.runWithWriteTransaction(
[Stores.config, Stores.exchanges, Stores.coins, Stores.denominations], [
Stores.config,
Stores.exchanges,
Stores.coins,
Stores.denominations,
Stores.purchases,
Stores.proposals,
Stores.refreshGroups,
Stores.backupProviders,
Stores.tips,
Stores.recoupGroups,
Stores.reserves,
],
async (tx) => { async (tx) => {
const bs = await getWalletBackupState(ws, tx); const bs = await getWalletBackupState(ws, tx);
const exchanges: BackupExchange[] = []; const backupExchanges: BackupExchange[] = [];
const coinsByDenom: { [dph: string]: BackupCoin[] } = {}; const backupCoinsByDenom: { [dph: string]: BackupCoin[] } = {};
const denominationsByExchange: { const backupDenominationsByExchange: {
[url: string]: BackupDenomination[]; [url: string]: BackupDenomination[];
} = {}; } = {};
const reservesByExchange: { [url: string]: BackupReserve[] } = {}; const backupReservesByExchange: { [url: string]: BackupReserve[] } = {};
const backupPurchases: BackupPurchase[] = [];
const backupProposals: BackupProposal[] = [];
const backupRefreshGroups: BackupRefreshGroup[] = [];
const backupBackupProviders: BackupBackupProvider[] = [];
const backupTips: BackupTip[] = [];
const backupRecoupGroups: BackupRecoupGroup[] = [];
await tx.iter(Stores.reserves).forEach((reserve) => {
// FIXME: implement
});
await tx.iter(Stores.tips).forEach((tip) => {
backupTips.push({
exchange_base_url: tip.exchangeBaseUrl,
merchant_base_url: tip.merchantBaseUrl,
merchant_tip_id: tip.merchantTipId,
wallet_tip_id: tip.walletTipId,
secret_seed: tip.secretSeed,
selected_denoms: tip.denomsSel.selectedDenoms.map((x) => ({
count: x.count,
denom_pub_hash: x.denomPubHash,
})),
timestam_picked_up: tip.pickedUpTimestamp,
timestamp_accepted: tip.acceptedTimestamp,
timestamp_created: tip.createdTimestamp,
timestamp_expiration: tip.tipExpiration,
tip_amount_raw: Amounts.stringify(tip.tipAmountRaw),
});
});
await tx.iter(Stores.recoupGroups).forEach((recoupGroup) => {
backupRecoupGroups.push({
recoup_group_id: recoupGroup.recoupGroupId,
timestamp_started: recoupGroup.timestampStarted,
timestamp_finished: recoupGroup.timestampFinished,
coins: recoupGroup.coinPubs.map((x, i) => ({
coin_pub: x,
recoup_finished: recoupGroup.recoupFinishedPerCoin[i],
old_amount: Amounts.stringify(recoupGroup.oldAmountPerCoin[i]),
})),
});
});
await tx.iter(Stores.backupProviders).forEach((bp) => {
backupBackupProviders.push({
annual_fee: Amounts.stringify(bp.annualFee),
base_url: canonicalizeBaseUrl(bp.baseUrl),
pay_proposal_ids: [],
storage_limit_in_megabytes: bp.storageLimitInMegabytes,
supported_protocol_version: bp.supportedProtocolVersion,
});
});
await tx.iter(Stores.coins).forEach((coin) => { await tx.iter(Stores.coins).forEach((coin) => {
let bcs: BackupCoinSource; let bcs: BackupCoinSource;
@ -183,7 +261,7 @@ export async function exportBackup(
break; break;
} }
const coins = (coinsByDenom[coin.denomPubHash] ??= []); const coins = (backupCoinsByDenom[coin.denomPubHash] ??= []);
coins.push({ coins.push({
blinding_key: coin.blindingKey, blinding_key: coin.blindingKey,
coin_priv: coin.coinPriv, coin_priv: coin.coinPriv,
@ -195,11 +273,11 @@ export async function exportBackup(
}); });
await tx.iter(Stores.denominations).forEach((denom) => { await tx.iter(Stores.denominations).forEach((denom) => {
const backupDenoms = (denominationsByExchange[ const backupDenoms = (backupDenominationsByExchange[
denom.exchangeBaseUrl denom.exchangeBaseUrl
] ??= []); ] ??= []);
backupDenoms.push({ backupDenoms.push({
coins: coinsByDenom[denom.denomPubHash] ?? [], coins: backupCoinsByDenom[denom.denomPubHash] ?? [],
denom_pub: denom.denomPub, denom_pub: denom.denomPub,
fee_deposit: Amounts.stringify(denom.feeDeposit), fee_deposit: Amounts.stringify(denom.feeDeposit),
fee_refresh: Amounts.stringify(denom.feeRefresh), fee_refresh: Amounts.stringify(denom.feeRefresh),
@ -247,7 +325,7 @@ export async function exportBackup(
} }
}); });
exchanges.push({ backupExchanges.push({
base_url: ex.baseUrl, base_url: ex.baseUrl,
accounts: ex.wireInfo.accounts.map((x) => ({ accounts: ex.wireInfo.accounts.map((x) => ({
payto_uri: x.payto_uri, payto_uri: x.payto_uri,
@ -271,8 +349,132 @@ export async function exportBackup(
})), })),
tos_etag_accepted: ex.termsOfServiceAcceptedEtag, tos_etag_accepted: ex.termsOfServiceAcceptedEtag,
tos_etag_last: ex.termsOfServiceLastEtag, tos_etag_last: ex.termsOfServiceLastEtag,
denominations: denominationsByExchange[ex.baseUrl] ?? [], denominations: backupDenominationsByExchange[ex.baseUrl] ?? [],
reserves: reservesByExchange[ex.baseUrl] ?? [], reserves: backupReservesByExchange[ex.baseUrl] ?? [],
});
});
const purchaseProposalIdSet = new Set<string>();
await tx.iter(Stores.purchases).forEach((purch) => {
const refunds: BackupRefundItem[] = [];
purchaseProposalIdSet.add(purch.proposalId);
for (const refundKey of Object.keys(purch.refunds)) {
const ri = purch.refunds[refundKey];
const common = {
coin_pub: ri.coinPub,
execution_time: ri.executionTime,
obtained_time: ri.obtainedTime,
refund_amount: Amounts.stringify(ri.refundAmount),
rtransaction_id: ri.rtransactionId,
total_refresh_cost_bound: Amounts.stringify(
ri.totalRefreshCostBound,
),
};
switch (ri.type) {
case RefundState.Applied:
refunds.push({ type: BackupRefundState.Applied, ...common });
break;
case RefundState.Failed:
refunds.push({ type: BackupRefundState.Failed, ...common });
break;
case RefundState.Pending:
refunds.push({ type: BackupRefundState.Pending, ...common });
break;
}
}
backupPurchases.push({
clock_created: 1,
contract_terms_raw: purch.contractTermsRaw,
auto_refund_deadline: purch.autoRefundDeadline,
merchant_pay_sig: purch.merchantPaySig,
pay_coins: purch.payCoinSelection.coinPubs.map((x, i) => ({
coin_pub: x,
contribution: Amounts.stringify(
purch.payCoinSelection.coinContributions[i],
),
})),
proposal_id: purch.proposalId,
refunds,
timestamp_accept: purch.timestampAccept,
timestamp_first_successful_pay: purch.timestampFirstSuccessfulPay,
timestamp_last_refund_status: purch.timestampLastRefundStatus,
abort_status:
purch.abortStatus === AbortStatus.None
? undefined
: purch.abortStatus,
nonce_priv: purch.noncePriv,
});
});
await tx.iter(Stores.proposals).forEach((prop) => {
if (purchaseProposalIdSet.has(prop.proposalId)) {
return;
}
let propStatus: BackupProposalStatus;
switch (prop.proposalStatus) {
case ProposalStatus.ACCEPTED:
return;
case ProposalStatus.DOWNLOADING:
case ProposalStatus.PROPOSED:
propStatus = BackupProposalStatus.Proposed;
break;
case ProposalStatus.PERMANENTLY_FAILED:
propStatus = BackupProposalStatus.PermanentlyFailed;
break;
case ProposalStatus.REFUSED:
propStatus = BackupProposalStatus.Refused;
break;
case ProposalStatus.REPURCHASE:
propStatus = BackupProposalStatus.Repurchase;
break;
}
backupProposals.push({
claim_token: prop.claimToken,
nonce_priv: prop.noncePriv,
proposal_id: prop.noncePriv,
proposal_status: propStatus,
repurchase_proposal_id: prop.repurchaseProposalId,
timestamp: prop.timestamp,
contract_terms_raw: prop.download?.contractTermsRaw,
download_session_id: prop.downloadSessionId,
});
});
await tx.iter(Stores.refreshGroups).forEach((rg) => {
const oldCoins: BackupRefreshOldCoin[] = [];
for (let i = 0; i < rg.oldCoinPubs.length; i++) {
let refreshSession: BackupRefreshSession | undefined;
const s = rg.refreshSessionPerCoin[i];
if (s) {
refreshSession = {
new_denoms: s.newDenoms.map((x) => ({
count: x.count,
denom_pub_hash: x.denomPubHash,
})),
session_secret_seed: s.sessionSecretSeed,
noreveal_index: s.norevealIndex,
};
}
oldCoins.push({
coin_pub: rg.oldCoinPubs[i],
estimated_output_amount: Amounts.stringify(
rg.estimatedOutputPerCoin[i],
),
finished: rg.finishedPerCoin[i],
input_amount: Amounts.stringify(rg.inputPerCoin[i]),
refresh_session: refreshSession,
});
}
backupRefreshGroups.push({
reason: rg.reason as any,
refresh_group_id: rg.refreshGroupId,
timestamp_started: rg.timestampCreated,
timestamp_finished: rg.timestampFinished,
old_coins: oldCoins,
}); });
}); });
@ -284,16 +486,16 @@ export async function exportBackup(
schema_id: "gnu-taler-wallet-backup-content", schema_id: "gnu-taler-wallet-backup-content",
schema_version: 1, schema_version: 1,
clocks: bs.clocks, clocks: bs.clocks,
exchanges: exchanges, exchanges: backupExchanges,
wallet_root_pub: bs.walletRootPub, wallet_root_pub: bs.walletRootPub,
backup_providers: [], backup_providers: backupBackupProviders,
current_device_id: bs.deviceId, current_device_id: bs.deviceId,
proposals: [], proposals: backupProposals,
purchase_tombstones: [], purchase_tombstones: [],
purchases: [], purchases: backupPurchases,
recoup_groups: [], recoup_groups: backupRecoupGroups,
refresh_groups: [], refresh_groups: backupRefreshGroups,
tips: [], tips: backupTips,
timestamp: bs.lastBackupTimestamp, timestamp: bs.lastBackupTimestamp,
trusted_auditors: {}, trusted_auditors: {},
trusted_exchanges: {}, trusted_exchanges: {},

View File

@ -711,6 +711,7 @@ export async function createRefreshGroup(
retryInfo: initRetryInfo(), retryInfo: initRetryInfo(),
inputPerCoin, inputPerCoin,
estimatedOutputPerCoin, estimatedOutputPerCoin,
timestampCreated: getTimestampNow(),
}; };
if (oldCoinPubs.length == 0) { if (oldCoinPubs.length == 0) {

View File

@ -158,6 +158,8 @@ async function applySuccessfulRefund(
refundAmount: Amounts.parseOrThrow(r.refund_amount), refundAmount: Amounts.parseOrThrow(r.refund_amount),
refundFee: denom.feeRefund, refundFee: denom.feeRefund,
totalRefreshCostBound, totalRefreshCostBound,
coinPub: r.coin_pub,
rtransactionId: r.rtransaction_id,
}; };
} }
@ -208,6 +210,8 @@ async function storePendingRefund(
refundAmount: Amounts.parseOrThrow(r.refund_amount), refundAmount: Amounts.parseOrThrow(r.refund_amount),
refundFee: denom.feeRefund, refundFee: denom.feeRefund,
totalRefreshCostBound, totalRefreshCostBound,
coinPub: r.coin_pub,
rtransactionId: r.rtransaction_id,
}; };
} }
@ -259,6 +263,8 @@ async function storeFailedRefund(
refundAmount: Amounts.parseOrThrow(r.refund_amount), refundAmount: Amounts.parseOrThrow(r.refund_amount),
refundFee: denom.feeRefund, refundFee: denom.feeRefund,
totalRefreshCostBound, totalRefreshCostBound,
coinPub: r.coin_pub,
rtransactionId: r.rtransaction_id,
}; };
if (p.abortStatus === AbortStatus.AbortRefund) { if (p.abortStatus === AbortStatus.AbortRefund) {

View File

@ -25,10 +25,10 @@ import {
import * as Amounts from "../util/amounts"; import * as Amounts from "../util/amounts";
import { import {
Stores, Stores,
TipPlanchet,
CoinRecord, CoinRecord,
CoinSourceType, CoinSourceType,
CoinStatus, CoinStatus,
DenominationRecord,
} from "../types/dbTypes"; } from "../types/dbTypes";
import { import {
getExchangeWithdrawalInfo, getExchangeWithdrawalInfo,
@ -50,6 +50,7 @@ import { checkDbInvariant } from "../util/invariants";
import { TalerErrorCode } from "../TalerErrorCode"; import { TalerErrorCode } from "../TalerErrorCode";
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries"; import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries";
import { j2s } from "../util/helpers"; import { j2s } from "../util/helpers";
import { DerivedTipPlanchet } from '../types/cryptoTypes';
const logger = new Logger("operations/tip.ts"); const logger = new Logger("operations/tip.ts");
@ -201,39 +202,34 @@ async function processTipImpl(
const denomsForWithdraw = tipRecord.denomsSel; const denomsForWithdraw = tipRecord.denomsSel;
if (!tipRecord.planchets) {
const planchets: TipPlanchet[] = [];
for (const sd of denomsForWithdraw.selectedDenoms) {
const denom = await ws.db.get(Stores.denominations, [
tipRecord.exchangeBaseUrl,
sd.denomPubHash,
]);
if (!denom) {
throw Error("denom does not exist anymore");
}
for (let i = 0; i < sd.count; i++) {
const r = await ws.cryptoApi.createTipPlanchet(denom);
planchets.push(r);
}
}
await ws.db.mutate(Stores.tips, walletTipId, (r) => {
if (!r.planchets) {
r.planchets = planchets;
}
return r;
});
}
tipRecord = await ws.db.get(Stores.tips, walletTipId); tipRecord = await ws.db.get(Stores.tips, walletTipId);
checkDbInvariant(!!tipRecord, "tip record should be in database"); checkDbInvariant(!!tipRecord, "tip record should be in database");
checkDbInvariant(!!tipRecord.planchets, "tip record should have planchets");
const planchets: DerivedTipPlanchet[] = [];
// Planchets in the form that the merchant expects // Planchets in the form that the merchant expects
const planchetsDetail: TipPlanchetDetail[] = tipRecord.planchets.map((p) => ({ const planchetsDetail: TipPlanchetDetail[] = [];
coin_ev: p.coinEv, const denomForPlanchet: { [index: number]: DenominationRecord} = [];
denom_pub_hash: p.denomPubHash,
})); for (const dh of denomsForWithdraw.selectedDenoms) {
const denom = await ws.db.get(Stores.denominations, [
tipRecord.exchangeBaseUrl,
dh.denomPubHash,
]);
checkDbInvariant(!!denom, "denomination should be in database");
denomForPlanchet[planchets.length] = denom;
for (let i = 0; i < dh.count; i++) {
const p = await ws.cryptoApi.createTipPlanchet({
denomPub: dh.denomPubHash,
planchetIndex: planchets.length,
secretSeed: tipRecord.secretSeed,
});
planchets.push(p);
planchetsDetail.push({
coin_ev: p.coinEv,
denom_pub_hash: denom.denomPubHash,
});
}
}
const tipStatusUrl = new URL( const tipStatusUrl = new URL(
`tips/${tipRecord.merchantTipId}/pickup`, `tips/${tipRecord.merchantTipId}/pickup`,
@ -264,7 +260,7 @@ async function processTipImpl(
codecForTipResponse(), codecForTipResponse(),
); );
if (response.blind_sigs.length !== tipRecord.planchets.length) { if (response.blind_sigs.length !== planchets.length) {
throw Error("number of tip responses does not match requested planchets"); throw Error("number of tip responses does not match requested planchets");
} }
@ -273,18 +269,19 @@ async function processTipImpl(
for (let i = 0; i < response.blind_sigs.length; i++) { for (let i = 0; i < response.blind_sigs.length; i++) {
const blindedSig = response.blind_sigs[i].blind_sig; const blindedSig = response.blind_sigs[i].blind_sig;
const planchet = tipRecord.planchets[i]; const denom = denomForPlanchet[i];
const planchet = planchets[i];
const denomSig = await ws.cryptoApi.rsaUnblind( const denomSig = await ws.cryptoApi.rsaUnblind(
blindedSig, blindedSig,
planchet.blindingKey, planchet.blindingKey,
planchet.denomPub, denom.denomPub,
); );
const isValid = await ws.cryptoApi.rsaVerify( const isValid = await ws.cryptoApi.rsaVerify(
planchet.coinPub, planchet.coinPub,
denomSig, denomSig,
planchet.denomPub, denom.denomPub,
); );
if (!isValid) { if (!isValid) {
@ -312,9 +309,9 @@ async function processTipImpl(
coinIndex: i, coinIndex: i,
walletTipId: walletTipId, walletTipId: walletTipId,
}, },
currentAmount: planchet.coinValue, currentAmount: denom.value,
denomPub: planchet.denomPub, denomPub: denom.denomPub,
denomPubHash: planchet.denomPubHash, denomPubHash: denom.denomPubHash,
denomSig: denomSig, denomSig: denomSig,
exchangeBaseUrl: tipRecord.exchangeBaseUrl, exchangeBaseUrl: tipRecord.exchangeBaseUrl,
status: CoinStatus.Fresh, status: CoinStatus.Fresh,

View File

@ -92,7 +92,6 @@ export async function getTransactions(
Stores.purchases, Stores.purchases,
Stores.refreshGroups, Stores.refreshGroups,
Stores.reserves, Stores.reserves,
Stores.reserveHistory,
Stores.tips, Stores.tips,
Stores.withdrawalGroups, Stores.withdrawalGroups,
Stores.planchets, Stores.planchets,

View File

@ -24,6 +24,20 @@
* 1. Exchange/auditor trust isn't exported yet * 1. Exchange/auditor trust isn't exported yet
* (see https://bugs.gnunet.org/view.php?id=6448) * (see https://bugs.gnunet.org/view.php?id=6448)
* 2. Reports to the auditor (cryptographic proofs and/or diagnostics) aren't exported yet * 2. Reports to the auditor (cryptographic proofs and/or diagnostics) aren't exported yet
* 3. "Ghost spends", where a coin is spent unexpectedly by another wallet
* and a corresponding transaction (that is missing some details!) should
* be added to the transaction history, aren't implemented yet.
* 4. Clocks for denom/coin selections aren't properly modeled yet.
* (Needed for re-denomination of withdrawal / re-selection of coins)
* 5. Preferences about how currencies are to be displayed
* aren't exported yet (and not even implemented in wallet-core).
* 6. Returning money to own bank account isn't supported/exported yet.
* 7. Peer-to-peer payments aren't supported yet.
*
* Questions:
* 1. What happens when two backups are merged that have
* the same coin in different refresh groups?
* => Both are added, one will eventually fail
* *
* General considerations / decisions: * General considerations / decisions:
* 1. Information about previously occurring errors and * 1. Information about previously occurring errors and
@ -318,6 +332,9 @@ export interface BackupRecoupGroup {
/** /**
* Timestamp when the recoup finished. * Timestamp when the recoup finished.
*
* (That means all coins have been recouped and coins to
* be refreshed have been put in a refresh group.)
*/ */
timestamp_finished: Timestamp | undefined; timestamp_finished: Timestamp | undefined;
@ -326,15 +343,9 @@ export interface BackupRecoupGroup {
*/ */
coins: { coins: {
coin_pub: string; coin_pub: string;
finished: boolean; recoup_finished: boolean;
old_amount: BackupAmountString; old_amount: BackupAmountString;
}[]; }[];
/**
* Public keys of coins that should be scheduled for refreshing
* after all individual recoups are done.
*/
recoup_refresh_coins: string[];
} }
/** /**
@ -465,6 +476,11 @@ export interface BackupTip {
*/ */
merchant_tip_id: string; merchant_tip_id: string;
/**
* Secret seed used for the tipping planchets.
*/
secret_seed: string;
/** /**
* Has the user accepted the tip? Only after the tip has been accepted coins * Has the user accepted the tip? Only after the tip has been accepted coins
* withdrawn from the tip may be used. * withdrawn from the tip may be used.
@ -502,15 +518,6 @@ export interface BackupTip {
*/ */
merchant_base_url: string; merchant_base_url: string;
/**
* Planchets, the members included in TipPlanchetDetail will be sent to the
* merchant.
*/
planchets?: {
blinding_key: string;
coin_priv: string;
}[];
/** /**
* Selected denominations. Determines the effective tip amount. * Selected denominations. Determines the effective tip amount.
*/ */
@ -543,7 +550,10 @@ export interface BackupRefreshSession {
/** /**
* Hased denominations of the newly requested coins. * Hased denominations of the newly requested coins.
*/ */
new_denom_hashes: string[]; new_denoms: {
count: number;
denom_pub_hash: string;
}[];
/** /**
* Seed used to derive the planchets and * Seed used to derive the planchets and
@ -557,6 +567,39 @@ export interface BackupRefreshSession {
noreveal_index?: number; noreveal_index?: number;
} }
/**
* Refresh session for one coin inside a refresh group.
*/
export interface BackupRefreshOldCoin {
/**
* Public key of the old coin,
*/
coin_pub: string;
/**
* Requested amount to refresh. Must be subtracted from the coin's remaining
* amount as soon as the coin is added to the refresh group.
*/
input_amount: BackupAmountString;
/**
* Estimated output (may change if it takes a long time to create the
* actual session).
*/
estimated_output_amount: BackupAmountString;
/**
* Did the refresh session finish (or was it unnecessary/impossible to create
* one)
*/
finished: boolean;
/**
* Refresh session (if created) or undefined it not created yet.
*/
refresh_session: BackupRefreshSession | undefined;
}
/** /**
* Information about one refresh group. * Information about one refresh group.
* *
@ -570,35 +613,9 @@ export interface BackupRefreshGroup {
/** /**
* Details per old coin. * Details per old coin.
*/ */
old_coins: { old_coins: BackupRefreshOldCoin[];
/**
* Public key of the old coin,
*/
coin_pub: string;
/** timestamp_started: Timestamp;
* Requested amount to refresh. Must be subtracted from the coin's remaining
* amount as soon as the coin is added to the refresh group.
*/
input_amount: BackupAmountString;
/**
* Estimated output (may change if it takes a long time to create the
* actual session).
*/
estimated_output_amount: BackupAmountString;
/**
* Did the refresh session finish (or was it unnecessary/impossible to create
* one)
*/
finished: boolean;
/**
* Refresh session (if created) or undefined it not created yet.
*/
refresh_session: BackupRefreshSession | undefined;
}[];
/** /**
* Timestamp when the refresh group finished. * Timestamp when the refresh group finished.
@ -741,22 +758,23 @@ export interface BackupPurchase {
*/ */
contract_terms_raw: string; contract_terms_raw: string;
/**
* Private key for the nonce. Might eventually be used
* to prove ownership of the contract.
*/
nonce_priv: string;
pay_coins: { pay_coins: {
/** /**
* Public keys of the coins that were selected. * Public keys of the coins that were selected.
*/ */
coin_pubs: string[]; coin_pub: string;
/**
* Deposit permission signature of each coin.
*/
coin_sigs: string[];
/** /**
* Amount that each coin contributes. * Amount that each coin contributes.
*/ */
contribution: BackupAmountString; contribution: BackupAmountString;
}; }[];
/** /**
* Timestamp of the first time that sending a payment to the merchant * Timestamp of the first time that sending a payment to the merchant
@ -1132,6 +1150,9 @@ export interface BackupReserveHistoryCreditItem {
matched_exchange_transaction?: ReserveCreditTransaction; matched_exchange_transaction?: ReserveCreditTransaction;
} }
/**
* Reserve history item for a withdrawal
*/
export interface BackupReserveHistoryWithdrawItem { export interface BackupReserveHistoryWithdrawItem {
type: WalletReserveHistoryItemType.Withdraw; type: WalletReserveHistoryItemType.Withdraw;
@ -1141,7 +1162,7 @@ export interface BackupReserveHistoryWithdrawItem {
* Hash of the blinded coin. * Hash of the blinded coin.
* *
* When this value is set, it indicates that a withdrawal is active * When this value is set, it indicates that a withdrawal is active
* in the wallet for the * in the wallet for the reserve.
*/ */
expected_coin_ev_hash?: string; expected_coin_ev_hash?: string;
@ -1183,13 +1204,11 @@ export type BackupReserveHistoryItem =
| BackupReserveHistoryRecoupItem | BackupReserveHistoryRecoupItem
| BackupReserveHistoryClosingItem; | BackupReserveHistoryClosingItem;
export enum ProposalStatus { export enum BackupProposalStatus {
/** /**
* Not downloaded yet. * Proposed (and either downloaded or not,
*/ * depending on whether contract terms are present),
Downloading = "downloading", * but the user needs to accept/reject it.
/**
* Proposal downloaded, but the user needs to accept/reject it.
*/ */
Proposed = "proposed", Proposed = "proposed",
/** /**
@ -1202,6 +1221,8 @@ export enum ProposalStatus {
Refused = "refused", Refused = "refused",
/** /**
* Downloading or processing the proposal has failed permanently. * Downloading or processing the proposal has failed permanently.
*
* FIXME: Should this be modeled as a "misbehavior report" instead?
*/ */
PermanentlyFailed = "permanently-failed", PermanentlyFailed = "permanently-failed",
/** /**
@ -1235,11 +1256,6 @@ export interface BackupProposal {
*/ */
nonce_priv: string; nonce_priv: string;
/**
* Public key for the nonce.
*/
nonce_pub: string;
/** /**
* Claim token initially given by the merchant. * Claim token initially given by the merchant.
*/ */
@ -1248,7 +1264,7 @@ export interface BackupProposal {
/** /**
* Status of the proposal. * Status of the proposal.
*/ */
proposal_status: ProposalStatus; proposal_status: BackupProposalStatus;
/** /**
* Proposal that this one got "redirected" to as part of * Proposal that this one got "redirected" to as part of

View File

@ -109,3 +109,19 @@ export interface DerivedRefreshSession {
*/ */
meltValueWithFee: AmountJson; meltValueWithFee: AmountJson;
} }
export interface DeriveTipRequest {
secretSeed: string;
denomPub: string;
planchetIndex: number;
}
/**
* Tipping planchet stored in the database.
*/
export interface DerivedTipPlanchet {
blindingKey: string;
coinEv: string;
coinPriv: string;
coinPub: string;
}

View File

@ -109,14 +109,6 @@ export interface WalletReserveHistoryCreditItem {
export interface WalletReserveHistoryWithdrawItem { export interface WalletReserveHistoryWithdrawItem {
expectedAmount?: AmountJson; expectedAmount?: AmountJson;
/**
* Hash of the blinded coin.
*
* When this value is set, it indicates that a withdrawal is active
* in the wallet for the
*/
expectedCoinEvHash?: string;
type: WalletReserveHistoryItemType.Withdraw; type: WalletReserveHistoryItemType.Withdraw;
/** /**
@ -921,11 +913,9 @@ export interface TipRecord {
merchantBaseUrl: string; merchantBaseUrl: string;
/** /**
* Planchets, the members included in TipPlanchetDetail will be sent to the * Denomination selection made by the wallet for picking up
* merchant. * this tip.
*/ */
planchets?: TipPlanchet[];
denomsSel: DenomSelectionState; denomsSel: DenomSelectionState;
/** /**
@ -933,6 +923,11 @@ export interface TipRecord {
*/ */
walletTipId: string; walletTipId: string;
/**
* Secret seed used to derive planchets for this tip.
*/
secretSeed: string;
/** /**
* The merchant's identifier for this tip. * The merchant's identifier for this tip.
*/ */
@ -984,6 +979,8 @@ export interface RefreshGroupRecord {
*/ */
finishedPerCoin: boolean[]; finishedPerCoin: boolean[];
timestampCreated: Timestamp;
/** /**
* Timestamp when the refresh session finished. * Timestamp when the refresh session finished.
*/ */
@ -1023,19 +1020,6 @@ export interface RefreshSessionRecord {
norevealIndex?: number; norevealIndex?: number;
} }
/**
* Tipping planchet stored in the database.
*/
export interface TipPlanchet {
blindingKey: string;
coinEv: string;
coinPriv: string;
coinPub: string;
coinValue: AmountJson;
denomPubHash: string;
denomPub: string;
}
/** /**
* Wire fee for one wire method as stored in the * Wire fee for one wire method as stored in the
* wallet's database. * wallet's database.
@ -1106,6 +1090,7 @@ export interface WalletRefundItemCommon {
obtainedTime: Timestamp; obtainedTime: Timestamp;
refundAmount: AmountJson; refundAmount: AmountJson;
refundFee: AmountJson; refundFee: AmountJson;
/** /**
@ -1116,6 +1101,10 @@ export interface WalletRefundItemCommon {
* coin are refreshed in the same refresh operation. * coin are refreshed in the same refresh operation.
*/ */
totalRefreshCostBound: AmountJson; totalRefreshCostBound: AmountJson;
coinPub: string;
rtransactionId: number;
} }
/** /**
@ -1266,11 +1255,24 @@ export interface PurchaseRecord {
*/ */
proposalId: string; proposalId: string;
/**
* Private key for the nonce.
*/
noncePriv: string;
/**
* Public key for the nonce.
*/
noncePub: string;
/** /**
* Contract terms we got from the merchant. * Contract terms we got from the merchant.
*/ */
contractTermsRaw: string; contractTermsRaw: string;
/**
* Parsed contract terms.
*/
contractData: WalletContractData; contractData: WalletContractData;
/** /**

View File

@ -14,6 +14,12 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
/**
* Helpers for dealing with reserve histories.
*
* @author Florian Dold <dold@taler.net>
*/
/** /**
* Imports. * Imports.
*/ */
@ -31,11 +37,8 @@ import { deepCopy } from "./helpers";
import { AmountJson } from "../util/amounts"; import { AmountJson } from "../util/amounts";
/** /**
* Helpers for dealing with reserve histories. * Result of a reserve reconciliation.
*
* @author Florian Dold <dold@taler.net>
*/ */
export interface ReserveReconciliationResult { export interface ReserveReconciliationResult {
/** /**
* The wallet's local history reconciled with the exchange's reserve history. * The wallet's local history reconciled with the exchange's reserve history.