Skip to content

Commit 246589f

Browse files
committed
wallet: Deniability API
This PR is the wallet API and implementation portion of the GUI PR ( bitcoin-core/gui#733 ) which is an implementation of the ideas in Paul Sztorc's blog post "Deniability - Unilateral Transaction Meta-Privacy"(https://www.truthcoin.info/blog/deniability/). The GUI PR has all the details and screenshots of the GUI additions. Here I'll just copy the relevant context for the wallet API changes: " In short, Paul's idea is to periodically split coins and send them to yourself, making it look like common "spend" transactions, such that blockchain ownership analysis becomes more difficult, and thus improving the user's privacy. I've implemented this as an additional "Deniability" wallet view. The majority of the code is in a new deniabilitydialog.cpp/h source files containing a new DeniabilityDialog class, hooked up to the WalletView class.  " While the Deniability dialog can be implemented entirely with the existing API, adding the core "deniabilization" functions to the CWallet and interfaces::Wallet API allows us to implement the GUI portion with much less code, and more importantly allows us to add RPC support and more thorough unit tests.
1 parent a13f374 commit 246589f

File tree

6 files changed

+480
-0
lines changed

6 files changed

+480
-0
lines changed

src/interfaces/wallet.h

+19
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,15 @@ class Wallet
151151
WalletValueMap value_map,
152152
WalletOrderForm order_form) = 0;
153153

154+
virtual std::pair<unsigned int, bool> calculateDeniabilizationCycles(const COutPoint& outpoint) = 0;
155+
156+
virtual util::Result<CTransactionRef> createDeniabilizationTransaction(const std::set<COutPoint>& inputs,
157+
unsigned int confirm_target,
158+
unsigned int deniabilization_cycles,
159+
bool sign,
160+
bool& insufficient_amount,
161+
CAmount& fee) = 0;
162+
154163
//! Return whether transaction can be abandoned.
155164
virtual bool transactionCanBeAbandoned(const uint256& txid) = 0;
156165

@@ -177,6 +186,13 @@ class Wallet
177186
std::vector<bilingual_str>& errors,
178187
uint256& bumped_txid) = 0;
179188

189+
//! Create a fee bump transaction for a deniabilization transaction
190+
virtual util::Result<CTransactionRef> createBumpDeniabilizationTransaction(const uint256& txid,
191+
unsigned int confirm_target,
192+
bool sign,
193+
CAmount& old_fee,
194+
CAmount& new_fee) = 0;
195+
180196
//! Get a transaction.
181197
virtual CTransactionRef getTx(const uint256& txid) = 0;
182198

@@ -248,6 +264,9 @@ class Wallet
248264
int* returned_target,
249265
FeeReason* reason) = 0;
250266

267+
//! Get the fee rate for deniabilization
268+
virtual CFeeRate getDeniabilizationFeeRate(unsigned int confirm_target) = 0;
269+
251270
//! Get tx confirm target.
252271
virtual unsigned int getConfirmTarget() = 0;
253272

src/wallet/feebumper.cpp

+83
Original file line numberDiff line numberDiff line change
@@ -365,5 +365,88 @@ Result CommitTransaction(CWallet& wallet, const uint256& txid, CMutableTransacti
365365
return Result::OK;
366366
}
367367

368+
Result CreateRateBumpDeniabilizationTransaction(CWallet& wallet, const uint256& txid, unsigned int confirm_target, bool sign, bilingual_str& error, CAmount& old_fee, CAmount& new_fee, CTransactionRef& new_tx)
369+
{
370+
CCoinControl coin_control = SetupDeniabilizationCoinControl(confirm_target);
371+
coin_control.m_feerate = CalculateDeniabilizationFeeRate(wallet, confirm_target);
372+
373+
LOCK(wallet.cs_wallet);
374+
375+
auto it = wallet.mapWallet.find(txid);
376+
if (it == wallet.mapWallet.end()) {
377+
error = Untranslated("Invalid or non-wallet transaction id");
378+
return Result::INVALID_ADDRESS_OR_KEY;
379+
}
380+
const CWalletTx& wtx = it->second;
381+
382+
// Retrieve all of the UTXOs and add them to coin control
383+
// While we're here, calculate the input amount
384+
std::map<COutPoint, Coin> coins;
385+
CAmount input_value = 0;
386+
for (const CTxIn& txin : wtx.tx->vin) {
387+
coins[txin.prevout]; // Create empty map entry keyed by prevout.
388+
}
389+
wallet.chain().findCoins(coins);
390+
for (const CTxIn& txin : wtx.tx->vin) {
391+
const Coin& coin = coins.at(txin.prevout);
392+
if (coin.out.IsNull()) {
393+
error = Untranslated(strprintf("%s:%u is already spent", txin.prevout.hash.GetHex(), txin.prevout.n));
394+
return Result::MISC_ERROR;
395+
}
396+
if (!wallet.IsMine(txin.prevout)) {
397+
error = Untranslated("All inputs must be from our wallet.");
398+
return Result::MISC_ERROR;
399+
}
400+
coin_control.Select(txin.prevout);
401+
input_value += coin.out.nValue;
402+
}
403+
404+
std::vector<bilingual_str> dymmy_errors;
405+
Result result = PreconditionChecks(wallet, wtx, /*require_mine=*/true, dymmy_errors);
406+
if (result != Result::OK) {
407+
error = dymmy_errors.front();
408+
return result;
409+
}
410+
411+
// Calculate the old output amount.
412+
CAmount output_value = 0;
413+
for (const auto& old_output : wtx.tx->vout) {
414+
output_value += old_output.nValue;
415+
}
416+
417+
old_fee = input_value - output_value;
418+
419+
std::vector<CRecipient> recipients;
420+
for (const auto& output : wtx.tx->vout) {
421+
CRecipient recipient = {output.scriptPubKey, output.nValue, false};
422+
recipients.push_back(recipient);
423+
}
424+
// the last recipient gets the old fee
425+
recipients.back().nAmount += old_fee;
426+
// and pays the new fee
427+
recipients.back().fSubtractFeeFromAmount = true;
428+
// we don't expect to get change, but we provide the address to prevent CreateTransactionInternal from generating a change address
429+
ExtractDestination(recipients.back().scriptPubKey, coin_control.destChange);
430+
431+
for (const auto& inputs : wtx.tx->vin) {
432+
coin_control.Select(COutPoint(inputs.prevout));
433+
}
434+
435+
constexpr int RANDOM_CHANGE_POSITION = -1;
436+
auto res = CreateTransaction(wallet, recipients, RANDOM_CHANGE_POSITION, coin_control, sign);
437+
if (!res) {
438+
error = util::ErrorString(res);
439+
return Result::WALLET_ERROR;
440+
}
441+
442+
// make sure we didn't get a change position assigned (we don't expect to use the channge address)
443+
Assert(res->change_pos == RANDOM_CHANGE_POSITION);
444+
// write back the new fee
445+
new_fee = res->fee;
446+
// write back the transaction
447+
new_tx = res->tx;
448+
return Result::OK;
449+
}
450+
368451
} // namespace feebumper
369452
} // namespace wallet

