diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index 8c31112fc94850..76578533d1fd19 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -151,6 +151,13 @@ class Wallet WalletValueMap value_map, WalletOrderForm order_form) = 0; + virtual util::Result createDeniabilizationTransaction(const std::set& 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; @@ -177,6 +184,13 @@ class Wallet std::vector& errors, uint256& bumped_txid) = 0; + //! Create a fee bump transaction for a deniabilization transaction + virtual util::Result 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; @@ -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; diff --git a/src/wallet/feebumper.cpp b/src/wallet/feebumper.cpp index b6b1fa1d3e256c..0e2440e6b4e43c 100644 --- a/src/wallet/feebumper.cpp +++ b/src/wallet/feebumper.cpp @@ -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 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 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 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 diff --git a/src/wallet/feebumper.h b/src/wallet/feebumper.h index 53cf16e0f1925e..c2ac4c9905a81b 100644 --- a/src/wallet/feebumper.h +++ b/src/wallet/feebumper.h @@ -69,6 +69,15 @@ Result CommitTransaction(CWallet& wallet, std::vector& 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: diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index cd438cfe2f399b..0de6fd6560e551 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -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 createDeniabilizationTransaction(const std::set& 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 { @@ -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 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); @@ -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(); } diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp index b14a30921b6930..f8b234f8ac9ecf 100644 --- a/src/wallet/spend.cpp +++ b/src/wallet/spend.cpp @@ -1248,4 +1248,225 @@ bool FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& nFeeRet, return true; } + +constexpr int NUM_DENIABILIZATION_OUTPUTS = 2; + +static uint CalculateDeniabilizationTxSize(const CTxDestination& address, CAmount value, uint numTxIn) +{ + // Calculation based on the comments and code in GetDustThreshold and CreateTransactionInternal + CScript script = GetScriptForDestination(address); + uint txOutSize = (uint)GetSerializeSize(CTxOut(value, script), PROTOCOL_VERSION); + + const size_t txOutCount = NUM_DENIABILIZATION_OUTPUTS; + uint txSize = 10 + GetSizeOfCompactSize(txOutCount); // bytes for output count + txSize += txOutSize * txOutCount; + + int witnessversion = 0; + std::vector witnessprogram; + if (script.IsWitnessProgram(witnessversion, witnessprogram)) { + txSize += (uint)(numTxIn * 67.75f + 0.5f); + } else { + txSize += numTxIn * 148; + } + return txSize; +} + +float CalculateDeniabilizationProbability(uint deniabilization_cycles) +{ + // 100%, 50%, 25%, 13%, 6%, 3%, 2%, 1% + return powf(0.5f, deniabilization_cycles); +} + +bool IsDeniabilizationWorthwhile(CAmount total_value, CAmount fee_estimate) +{ + constexpr CAmount value_to_fee_ratio = 10; + return total_value > fee_estimate * value_to_fee_ratio; +} + +CCoinControl SetupDeniabilizationCoinControl(uint confirm_target) +{ + CCoinControl coin_control; + coin_control.m_avoid_address_reuse = true; + coin_control.m_avoid_partial_spends = true; + coin_control.m_allow_other_inputs = false; + coin_control.m_signal_bip125_rbf = true; + coin_control.m_confirm_target = confirm_target; + // we'll automatically bump the fee if economical ends up not confirming by the next deniabilization cycle + coin_control.m_fee_mode = FeeEstimateMode::ECONOMICAL; + return coin_control; +} + +CFeeRate CalculateDeniabilizationFeeRate(const CWallet& wallet, uint confirm_target) +{ + CCoinControl coin_control = SetupDeniabilizationCoinControl(confirm_target); + + CFeeRate requiredFeeRate = GetRequiredFeeRate(wallet); + FeeCalculation fee_calc; + CFeeRate minFeeRate = GetMinimumFeeRate(wallet, coin_control, &fee_calc); + if (fee_calc.reason == FeeReason::FALLBACK || requiredFeeRate > minFeeRate) + return requiredFeeRate; + return minFeeRate; +} + +static CAmount CalculateDeniabilizationTxFee(const CTxDestination& shared_destination, CAmount total_value, uint num_utxos, const CFeeRate& fee_rate) +{ + Assert(num_utxos > 0); + uint deniabilization_tx_size = CalculateDeniabilizationTxSize(shared_destination, total_value, num_utxos); + return fee_rate.GetFee(deniabilization_tx_size); +} + +CAmount CalculateDeniabilizationFeeEstimate(const CTxDestination& shared_destination, CAmount total_value, uint num_utxos, uint deniabilization_cycles, const CFeeRate& fee_rate) +{ + float deniabilizationProbability = CalculateDeniabilizationProbability(deniabilization_cycles); + // convert to integer percent to truncate and check for zero probability + uint deniabilizationProbabilityPercent = deniabilizationProbability * 100; + if (deniabilizationProbabilityPercent == 0) { + return 0; + } + + // this cycle will use all the UTXOs, while following cycles will have just one UTXO + CAmount deniabilizationFee = CalculateDeniabilizationTxFee(shared_destination, total_value, num_utxos, fee_rate); + + // calculate the fees from future deniabilization cycles + CAmount futureDeniabilizationFee = CalculateDeniabilizationFeeEstimate(shared_destination, total_value / NUM_DENIABILIZATION_OUTPUTS, 1, deniabilization_cycles + 1, fee_rate) * 2; + + // if it's worthwhile to do future deniabilizations then add them to this cycle estimate + if (IsDeniabilizationWorthwhile(total_value, deniabilizationFee + futureDeniabilizationFee)) { + deniabilizationFee += futureDeniabilizationFee; + } + return deniabilizationFee; +} + +util::Result CreateDeniabilizationTransaction( + CWallet& wallet, + const std::set& inputs, + uint confirm_target, + uint deniabilization_cycles, + bool sign, + bool& insufficient_amount) +{ + if (inputs.empty()) { + return util::Error{_("Inputs must not be empty")}; + } + + CCoinControl coin_control = SetupDeniabilizationCoinControl(confirm_target); + for (const auto& input : inputs) { + coin_control.Select(input); + } + Assert(coin_control.HasSelected()); + CFeeRate deniabilization_fee_rate = CalculateDeniabilizationFeeRate(wallet, confirm_target); + coin_control.m_feerate = deniabilization_fee_rate; + + LOCK(wallet.cs_wallet); + + FastRandomContext rng_fast; + CoinSelectionParams coin_selection_params{rng_fast}; + auto res_fetch_inputs = FetchSelectedInputs(wallet, coin_control, coin_selection_params); + if (!res_fetch_inputs) { + return util::Error{util::ErrorString(res_fetch_inputs)}; + } + PreSelectedInputs preset_inputs = *res_fetch_inputs; + CAmount total_amount = preset_inputs.total_amount; + + // validate that all UTXOs share the same address + std::optional op_shared_destination; + for (const auto& coin : preset_inputs.coins) { + CTxDestination destination = CNoDestination(); + if (ExtractDestination(coin->txout.scriptPubKey, destination) && !op_shared_destination) { + op_shared_destination = destination; + } + if (!op_shared_destination || !(*op_shared_destination == destination)) { + return util::Error{_("Input addresses must all match.")}; + } + } + Assert(op_shared_destination); + CTxDestination shared_destination = *op_shared_destination; + + CFeeRate discard_feerate = GetDiscardRate(wallet); + CAmount dust_threshold = GetDustThreshold(CTxOut(total_amount, GetScriptForDestination(shared_destination)), discard_feerate); + + // deniabilize the UTXOs by splitting the value randomly + // find a split that leaves enough amount post split to finish the deniabilization process in each new UTXO + CAmount min_post_split_amount = CalculateDeniabilizationFeeEstimate(shared_destination, total_amount / NUM_DENIABILIZATION_OUTPUTS, 1, deniabilization_cycles + 1, deniabilization_fee_rate) + dust_threshold; + CAmount estimated_tx_fee = CalculateDeniabilizationTxFee(shared_destination, total_amount, preset_inputs.coins.size(), deniabilization_fee_rate); + + CAmount total_random_range = total_amount - min_post_split_amount * NUM_DENIABILIZATION_OUTPUTS - estimated_tx_fee; + if (total_random_range < 0) { + insufficient_amount = true; + return util::Error{strprintf(_("Insufficient amount (%d) for a deniabilization transaction, min amount (%d), tx fee (%d)."), total_amount, min_post_split_amount, estimated_tx_fee)}; + } + + std::optional op_output_type = OutputTypeFromDestination(shared_destination); + if (!op_output_type) { + op_output_type = wallet.m_default_change_type; + } + if (!op_output_type) { + return util::Error{_("Unable to determine output type.")}; + } + OutputType output_type = *op_output_type; + + const int num_recipients = NUM_DENIABILIZATION_OUTPUTS; + std::vector recipients(num_recipients); + std::list reservedests; + constexpr bool reservdest_internal = false; // TODO: Should this be "true" or "false". What does "internal" mean? + for (int recipient_index = 0; recipient_index < num_recipients; recipient_index++) { + bool lastRecipient = recipient_index == (num_recipients - 1); + if (!lastRecipient) { + // all recipients except for the last one, + // calculate a random range based on the remaining total random range and the number of remaining recipients + // then generate a random amount within that range + CAmount random_range = total_random_range / (num_recipients - recipient_index - 1); + CAmount random_amount = 0; + if (random_range > 0) { + random_amount = GetRand(random_range); + Assert(total_random_range >= random_amount); + total_random_range -= random_amount; + } + recipients[recipient_index].nAmount = min_post_split_amount + random_amount; + } else { + // the last recipient takes any leftover random amount and the estimated fee + recipients[recipient_index].nAmount = min_post_split_amount + total_random_range + estimated_tx_fee; + } + + // the last recipient pays the tx fees + recipients[recipient_index].fSubtractFeeFromAmount = lastRecipient; + + auto& reservedest = reservedests.emplace_back(&wallet, output_type); + CTxDestination dest; + auto op_dest = reservedest.GetReservedDestination(reservdest_internal); + if (!op_dest) { + return util::Error{_("Failed to reserve a new address.") + Untranslated(" ") + util::ErrorString(op_dest)}; + } + dest = *op_dest; + recipients[recipient_index].scriptPubKey = GetScriptForDestination(dest); + if (lastRecipient) { + // we don't expect to get change, but we provide the address to prevent CreateTransactionInternal from generating a change address + coin_control.destChange = dest; + } + } + + CAmount recipient_amount = std::accumulate(recipients.cbegin(), recipients.cend(), CAmount{0}, [](CAmount sum, const CRecipient& recipient) { return sum + recipient.nAmount; }); + Assert(total_amount == recipient_amount); + + constexpr int RANDOM_CHANGE_POSITION = -1; + auto res = CreateTransactionInternal(wallet, recipients, RANDOM_CHANGE_POSITION, coin_control, sign); + if (!res) { + TRACE4(coin_selection, normal_create_tx_internal, wallet.GetName().c_str(), false, 0, 0); + return res; + } + + // the transaction was created successfully + TRACE4(coin_selection, normal_create_tx_internal, wallet.GetName().c_str(), true, res->fee, res->change_pos); + // 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); + // add to the address book and commit the reserved destinations + for (auto& reservedest : reservedests) { + auto op_dest = reservedest.GetReservedDestination(reservdest_internal); + Assert(op_dest); + wallet.SetAddressBook(*op_dest, "deniability", AddressPurpose::RECEIVE); + reservedest.KeepDestination(); + } + return res; +} + } // namespace wallet diff --git a/src/wallet/spend.h b/src/wallet/spend.h index cc9ccf30118f66..4f482d56afd162 100644 --- a/src/wallet/spend.h +++ b/src/wallet/spend.h @@ -223,6 +223,38 @@ util::Result CreateTransaction(CWallet& wallet, const * calling CreateTransaction(); */ bool FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& nFeeRet, int& nChangePosInOut, bilingual_str& error, bool lockUnspents, const std::set& setSubtractFeeFromOutputs, CCoinControl); + +/** + * Calculate the probability for a deniabilization transaction given the number of deniabilization cycles already performed + */ +float CalculateDeniabilizationProbability(uint deniabilization_cycles); + +/** + * Determine if it's worth performing deniabilization given a coin amount and fee estimate (see CalculateDeniabilizationFeeEstimate) + */ +bool IsDeniabilizationWorthwhile(CAmount total_value, CAmount fee_estimate); + +/** + * Setup a coin control to be used in deniabilization transactions + */ +CCoinControl SetupDeniabilizationCoinControl(uint confirm_target); + +/** + * Estimate the total deniabilization transaction fees for a given set of UTXOs that share an input destination + */ +CAmount CalculateDeniabilizationFeeEstimate(const CTxDestination& shared_destination, CAmount total_value, uint num_utxos, uint deniabilization_cycles, const CFeeRate& fee_rate); + +/** + * Calculate the fee rate for a deniabilization transaction + */ +CFeeRate CalculateDeniabilizationFeeRate(const CWallet& wallet, uint confirm_target); + +/** + * Create a deniabilization transaction with the provided set of inputs (must share the same destination) + * confirm_target is the confirmation target for the deniabilization transaction + * deniabilization_cycles is the number of deniabilization cycles these inputs have already had + */ +util::Result CreateDeniabilizationTransaction(CWallet& wallet, const std::set& inputs, uint confirm_target, uint deniabilization_cycles, bool sign, bool& insufficient_amount); } // namespace wallet #endif // BITCOIN_WALLET_SPEND_H