Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -1784,4 +1784,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