mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-03-02 01:36:13 +00:00
Merge bitcoin/bitcoin#33199: fees: enable CBlockPolicyEstimator return sub 1 sat/vb fee rate estimates
8966352df3fc56fd2c00a45eecd292a240a34546 doc: add release notes (ismaelsadeeq)
704a09fe7187d5e4c949dea05baba7fe13bdb676 test: ensure fee estimator provide fee rate estimate < 1 s/vb (ismaelsadeeq)
243e48cf493378acd3a4bde638765544ade9f7b2 fees: delete unused dummy field (ismaelsadeeq)
fc4fbda42af1e84f74acbd8b6a0f384d2711c85b fees: bump fees file version (ismaelsadeeq)
b54dedcc8563286861b2ccda68bc246ad61338c0 fees: reduce `MIN_BUCKET_FEERATE` to 100 (ismaelsadeeq)
Pull request description:
This is a simple PR that updates the block policy estimator’s `MIN_BUCKET_FEERATE` constant to be 100, which is identical to the policy `DEFAULT_MIN_RELAY_TX_FEE`.
This change enables the block policy fee rate estimator to return sub-1 sat/vB fee rate estimates.
The change is correct because the estimator creates buckets of fee rates from
`MIN_BUCKET_FEERATE`,
`MIN_BUCKET_FEERATE` + `FEE_SPACING`,
`MIN_BUCKET_FEERATE` + `2 * FEE_SPACING`,
… up to `MAX_BUCKET_FEERATE`.
This means it will record sub-1 sat/vB fee rates in the buckets and may return them as a fee rate estimate when that bucket is the lowest one with sufficient transactions for a given target.
---
While touching this part of the fee estimator code, this PR got rid of the dummy value persisted in the file
ACKs for top commit:
achow101:
ACK 8966352df3fc56fd2c00a45eecd292a240a34546
musaHaruna:
ACK [8966352](8966352df3)
polespinasa:
ACK 8966352df3fc56fd2c00a45eecd292a240a34546
Tree-SHA512: 9424e0820fcbe124adf5e5d9101a8a2983ba802887101875b2d1856d700c8b563d7a6f6ca3e3db29cb939f786719603aadbf5480d59d55d0951ed1c0caa49868
This commit is contained in:
commit
d0998cbe34
9
doc/release-notes-33199.md
Normal file
9
doc/release-notes-33199.md
Normal file
@ -0,0 +1,9 @@
|
||||
Fee Estimation
|
||||
========================
|
||||
|
||||
- The Bitcoin Core fee estimator minimum fee rate bucket was updated from **1 sat/vB** to **0.1 sat/vB**,
|
||||
which matches the node’s default `minrelayfee`.
|
||||
This means that for a given confirmation target, if a sub-1 sat/vB fee rate bucket is the minimum tracked
|
||||
with sufficient data, its average value will be returned as the fee rate estimate.
|
||||
|
||||
- Note: Restarting a node with this change invalidates previously saved estimates in `fee_estimates.dat`, the fee estimator will start tracking fresh stats.
|
||||
@ -33,8 +33,8 @@
|
||||
#include <utility>
|
||||
|
||||
// The current format written, and the version required to read. Must be
|
||||
// increased to at least 289900+1 on the next breaking change.
|
||||
constexpr int CURRENT_FEES_FILE_VERSION{149900};
|
||||
// increased to at least 309900+1 on the next breaking change.
|
||||
constexpr int CURRENT_FEES_FILE_VERSION{309900};
|
||||
|
||||
static constexpr double INF_FEERATE = 1e99;
|
||||
|
||||
@ -980,7 +980,6 @@ bool CBlockPolicyEstimator::Write(AutoFile& fileout) const
|
||||
try {
|
||||
LOCK(m_cs_fee_estimator);
|
||||
fileout << CURRENT_FEES_FILE_VERSION;
|
||||
fileout << int{0}; // Unused dummy field. Written files may contain any value in [0, 289900]
|
||||
fileout << nBestSeenHeight;
|
||||
if (BlockSpan() > HistoricalBlockSpan()/2) {
|
||||
fileout << firstRecordedHeight << nBestSeenHeight;
|
||||
@ -1004,8 +1003,8 @@ bool CBlockPolicyEstimator::Read(AutoFile& filein)
|
||||
{
|
||||
try {
|
||||
LOCK(m_cs_fee_estimator);
|
||||
int nVersionRequired, dummy;
|
||||
filein >> nVersionRequired >> dummy;
|
||||
int nVersionRequired;
|
||||
filein >> nVersionRequired;
|
||||
if (nVersionRequired > CURRENT_FEES_FILE_VERSION) {
|
||||
throw std::runtime_error{strprintf("File version (%d) too high to be read.", nVersionRequired)};
|
||||
}
|
||||
|
||||
@ -180,13 +180,15 @@ private:
|
||||
static constexpr double SUFFICIENT_TXS_SHORT = 0.5;
|
||||
|
||||
/** Minimum and Maximum values for tracking feerates
|
||||
* The MIN_BUCKET_FEERATE should just be set to the lowest reasonable feerate we
|
||||
* might ever want to track. Historically this has been 1000 since it was
|
||||
* inheriting DEFAULT_MIN_RELAY_TX_FEE and changing it is disruptive as it
|
||||
* invalidates old estimates files. So leave it at 1000 unless it becomes
|
||||
* necessary to lower it, and then lower it substantially.
|
||||
* The MIN_BUCKET_FEERATE should just be set to the lowest reasonable feerate.
|
||||
* MIN_BUCKET_FEERATE has historically inherited DEFAULT_MIN_RELAY_TX_FEE.
|
||||
* It is hardcoded because changing it is disruptive, as it invalidates existing fee
|
||||
* estimate files.
|
||||
*
|
||||
* Whenever DEFAULT_MIN_RELAY_TX_FEE changes, this value should be updated
|
||||
* accordingly. At the same time CURRENT_FEES_FILE_VERSION should be bumped.
|
||||
*/
|
||||
static constexpr double MIN_BUCKET_FEERATE = 1000;
|
||||
static constexpr double MIN_BUCKET_FEERATE = 100;
|
||||
static constexpr double MAX_BUCKET_FEERATE = 1e7;
|
||||
|
||||
/** Spacing of FeeRate buckets
|
||||
|
||||
@ -25,6 +25,8 @@ from test_framework.wallet import MiniWallet
|
||||
|
||||
MAX_FILE_AGE = 60
|
||||
SECONDS_PER_HOUR = 60 * 60
|
||||
MIN_BUCKET_FEERATE = Decimal(100) / Decimal(COIN)
|
||||
TXS_COUNT = 24
|
||||
|
||||
def small_txpuzzle_randfee(
|
||||
wallet, from_node, conflist, unconflist, amount, min_fee, fee_increment, batch_reqs
|
||||
@ -163,8 +165,18 @@ class EstimateFeeTest(BitcoinTestFramework):
|
||||
# Node2 is a stingy miner, that
|
||||
# produces too small blocks (room for only 55 or so transactions)
|
||||
|
||||
def update_utxo(self, mined):
|
||||
# update which txouts are confirmed
|
||||
newmem = []
|
||||
for utx in self.memutxo:
|
||||
if utx["txid"] in mined:
|
||||
self.confutxo.append(utx)
|
||||
else:
|
||||
newmem.append(utx)
|
||||
self.memutxo = newmem
|
||||
|
||||
def transact_and_mine(self, numblocks, mining_node):
|
||||
min_fee = Decimal("0.00001")
|
||||
min_fee = MIN_BUCKET_FEERATE
|
||||
# We will now mine numblocks blocks generating on average 100 transactions between each block
|
||||
# We shuffle our confirmed txout set before each set of transactions
|
||||
# small_txpuzzle_randfee will use the transactions that have inputs already in the chain when possible
|
||||
@ -190,14 +202,7 @@ class EstimateFeeTest(BitcoinTestFramework):
|
||||
node.batch(batch_sendtx_reqs)
|
||||
self.sync_mempools(wait=0.1)
|
||||
mined = mining_node.getblock(self.generate(mining_node, 1)[0], True)["tx"]
|
||||
# update which txouts are confirmed
|
||||
newmem = []
|
||||
for utx in self.memutxo:
|
||||
if utx["txid"] in mined:
|
||||
self.confutxo.append(utx)
|
||||
else:
|
||||
newmem.append(utx)
|
||||
self.memutxo = newmem
|
||||
self.update_utxo(mined)
|
||||
|
||||
def initial_split(self, node):
|
||||
"""Split two coinbase UTxOs into many small coins"""
|
||||
@ -244,7 +249,7 @@ class EstimateFeeTest(BitcoinTestFramework):
|
||||
check_smart_estimates(self.nodes[1], self.fees_per_kb)
|
||||
self.restart_node(1)
|
||||
|
||||
def sanity_check_rbf_estimates(self, utxos):
|
||||
def sanity_check_rbf_estimates(self):
|
||||
"""During 5 blocks, broadcast low fee transactions. Only 10% of them get
|
||||
confirmed and the remaining ones get RBF'd with a high fee transaction at
|
||||
the next block.
|
||||
@ -261,19 +266,20 @@ class EstimateFeeTest(BitcoinTestFramework):
|
||||
utxos_to_respend = []
|
||||
txids_to_replace = []
|
||||
|
||||
assert_greater_than_or_equal(len(utxos), 250)
|
||||
assert_greater_than_or_equal(len(self.confutxo), 250)
|
||||
for _ in range(5):
|
||||
# Broadcast 45 low fee transactions that will need to be RBF'd
|
||||
txs = []
|
||||
for _ in range(45):
|
||||
u = utxos.pop(0)
|
||||
u = self.confutxo.pop(0)
|
||||
tx = make_tx(self.wallet, u, low_feerate)
|
||||
utxos_to_respend.append(u)
|
||||
txids_to_replace.append(tx["txid"])
|
||||
txs.append(tx)
|
||||
# Broadcast 5 low fee transaction which don't need to
|
||||
for _ in range(5):
|
||||
tx = make_tx(self.wallet, utxos.pop(0), low_feerate)
|
||||
tx = make_tx(self.wallet, self.confutxo.pop(0), low_feerate)
|
||||
self.memutxo.append(tx["new_utxo"])
|
||||
txs.append(tx)
|
||||
batch_send_tx = [node.sendrawtransaction.get_request(tx["hex"]) for tx in txs]
|
||||
for n in self.nodes:
|
||||
@ -282,11 +288,13 @@ class EstimateFeeTest(BitcoinTestFramework):
|
||||
self.sync_mempools(wait=0.1, nodes=[node, miner])
|
||||
for txid in txids_to_replace:
|
||||
miner.prioritisetransaction(txid=txid, fee_delta=-COIN)
|
||||
self.generate(miner, 1)
|
||||
mined = miner.getblock(self.generate(miner, 1)[0], True)["tx"]
|
||||
self.update_utxo(mined)
|
||||
# RBF the low-fee transactions
|
||||
while len(utxos_to_respend) > 0:
|
||||
u = utxos_to_respend.pop(0)
|
||||
tx = make_tx(self.wallet, u, high_feerate)
|
||||
self.memutxo.append(tx["new_utxo"])
|
||||
node.sendrawtransaction(tx["hex"])
|
||||
txs.append(tx)
|
||||
dec_txs = [res["result"] for res in node.batch([node.decoderawtransaction.get_request(tx["hex"]) for tx in txs])]
|
||||
@ -295,7 +303,8 @@ class EstimateFeeTest(BitcoinTestFramework):
|
||||
|
||||
# Mine the last replacement txs
|
||||
self.sync_mempools(wait=0.1, nodes=[node, miner])
|
||||
self.generate(miner, 1)
|
||||
mined = miner.getblock(self.generate(miner, 1)[0], True)["tx"]
|
||||
self.update_utxo(mined)
|
||||
|
||||
# Only 10% of the transactions were really confirmed with a low feerate,
|
||||
# the rest needed to be RBF'd. We must return the 90% conf rate feerate.
|
||||
@ -403,30 +412,40 @@ class EstimateFeeTest(BitcoinTestFramework):
|
||||
self.sync_blocks()
|
||||
assert_equal(self.nodes[0].estimatesmartfee(1)["errors"], ["Insufficient data or no feerate found"])
|
||||
|
||||
def broadcast_and_mine(self, broadcaster, miner, feerate, count):
|
||||
"""Broadcast and mine some number of transactions with a specified fee rate."""
|
||||
def broadcast_many(self, broadcaster, feerate, count, miner=None):
|
||||
"""Broadcast and maybe mine some number of transactions with a specified fee rate."""
|
||||
for _ in range(count):
|
||||
self.wallet.send_self_transfer(from_node=broadcaster, fee_rate=feerate)
|
||||
self.sync_mempools()
|
||||
self.generate(miner, 1)
|
||||
tx = self.wallet.send_self_transfer(from_node=broadcaster, fee_rate=feerate, confirmed_only=True, utxo_to_spend=self.confutxo.pop(0))
|
||||
self.memutxo.append(tx["new_utxo"])
|
||||
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 test_estimation_modes(self):
|
||||
low_feerate = Decimal("0.001")
|
||||
high_feerate = Decimal("0.005")
|
||||
tx_count = 24
|
||||
# Broadcast and mine high fee transactions for the first 12 blocks.
|
||||
for _ in range(12):
|
||||
self.broadcast_and_mine(self.nodes[1], self.nodes[2], high_feerate, tx_count)
|
||||
self.broadcast_many(self.nodes[1], high_feerate, TXS_COUNT, 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_and_mine(self.nodes[1], self.nodes[2], low_feerate, tx_count)
|
||||
self.broadcast_many(self.nodes[1], low_feerate, TXS_COUNT, 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)
|
||||
|
||||
def test_sub_1s_per_vb_estimates(self):
|
||||
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])
|
||||
assert_equal(feerate_0_5_s_per_vb, self.nodes[0].estimatesmartfee(1)["feerate"])
|
||||
|
||||
|
||||
def run_test(self):
|
||||
self.log.info("This test is time consuming, please be patient")
|
||||
@ -468,12 +487,16 @@ class EstimateFeeTest(BitcoinTestFramework):
|
||||
self.clear_estimates()
|
||||
|
||||
self.log.info("Testing estimates with RBF.")
|
||||
self.sanity_check_rbf_estimates(self.confutxo + self.memutxo)
|
||||
self.sanity_check_rbf_estimates()
|
||||
|
||||
self.clear_estimates()
|
||||
self.log.info("Test estimatesmartfee modes")
|
||||
self.test_estimation_modes()
|
||||
|
||||
self.clear_estimates()
|
||||
self.log.info("Test that estimatesmartfee returns a sub 1s/vb fee rate estimate")
|
||||
self.test_sub_1s_per_vb_estimates()
|
||||
|
||||
self.log.info("Testing that fee estimation is disabled in blocksonly.")
|
||||
self.restart_node(0, ["-blocksonly"])
|
||||
assert_raises_rpc_error(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user