Merge bitcoin/bitcoin#35830: fees: Return false for incompatible fee estimates

b9d573e4a9 fees: Return false for incompatible fee estimates (Hao Xu)

Pull request description:

  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.

ACKs for top commit:
  maflcko:
    review ACK b9d573e4a9 📩
  sedited:
    ACK b9d573e4a9

Tree-SHA512: d6ec5986122716ad2c6fb5305626aec71b4b416242791547a12f86fb210d768c87fcb9615fd50259908656766bbe085d55ae90e1d72eaee24d438eecfd9c6dd4
This commit is contained in:
merge-script
2026-08-07 14:27:00 +02:00

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());