diff --git a/src/init.cpp b/src/init.cpp index 652053400a..d3af3d9bc6 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -725,7 +725,7 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc) argsman.AddArg("-mainchainrpcuser=", "The rpc username that the daemon will use to connect to the trusted mainchain daemon to validate peg-ins, if enabled. (default: cookie auth)", ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE, OptionsCategory::ELEMENTS); argsman.AddArg("-mainchainrpcpassword=", "The rpc password which the daemon will use to connect to the trusted mainchain daemon to validate peg-ins, if enabled. (default: cookie auth)", ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE, OptionsCategory::ELEMENTS); argsman.AddArg("-mainchainrpccookiefile=", "The bitcoind cookie auth path which the daemon will use to connect to the trusted mainchain daemon to validate peg-ins. (default: `/regtest/.cookie`)", ArgsManager::ALLOW_ANY, OptionsCategory::ELEMENTS); - argsman.AddArg("-mainchainrpctimeout=", strprintf("Timeout in seconds during mainchain RPC requests, or 0 for no timeout. (default: %d)", DEFAULT_HTTP_CLIENT_TIMEOUT), ArgsManager::ALLOW_ANY, OptionsCategory::ELEMENTS); + argsman.AddArg("-mainchainrpctimeout=", strprintf("Timeout in seconds for mainchain RPC requests. Note that a value of 0 does not disable the timeout: libevent then applies its own internal defaults (45s connect, 50s read/write). Mainchain RPC calls made while validating peg-ins are additionally capped at %d seconds. (default: %d)", MAX_VALIDATION_RPC_TIMEOUT, DEFAULT_HTTP_CLIENT_TIMEOUT), ArgsManager::ALLOW_ANY, OptionsCategory::ELEMENTS); argsman.AddArg("-peginconfirmationdepth=", strprintf("Peg-in claims must be this deep to be considered valid. (default: %d)", DEFAULT_PEGIN_CONFIRMATION_DEPTH), ArgsManager::ALLOW_ANY, OptionsCategory::ELEMENTS); argsman.AddArg("-parentpubkeyprefix", strprintf("The byte prefix, in decimal, of the parent chain's base58 pubkey address. (default: %d)", 111), ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); argsman.AddArg("-parentscriptprefix", strprintf("The byte prefix, in decimal, of the parent chain's base58 script address. (default: %d)", 196), ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); diff --git a/src/mainchainrpc.cpp b/src/mainchainrpc.cpp index 7a0ee9cecf..0115d7ad33 100644 --- a/src/mainchainrpc.cpp +++ b/src/mainchainrpc.cpp @@ -14,6 +14,8 @@ #include #include +#include + /** Reply structure for request_done to fill in */ struct HTTPReply { @@ -77,7 +79,7 @@ static void http_error_cb(enum evhttp_request_error err, void *ctx) } #endif -UniValue CallMainChainRPC(const std::string& strMethod, const UniValue& params) +UniValue CallMainChainRPC(const std::string& strMethod, const UniValue& params, int timeout) { std::string host = gArgs.GetArg("-mainchainrpchost", DEFAULT_RPCCONNECT); int port = gArgs.GetIntArg("-mainchainrpcport", BaseParams().MainchainRPCPort()); @@ -87,7 +89,10 @@ UniValue CallMainChainRPC(const std::string& strMethod, const UniValue& params) // Synchronously look up hostname raii_evhttp_connection evcon = obtain_evhttp_connection_base(base.get(), host, port); - evhttp_connection_set_timeout(evcon.get(), gArgs.GetIntArg("-mainchainrpctimeout", DEFAULT_HTTP_CLIENT_TIMEOUT)); + if (timeout < 0) { + timeout = gArgs.GetIntArg("-mainchainrpctimeout", DEFAULT_HTTP_CLIENT_TIMEOUT); + } + evhttp_connection_set_timeout(evcon.get(), timeout); HTTPReply response; raii_evhttp_request req = obtain_evhttp_request(http_request_done, (void*)&response); @@ -150,13 +155,22 @@ UniValue CallMainChainRPC(const std::string& strMethod, const UniValue& params) return reply; } +int GetValidationRPCTimeout(const ArgsManager& argsman) +{ + const int64_t timeout = argsman.GetIntArg("-mainchainrpctimeout", DEFAULT_HTTP_CLIENT_TIMEOUT); + return static_cast(std::clamp(timeout, 1, MAX_VALIDATION_RPC_TIMEOUT)); +} + bool IsConfirmedBitcoinBlock(const uint256& hash, const int nMinConfirmationDepth, const int nbTxs) { LogPrintf("Checking for confirmed bitcoin block with hash %s, mindepth %d, nbtxs %d\n", hash.ToString().c_str(), nMinConfirmationDepth, nbTxs); try { UniValue params(UniValue::VARR); params.push_back(hash.GetHex()); - UniValue reply = CallMainChainRPC("getblockheader", params); + // This call is made while holding cs_main during block connection + // and mempool acceptance, and failure is simply retried later, so a + // bounded timeout is used instead of the full -mainchainrpctimeout. + UniValue reply = CallMainChainRPC("getblockheader", params, GetValidationRPCTimeout(gArgs)); const UniValue& errval = reply.find_value("error"); if (!errval.isNull()) { LogPrintf("WARNING: Got error reply from bitcoind getblockheader: %s\n", errval.write()); diff --git a/src/mainchainrpc.h b/src/mainchainrpc.h index 5528d4985e..6880a3b6d7 100644 --- a/src/mainchainrpc.h +++ b/src/mainchainrpc.h @@ -15,9 +15,16 @@ #include +class ArgsManager; + static const bool DEFAULT_NAMED=false; static const char DEFAULT_RPCCONNECT[] = "127.0.0.1"; static const int DEFAULT_HTTP_CLIENT_TIMEOUT=900; +// Hard cap (in seconds) on the timeout of mainchain RPC calls made while +// validating peg-ins. These calls are issued synchronously while cs_main is +// held, so the cap bounds how long an unresponsive mainchain daemon can +// stall block connection and mempool acceptance. +static const int MAX_VALIDATION_RPC_TIMEOUT=30; // // Exception thrown on connection error. This error is used to determine @@ -33,7 +40,16 @@ class CConnectionFailed : public std::runtime_error }; -UniValue CallMainChainRPC(const std::string& strMethod, const UniValue& params); +// If timeout is negative (the default), the value of -mainchainrpctimeout +// (or DEFAULT_HTTP_CLIENT_TIMEOUT if unset) is used. +UniValue CallMainChainRPC(const std::string& strMethod, const UniValue& params, int timeout = -1); + +// Returns the timeout to use for mainchain RPC calls made while validating +// peg-ins: the value of -mainchainrpctimeout clamped into the range +// [1, MAX_VALIDATION_RPC_TIMEOUT]. Note that a -mainchainrpctimeout of 0 +// does not disable the timeout (libevent substitutes its own internal +// defaults), so it is clamped like any other out-of-range value. +int GetValidationRPCTimeout(const ArgsManager& argsman); // Verify if the block with given hash has at least the specified minimum number // of confirmations. diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index a5d586ab18..810bed91a7 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -54,6 +54,7 @@ add_executable(test_elements key_io_tests.cpp key_tests.cpp logging_tests.cpp + mainchainrpc_tests.cpp mempool_tests.cpp merkle_tests.cpp merkleblock_tests.cpp diff --git a/src/test/mainchainrpc_tests.cpp b/src/test/mainchainrpc_tests.cpp new file mode 100644 index 0000000000..632fdd49fe --- /dev/null +++ b/src/test/mainchainrpc_tests.cpp @@ -0,0 +1,40 @@ +// Copyright (c) 2026 The Elements Core developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include + +#include +#include + +BOOST_FIXTURE_TEST_SUITE(mainchainrpc_tests, BasicTestingSetup) + +BOOST_AUTO_TEST_CASE(validation_rpc_timeout) +{ + ArgsManager argsman; + + // Unset -mainchainrpctimeout: the 900s default is capped + BOOST_CHECK_EQUAL(GetValidationRPCTimeout(argsman), MAX_VALIDATION_RPC_TIMEOUT); + + // Values below the cap are honored + argsman.ForceSetArg("-mainchainrpctimeout", "10"); + BOOST_CHECK_EQUAL(GetValidationRPCTimeout(argsman), 10); + + // Values above the cap are clamped to it + argsman.ForceSetArg("-mainchainrpctimeout", "3600"); + BOOST_CHECK_EQUAL(GetValidationRPCTimeout(argsman), MAX_VALIDATION_RPC_TIMEOUT); + + // The cap boundary itself is honored + argsman.ForceSetArg("-mainchainrpctimeout", strprintf("%d", MAX_VALIDATION_RPC_TIMEOUT)); + BOOST_CHECK_EQUAL(GetValidationRPCTimeout(argsman), MAX_VALIDATION_RPC_TIMEOUT); + + // 0 does not mean "no timeout" (libevent substitutes its own internal + // defaults for 0), so it is clamped to the lower bound instead + argsman.ForceSetArg("-mainchainrpctimeout", "0"); + BOOST_CHECK_EQUAL(GetValidationRPCTimeout(argsman), 1); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/validation.cpp b/src/validation.cpp index 06ba5fb762..fc4f5b5421 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -803,7 +803,10 @@ class MemPoolAccept params.push_back(txid.GetHex()); params.push_back(2); params.push_back(blockhash.GetHex()); - UniValue result = CallMainChainRPC("getrawtransaction", params); + // This call is made while holding cs_main and mempool.cs + // during mempool acceptance, so use the bounded validation + // timeout rather than the full -mainchainrpctimeout. + UniValue result = CallMainChainRPC("getrawtransaction", params, GetValidationRPCTimeout(gArgs)); if (result["error"].isStr()) { return state.Invalid(TxValidationResult::TX_NOT_STANDARD, "pegin-subsidy-mainchain-error", result["error"]["message"].get_str()); } else {