Skip to content

Commit 4fd852b

Browse files
committed
Deniability API
This PR is the wallet API and implementation portion of the GUI PR ( #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. ----- Implemented basic deniability unit tests to wallet_tests ----- Implemented a new 'walletdeniabilizecoin' RPC. ----- Implemented fingerprint spoofing for deniabilization (and fee bump) transactions. Currently spoofing with data for 6 different wallet implementations, with 4 specific fingerprint-able behaviors (version, anti-fee-sniping, bip69 ordering, no-rbf).
1 parent 058af75 commit 4fd852b

File tree

10 files changed

+865
-0
lines changed

10 files changed

+865
-0
lines changed

src/interfaces/wallet.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,16 @@ class Wallet
153153
WalletValueMap value_map,
154154
WalletOrderForm order_form) = 0;
155155

156+
virtual std::pair<unsigned int, bool> calculateDeniabilizationCycles(const COutPoint& outpoint) = 0;
157+
158+
virtual util::Result<CTransactionRef> createDeniabilizationTransaction(const std::set<COutPoint>& inputs,
159+
const std::optional<OutputType>& opt_output_type,
160+
unsigned int confirm_target,
161+
unsigned int deniabilization_cycles,
162+
bool sign,
163+
bool& insufficient_amount,
164+
CAmount& fee) = 0;
165+
156166
//! Return whether transaction can be abandoned.
157167
virtual bool transactionCanBeAbandoned(const uint256& txid) = 0;
158168

@@ -179,6 +189,13 @@ class Wallet
179189
std::vector<bilingual_str>& errors,
180190
uint256& bumped_txid) = 0;
181191

192+
//! Create a fee bump transaction for a deniabilization transaction
193+
virtual util::Result<CTransactionRef> createBumpDeniabilizationTransaction(const uint256& txid,
194+
unsigned int confirm_target,
195+
bool sign,
196+
CAmount& old_fee,
197+
CAmount& new_fee) = 0;
198+
182199
//! Get a transaction.
183200
virtual CTransactionRef getTx(const uint256& txid) = 0;
184201

@@ -250,6 +267,9 @@ class Wallet
250267
int* returned_target,
251268
FeeReason* reason) = 0;
252269

270+
//! Get the fee rate for deniabilization
271+
virtual CFeeRate getDeniabilizationFeeRate(unsigned int confirm_target) = 0;
272+
253273
//! Get tx confirm target.
254274
virtual unsigned int getConfirmTarget() = 0;
255275

src/rpc/client.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,9 @@ static const CRPCConvertParam vRPCConvertParams[] =
165165
{ "walletcreatefundedpsbt", 3, "replaceable"},
166166
{ "walletcreatefundedpsbt", 3, "solving_data"},
167167
{ "walletcreatefundedpsbt", 4, "bip32derivs" },
168+
{ "walletdeniabilizecoin", 0, "inputs" },
169+
{ "walletdeniabilizecoin", 2, "conf_target" },
170+
{ "walletdeniabilizecoin", 3, "add_to_wallet" },
168171
{ "walletprocesspsbt", 1, "sign" },
169172
{ "walletprocesspsbt", 3, "bip32derivs" },
170173
{ "walletprocesspsbt", 4, "finalize" },

src/wallet/feebumper.cpp

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,5 +386,103 @@ Result CommitTransaction(CWallet& wallet, const uint256& txid, CMutableTransacti
386386
return Result::OK;
387387
}
388388

