Skip to content

Commit

Permalink
wallet: Deniability API
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
denavila committed May 26, 2023
1 parent a13f374 commit a035b42
Show file tree
Hide file tree
Showing 6 changed files with 396 additions and 0 deletions.
17 changes: 17 additions & 0 deletions src/interfaces/wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,13 @@ class Wallet
WalletValueMap value_map,
WalletOrderForm order_form) = 0;

virtual util::Result<CTransactionRef> createDeniabilizationTransaction(const std::set<COutPoint>& inputs,
uint confirm_target,
uint deniabilization_cycles,
bool sign,
bool& insufficient_amount,
CAmount& fee) = 0;

//! Return whether transaction can be abandoned.
virtual bool transactionCanBeAbandoned(const uint256& txid) = 0;

Expand All @@ -177,6 +184,13 @@ class Wallet
std::vector<bilingual_str>& errors,
uint256& bumped_txid) = 0;

//! Create a fee bump transaction for a deniabilization transaction
virtual util::Result<CTransactionRef> createBumpDeniabilizationTransaction(const uint256& txid,
uint confirm_target,
bool sign,
CAmount& old_fee,
CAmount& new_fee) = 0;

//! Get a transaction.
virtual CTransactionRef getTx(const uint256& txid) = 0;

Expand Down Expand Up @@ -248,6 +262,9 @@ class Wallet
int* returned_target,
FeeReason* reason) = 0;

//! Get the fee rate for deniabilization
virtual CFeeRate getDeniabilizationFeeRate(uint confirm_target) = 0;

//! Get tx confirm target.
virtual unsigned int getConfirmTarget() = 0;

Expand Down
83 changes: 83 additions & 0 deletions src/wallet/feebumper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -365,5 +365,88 @@ Result CommitTransaction(CWallet& wallet, const uint256& txid, CMutableTransacti
return Result::OK;
}

Result CreateRateBumpDeniabilizationTransaction(CWallet& wallet, const uint256& txid, uint confirm_target, bool sign, bilingual_str& error, CAmount& old_fee, CAmount& new_fee, CTransactionRef& new_tx)
{
CCoinControl coin_control = SetupDeniabilizationCoinControl(confirm_target);
coin_control.m_feerate = CalculateDeniabilizationFeeRate(wallet, confirm_target);

LOCK(wallet.cs_wallet);

auto it = wallet.mapWallet.find(txid);
if (it == wallet.mapWallet.end()) {
error = Untranslated("Invalid or non-wallet transaction id");
return Result::INVALID_ADDRESS_OR_KEY;
}
const CWalletTx& wtx = it->second;

// Retrieve all of the UTXOs and add them to coin control
// While we're here, calculate the input amount
std::map<COutPoint, Coin> coins;
CAmount input_value = 0;
for (const CTxIn& txin : wtx.tx->vin) {
coins[txin.prevout]; // Create empty map entry keyed by prevout.
}
wallet.chain().findCoins(coins);
for (const CTxIn& txin : wtx.tx->vin) {
const Coin& coin = coins.at(txin.prevout);
if (coin.out.IsNull()) {
error = Untranslated(strprintf("%s:%u is already spent", txin.prevout.hash.GetHex(), txin.prevout.n));
return Result::MISC_ERROR;
}
if (!wallet.IsMine(txin.prevout)) {
error = Untranslated("All inputs must be from our wallet.");
return Result::MISC_ERROR;
}
coin_control.Select(txin.prevout);
input_value += coin.out.nValue;
}

std::vector<bilingual_str> dymmy_errors;
Result result = PreconditionChecks(wallet, wtx, /*require_mine=*/true, dymmy_errors);
if (result != Result::OK) {
error = dymmy_errors.front();
return result;
}

// Calculate the old output amount.
CAmount output_value = 0;
for (const auto& old_output : wtx.tx->vout) {
output_value += old_output.nValue;
}

old_fee = input_value - output_value;

std::vector<CRecipient> recipients;
for (const auto& output : wtx.tx->vout) {
CRecipient recipient = {output.scriptPubKey, output.nValue, false};
recipients.push_back(recipient);
}
// the last recipient gets the old fee
recipients.back().nAmount += old_fee;
// and pays the new fee
recipients.back().fSubtractFeeFromAmount = true;
// we don't expect to get change, but we provide the address to prevent CreateTransactionInternal from generating a change address
ExtractDestination(recipients.back().scriptPubKey, coin_control.destChange);

for (const auto& inputs : wtx.tx->vin) {
coin_control.Select(COutPoint(inputs.prevout));
}

constexpr int RANDOM_CHANGE_POSITION = -1;
auto res = CreateTransaction(wallet, recipients, RANDOM_CHANGE_POSITION, coin_control, sign);
if (!res) {
error = util::ErrorString(res);
return Result::WALLET_ERROR;
}

// make sure we didn't get a change position assigned (we don't expect to use the channge address)
Assert(res->change_pos == RANDOM_CHANGE_POSITION);
// write back the new fee
new_fee = res->fee;
// write back the transaction
new_tx = res->tx;
return Result::OK;
}

} // namespace feebumper
} // namespace wallet
9 changes: 9 additions & 0 deletions src/wallet/feebumper.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ Result CommitTransaction(CWallet& wallet,
std::vector<bilingual_str>& errors,
uint256& bumped_txid);

