From df969b672f7e940f60fe2ff8d01d9d48c056a740 Mon Sep 17 00:00:00 2001 From: Lawrence Nahum Date: Fri, 29 Oct 2021 17:39:28 +0200 Subject: [PATCH] add support to save fee estimates without shutting down the node --- src/policy/fees.cpp | 4 +- src/policy/fees.h | 2 +- src/rpc/fees.cpp | 26 +++++++++ src/test/fuzz/rpc.cpp | 1 + .../feature_fee_estimates_persist.py | 54 +++++++++++++++++++ test/functional/test_runner.py | 1 + 6 files changed, 86 insertions(+), 2 deletions(-) create mode 100755 test/functional/feature_fee_estimates_persist.py diff --git a/src/policy/fees.cpp b/src/policy/fees.cpp index 5f1d15c5f2..e264f0c2fb 100644 --- a/src/policy/fees.cpp +++ b/src/policy/fees.cpp @@ -947,13 +947,15 @@ void CBlockPolicyEstimator::Flush() { FlushFeeEstimates(); } -void CBlockPolicyEstimator::FlushFeeEstimates() +bool CBlockPolicyEstimator::FlushFeeEstimates() const { AutoFile est_file{fsbridge::fopen(m_estimation_filepath, "wb")}; if (est_file.IsNull() || !Write(est_file)) { LogPrintf("Failed to write fee estimates to %s. Continue anyway.\n", fs::PathToString(m_estimation_filepath)); + return false; } else { LogPrintf("Flushed fee estimates to %s.\n", fs::PathToString(m_estimation_filepath.filename())); + return true; } } diff --git a/src/policy/fees.h b/src/policy/fees.h index a95cc19dd4..73a65d9c5e 100644 --- a/src/policy/fees.h +++ b/src/policy/fees.h @@ -256,7 +256,7 @@ public: EXCLUSIVE_LOCKS_REQUIRED(!m_cs_fee_estimator); /** Record current fee estimations. */ - void FlushFeeEstimates() + bool FlushFeeEstimates() const EXCLUSIVE_LOCKS_REQUIRED(!m_cs_fee_estimator); /** Calculates the age of the file, since last modified */ diff --git a/src/rpc/fees.cpp b/src/rpc/fees.cpp index 10caa9ed2e..c218495223 100644 --- a/src/rpc/fees.cpp +++ b/src/rpc/fees.cpp @@ -214,10 +214,36 @@ static RPCHelpMan estimaterawfee() }; } +static RPCHelpMan savefeeestimates() +{ + return RPCHelpMan{"savefeeestimates", + "\nDumps the fee estimates to disk. It will fail until the previous dump is fully loaded.\n", + {}, + RPCResult{RPCResult::Type::NONE, "", ""}, + RPCExamples{ + HelpExampleCli("savefeeestimates", "") + + HelpExampleRpc("savefeeestimates", "") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + static Mutex dump_mutex; + LOCK(dump_mutex); + CBlockPolicyEstimator& fee_estimator = EnsureAnyFeeEstimator(request.context); + + if (!fee_estimator.FlushFeeEstimates()) { + throw JSONRPCError(RPC_MISC_ERROR, "Unable to dump fee estimates to disk"); + } + + return NullUniValue; +}, + }; +} + void RegisterFeeRPCCommands(CRPCTable& t) { static const CRPCCommand commands[]{ {"util", &estimatesmartfee}, + {"util", &savefeeestimates}, {"hidden", &estimaterawfee}, }; for (const auto& c : commands) { diff --git a/src/test/fuzz/rpc.cpp b/src/test/fuzz/rpc.cpp index 9122617e46..f2dde09e09 100644 --- a/src/test/fuzz/rpc.cpp +++ b/src/test/fuzz/rpc.cpp @@ -85,6 +85,7 @@ const std::vector RPC_COMMANDS_NOT_SAFE_FOR_FUZZING{ "importwallet", // avoid reading from disk "loadtxoutset", // avoid reading from disk "loadwallet", // avoid reading from disk + "savefeeestimates", // disabled as a precautionary measure: may take a file path argument in the future "savemempool", // disabled as a precautionary measure: may take a file path argument in the future "setban", // avoid DNS lookups "stop", // avoid shutdown state diff --git a/test/functional/feature_fee_estimates_persist.py b/test/functional/feature_fee_estimates_persist.py new file mode 100755 index 0000000000..0977eb0272 --- /dev/null +++ b/test/functional/feature_fee_estimates_persist.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# Copyright (c) 2014-2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test fee estimates persistence. + +By default, bitcoind will dump fee estimates on shutdown and +then reload it on startup. + +Test is as follows: + + - start node0 + - call the savefeeestimates RPC and verify the RPC succeeds and + that the file exists + - make the file read only and attempt to call the savefeeestimates RPC + with the expecation that it will fail + - move the read only file and shut down the node, verify the node writes + on shutdown a file that is identical to the one we saved via the RPC + +""" + +import filecmp +import os + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_raises_rpc_error + + +class FeeEstimatesPersistTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + + def run_test(self): + fee_estimatesdat = os.path.join(self.nodes[0].chain_path, 'fee_estimates.dat') + self.log.debug('Verify the fee_estimates.dat file does not exists on start up') + assert not os.path.isfile(fee_estimatesdat) + self.nodes[0].savefeeestimates() + self.log.debug('Verify the fee_estimates.dat file exists after calling savefeeestimates RPC') + assert os.path.isfile(fee_estimatesdat) + self.log.debug("Prevent bitcoind from writing fee_estimates.dat to disk. Verify that `savefeeestimates` fails") + fee_estimatesdatold = fee_estimatesdat + '.old' + os.rename(fee_estimatesdat, fee_estimatesdatold) + os.mkdir(fee_estimatesdat) + assert_raises_rpc_error(-1, "Unable to dump fee estimates to disk", self.nodes[0].savefeeestimates) + os.rmdir(fee_estimatesdat) + self.stop_nodes() + self.log.debug("Verify that fee_estimates are written on shutdown") + assert os.path.isfile(fee_estimatesdat) + self.log.debug("Verify that the fee estimates from a shutdown are identical from the ones from savefeeestimates") + assert filecmp.cmp(fee_estimatesdat, fee_estimatesdatold) + + +if __name__ == "__main__": + FeeEstimatesPersistTest(__file__).main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index b85bf1c668..482b2f756d 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -176,6 +176,7 @@ BASE_SCRIPTS = [ 'wallet_fast_rescan.py --descriptors', 'wallet_gethdkeys.py --descriptors', 'wallet_createwalletdescriptor.py --descriptors', + 'feature_fee_estimates_persist.py', 'interface_zmq.py', 'rpc_invalid_address_message.py', 'rpc_validateaddress.py',