389+
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)
390+
{
391+
CCoinControl coin_control = SetupDeniabilizationCoinControl(confirm_target);
392+
coin_control.m_feerate = CalculateDeniabilizationFeeRate(wallet, confirm_target);
393+
394+
LOCK(wallet.cs_wallet);
395+
396+
auto it = wallet.mapWallet.find(txid);
397+
if (it == wallet.mapWallet.end()) {
398+
error = Untranslated("Invalid or non-wallet transaction id");
399+
return Result::INVALID_ADDRESS_OR_KEY;
400+
}
401+
const CWalletTx& wtx = it->second;
402+
403+
// Retrieve all of the UTXOs and add them to coin control
404+
// While we're here, calculate the input amount
405+
std::map<COutPoint, Coin> coins;
406+
CAmount input_value = 0;
407+
for (const CTxIn& txin : wtx.tx->vin) {
408+
coins[txin.prevout]; // Create empty map entry keyed by prevout.
409+
}
410+
wallet.chain().findCoins(coins);
411+
for (const CTxIn& txin : wtx.tx->vin) {
412+
const Coin& coin = coins.at(txin.prevout);
413+
if (coin.out.IsNull()) {
414+
error = Untranslated(strprintf("%s:%u is already spent", txin.prevout.hash.GetHex(), txin.prevout.n));
415+
return Result::MISC_ERROR;
416+
}
417+
if (!wallet.IsMine(txin.prevout)) {
418+
error = Untranslated("All inputs must be from our wallet.");
419+
return Result::MISC_ERROR;
420+
}
421+
coin_control.Select(txin.prevout);
422+
input_value += coin.out.nValue;
423+
}
424+
425+
std::vector<bilingual_str> dymmy_errors;
426+
Result result = PreconditionChecks(wallet, wtx, /*require_mine=*/true, dymmy_errors);
427+
if (result != Result::OK) {
428+
error = dymmy_errors.front();
429+
return result;
430+
}
431+
432+
// Calculate the old output amount.
433+
CAmount output_value = 0;
434+
for (const auto& old_output : wtx.tx->vout) {
435+
output_value += old_output.nValue;
436+
}
437+
438+
old_fee = input_value - output_value;
439+
440+
std::vector<CRecipient> recipients;
441+
for (const auto& output : wtx.tx->vout) {
442+
CTxDestination destination = CNoDestination();
443+
ExtractDestination(output.scriptPubKey, destination);
444+
CRecipient recipient = {destination, output.nValue, false};
445+
recipients.push_back(recipient);
446+
}
447+
// the last recipient gets the old fee
448+
recipients.back().nAmount += old_fee;
449+
// and pays the new fee
450+
recipients.back().fSubtractFeeFromAmount = true;
451+
// we don't expect to get change, but we provide the address to prevent CreateTransactionInternal from generating a change address
452+
coin_control.destChange = recipients.back().dest;
453+
454+
for (const auto& inputs : wtx.tx->vin) {
455+
coin_control.Select(COutPoint(inputs.prevout));
456+
}
457+
458+
auto res = CreateTransaction(wallet, recipients, std::nullopt, coin_control, /*sign=*/false);
459+
if (!res) {
460+
error = util::ErrorString(res);
461+
return Result::WALLET_ERROR;
462+
}
463+
464+
// make sure we didn't get a change position assigned (we don't expect to use the channge address)
465+
Assert(!res->change_pos.has_value());
466+
467+
// spoof the transaction fingerprint to increase the transaction privacy
468+
{
469+
FastRandomContext rng_fast;
470+
CMutableTransaction spoofedTx(*res->tx);
471+
SpoofTransactionFingerprint(spoofedTx, rng_fast, coin_control.m_signal_bip125_rbf);
472+
if (sign && !wallet.SignTransaction(spoofedTx)) {
473+
error = Untranslated("Signing the deniabilization fee bump transaction failed.");
474+
return Result::MISC_ERROR;
475+
}
476+
// store the spoofed transaction in the result
477+
res->tx = MakeTransactionRef(std::move(spoofedTx));
478+
}
479+
480+
// write back the new fee
481+
new_fee = res->fee;
482+
// write back the transaction
483+
new_tx = res->tx;
484+
return Result::OK;
485+
}
486+
389487
} // namespace feebumper
390488
} // namespace wallet