Result CreateRateBumpDeniabilizationTransaction(CWallet& wallet,
const uint256& txid,
uint confirm_target,
bool sign,
bilingual_str& error,
CAmount& old_fee,
CAmount& new_fee,
CTransactionRef& new_tx);

struct SignatureWeights
{
private:
Expand Down
34 changes: 34 additions & 0 deletions src/wallet/interfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,22 @@ class WalletImpl : public Wallet
LOCK(m_wallet->cs_wallet);
m_wallet->CommitTransaction(std::move(tx), std::move(value_map), std::move(order_form));
}
util::Result<CTransactionRef> createDeniabilizationTransaction(const std::set<COutPoint>& inputs,
uint confirm_target,
uint deniabilization_cycles,
bool sign,
bool& insufficient_amount,
CAmount& fee) override
{
LOCK(m_wallet->cs_wallet); // TODO - Do we need a lock here?
auto res = CreateDeniabilizationTransaction(*m_wallet, inputs, confirm_target, deniabilization_cycles, sign, insufficient_amount);
if (!res) {
return util::Error{util::ErrorString(res)};
}
const auto& txr = *res;
fee = txr.fee;
return txr.tx;
}
bool transactionCanBeAbandoned(const uint256& txid) override { return m_wallet->TransactionCanBeAbandoned(txid); }
bool abandonTransaction(const uint256& txid) override
{
Expand Down Expand Up @@ -324,6 +340,20 @@ class WalletImpl : public Wallet
return feebumper::CommitTransaction(*m_wallet.get(), txid, std::move(mtx), errors, bumped_txid) ==
feebumper::Result::OK;
}
util::Result<CTransactionRef> createBumpDeniabilizationTransaction(const uint256& txid,
uint confirm_target,
bool sign,
CAmount& old_fee,
CAmount& new_fee) override
{
bilingual_str error;
CTransactionRef new_tx;
auto res = feebumper::CreateRateBumpDeniabilizationTransaction(*m_wallet.get(), txid, confirm_target, sign, error, old_fee, new_fee, new_tx);
if (res != feebumper::Result::OK) {
return util::Error{error};
}
return new_tx;
}
CTransactionRef getTx(const uint256& txid) override
{
LOCK(m_wallet->cs_wallet);
Expand Down Expand Up @@ -506,6 +536,10 @@ class WalletImpl : public Wallet
if (reason) *reason = fee_calc.reason;
return result;
}
CFeeRate getDeniabilizationFeeRate(uint confirm_target) override
{
return CalculateDeniabilizationFeeRate(*m_wallet, confirm_target);
}
unsigned int getConfirmTarget() override { return m_wallet->m_confirm_target; }
bool hdEnabled() override { return m_wallet->IsHDEnabled(); }
bool canGetAddresses() override { return m_wallet->CanGetAddresses(); }
Expand Down
Loading

0 comments on commit a035b42

Please sign in to comment.