Skip to content

Commit

Permalink
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 ( #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).
  • Loading branch information
denavila committed Aug 1, 2024
1 parent 9774a95 commit c2c12c0
Show file tree
Hide file tree
Showing 10 changed files with 865 additions and 0 deletions.
20 changes: 20 additions & 0 deletions src/interfaces/wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,16 @@ class Wallet
WalletValueMap value_map,
WalletOrderForm order_form) = 0;

virtual std::pair<unsigned int, bool> calculateDeniabilizationCycles(const COutPoint& outpoint) = 0;

virtual util::Result<CTransactionRef> createDeniabilizationTransaction(const std::set<COutPoint>& inputs,
const std::optional<OutputType>& opt_output_type,
unsigned int confirm_target,
unsigned int 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 @@ -184,6 +194,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,
unsigned int 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 @@ -255,6 +272,9 @@ class Wallet
int* returned_target,
FeeReason* reason) = 0;

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

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

Expand Down
3 changes: 3 additions & 0 deletions src/rpc/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "walletcreatefundedpsbt", 3, "solving_data"},
{ "walletcreatefundedpsbt", 3, "max_tx_weight"},
{ "walletcreatefundedpsbt", 4, "bip32derivs" },
{ "walletdeniabilizecoin", 0, "inputs" },
{ "walletdeniabilizecoin", 2, "conf_target" },
{ "walletdeniabilizecoin", 3, "add_to_wallet" },
{ "walletprocesspsbt", 1, "sign" },
{ "walletprocesspsbt", 3, "bip32derivs" },
{ "walletprocesspsbt", 4, "finalize" },
Expand Down
98 changes: 98 additions & 0 deletions src/wallet/feebumper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -387,5 +387,103 @@ Result CommitTransaction(CWallet& wallet, const uint256& txid, CMutableTransacti
return Result::OK;
}

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)
{
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) {
CTxDestination destination = CNoDestination();
ExtractDestination(output.scriptPubKey, destination);
CRecipient recipient = {destination, 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
coin_control.destChange = recipients.back().dest;

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

auto res = CreateTransaction(wallet, recipients, std::nullopt, coin_control, /*sign=*/false);
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.has_value());

// spoof the transaction fingerprint to increase the transaction privacy
{
FastRandomContext rng_fast;
CMutableTransaction spoofedTx(*res->tx);
SpoofTransactionFingerprint(spoofedTx, rng_fast, coin_control.m_signal_bip125_rbf);
if (sign && !wallet.SignTransaction(spoofedTx)) {
error = Untranslated("Signing the deniabilization fee bump transaction failed.");
return Result::MISC_ERROR;
}
// store the spoofed transaction in the result
res->tx = MakeTransactionRef(std::move(spoofedTx));
}

// 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 @@ -72,6 +72,15 @@ Result CommitTransaction(CWallet& wallet,
std::vector<bilingual_str>& errors,
uint256& bumped_txid);

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);

struct SignatureWeights
{
private:
Expand Down
40 changes: 40 additions & 0 deletions src/wallet/interfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,28 @@ class WalletImpl : public Wallet
LOCK(m_wallet->cs_wallet);
m_wallet->CommitTransaction(std::move(tx), std::move(value_map), std::move(order_form));
}
std::pair<unsigned int, bool> calculateDeniabilizationCycles(const COutPoint& outpoint) override
{
LOCK(m_wallet->cs_wallet); // TODO - Do we need a lock here?
return CalculateDeniabilizationCycles(*m_wallet, outpoint);
}
util::Result<CTransactionRef> createDeniabilizationTransaction(const std::set<COutPoint>& inputs,
const std::optional<OutputType>& opt_output_type,
unsigned int confirm_target,
unsigned int 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, opt_output_type, 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 @@ -328,6 +350,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,
unsigned int 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 @@ -510,6 +546,10 @@ class WalletImpl : public Wallet
if (reason) *reason = fee_calc.reason;
return result;
}
CFeeRate getDeniabilizationFeeRate(unsigned int 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
125 changes: 125 additions & 0 deletions src/wallet/rpc/spend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1782,4 +1782,129 @@ RPCHelpMan walletcreatefundedpsbt()
},
};
}