src/wallet/feebumper.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,15 @@ Result CommitTransaction(CWallet& wallet,
7272
std::vector<bilingual_str>& errors,
7373
uint256& bumped_txid);
7474

75+
Result CreateRateBumpDeniabilizationTransaction(CWallet& wallet,
76+
const uint256& txid,
77+
unsigned int confirm_target,
78+
bool sign,
79+
bilingual_str& error,
80+
CAmount& old_fee,
81+
CAmount& new_fee,
82+
CTransactionRef& new_tx);
83+
7584
struct SignatureWeights
7685
{
7786
private:

src/wallet/interfaces.cpp

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,28 @@ class WalletImpl : public Wallet
297297
LOCK(m_wallet->cs_wallet);
298298
m_wallet->CommitTransaction(std::move(tx), std::move(value_map), std::move(order_form));
299299
}
300+
std::pair<unsigned int, bool> calculateDeniabilizationCycles(const COutPoint& outpoint) override
301+
{
302+
LOCK(m_wallet->cs_wallet); // TODO - Do we need a lock here?
303+
return CalculateDeniabilizationCycles(*m_wallet, outpoint);
304+
}
305+
util::Result<CTransactionRef> createDeniabilizationTransaction(const std::set<COutPoint>& inputs,
306+
const std::optional<OutputType>& opt_output_type,
307+
unsigned int confirm_target,
308+
unsigned int deniabilization_cycles,
309+
bool sign,
310+
bool& insufficient_amount,
311+
CAmount& fee) override
312+
{
313+
LOCK(m_wallet->cs_wallet); // TODO - Do we need a lock here?
314+
auto res = CreateDeniabilizationTransaction(*m_wallet, inputs, opt_output_type, confirm_target, deniabilization_cycles, sign, insufficient_amount);
315+
if (!res) {
316+
return util::Error{util::ErrorString(res)};
317+
}
318+
const auto& txr = *res;
319+
fee = txr.fee;
320+
return txr.tx;
321+
}
300322
bool transactionCanBeAbandoned(const uint256& txid) override { return m_wallet->TransactionCanBeAbandoned(txid); }
301323
bool abandonTransaction(const uint256& txid) override
302324
{
@@ -326,6 +348,20 @@ class WalletImpl : public Wallet
326348
return feebumper::CommitTransaction(*m_wallet.get(), txid, std::move(mtx), errors, bumped_txid) ==
327349
feebumper::Result::OK;
328350
}
351+
util::Result<CTransactionRef> createBumpDeniabilizationTransaction(const uint256& txid,
352+
unsigned int confirm_target,
353+
bool sign,
354+
CAmount& old_fee,
355+
CAmount& new_fee) override
356+
{
357+
bilingual_str error;
358+
CTransactionRef new_tx;
359+
auto res = feebumper::CreateRateBumpDeniabilizationTransaction(*m_wallet.get(), txid, confirm_target, sign, error, old_fee, new_fee, new_tx);
360+
if (res != feebumper::Result::OK) {
361+
return util::Error{error};
362+
}
363+
return new_tx;
364+
}
329365
CTransactionRef getTx(const uint256& txid) override
330366
{
331367
LOCK(m_wallet->cs_wallet);
@@ -508,6 +544,10 @@ class WalletImpl : public Wallet
508544
if (reason) *reason = fee_calc.reason;
509545
return result;
510546
}
547+
CFeeRate getDeniabilizationFeeRate(unsigned int confirm_target) override
548+
{
549+
return CalculateDeniabilizationFeeRate(*m_wallet, confirm_target);
550+
}
511551
unsigned int getConfirmTarget() override { return m_wallet->m_confirm_target; }
512552
bool hdEnabled() override { return m_wallet->IsHDEnabled(); }
513553
bool canGetAddresses() override { return m_wallet->CanGetAddresses(); }

