From 01cbc248cd88aa62c221eea7a1fbd3ce065563f0 Mon Sep 17 00:00:00 2001 From: GracedEternalKingCabbageMan <151803062+GracedEternalKingCabbageMan@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:23:08 +0200 Subject: [PATCH 1/2] rpc: correct the -mainchainrpctimeout=0 help text The help text claims a value of 0 disables the timeout. It does not: with the pinned libevent 2.1.12, a timeout of 0 is treated as unset and libevent substitutes its own internal defaults (45s connect, 50s read/write). There is no way to disable the timeout via this option. --- src/init.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/init.cpp b/src/init.cpp index 652053400a..10cd160e5b 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). (default: %d)", 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); From f00cd74f27ac4efc2078fa310bb14cf5d5437125 Mon Sep 17 00:00:00 2001 From: GracedEternalKingCabbageMan <151803062+GracedEternalKingCabbageMan@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:23:08 +0200 Subject: [PATCH 2/2] validation: cap mainchain RPC timeout during peg-in validation Peg-in validation (-validatepegin, default on for chains with a federated peg) issues synchronous mainchain RPC calls while holding cs_main: during block connection (ConnectTip via CheckPeginRipeness; ConnectBlock via Consensus::CheckTxInputs), during mempool acceptance (PreChecks via CheckPeginSubsidyAndMinimum), and during mempool epoch ejection (removeForBlock). Each call uses the full -mainchainrpctimeout (default 900s) with a fresh blocking TCP connection and no caching, so a mainchain daemon that accepts connections but stops responding can stall block validation and mempool acceptance for up to 15 minutes per peg-in input. IsConfirmedBitcoinBlock treats RPC failure as 'not confirmed, retry later', and the getrawtransaction call in CheckPeginSubsidyAndMinimum fails the transaction into the same retryable mempool-rejection path, so blocking for the full timeout has no benefit on these paths. Give CallMainChainRPC an optional timeout parameter (negative preserves the existing behavior for all other callers), add GetValidationRPCTimeout() clamping -mainchainrpctimeout into [1, MAX_VALIDATION_RPC_TIMEOUT] (30s), and use it for the getblockheader call in IsConfirmedBitcoinBlock and the getrawtransaction call in CheckPeginSubsidyAndMinimum. This also bounds the advisory maturity check in createrawpegin. The startup MainchainRPCCheck keeps the full configured timeout. The help text now documents the cap. --- src/init.cpp | 2 +- src/mainchainrpc.cpp | 20 ++++++++++++++--- src/mainchainrpc.h | 18 ++++++++++++++- src/test/CMakeLists.txt | 1 + src/test/mainchainrpc_tests.cpp | 40 +++++++++++++++++++++++++++++++++ src/validation.cpp | 5 ++++- 6 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 src/test/mainchainrpc_tests.cpp diff --git a/src/init.cpp b/src/init.cpp index 10cd160e5b..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 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). (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 {