fees: move fee_estimates.dat into fees directory

Move block policy fee estimates from fee_estimates.dat to
fees/block_policy_estimates.dat.

On startup, migrate the legacy file to the new path when only the legacy
file exists. If both files exist, keep the new file and remove the
legacy file.

Rename the block policy estimator args source files to the generic
estimator_args.{cpp,h} names and rename FeeestPath to
BlockPolicyFeeEstPath while the path helper is moved into the shared fee
estimator argument code.
This commit is contained in:
ismaelsadeeq
2025-11-21 14:44:43 +00:00
parent 0db2b69e6d
commit 7dcb37989d
14 changed files with 155 additions and 79 deletions

View File

@@ -31,6 +31,7 @@ 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"
BLOCK_POLICY_ESTIMATOR_FILE_PATH = "fees/block_policy_estimates.dat"
def small_txpuzzle_randfee(
wallet, from_node, conflist, unconflist, amount, min_fee, fee_increment, batch_reqs
@@ -333,69 +334,91 @@ class EstimateFeeTest(BitcoinTestFramework):
self.restart_node(0)
assert_equal(self.nodes[0].estimatesmartfee(1, "economical", {"fee_rate_estimator": "block_policy"})["feerate"], fee_rate)
fee_dat = self.nodes[0].chain_path / "fee_estimates.dat"
block_policy_fee_dat = self.nodes[0].chain_path / BLOCK_POLICY_ESTIMATOR_FILE_PATH
legacy_fee_dat = self.nodes[0].chain_path / "fee_estimates.dat"
# Stop the node and backdate the fee_estimates.dat file more than MAX_FILE_AGE
# If only the legacy fee_estimates.dat file exists, it is migrated to
# the new block policy estimator path.
self.stop_node(0)
last_modified_time = time.time() - (MAX_FILE_AGE + 1) * SECONDS_PER_HOUR
os.utime(fee_dat, (last_modified_time, last_modified_time))
os.rename(block_policy_fee_dat, legacy_fee_dat)
self.start_node(0)
assert_equal(self.nodes[0].estimatesmartfee(1, "economical", {"fee_rate_estimator": "block_policy"})["feerate"], fee_rate)
self.stop_node(0)
assert_equal(os.path.isfile(block_policy_fee_dat), True)
assert_equal(os.path.isfile(legacy_fee_dat), False)
# Start node and ensure the fee_estimates.dat file was not read
# If both files exist, the new block policy estimator path is used and
# the obsolete legacy file is removed.
with open(legacy_fee_dat, "wb") as f:
f.write(b"ignored legacy fee estimates")
self.start_node(0)
assert_equal(self.nodes[0].estimatesmartfee(1, "economical", {"fee_rate_estimator": "block_policy"})["feerate"], fee_rate)
self.stop_node(0)
assert_equal(os.path.isfile(legacy_fee_dat), False)
# Stop the node and backdate the block policy estimator file more than MAX_FILE_AGE
last_modified_time = time.time() - (MAX_FILE_AGE + 1) * SECONDS_PER_HOUR
os.utime(block_policy_fee_dat, (last_modified_time, last_modified_time))
# Start node and ensure the block policy estimator file was not read
self.start_node(0)
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):
fee_dat = self.nodes[0].chain_path / "fee_estimates.dat"
os.remove(fee_dat) if os.path.exists(fee_dat) else None
block_policy_fees_dat = self.nodes[0].chain_path / BLOCK_POLICY_ESTIMATOR_FILE_PATH
os.remove(block_policy_fees_dat) if os.path.exists(block_policy_fees_dat) else None
if os.path.isdir(block_policy_fees_dat.parent):
os.rmdir(block_policy_fees_dat.parent)
# Verify that fee_estimates.dat does not exist
assert_equal(os.path.isfile(fee_dat), False)
# Verify if the string "Flushed fee estimates to fee_estimates.dat." is present in the debug log file.
# Verify that block policy estimator file and its parent directory do not exist
assert_equal(os.path.isfile(block_policy_fees_dat), False)
assert_equal(os.path.isdir(block_policy_fees_dat.parent), False)
# Verify if the string "Flushed fee estimates to block_policy_estimates.dat." is present in the debug log file.
# If present, it indicates that fee estimates have been successfully flushed to disk.
expected_messages = [f"Flushed fee estimates to {fee_dat}."]
expected_messages = [f"Flushed fee estimates to {block_policy_fees_dat}."]
with self.nodes[0].assert_debug_log(expected_msgs=expected_messages, timeout=1):
# Mock the scheduler for an hour to flush fee estimates to fee_estimates.dat
# Mock the scheduler for an hour to flush fee estimates to block_policy_estimates.dat
self.nodes[0].mockscheduler(SECONDS_PER_HOUR)
# Verify that fee estimates were flushed and fee_estimates.dat file is created
assert_equal(os.path.isfile(fee_dat), True)
# Verify that fee estimates were flushed and block policy estimator directory and file are created
assert_equal(os.path.isdir(block_policy_fees_dat.parent), True)
assert_equal(os.path.isfile(block_policy_fees_dat), True)
# Verify that the estimates remain the same if there are no blocks in the flush interval
block_hash_before = self.nodes[0].getbestblockhash()
fee_dat_initial_content = open(fee_dat, "rb").read()
block_policy_dat_initial_content = open(block_policy_fees_dat, "rb").read()
with self.nodes[0].assert_debug_log(expected_msgs=expected_messages, timeout=1):
# Mock the scheduler for an hour to flush fee estimates to fee_estimates.dat
# Mock the scheduler for an hour to flush fee estimates to the block policy estimator file
self.nodes[0].mockscheduler(SECONDS_PER_HOUR)
# Verify that there were no blocks in between the flush interval
assert_equal(block_hash_before, self.nodes[0].getbestblockhash())
fee_dat_current_content = open(fee_dat, "rb").read()
assert_equal(fee_dat_current_content, fee_dat_initial_content)
block_policy_fee_dat_current_content = open(block_policy_fees_dat, "rb").read()
assert_equal(block_policy_dat_initial_content, block_policy_fee_dat_current_content)
# Verify that the estimates remain the same after shutdown with no blocks before shutdown
self.restart_node(0)
fee_dat_current_content = open(fee_dat, "rb").read()
assert_equal(fee_dat_current_content, fee_dat_initial_content)
block_policy_fee_dat_current_content = open(block_policy_fees_dat, "rb").read()
assert_equal(block_policy_dat_initial_content, block_policy_fee_dat_current_content)
# Verify that the estimates are not the same if new blocks were produced in the flush interval
with self.nodes[0].assert_debug_log(expected_msgs=expected_messages, timeout=1):
# Mock the scheduler for an hour to flush fee estimates to fee_estimates.dat
# Mock the scheduler for an hour to flush fee estimates to block_policy_estimates.dat
self.generate(self.nodes[0], 5, sync_fun=self.no_op)
self.nodes[0].mockscheduler(SECONDS_PER_HOUR)
fee_dat_current_content = open(fee_dat, "rb").read()
assert_not_equal(fee_dat_current_content, fee_dat_initial_content)
fee_dat_initial_content = fee_dat_current_content
block_policy_fee_dat_current_content = open(block_policy_fees_dat, "rb").read()
assert_not_equal(block_policy_fee_dat_current_content, block_policy_dat_initial_content)
block_policy_dat_initial_content = block_policy_fee_dat_current_content
# Generate blocks before shutdown and verify that the fee estimates are not the same
self.generate(self.nodes[0], 5, sync_fun=self.no_op)
self.restart_node(0)
fee_dat_current_content = open(fee_dat, "rb").read()
assert_not_equal(fee_dat_current_content, fee_dat_initial_content)
block_policy_fee_dat_current_content = open(block_policy_fees_dat, "rb").read()
assert_not_equal(block_policy_dat_initial_content, block_policy_fee_dat_current_content)
def test_acceptstalefeeestimates_option(self):
@@ -404,20 +427,20 @@ class EstimateFeeTest(BitcoinTestFramework):
self.stop_node(0)
fee_dat = self.nodes[0].chain_path / "fee_estimates.dat"
fee_dat = self.nodes[0].chain_path / BLOCK_POLICY_ESTIMATOR_FILE_PATH
# Stop the node and backdate the fee_estimates.dat file more than MAX_FILE_AGE
# Stop the node and backdate the block policy estimator file more than MAX_FILE_AGE
last_modified_time = time.time() - (MAX_FILE_AGE + 1) * SECONDS_PER_HOUR
os.utime(fee_dat, (last_modified_time, last_modified_time))
# Restart node with -acceptstalefeeestimates option to ensure fee_estimate.dat file is read
# Restart node with -acceptstalefeeestimates option to ensure block policy estimator file is read
self.start_node(0,extra_args=["-acceptstalefeeestimates"])
assert_equal(self.nodes[0].estimatesmartfee(1, "economical", {"fee_rate_estimator": "block_policy"})["feerate"], fee_rate)
def clear_estimates(self):
self.log.info("Restarting node with fresh estimation")
self.stop_node(0)
fee_dat = self.nodes[0].chain_path / "fee_estimates.dat"
fee_dat = self.nodes[0].chain_path / BLOCK_POLICY_ESTIMATOR_FILE_PATH
os.remove(fee_dat)
self.start_node(0)
self.connect_nodes(0, 1)
@@ -552,7 +575,7 @@ class EstimateFeeTest(BitcoinTestFramework):
self.log.info("Testing estimates with single transactions.")
self.sanity_check_estimates_range()
self.log.info("Test fee_estimates.dat is flushed periodically")
self.log.info("Test fees data is flushed periodically")
self.test_estimate_dat_is_flushed_periodically()
# check that estimatesmartfee feerate is greater than or equal to maximum of mempoolminfee and minrelaytxfee
@@ -564,7 +587,7 @@ class EstimateFeeTest(BitcoinTestFramework):
self.log.info("Test acceptstalefeeestimates option")
self.test_acceptstalefeeestimates_option()
self.log.info("Test reading old fee_estimates.dat")
self.log.info("Test reading old block policy estimator file")
self.test_old_fee_estimate_file()
self.clear_estimates()

View File

@@ -965,6 +965,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
return os.path.join(cache_node_dir, self.chain, *paths)
os.rmdir(cache_path('wallets')) # Remove empty wallets dir
shutil.rmtree(cache_path('fees'), ignore_errors=True)
for entry in os.listdir(cache_path()):
if entry not in ['chainstate', 'blocks', 'indexes']: # Only indexes, chainstate and blocks folders
os.remove(cache_path(entry))