// clang-format off
RPCHelpMan walletdeniabilizecoin()
{
return RPCHelpMan{"walletdeniabilizecoin",
"\nDeniabilize one or more UTXOs that share the same address.\n",
{
{"inputs", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Specify inputs (must share the same address). A JSON array of JSON objects",
{
{"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"},
{"vout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The output number"},
},
},
{"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."},
{"conf_target", RPCArg::Type::NUM, RPCArg::DefaultHint{"wallet -txconfirmtarget"}, "Confirmation target in blocks"},
{"add_to_wallet", RPCArg::Type::BOOL, RPCArg::Default{true}, "When false, returns the serialized transaction without broadcasting or adding it to the wallet"},
},
RPCResult{
RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::STR_HEX, "txid", "The deniabilization transaction id."},
{RPCResult::Type::STR_AMOUNT, "fee", "The fee used in the deniabilization transaction."},
{RPCResult::Type::STR_HEX, "hex", /*optional=*/true, "If add_to_wallet is false, the hex-encoded raw transaction with signature(s)"},
}
},
RPCExamples{
"\nDeniabilize a single UTXO\n"
+ HelpExampleCli("walletdeniabilizecoin", "'[{\"txid\":\"4c14d20709daef476854fe7ef75bdfcfd5a7636a431b4622ec9481f297e12e8c\", \"vout\": 0}]'") +
"\nDeniabilize a single UTXO using a specific output type\n"
+ HelpExampleCli("walletdeniabilizecoin", "'[{\"txid\":\"4c14d20709daef476854fe7ef75bdfcfd5a7636a431b4622ec9481f297e12e8c\", \"vout\": 0}]' bech32") +
"\nDeniabilize a single UTXO with an explicit confirmation target\n"
+ HelpExampleCli("walletdeniabilizecoin", "'[{\"txid\":\"4c14d20709daef476854fe7ef75bdfcfd5a7636a431b4622ec9481f297e12e8c\", \"vout\": 0}]' null 144") +
"\nDeniabilize a single UTXO without broadcasting the transaction\n"
+ HelpExampleCli("walletdeniabilizecoin", "'[{\"txid\":\"4c14d20709daef476854fe7ef75bdfcfd5a7636a431b4622ec9481f297e12e8c\", \"vout\": 0}]' null 6 false")
},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{
std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request);
if (!pwallet) return UniValue::VNULL;

std::optional<CScript> shared_script;
std::set<COutPoint> inputs;
unsigned int deniabilization_cycles = UINT_MAX;
for (const UniValue& input : request.params[0].get_array().getValues()) {
Txid txid = Txid::FromUint256(ParseHashO(input, "txid"));

const UniValue& vout_v = input.find_value("vout");
if (!vout_v.isNum()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, missing vout key");
}
int nOutput = vout_v.getInt<int>();
if (nOutput < 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, vout cannot be negative");
}

COutPoint outpoint(txid, nOutput);
LOCK(pwallet->cs_wallet);
auto walletTx = pwallet->GetWalletTx(outpoint.hash);
if (!walletTx) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, txid not found in wallet.");
}
if (outpoint.n >= walletTx->tx->vout.size()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, vout is out of range");
}
const auto& output = walletTx->tx->vout[outpoint.n];

isminetype mine = pwallet->IsMine(output);
if (mine == ISMINE_NO) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, transaction's output doesn't belong to this wallet.");
}

bool spendable = (mine & ISMINE_SPENDABLE) != ISMINE_NO;
if (spendable) {
auto script = FindNonChangeParentOutput(*pwallet, outpoint).scriptPubKey;
if (!shared_script) {
shared_script = script;
}
else if (!(*shared_script == script)) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, inputs must share the same address");
}
} else {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, inputs must be spendable and have a valid address");
}

inputs.emplace(outpoint);
auto cycles_res = CalculateDeniabilizationCycles(*pwallet, outpoint);
deniabilization_cycles = std::min(deniabilization_cycles, cycles_res.first);
}

if (inputs.empty()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, inputs must not be empty");
}

std::optional<OutputType> opt_output_type = !request.params[1].isNull() ? ParseOutputType(request.params[1].get_str()) : std::nullopt;
unsigned int confirm_target = !request.params[2].isNull() ? request.params[2].getInt<unsigned int>() : pwallet->m_confirm_target;
const bool add_to_wallet = !request.params[3].isNull() ? request.params[3].get_bool() : true;

CTransactionRef tx;
CAmount tx_fee = 0;
{
bool sign = !pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS);
bool insufficient_amount = false;
auto res = CreateDeniabilizationTransaction(*pwallet, inputs, opt_output_type, confirm_target, deniabilization_cycles, sign, insufficient_amount);
if (!res) {
throw JSONRPCError(RPC_TRANSACTION_ERROR, ErrorString(res).original);
}
tx = res->tx;
tx_fee = res->fee;
}

UniValue result(UniValue::VOBJ);
result.pushKV("txid", tx->GetHash().GetHex());
if (add_to_wallet) {
pwallet->CommitTransaction(tx, {}, /*orderForm=*/{});
} else {
std::string hex{EncodeHexTx(*tx)};
result.pushKV("hex", hex);
}
result.pushKV("fee", ValueFromAmount(tx_fee));
return result;
}
};
}
// clang-format on

} // namespace wallet
2 changes: 2 additions & 0 deletions src/wallet/rpc/wallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,7 @@ RPCHelpMan send();
RPCHelpMan sendall();
RPCHelpMan walletprocesspsbt();
RPCHelpMan walletcreatefundedpsbt();
RPCHelpMan walletdeniabilizecoin();
RPCHelpMan signrawtransactionwithwallet();

// signmessage
Expand Down Expand Up @@ -1172,6 +1173,7 @@ Span<const CRPCCommand> GetWalletRPCCommands()
{"wallet", &walletpassphrase},
{"wallet", &walletpassphrasechange},
{"wallet", &walletprocesspsbt},
{"wallet", &walletdeniabilizecoin},
};
return commands;
}
Expand Down
Loading

0 comments on commit c2c12c0

Please sign in to comment.