Skip to content

Commit 3315b13

Browse files
committed
dustdynamic: Support specifying a multiplier (default to 3)
Historically, dustrelayfee was based on 3x the cost to spend an output, so use that for the default multiplier
1 parent fc986f5 commit 3315b13

File tree

9 files changed

+68
-37
lines changed

9 files changed

+68
-37
lines changed

src/init.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -640,8 +640,9 @@ void SetupServerArgs(ArgsManager& argsman)
640640
argsman.AddArg("-acceptnonstdtxn", strprintf("Relay and mine \"non-standard\" transactions (test networks only; default: %u)", DEFAULT_ACCEPT_NON_STD_TXN), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::NODE_RELAY);
641641
argsman.AddArg("-incrementalrelayfee=<amt>", strprintf("Fee rate (in %s/kvB) used to define cost of relay, used for mempool limiting and replacement policy. (default: %s)", CURRENCY_UNIT, FormatMoney(DEFAULT_INCREMENTAL_RELAY_FEE)), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::NODE_RELAY);
642642
argsman.AddArg("-dustrelayfee=<amt>", strprintf("Fee rate (in %s/kvB) used to define dust, the value of an output such that it will cost more than its value in fees at this fee rate to spend it. (default: %s)", CURRENCY_UNIT, FormatMoney(DUST_RELAY_TX_FEE)), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
643-
argsman.AddArg("-dustdynamic=off|target:<blocks>|mempool:<kvB>",
644-
strprintf("Automatically raise dustrelayfee based on either the expected fee to be mined within <blocks> blocks, or to be within the best <kvB> kvB of this node's mempool. (default: %s)",
643+
argsman.AddArg("-dustdynamic=off|[<multiplier>*]target:<blocks>|[<multiplier>*]mempool:<kvB>",
644+
strprintf("Automatically raise dustrelayfee based on either the expected fee to be mined within <blocks> blocks, or to be within the best <kvB> kvB of this node's mempool. If unspecified, multiplier is %s. (default: %s)",
645+
DEFAULT_DUST_RELAY_MULTIPLIER,
645646
DEFAULT_DUST_DYNAMIC), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
646647
argsman.AddArg("-acceptstalefeeestimates", strprintf("Read fee estimates even if they are stale (%sdefault: %u) fee estimates are considered stale if they are %s hours old", "regtest only; ", DEFAULT_ACCEPT_STALE_FEE_ESTIMATES, Ticks<std::chrono::hours>(MAX_FILE_AGE)), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
647648
argsman.AddArg("-bytespersigop", strprintf("Equivalent bytes per sigop in transactions for relay and mining (default: %u)", DEFAULT_BYTES_PER_SIGOP), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);

src/kernel/mempool_options.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ struct MemPoolOptions {
5858
CFeeRate dust_relay_feerate_floor{DUST_RELAY_TX_FEE};
5959
/** Negative for a target number of blocks, positive for target kB into current mempool. */
6060
int32_t dust_relay_target{0};
61+
/** Multiplier for dustdynamic assignments, in thousandths. */
62+
int dust_relay_multiplier{DEFAULT_DUST_RELAY_MULTIPLIER};
6163
/**
6264
* A data carrying output is an unspendable output containing data. The script
6365
* type is designated as TxoutType::NULL_DATA.

src/node/mempool_args.cpp

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
#include <chrono>
2727
#include <cstdint>
2828
#include <memory>
29+
#include <string_view>
30+
#include <utility>
2931

3032
using common::AmountErrMsg;
3133
using kernel::MemPoolLimits;
@@ -44,11 +46,23 @@ void ApplyArgsManOptions(const ArgsManager& argsman, MemPoolLimits& mempool_limi
4446
}
4547
}
4648

47-
util::Result<int32_t> ParseDustDynamicOpt(const std::string& optstr, const unsigned int max_fee_estimate_blocks)
49+
util::Result<std::pair<int32_t, int>> ParseDustDynamicOpt(std::string_view optstr, const unsigned int max_fee_estimate_blocks)
4850
{
4951
if (optstr == "0" || optstr == "off") {
50-
return 0;
51-
} else if (optstr.rfind("target:", 0) == 0) {
52+
return std::pair<int32_t, int>(0, DEFAULT_DUST_RELAY_MULTIPLIER);
53+
}
54+
55+
int multiplier{DEFAULT_DUST_RELAY_MULTIPLIER};
56+
if (auto pos = optstr.find('*'); pos != optstr.npos) {
57+
int64_t parsed;
58+
if ((!ParseFixedPoint(optstr.substr(0, pos), 3, &parsed)) || parsed > std::numeric_limits<int>::max() || parsed < 1) {
59+
return util::Error{_("failed to parse multiplier")};
60+
}
61+
multiplier = parsed;
62+
optstr.remove_prefix(pos + 1);
63+
}
64+
65+
if (optstr.rfind("target:", 0) == 0) {
5266
if (!max_fee_estimate_blocks) {
5367
return util::Error{_("\"target\" mode requires fee estimator (disabled)")};
5468
}
@@ -62,7 +76,7 @@ util::Result<int32_t> ParseDustDynamicOpt(const std::string& optstr, const unsig
6276
if (*val > max_fee_estimate_blocks) {
6377
return util::Error{strprintf(_("target can only be at most %s blocks"), max_fee_estimate_blocks)};
6478
}
65-
return -*val;
79+
return std::pair<int32_t, int>(-*val, multiplier);
6680
} else if (optstr.rfind("mempool:", 0) == 0) {
6781
const auto val = ToIntegral<int32_t>(optstr.substr(8));
6882
if (!val) {
@@ -71,7 +85,7 @@ util::Result<int32_t> ParseDustDynamicOpt(const std::string& optstr, const unsig
7185
if (*val < 1) {
7286
return util::Error{_("mempool position must be at least 1 kB")};
7387
}
74-
return *val;
88+
return std::pair<int32_t, int>(*val, multiplier);
7589
} else {
7690
return util::Error{strprintf(_("\"%s\""), optstr)};
7791
}
@@ -124,7 +138,8 @@ util::Result<void> ApplyArgsManOptions(const ArgsManager& argsman, const CChainP
124138
if (!parsed) {
125139
return util::Error{strprintf(_("Invalid mode for dustdynamic: %s"), util::ErrorString(parsed))};
126140
}
127-
mempool_opts.dust_relay_target = *parsed;
141+
mempool_opts.dust_relay_target = parsed->first;
142+
mempool_opts.dust_relay_multiplier = parsed->second;
128143
}
129144

130145
mempool_opts.permit_bare_pubkey = argsman.GetBoolArg("-permitbarepubkey", DEFAULT_PERMIT_BAREPUBKEY);

src/node/mempool_args.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
#include <util/result.h>
99

1010
#include <cstdint>
11-
#include <string>
11+
#include <string_view>
12+
#include <utility>
1213

1314
class ArgsManager;
1415
class CChainParams;
@@ -17,7 +18,7 @@ namespace kernel {
1718
struct MemPoolOptions;
1819
};
1920

20-
[[nodiscard]] util::Result<int32_t> ParseDustDynamicOpt(const std::string& optstr, unsigned int max_fee_estimate_blocks);
21+
[[nodiscard]] util::Result<std::pair<int32_t, int>> ParseDustDynamicOpt(std::string_view optstr, unsigned int max_fee_estimate_blocks);
2122

2223
/**
2324
* Overlay the options set in \p argsman on top of corresponding members in \p mempool_opts.

src/policy/feerate.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class CFeeRate
7171
std::string ToString(const FeeEstimateMode& fee_estimate_mode = FeeEstimateMode::BTC_KVB) const;
7272
friend CFeeRate operator*(const CFeeRate& f, int a) { return CFeeRate(a * f.nSatoshisPerK); }
7373
friend CFeeRate operator*(int a, const CFeeRate& f) { return CFeeRate(a * f.nSatoshisPerK); }
74+
friend CFeeRate operator/(const CFeeRate& f, int a) { return CFeeRate(f.nSatoshisPerK / a); }
7475

7576
SERIALIZE_METHODS(CFeeRate, obj) { READWRITE(obj.nSatoshisPerK); }
7677
};

src/policy/policy.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ static constexpr unsigned int MAX_STANDARD_SCRIPTSIG_SIZE{1650};
6060
* outputs below the new threshold */
6161
static constexpr unsigned int DUST_RELAY_TX_FEE{3000};
6262
static const std::string DEFAULT_DUST_DYNAMIC{"off"};
63+
static const int DEFAULT_DUST_RELAY_MULTIPLIER{3'000};
6364
static const std::string DEFAULT_SPKREUSE{"allow"};
6465
/** Default for -minrelaytxfee, minimum relay fee for transactions */
6566
static constexpr unsigned int DEFAULT_MIN_RELAY_TX_FEE{1000};

src/rpc/mempool.cpp

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -727,12 +727,19 @@ UniValue MempoolInfoToJSON(const CTxMemPool& pool, const std::optional<MempoolHi
727727
ret.pushKV("incrementalrelayfee", ValueFromAmount(pool.m_opts.incremental_relay_feerate.GetFeePerK()));
728728
ret.pushKV("dustrelayfee", ValueFromAmount(pool.m_opts.dust_relay_feerate.GetFeePerK()));
729729
ret.pushKV("dustrelayfeefloor", ValueFromAmount(pool.m_opts.dust_relay_feerate_floor.GetFeePerK()));
730-
if (pool.m_opts.dust_relay_target < 0) {
731-
ret.pushKV("dustdynamic", strprintf("target:%u", -pool.m_opts.dust_relay_target));
732-
} else if (pool.m_opts.dust_relay_target > 0) {
733-
ret.pushKV("dustdynamic", strprintf("mempool:%u", pool.m_opts.dust_relay_target));
734-
} else {
730+
if (pool.m_opts.dust_relay_target == 0) {
735731
ret.pushKV("dustdynamic", "off");
732+
} else {
733+
std::string multiplier_str = strprintf("%u", pool.m_opts.dust_relay_multiplier / 1000);
734+
if (pool.m_opts.dust_relay_multiplier % 1000) {
735+
multiplier_str += strprintf(".%03u", pool.m_opts.dust_relay_multiplier % 1000);
736+
while (multiplier_str.back() == '0') multiplier_str.pop_back();
737+
}
738+
if (pool.m_opts.dust_relay_target < 0) {
739+
ret.pushKV("dustdynamic", strprintf("%s*target:%u", multiplier_str, -pool.m_opts.dust_relay_target));
740+
} else { // pool.m_opts.dust_relay_target > 0
741+
ret.pushKV("dustdynamic", strprintf("%s*mempool:%u", multiplier_str, pool.m_opts.dust_relay_target));
742+
}
736743
}
737744
ret.pushKV("unbroadcastcount", uint64_t{pool.GetUnbroadcastTxs().size()});
738745
ret.pushKV("fullrbf", (pool.m_opts.rbf_policy == RBFPolicy::Always));

src/txmempool.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,8 @@ void CTxMemPool::UpdateDynamicDustFeerate()
714714
}
715715
}
716716

717+
est_feerate = (est_feerate * m_opts.dust_relay_multiplier) / 1'000;
718+
717719
if (est_feerate < m_opts.dust_relay_feerate_floor) {
718720
est_feerate = m_opts.dust_relay_feerate_floor;
719721
}

test/functional/feature_fee_estimation.py

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -251,22 +251,32 @@ def sanity_check_estimates_range(self):
251251
self.log.info("Final estimates after emptying mempools")
252252
check_estimates(self.nodes[1], self.fees_per_kb)
253253

254+
def test_feerate_dustrelayfee_common(self, node, multiplier, dust_mode, desc, expected_base):
255+
dust_parameter = f"-dustdynamic={dust_mode}".replace('=3*', '=')
256+
self.log.info(f"Test dust limit setting {dust_parameter} (fee estimation for {desc})")
257+
self.restart_node(0, extra_args=[dust_parameter])
258+
assert_equal(node.getmempoolinfo()['dustdynamic'], dust_mode)
259+
with node.busy_wait_for_debug_log([b'Updating dust feerate']):
260+
node.mockscheduler(SECONDS_PER_HOUR)
261+
mempool_info = node.getmempoolinfo()
262+
assert_equal(mempool_info['dustrelayfee'], satoshi_round(expected_base() * multiplier))
263+
assert mempool_info['dustrelayfee'] > mempool_info['dustrelayfeefloor']
264+
265+
def test_feerate_dustrelayfee_target(self, node, multiplier, dustfee_target):
266+
dust_mode = f"{multiplier}*target:{dustfee_target}"
267+
self.test_feerate_dustrelayfee_common(node, multiplier, dust_mode, f'{dustfee_target} blocks', lambda: node.estimaterawfee(dustfee_target, target_success_threshold)['long']['feerate'])
268+
269+
def test_feerate_dustrelayfee_mempool(self, node, multiplier, dustfee_kB):
270+
dust_mode = f"{multiplier}*mempool:{dustfee_kB}"
271+
self.test_feerate_dustrelayfee_common(node, multiplier, dust_mode, f'{dustfee_kB} kB into mempool', lambda: get_feerate_into_mempool(node, dustfee_kB))
272+
254273
def test_feerate_dustrelayfee(self):
255274
node = self.nodes[0]
256275

257276
# test dustdynamic=target:<blocks>
258277
for dustfee_target in (2, 8, 1008):
259-
dust_mode = f"target:{dustfee_target}"
260-
dust_parameter = f"-dustdynamic={dust_mode}"
261-
self.log.info(f"Test dust limit setting {dust_parameter} (fee estimation for {dustfee_target} blocks)")
262-
self.restart_node(0, extra_args=[dust_parameter])
263-
assert_equal(node.getmempoolinfo()['dustdynamic'], dust_mode)
264-
with node.busy_wait_for_debug_log([b'Updating dust feerate']):
265-
node.mockscheduler(SECONDS_PER_HOUR)
266-
fee_est = node.estimaterawfee(dustfee_target, target_success_threshold)['long']['feerate']
267-
mempool_info = node.getmempoolinfo()
268-
assert_equal(mempool_info['dustrelayfee'], fee_est)
269-
assert mempool_info['dustrelayfee'] > mempool_info['dustrelayfeefloor']
278+
for multiplier in (Decimal('0.5'), 1, 3, Decimal('3.3'), 10, Decimal('10.001')):
279+
self.test_feerate_dustrelayfee_target(node, multiplier, dustfee_target)
270280

271281
# Fill mempool up
272282
mempool_size = 0
@@ -288,17 +298,8 @@ def test_feerate_dustrelayfee(self):
288298

289299
# test dustdynamic=mempool:<kB>
290300
for dustfee_kB in (1, 10, 50):
291-
dust_mode = f"mempool:{dustfee_kB}"
292-
dust_parameter = f"-dustdynamic={dust_mode}"
293-
self.log.info(f"Test dust limit setting {dust_parameter} (fee estimation for {dustfee_kB} kB into mempool)")
294-
self.restart_node(0, extra_args=[dust_parameter])
295-
assert_equal(node.getmempoolinfo()['dustdynamic'], dust_mode)
296-
with node.busy_wait_for_debug_log([b'Updating dust feerate']):
297-
node.mockscheduler(SECONDS_PER_HOUR)
298-
feerate_into_mempool = get_feerate_into_mempool(node, dustfee_kB)
299-
mempool_info = node.getmempoolinfo()
300-
assert_equal(mempool_info['dustrelayfee'], feerate_into_mempool)
301-
assert mempool_info['dustrelayfee'] > mempool_info['dustrelayfeefloor']
301+
for multiplier in (Decimal('0.5'), 1, 3, Decimal('3.3'), 10, Decimal('10.001')):
302+
self.test_feerate_dustrelayfee_mempool(node, multiplier, dustfee_kB)
302303

303304
# Restore nodes to a normal state, wiping the mempool
304305
self.stop_node(0)

0 commit comments

Comments
 (0)