fees: Return false for incompatible fee estimates

policy_estimator_io deliberately reuses a CBlockPolicyEstimator because
constructing one for every fuzz input severely reduces throughput.
However, Read() returns true for an incompatible old fee estimates file
without replacing the estimator state. The target then calls Write()
with state loaded by a previous input, making coverage depend on corpus
order.

Return false for incompatible files so the target skips Write() when no
state was loaded. This keeps the estimator reuse optimization instead of
resetting the expensive object before every fuzz input.

For the in-tree production caller, incompatible files remain non-fatal
and the estimator still starts from its default state. Read() now
reports failure, so startup emits one additional non-fatal warning.
Node startup and estimator state are unchanged, as is RPC behavior.

Co-authored-by: maflcko <6399679+maflcko@users.noreply.github.com>
This commit is contained in:
Hao Xu
2026-07-28 22:17:01 +08:00
parent c4fbd3c721
commit b9d573e4a9

View File

@@ -1008,51 +1008,51 @@ bool CBlockPolicyEstimator::Read(AutoFile& filein)
if (nVersionRequired > CURRENT_FEES_FILE_VERSION) {
throw std::runtime_error{strprintf("File version (%d) too high to be read.", nVersionRequired)};
}
if (nVersionRequired < CURRENT_FEES_FILE_VERSION) {
throw std::runtime_error{strprintf("File version (%d) incompatible: Too old to be read", nVersionRequired)};
}
// Read fee estimates file into temporary variables so existing data
// structures aren't corrupted if there is an exception.
unsigned int nFileBestSeenHeight;
filein >> nFileBestSeenHeight;
if (nVersionRequired < CURRENT_FEES_FILE_VERSION) {
LogWarning("Incompatible old fee estimation data (non-fatal). Version: %d", nVersionRequired);
} else { // nVersionRequired == CURRENT_FEES_FILE_VERSION
unsigned int nFileHistoricalFirst, nFileHistoricalBest;
filein >> nFileHistoricalFirst >> nFileHistoricalBest;
if (nFileHistoricalFirst > nFileHistoricalBest || nFileHistoricalBest > nFileBestSeenHeight) {
throw std::runtime_error("Corrupt estimates file. Historical block range for estimates is invalid");
}
std::vector<double> fileBuckets;
filein >> Using<VectorFormatter<EncodedDoubleFormatter>>(fileBuckets);
size_t numBuckets = fileBuckets.size();
if (numBuckets <= 1 || numBuckets > 1000) {
throw std::runtime_error("Corrupt estimates file. Must have between 2 and 1000 feerate buckets");
}
std::unique_ptr<TxConfirmStats> fileFeeStats(new TxConfirmStats(buckets, bucketMap, MED_BLOCK_PERIODS, MED_DECAY, MED_SCALE));
std::unique_ptr<TxConfirmStats> fileShortStats(new TxConfirmStats(buckets, bucketMap, SHORT_BLOCK_PERIODS, SHORT_DECAY, SHORT_SCALE));
std::unique_ptr<TxConfirmStats> fileLongStats(new TxConfirmStats(buckets, bucketMap, LONG_BLOCK_PERIODS, LONG_DECAY, LONG_SCALE));
fileFeeStats->Read(filein, numBuckets);
fileShortStats->Read(filein, numBuckets);
fileLongStats->Read(filein, numBuckets);
// Fee estimates file parsed correctly
// Copy buckets from file and refresh our bucketmap
buckets = fileBuckets;
bucketMap.clear();
for (unsigned int i = 0; i < buckets.size(); i++) {
bucketMap[buckets[i]] = i;
}
// Destroy old TxConfirmStats and point to new ones that already reference buckets and bucketMap
feeStats = std::move(fileFeeStats);
shortStats = std::move(fileShortStats);
longStats = std::move(fileLongStats);
nBestSeenHeight = nFileBestSeenHeight;
historicalFirst = nFileHistoricalFirst;
historicalBest = nFileHistoricalBest;
// nVersionRequired == CURRENT_FEES_FILE_VERSION
unsigned int nFileHistoricalFirst, nFileHistoricalBest;
filein >> nFileHistoricalFirst >> nFileHistoricalBest;
if (nFileHistoricalFirst > nFileHistoricalBest || nFileHistoricalBest > nFileBestSeenHeight) {
throw std::runtime_error("Corrupt estimates file. Historical block range for estimates is invalid");
}
std::vector<double> fileBuckets;
filein >> Using<VectorFormatter<EncodedDoubleFormatter>>(fileBuckets);
size_t numBuckets = fileBuckets.size();
if (numBuckets <= 1 || numBuckets > 1000) {
throw std::runtime_error("Corrupt estimates file. Must have between 2 and 1000 feerate buckets");
}
std::unique_ptr<TxConfirmStats> fileFeeStats(new TxConfirmStats(buckets, bucketMap, MED_BLOCK_PERIODS, MED_DECAY, MED_SCALE));
std::unique_ptr<TxConfirmStats> fileShortStats(new TxConfirmStats(buckets, bucketMap, SHORT_BLOCK_PERIODS, SHORT_DECAY, SHORT_SCALE));
std::unique_ptr<TxConfirmStats> fileLongStats(new TxConfirmStats(buckets, bucketMap, LONG_BLOCK_PERIODS, LONG_DECAY, LONG_SCALE));
fileFeeStats->Read(filein, numBuckets);
fileShortStats->Read(filein, numBuckets);
fileLongStats->Read(filein, numBuckets);
// Fee estimates file parsed correctly
// Copy buckets from file and refresh our bucketmap
buckets = fileBuckets;
bucketMap.clear();
for (unsigned int i = 0; i < buckets.size(); i++) {
bucketMap[buckets[i]] = i;
}
// Destroy old TxConfirmStats and point to new ones that already reference buckets and bucketMap
feeStats = std::move(fileFeeStats);
shortStats = std::move(fileShortStats);
longStats = std::move(fileLongStats);
nBestSeenHeight = nFileBestSeenHeight;
historicalFirst = nFileHistoricalFirst;
historicalBest = nFileHistoricalBest;
}
catch (const std::exception& e) {
LogWarning("Unable to read policy estimator data (non-fatal): %s", e.what());