Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F13115767
D7807.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
14 KB
Subscribers
None
D7807.diff
View Options
diff --git a/src/dummywallet.cpp b/src/dummywallet.cpp
--- a/src/dummywallet.cpp
+++ b/src/dummywallet.cpp
@@ -31,10 +31,10 @@
void DummyWalletInit::AddWalletOptions() const {
std::vector<std::string> opts = {
"-avoidpartialspends", "-disablewallet", "-fallbackfee=<amt>",
- "-keypool=<n>", "-maxtxfee=<amt>", "-mintxfee=<amt>", "-paytxfee=<amt>",
- "-rescan", "-salvagewallet", "-spendzeroconfchange", "-upgradewallet",
- "-wallet=<path>", "-walletbroadcast", "-walletdir=<dir>",
- "-walletnotify=<cmd>", "-zapwallettxes=<mode>",
+ "-keypool=<n>", "-maxapsfee=<n>", "-maxtxfee=<amt>", "-mintxfee=<amt>",
+ "-paytxfee=<amt>", "-rescan", "-salvagewallet", "-spendzeroconfchange",
+ "-upgradewallet", "-wallet=<path>", "-walletbroadcast",
+ "-walletdir=<dir>", "-walletnotify=<cmd>", "-zapwallettxes=<mode>",
// Wallet debug options
"-dblogsize=<n>", "-flushwallet", "-privdb", "-walletrejectlongchains"};
gArgs.AddHiddenArgs(opts);
diff --git a/src/wallet/init.cpp b/src/wallet/init.cpp
--- a/src/wallet/init.cpp
+++ b/src/wallet/init.cpp
@@ -61,6 +61,13 @@
strprintf("Set key pool size to <n> (default: %u)",
DEFAULT_KEYPOOL_SIZE),
ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
+ gArgs.AddArg(
+ "-maxapsfee=<n>",
+ strprintf(
+ "Spend up to this amount in additional (absolute) fees (in %s) if "
+ "it allows the use of partial spend avoidance (default: %s)",
+ CURRENCY_UNIT, FormatMoney(DEFAULT_MAX_AVOIDPARTIALSPEND_FEE)),
+ ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
gArgs.AddArg(
"-maxtxfee=<amt>",
strprintf("Maximum total fees (in %s) to use in a single wallet "
diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h
--- a/src/wallet/wallet.h
+++ b/src/wallet/wallet.h
@@ -16,6 +16,7 @@
#include <ui_interface.h>
#include <util/strencodings.h>
#include <util/system.h>
+#include <util/translation.h>
#include <validationinterface.h>
#include <wallet/coinselection.h>
#include <wallet/crypter.h>
@@ -76,6 +77,19 @@
static const Amount DEFAULT_FALLBACK_FEE = Amount::zero();
//! -mintxfee default
static const Amount DEFAULT_TRANSACTION_MINFEE_PER_KB = 1000 * SATOSHI;
+/**
+ * maximum fee increase allowed to do partial spend avoidance, even for nodes
+ * with this feature disabled by default
+ *
+ * A value of -1 disables this feature completely.
+ * A value of 0 (current default) means to attempt to do partial spend
+ * avoidance, and use its results if the fees remain *unchanged* A value > 0
+ * means to do partial spend avoidance if the fee difference against a regular
+ * coin selection instance is in the range [0..value].
+ */
+static const Amount DEFAULT_MAX_AVOIDPARTIALSPEND_FEE = Amount::zero();
+//! discourage APS fee higher than this amount
+constexpr Amount HIGH_APS_FEE{COIN / 10000};
//! minimum recommended increment for BIP 125 replacement txs
static const Amount WALLET_INCREMENTAL_RELAY_FEE(5000 * SATOSHI);
//! Default for -spendzeroconfchange
@@ -770,6 +784,12 @@
*/
int m_last_block_processed_height GUARDED_BY(cs_wallet) = -1;
+ bool CreateTransactionInternal(interfaces::Chain::Lock &locked_chain,
+ const std::vector<CRecipient> &vecSend,
+ CTransactionRef &tx, Amount &nFeeRet,
+ int &nChangePosInOut, bilingual_str &error,
+ const CCoinControl &coin_control, bool sign);
+
public:
const CChainParams &chainParams;
/*
@@ -1123,6 +1143,8 @@
* -fallbackfee
*/
CFeeRate m_fallback_fee{DEFAULT_FALLBACK_FEE};
+ //! note: this is absolute fee, not fee rate
+ Amount m_max_aps_fee{DEFAULT_MAX_AVOIDPARTIALSPEND_FEE};
OutputType m_default_address_type{DEFAULT_ADDRESS_TYPE};
OutputType m_default_change_type{DEFAULT_CHANGE_TYPE};
/**
diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp
--- a/src/wallet/wallet.cpp
+++ b/src/wallet/wallet.cpp
@@ -2880,15 +2880,17 @@
return m_default_address_type;
}
-bool CWallet::CreateTransaction(interfaces::Chain::Lock &locked_chainIn,
- const std::vector<CRecipient> &vecSend,
- CTransactionRef &tx, Amount &nFeeRet,
- int &nChangePosInOut, bilingual_str &error,
- const CCoinControl &coinControl, bool sign) {
+bool CWallet::CreateTransactionInternal(interfaces::Chain::Lock &locked_chainIn,
+ const std::vector<CRecipient> &vecSend,
+ CTransactionRef &tx, Amount &nFeeRet,
+ int &nChangePosInOut,
+ bilingual_str &error,
+ const CCoinControl &coin_control,
+ bool sign) {
Amount nValue = Amount::zero();
const OutputType change_type = TransactionChangeType(
- coinControl.m_change_type ? *coinControl.m_change_type
- : m_default_change_type,
+ coin_control.m_change_type ? *coin_control.m_change_type
+ : m_default_change_type,
vecSend);
ReserveDestination reservedest(this, change_type);
int nChangePosRequest = nChangePosInOut;
@@ -2921,7 +2923,7 @@
LOCK(cs_wallet);
std::vector<COutput> vAvailableCoins;
- AvailableCoins(*locked_chain, vAvailableCoins, true, &coinControl);
+ AvailableCoins(*locked_chain, vAvailableCoins, true, &coin_control);
// Parameters for coin selection, init with dummy
CoinSelectionParams coin_selection_params;
@@ -2931,8 +2933,8 @@
CScript scriptChange;
// coin control: send change to custom address
- if (!boost::get<CNoDestination>(&coinControl.destChange)) {
- scriptChange = GetScriptForDestination(coinControl.destChange);
+ if (!boost::get<CNoDestination>(&coin_control.destChange)) {
+ scriptChange = GetScriptForDestination(coin_control.destChange);
// no coin control: send change to newly generated address
} else {
@@ -2965,7 +2967,7 @@
GetSerializeSize(change_prototype_txout);
// Get the fee rate to use effective values in coin selection
- CFeeRate nFeeRateNeeded = GetMinimumFeeRate(*this, coinControl);
+ CFeeRate nFeeRateNeeded = GetMinimumFeeRate(*this, coin_control);
nFeeRet = Amount::zero();
bool pick_new_inputs = true;
@@ -3042,7 +3044,7 @@
this);
coin_selection_params.effective_fee = nFeeRateNeeded;
if (!SelectCoins(vAvailableCoins, nValueToSelect, setCoins,
- nValueIn, coinControl, coin_selection_params,
+ nValueIn, coin_control, coin_selection_params,
bnb_used)) {
// If BnB was used, it was the first pass. No longer the
// first pass and continue loop with knapsack.
@@ -3095,13 +3097,13 @@
CTransaction txNewConst(txNew);
int nBytes = CalculateMaximumSignedTxSize(
- txNewConst, this, coinControl.fAllowWatchOnly);
+ txNewConst, this, coin_control.fAllowWatchOnly);
if (nBytes < 0) {
error = _("Signing transaction failed");
return false;
}
- Amount nFeeNeeded = GetMinimumFee(*this, nBytes, coinControl);
+ Amount nFeeNeeded = GetMinimumFee(*this, nBytes, coin_control);
if (nFeeRet >= nFeeNeeded) {
// Reduce fee to only the needed amount if possible. This
@@ -3121,7 +3123,7 @@
unsigned int tx_size_with_change =
nBytes + coin_selection_params.change_output_size + 2;
Amount fee_needed_with_change =
- GetMinimumFee(*this, tx_size_with_change, coinControl);
+ GetMinimumFee(*this, tx_size_with_change, coin_control);
Amount minimum_value_for_change = GetDustThreshold(
change_prototype_txout, chain().relayDustFee());
if (nFeeRet >=
@@ -3253,6 +3255,46 @@
return true;
}
+bool CWallet::CreateTransaction(interfaces::Chain::Lock &locked_chain,
+ const std::vector<CRecipient> &vecSend,
+ CTransactionRef &tx, Amount &nFeeRet,
+ int &nChangePosInOut, bilingual_str &error,
+ const CCoinControl &coin_control, bool sign) {
+ int nChangePosIn = nChangePosInOut;
+ CTransactionRef tx2 = tx;
+ bool res =
+ CreateTransactionInternal(locked_chain, vecSend, tx, nFeeRet,
+ nChangePosInOut, error, coin_control, sign);
+ // try with avoidpartialspends unless it's enabled already
+ if (res &&
+ nFeeRet >
+ Amount::zero() /* 0 means non-functional fee rate estimation */
+ && m_max_aps_fee > (-1 * SATOSHI) &&
+ !coin_control.m_avoid_partial_spends) {
+ CCoinControl tmp_cc = coin_control;
+ tmp_cc.m_avoid_partial_spends = true;
+ Amount nFeeRet2;
+ int nChangePosInOut2 = nChangePosIn;
+ // fired and forgotten; if an error occurs, we discard the results
+ bilingual_str error2;
+ if (CreateTransactionInternal(locked_chain, vecSend, tx2, nFeeRet2,
+ nChangePosInOut2, error2, tmp_cc, sign)) {
+ // if fee of this alternative one is within the range of the max
+ // fee, we use this one
+ const bool use_aps = nFeeRet2 <= nFeeRet + m_max_aps_fee;
+ WalletLogPrintf(
+ "Fee non-grouped = %lld, grouped = %lld, using %s\n", nFeeRet,
+ nFeeRet2, use_aps ? "grouped" : "non-grouped");
+ if (use_aps) {
+ tx = tx2;
+ nFeeRet = nFeeRet2;
+ nChangePosInOut = nChangePosInOut2;
+ }
+ }
+ }
+ return res;
+}
+
void CWallet::CommitTransaction(
CTransactionRef tx, mapValue_t mapValue,
std::vector<std::pair<std::string, std::string>> orderForm) {
@@ -4172,6 +4214,23 @@
walletInstance->m_min_fee = CFeeRate(n);
}
+ if (gArgs.IsArgSet("-maxapsfee")) {
+ Amount n = Amount::zero();
+ if (gArgs.GetArg("-maxapsfee", "") == "-1") {
+ n = -1 * SATOSHI;
+ } else if (!ParseMoney(gArgs.GetArg("-maxapsfee", ""), n)) {
+ error = AmountErrMsg("maxapsfee", gArgs.GetArg("-maxapsfee", ""));
+ return nullptr;
+ }
+ if (n > HIGH_APS_FEE) {
+ warnings.push_back(
+ AmountHighWarn("-maxapsfee") + Untranslated(" ") +
+ _("This is the maximum transaction fee you pay to prioritize "
+ "partial spend avoidance over regular coin selection."));
+ }
+ walletInstance->m_max_aps_fee = n;
+ }
+
if (gArgs.IsArgSet("-fallbackfee")) {
Amount nFeePerK = Amount::zero();
if (!ParseMoney(gArgs.GetArg("-fallbackfee", ""), nFeePerK)) {
diff --git a/test/functional/wallet_groups.py b/test/functional/wallet_groups.py
--- a/test/functional/wallet_groups.py
+++ b/test/functional/wallet_groups.py
@@ -15,8 +15,9 @@
class WalletGroupTest(BitcoinTestFramework):
def set_test_params(self):
self.setup_clean_chain = True
- self.num_nodes = 3
- self.extra_args = [[], [], ['-avoidpartialspends']]
+ self.num_nodes = 4
+ self.extra_args = [
+ [], [], ['-avoidpartialspends'], ["-maxapsfee=0.0001"]]
self.rpc_timeout = 120
def skip_test_if_missing_module(self):
@@ -62,6 +63,52 @@
assert_approx(v[0], 0.2)
assert_approx(v[1], 1.3, 0.0001)
+ # Test 'avoid partial if warranted, even if disabled'
+ self.sync_all()
+ self.nodes[0].generate(1)
+ # Nodes 1-2 now have confirmed UTXOs (letters denote destinations):
+ # Node #1: Node #2:
+ # - A 1.0 - D0 1.0
+ # - B0 1.0 - D1 0.5
+ # - B1 0.5 - E0 1.0
+ # - C0 1.0 - E1 0.5
+ # - C1 0.5 - F ~1.3
+ # - D ~0.3
+ assert_approx(self.nodes[1].getbalance(), 4.3, 0.0001)
+ assert_approx(self.nodes[2].getbalance(), 4.3, 0.0001)
+ # Sending 1.4 btc should pick one 1.0 + one more. For node #1,
+ # this could be (A / B0 / C0) + (B1 / C1 / D). We ensure that it is
+ # B0 + B1 or C0 + C1, because this avoids partial spends while not being
+ # detrimental to transaction cost
+ txid3 = self.nodes[1].sendtoaddress(self.nodes[0].getnewaddress(), 1.4)
+ tx3 = self.nodes[1].getrawtransaction(txid3, True)
+ # tx3 should have 2 inputs and 2 outputs
+ assert_equal(2, len(tx3["vin"]))
+ assert_equal(2, len(tx3["vout"]))
+ # the accumulated value should be 1.5, so the outputs should be
+ # ~0.1 and 1.4 and should come from the same destination
+ values = sorted([vout["value"] for vout in tx3["vout"]])
+ assert_approx(values[0], 0.1, 0.0001)
+ assert_approx(values[1], 1.4)
+
+ input_txids = [vin["txid"] for vin in tx3["vin"]]
+ input_addrs = [self.nodes[1].gettransaction(
+ txid)['details'][0]['address'] for txid in input_txids]
+ assert_equal(input_addrs[0], input_addrs[1])
+ # Node 2 enforces avoidpartialspends so needs no checking here
+
+ # Test wallet option maxapsfee with Node 3
+ addr_aps = self.nodes[3].getnewaddress()
+ self.nodes[0].sendtoaddress(addr_aps, 1.0)
+ self.nodes[0].sendtoaddress(addr_aps, 1.0)
+ self.nodes[0].generate(1)
+ txid4 = self.nodes[3].sendtoaddress(self.nodes[0].getnewaddress(), 0.1)
+ tx4 = self.nodes[3].getrawtransaction(txid4, True)
+ # tx4 should have 2 inputs and 2 outputs although one output would
+ # have been enough and the transaction caused higher fees
+ assert_equal(2, len(tx4["vin"]))
+ assert_equal(2, len(tx4["vout"]))
+
# Empty out node2's wallet
self.nodes[2].sendtoaddress(address=self.nodes[0].getnewaddress(
), amount=self.nodes[2].getbalance(), subtractfeefromamount=True)
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Mar 1, 11:59 (37 m, 53 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5187753
Default Alt Text
D7807.diff (14 KB)
Attached To
D7807: wallet: always do avoid partial spends if fees are within a specified range
Event Timeline
Log In to Comment