src/wallet/rpc/spend.cpp

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1753,4 +1753,129 @@ RPCHelpMan walletcreatefundedpsbt()
17531753
},
17541754
};
17551755
}
1756+
1757+
// clang-format off
1758+
RPCHelpMan walletdeniabilizecoin()
1759+
{
1760+
return RPCHelpMan{"walletdeniabilizecoin",
1761+
"\nDeniabilize one or more UTXOs that share the same address.\n",
1762+
{
1763+
{"inputs", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Specify inputs (must share the same address). A JSON array of JSON objects",
1764+
{
1765+
{"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"},
1766+
{"vout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The output number"},
1767+
},
1768+
},
1769+
{"output_type", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Optional output type to use. Options are \"legacy\", \"p2sh-segwit\", \"bech32\" and \"bech32m\". If not specified the output type is inferred from the inputs."},
1770+
{"conf_target", RPCArg::Type::NUM, RPCArg::DefaultHint{"wallet -txconfirmtarget"}, "Confirmation target in blocks"},
1771+
{"add_to_wallet", RPCArg::Type::BOOL, RPCArg::Default{true}, "When false, returns the serialized transaction without broadcasting or adding it to the wallet"},
1772+
},
1773+
RPCResult{
1774+
RPCResult::Type::OBJ, "", "",
1775+
{
1776+
{RPCResult::Type::STR_HEX, "txid", "The deniabilization transaction id."},
1777+
{RPCResult::Type::STR_AMOUNT, "fee", "The fee used in the deniabilization transaction."},
1778+
{RPCResult::Type::STR_HEX, "hex", /*optional=*/true, "If add_to_wallet is false, the hex-encoded raw transaction with signature(s)"},
1779+
}
1780+
},
1781+
RPCExamples{
1782+
"\nDeniabilize a single UTXO\n"
1783+
+ HelpExampleCli("walletdeniabilizecoin", "'[{\"txid\":\"4c14d20709daef476854fe7ef75bdfcfd5a7636a431b4622ec9481f297e12e8c\", \"vout\": 0}]'") +
1784+
"\nDeniabilize a single UTXO using a specific output type\n"
1785+
+ HelpExampleCli("walletdeniabilizecoin", "'[{\"txid\":\"4c14d20709daef476854fe7ef75bdfcfd5a7636a431b4622ec9481f297e12e8c\", \"vout\": 0}]' bech32") +
1786+
"\nDeniabilize a single UTXO with an explicit confirmation target\n"
1787+
+ HelpExampleCli("walletdeniabilizecoin", "'[{\"txid\":\"4c14d20709daef476854fe7ef75bdfcfd5a7636a431b4622ec9481f297e12e8c\", \"vout\": 0}]' null 144") +
1788+
"\nDeniabilize a single UTXO without broadcasting the transaction\n"
1789+
+ HelpExampleCli("walletdeniabilizecoin", "'[{\"txid\":\"4c14d20709daef476854fe7ef75bdfcfd5a7636a431b4622ec9481f297e12e8c\", \"vout\": 0}]' null 6 false")
1790+
},
1791+
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
1792+
{
1793+
std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request);
1794+
if (!pwallet) return UniValue::VNULL;
1795+
1796+
std::optional<CScript> shared_script;
1797+
std::set<COutPoint> inputs;
1798+
unsigned int deniabilization_cycles = UINT_MAX;
1799+
for (const UniValue& input : request.params[0].get_array().getValues()) {
1800+
Txid txid = Txid::FromUint256(ParseHashO(input, "txid"));
1801+
1802+
const UniValue& vout_v = input.find_value("vout");
1803+
if (!vout_v.isNum()) {
1804+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, missing vout key");
1805+
}
1806+
int nOutput = vout_v.getInt<int>();
1807+
if (nOutput < 0) {
1808+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, vout cannot be negative");
1809+
}
1810+
1811+
COutPoint outpoint(txid, nOutput);
1812+
LOCK(pwallet->cs_wallet);
1813+
auto walletTx = pwallet->GetWalletTx(outpoint.hash);
1814+
if (!walletTx) {
1815+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, txid not found in wallet.");
1816+
}
1817+
if (outpoint.n >= walletTx->tx->vout.size()) {
1818+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, vout is out of range");
1819+
}
1820+
const auto& output = walletTx->tx->vout[outpoint.n];
1821+
1822+
isminetype mine = pwallet->IsMine(output);
1823+
if (mine == ISMINE_NO) {
1824+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, transaction's output doesn't belong to this wallet.");
1825+
}
1826+
1827+
bool spendable = (mine & ISMINE_SPENDABLE) != ISMINE_NO;
1828+
if (spendable) {
1829+
auto script = FindNonChangeParentOutput(*pwallet, outpoint).scriptPubKey;
1830+
if (!shared_script) {
1831+
shared_script = script;
1832+
}
1833+
else if (!(*shared_script == script)) {
1834+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, inputs must share the same address");
1835+
}
1836+
} else {
1837+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, inputs must be spendable and have a valid address");
1838+
}
1839+
1840+
inputs.emplace(outpoint);
1841+
auto cycles_res = CalculateDeniabilizationCycles(*pwallet, outpoint);
1842+
deniabilization_cycles = std::min(deniabilization_cycles, cycles_res.first);
1843+
}
1844+
1845+
if (inputs.empty()) {
1846+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, inputs must not be empty");
1847+
}
1848+
1849+
std::optional<OutputType> opt_output_type = !request.params[1].isNull() ? ParseOutputType(request.params[1].get_str()) : std::nullopt;
1850+
unsigned int confirm_target = !request.params[2].isNull() ? request.params[2].getInt<unsigned int>() : pwallet->m_confirm_target;
1851+
const bool add_to_wallet = !request.params[3].isNull() ? request.params[3].get_bool() : true;
1852+
1853+
CTransactionRef tx;
1854+
CAmount tx_fee = 0;
1855+
{
1856+
bool sign = !pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS);
1857+
bool insufficient_amount = false;
1858+
auto res = CreateDeniabilizationTransaction(*pwallet, inputs, opt_output_type, confirm_target, deniabilization_cycles, sign, insufficient_amount);
1859+
if (!res) {
1860+
throw JSONRPCError(RPC_TRANSACTION_ERROR, ErrorString(res).original);
1861+
}
1862+
tx = res->tx;
1863+
tx_fee = res->fee;
1864+
}
1865+
1866+
UniValue result(UniValue::VOBJ);
1867+
result.pushKV("txid", tx->GetHash().GetHex());
1868+
if (add_to_wallet) {
1869+
pwallet->CommitTransaction(tx, {}, /*orderForm=*/{});
1870+
} else {
1871+
std::string hex{EncodeHexTx(*tx)};
1872+
result.pushKV("hex", hex);
1873+
}
1874+
result.pushKV("fee", ValueFromAmount(tx_fee));
1875+
return result;
1876+
}
1877+
};
1878+
}
1879+
// clang-format on
1880+
17561881
} // namespace wallet

src/wallet/rpc/wallet.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,6 +1083,7 @@ RPCHelpMan send();
10831083
RPCHelpMan sendall();
10841084
RPCHelpMan walletprocesspsbt();
10851085
RPCHelpMan walletcreatefundedpsbt();
1086+
RPCHelpMan walletdeniabilizecoin();
10861087
RPCHelpMan signrawtransactionwithwallet();
10871088

10881089
// signmessage
@@ -1172,6 +1173,7 @@ Span<const CRPCCommand> GetWalletRPCCommands()
11721173
{"wallet", &walletpassphrase},
11731174
{"wallet", &walletpassphrasechange},
11741175
{"wallet", &walletprocesspsbt},
1176+
{"wallet", &walletdeniabilizecoin},
11751177
};
11761178
return commands;
11771179
}

0 commit comments

Comments
 (0)