src/wallet/feebumper.h

+9
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ Result CommitTransaction(CWallet& wallet,
6969
std::vector<bilingual_str>& errors,
7070
uint256& bumped_txid);
7171

72+
Result CreateRateBumpDeniabilizationTransaction(CWallet& wallet,
73+
const uint256& txid,
74+
unsigned int confirm_target,
75+
bool sign,
76+
bilingual_str& error,
77+
CAmount& old_fee,
78+
CAmount& new_fee,
79+
CTransactionRef& new_tx);
80+
7281
struct SignatureWeights
7382
{
7483
private:

src/wallet/interfaces.cpp

+39
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,27 @@ class WalletImpl : public Wallet
295295
LOCK(m_wallet->cs_wallet);
296296
m_wallet->CommitTransaction(std::move(tx), std::move(value_map), std::move(order_form));
297297
}
298+
std::pair<unsigned int, bool> calculateDeniabilizationCycles(const COutPoint& outpoint) override
299+
{
300+
LOCK(m_wallet->cs_wallet); // TODO - Do we need a lock here?
301+
return CalculateDeniabilizationCycles(*m_wallet, outpoint);
302+
}
303+
util::Result<CTransactionRef> createDeniabilizationTransaction(const std::set<COutPoint>& inputs,
304+
unsigned int confirm_target,
305+
unsigned int deniabilization_cycles,
306+
bool sign,
307+
bool& insufficient_amount,
308+
CAmount& fee) override
309+
{
310+
LOCK(m_wallet->cs_wallet); // TODO - Do we need a lock here?
311+
auto res = CreateDeniabilizationTransaction(*m_wallet, inputs, confirm_target, deniabilization_cycles, sign, insufficient_amount);
312+
if (!res) {
313+
return util::Error{util::ErrorString(res)};
314+
}
315+
const auto& txr = *res;
316+
fee = txr.fee;
317+
return txr.tx;
318+
}
298319
bool transactionCanBeAbandoned(const uint256& txid) override { return m_wallet->TransactionCanBeAbandoned(txid); }
299320
bool abandonTransaction(const uint256& txid) override
300321
{
@@ -324,6 +345,20 @@ class WalletImpl : public Wallet
324345
return feebumper::CommitTransaction(*m_wallet.get(), txid, std::move(mtx), errors, bumped_txid) ==
325346
feebumper::Result::OK;
326347
}
348+
util::Result<CTransactionRef> createBumpDeniabilizationTransaction(const uint256& txid,
349+
unsigned int confirm_target,
350+
bool sign,
351+
CAmount& old_fee,
352+
CAmount& new_fee) override
353+
{
354+
bilingual_str error;
355+
CTransactionRef new_tx;
356+
auto res = feebumper::CreateRateBumpDeniabilizationTransaction(*m_wallet.get(), txid, confirm_target, sign, error, old_fee, new_fee, new_tx);
357+
if (res != feebumper::Result::OK) {
358+
return util::Error{error};
359+
}
360+
return new_tx;
361+
}
327362
CTransactionRef getTx(const uint256& txid) override
328363
{
329364
LOCK(m_wallet->cs_wallet);
@@ -506,6 +541,10 @@ class WalletImpl : public Wallet
506541
if (reason) *reason = fee_calc.reason;
507542
return result;
508543
}
544+
CFeeRate getDeniabilizationFeeRate(unsigned int confirm_target) override
545+
{
546+
return CalculateDeniabilizationFeeRate(*m_wallet, confirm_target);
547+
}
509548
unsigned int getConfirmTarget() override { return m_wallet->m_confirm_target; }
510549
bool hdEnabled() override { return m_wallet->IsHDEnabled(); }
511550
bool canGetAddresses() override { return m_wallet->CanGetAddresses(); }

0 commit comments

Comments
 (0)