mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-09-11 21:20:39 +02:00
fees: return mempool estimates when it's lower than block policy
Integrate MemPoolFeeRateEstimator into FeeRateEstimatorManager. When both estimators succeed, select the lower of the block policy and mempool estimates. When either estimator fails, return its error instead of falling back to the block policy estimate: if the mempool estimator cannot produce an estimate, the combined estimate fails. Callers that want a block-policy-only estimate can request it explicitly via fee_rate_estimator option. estimatesmartfee now emits the estimator field only for successful manager-selected estimates. Add a test that ensures estimatesmartfee returns the mempool fee rate estimate when it is lower than the block policy estimate, and can request the mempool policy estimator explicitly Two wallet functional tests also need adjusting. When the mempool is too sparse to fill its percentile buckets, MemPoolFeeRateEstimator returns a relayable floor of max(min relay fee, mempool min fee), so in regtest getFeeRateEstimate now returns the min relay fee where the wallet previously had no estimate and fell back to a higher rate: - wallet_taproot.py: the cleanup sendall used automatic fee estimation. GetMinimumFeeRate previously fell back to the wallet fallback fee (fallbackfee, 20 sat/vB in the test framework); it now uses the min relay fee floor. At that lower feerate the wallet's underestimate of the taproot script-path witness size drops the effective feerate below min relay, so the transaction is rejected. Pin fee_rate=20 to match the framework fallbackfee. - wallet_bumpfee.py: GetDiscardRate() previously fell back to the wallet discard rate (-discardfee); it now takes the minimum of that and the estimate, so the min relay fee floor collapses the discard rate down to the dust relay feerate. The lower discard rate reduces the cost of change, so the ~614 sat leftover change in test_dust_to_fee is now retained instead of being dropped to fee. Rework the test to leave a sub-dust (20/270 sat) change that is dropped regardless of the discard rate. Co-authored-by: willcl-ark <will@256k1.dev>
This commit is contained in:
@@ -11,6 +11,9 @@ import time
|
||||
|
||||
from test_framework.messages import (
|
||||
COIN,
|
||||
DEFAULT_BLOCK_RESERVED_WEIGHT,
|
||||
MAX_BLOCK_WEIGHT,
|
||||
WITNESS_SCALE_FACTOR,
|
||||
)
|
||||
from test_framework.test_framework import BitcoinTestFramework
|
||||
from test_framework.util import (
|
||||
@@ -27,6 +30,7 @@ MAX_FILE_AGE = 60
|
||||
SECONDS_PER_HOUR = 60 * 60
|
||||
MIN_BUCKET_FEERATE = Decimal(100) / Decimal(COIN)
|
||||
TXS_COUNT = 24
|
||||
BLOCK_POLICY_ESTIMATOR_ERROR = "Insufficient data or no feerate found"
|
||||
|
||||
def small_txpuzzle_randfee(
|
||||
wallet, from_node, conflist, unconflist, amount, min_fee, fee_increment, batch_reqs
|
||||
@@ -140,6 +144,14 @@ def check_fee_estimates_btw_modes(node, expected_conservative, expected_economic
|
||||
assert_equal(fee_est_economical, expected_economical)
|
||||
assert_equal(fee_est_default, expected_economical)
|
||||
|
||||
def verify_estimate_response(estimate, feerate, errors):
|
||||
if feerate:
|
||||
assert_equal(estimate["feerate"], feerate)
|
||||
if errors:
|
||||
assert all(err in estimate["errors"] for err in errors)
|
||||
else:
|
||||
assert "errors" not in estimate
|
||||
|
||||
|
||||
class EstimateFeeTest(BitcoinTestFramework):
|
||||
def set_test_params(self):
|
||||
@@ -330,7 +342,7 @@ class EstimateFeeTest(BitcoinTestFramework):
|
||||
|
||||
# Start node and ensure the fee_estimates.dat file was not read
|
||||
self.start_node(0)
|
||||
assert_equal(self.nodes[0].estimatesmartfee(1, "economical", {"fee_rate_estimator": "block_policy"})["errors"], ["Insufficient data or no feerate found"])
|
||||
assert_equal(self.nodes[0].estimatesmartfee(1, "economical", {"fee_rate_estimator": "block_policy"})["errors"], [BLOCK_POLICY_ESTIMATOR_ERROR])
|
||||
|
||||
|
||||
def test_estimate_dat_is_flushed_periodically(self):
|
||||
@@ -413,35 +425,42 @@ class EstimateFeeTest(BitcoinTestFramework):
|
||||
self.sync_blocks()
|
||||
assert_equal(self.nodes[0].estimatesmartfee(1, "economical", {"fee_rate_estimator": "block_policy"})["errors"], ["Insufficient data or no feerate found"])
|
||||
|
||||
def broadcast_many(self, broadcaster, feerate, count, miner=None):
|
||||
def broadcast_and_maybe_mine(self, broadcaster, feerate, txs, blocks=1, miner=None):
|
||||
"""Broadcast and maybe mine some number of transactions with a specified fee rate."""
|
||||
tx_batch = []
|
||||
for _ in range(count):
|
||||
tx = self.wallet.create_self_transfer(fee_rate=feerate, utxo_to_spend=self.confutxo.pop(0))
|
||||
self.memutxo.append(tx["new_utxo"])
|
||||
tx_batch.append(tx)
|
||||
# To speed up the test, submit the transactions in batches to the nodes directly
|
||||
# avoiding having to wait for p2p to propagate them between the nodes.
|
||||
batch_send_tx = [broadcaster.sendrawtransaction.get_request(hexstring=tx["hex"]) for tx in tx_batch]
|
||||
for node in self.nodes:
|
||||
node.batch(batch_send_tx)
|
||||
self.sync_mempools(wait=0.1, nodes=[self.nodes[0], self.nodes[1], self.nodes[2]])
|
||||
if miner:
|
||||
mined = miner.getblock(self.generate(miner, 1)[0], True)["tx"]
|
||||
self.update_utxo(mined)
|
||||
for _ in range(blocks):
|
||||
tx_batch = []
|
||||
for _ in range(txs):
|
||||
tx = self.wallet.create_self_transfer(fee_rate=feerate, utxo_to_spend=self.confutxo.pop(0))
|
||||
self.memutxo.append(tx["new_utxo"])
|
||||
tx_batch.append(tx)
|
||||
# To speed up the test, submit the transactions in batches to the nodes directly
|
||||
# avoiding having to wait for p2p to propagate them between the nodes.
|
||||
batch_send_tx = [broadcaster.sendrawtransaction.get_request(hexstring=tx["hex"]) for tx in tx_batch]
|
||||
for node in self.nodes:
|
||||
node.batch(batch_send_tx)
|
||||
self.sync_mempools(wait=0.1, nodes=[self.nodes[0], self.nodes[1], self.nodes[2]])
|
||||
if miner:
|
||||
mined = miner.getblock(self.generate(miner, 1)[0], True)["tx"]
|
||||
self.update_utxo(mined)
|
||||
|
||||
def send_transactions(self, utxos, fee_rate, target_vsize):
|
||||
for utxo in utxos:
|
||||
self.wallet.send_self_transfer(
|
||||
from_node=self.nodes[0],
|
||||
utxo_to_spend=utxo,
|
||||
fee_rate=fee_rate,
|
||||
target_vsize=target_vsize,
|
||||
)
|
||||
|
||||
def test_estimation_modes(self):
|
||||
low_feerate = Decimal("0.001")
|
||||
high_feerate = Decimal("0.005")
|
||||
# Broadcast and mine high fee transactions for the first 12 blocks.
|
||||
for _ in range(12):
|
||||
self.broadcast_many(self.nodes[1], high_feerate, TXS_COUNT, self.nodes[2])
|
||||
self.broadcast_and_maybe_mine(self.nodes[1], high_feerate, TXS_COUNT, 12, self.nodes[2])
|
||||
check_fee_estimates_btw_modes(self.nodes[0], high_feerate, high_feerate)
|
||||
|
||||
# We now track 12 blocks; short horizon stats will start decaying.
|
||||
# Broadcast and mine low fee transactions for the next 4 blocks.
|
||||
for _ in range(4):
|
||||
self.broadcast_many(self.nodes[1], low_feerate, TXS_COUNT, self.nodes[2])
|
||||
self.broadcast_and_maybe_mine(self.nodes[1], low_feerate, TXS_COUNT, 4, self.nodes[2])
|
||||
# conservative mode will consider longer time horizons while economical mode does not
|
||||
# Check the fee estimates for both modes after mining low fee transactions.
|
||||
check_fee_estimates_btw_modes(self.nodes[0], high_feerate, low_feerate)
|
||||
@@ -450,10 +469,60 @@ class EstimateFeeTest(BitcoinTestFramework):
|
||||
feerate_0_5_s_per_vb = MIN_BUCKET_FEERATE * 5
|
||||
feerate_1_s_per_vb = Decimal(1000) / Decimal(COIN)
|
||||
for i in range(6):
|
||||
self.broadcast_many(self.nodes[1], feerate_0_5_s_per_vb, TXS_COUNT)
|
||||
self.broadcast_many(self.nodes[1], feerate_1_s_per_vb, TXS_COUNT, self.nodes[2])
|
||||
self.broadcast_and_maybe_mine(self.nodes[1], feerate_0_5_s_per_vb, TXS_COUNT)
|
||||
self.broadcast_and_maybe_mine(self.nodes[1], feerate_1_s_per_vb, TXS_COUNT, 1, self.nodes[2])
|
||||
assert_equal(feerate_0_5_s_per_vb, self.nodes[0].estimatesmartfee(1, "economical", {"fee_rate_estimator": "block_policy"})["feerate"])
|
||||
|
||||
|
||||
def test_estimatesmartfee_return_mempool_estimates(self):
|
||||
node0 = self.nodes[0]
|
||||
miner = self.nodes[1]
|
||||
self.log.info("Ensure node0's mempool is empty at the start")
|
||||
assert_equal(node0.getmempoolinfo()['size'], 0)
|
||||
self.log.info("Test estimatesmartfee with empty mempool and no block policy estimator data")
|
||||
estimate_after_restart = node0.estimatesmartfee(1, "economical", {"fee_rate_estimator": "none"})
|
||||
verify_estimate_response(estimate_after_restart, None, [BLOCK_POLICY_ESTIMATOR_ERROR])
|
||||
self.log.info("Populate block policy estimator with high-feerate history")
|
||||
# Generate high-feerate transactions and mine them over 6 blocks to give block policy data.
|
||||
high_feerate = Decimal("0.004")
|
||||
self.broadcast_and_maybe_mine(node0, high_feerate, TXS_COUNT, 6, miner)
|
||||
self.log.info("Test estimatesmartfee returns block policy estimator estimate when mempool is higher")
|
||||
# Add 10 large insane-feerate transactions enough to generate a block template
|
||||
num_txs = 10
|
||||
target_vsize = int(((MAX_BLOCK_WEIGHT - DEFAULT_BLOCK_RESERVED_WEIGHT) / WITNESS_SCALE_FACTOR) / num_txs)
|
||||
utxos = [self.wallet.get_utxo(confirmed_only=True) for _ in range(num_txs)]
|
||||
insane_feerate = Decimal("0.01")
|
||||
self.send_transactions(utxos, insane_feerate, target_vsize)
|
||||
estimate_after_spike = node0.estimatesmartfee(1, "economical", {"fee_rate_estimator": "none"})
|
||||
verify_estimate_response(estimate_after_spike, high_feerate, [])
|
||||
assert_equal(estimate_after_spike["estimator"], "block_policy")
|
||||
mempool_policy_estimate = node0.estimatesmartfee(1, "economical", {"fee_rate_estimator": "mempool_policy"})
|
||||
verify_estimate_response(mempool_policy_estimate, insane_feerate, [])
|
||||
# Confirm the spike transactions so they leave the mempool; the mined block
|
||||
# keeps the mempool representation healthy. Then broadcast fresh low-feerate
|
||||
# transactions so the mempool estimate is now the lower of the two.
|
||||
self.generate(node0, 1, sync_fun=lambda: None)
|
||||
assert_equal(node0.getmempoolinfo()['size'], 0)
|
||||
low_feerate = Decimal("0.00004")
|
||||
low_utxos = [self.wallet.get_utxo(confirmed_only=True) for _ in range(num_txs)]
|
||||
self.send_transactions(low_utxos, low_feerate, target_vsize)
|
||||
lower_estimate = node0.estimatesmartfee(1, "economical", {"fee_rate_estimator": "none"})
|
||||
verify_estimate_response(lower_estimate, low_feerate, [])
|
||||
|
||||
self.log.info("Test estimatesmartfee returns the fee rate floor when the mempool is empty but healthy")
|
||||
self.generate(node0, 1, sync_fun=lambda: None)
|
||||
assert_equal(node0.getmempoolinfo()['size'], 0)
|
||||
block_policy_estimate = node0.estimatesmartfee(1, "economical", {"fee_rate_estimator": "block_policy"})
|
||||
assert "feerate" in block_policy_estimate
|
||||
# With an empty but healthy mempool the mempool estimator has no percentile data,
|
||||
# so it falls back to the fee rate floor: the max of minrelaytxfee and mempoolminfee.
|
||||
# That floor is lower than the block policy estimate, so the combined estimator returns it.
|
||||
mempool_info = node0.getmempoolinfo()
|
||||
floor = max(mempool_info["minrelaytxfee"], mempool_info["mempoolminfee"])
|
||||
combined_estimate = node0.estimatesmartfee(1, "economical", {"fee_rate_estimator": "none"})
|
||||
verify_estimate_response(combined_estimate, floor, [])
|
||||
assert_equal(combined_estimate["estimator"], "mempool_policy")
|
||||
|
||||
def run_test(self):
|
||||
self.log.info("This test is time consuming, please be patient")
|
||||
self.log.info("Splitting inputs so we can generate tx's")
|
||||
@@ -504,6 +573,10 @@ class EstimateFeeTest(BitcoinTestFramework):
|
||||
self.log.info("Test that estimatesmartfee returns a sub 1s/vb fee rate estimate")
|
||||
self.test_sub_1s_per_vb_estimates()
|
||||
|
||||
self.log.info("Test that estimatesmartfee returns mempool estimates when lower")
|
||||
self.clear_estimates()
|
||||
self.test_estimatesmartfee_return_mempool_estimates()
|
||||
|
||||
self.log.info("Testing that fee estimation is disabled in blocksonly.")
|
||||
self.restart_node(0, ["-blocksonly"])
|
||||
assert_raises_rpc_error(
|
||||
|
||||
Reference in New Issue
Block a user