Repository navigation
validation: cap mainchain RPC timeout during peg-in validation (+ -mainchainrpctimeout=0 help text fix) - #1608
Conversation
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.
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.
|
Self-review addendum after a second pass; the description has been updated to match (edit note at the bottom).
CI hasn't run yet (workflow awaiting approval); would appreciate a maintainer approving the run once the fixup is pushed. |
GetValidationRPCTimeout clamped -mainchainrpctimeout into [1, cap], so an operator who had set 0 (documented until the previous commit as "no timeout", and in practice libevent's 45s/50s defaults) would have had peg-in validation calls time out after 1 second. Treat 0 and negative values as "no explicit limit" and return the cap instead. Also use constexpr and doc-comment style for the new constant and declaration.
validation: cap mainchain RPC timeout during peg-in validation
This PR contains two commits:
rpc: correct the -mainchainrpctimeout=0 help text— a one-line documentation fix.validation: cap mainchain RPC timeout during peg-in validation— the behavior change.Problem
Peg-in validation (
-validatepegin, default-on for chains with a federated peg such as Liquid)issues synchronous JSON-RPC calls to the configured mainchain daemon, from code paths that
hold
cs_main(and, for mempool acceptance,mempool.csas well):Chainstate::ConnectTip→CheckPeginRipenessgetblockheaderAssertLockHeld(cs_main)Consensus::CheckTxInputs, called from both block connection (ConnectBlock) and mempool acceptance (MemPoolAccept::PreChecks)getblockheadercs_main(+mempool.csat acceptance)MemPoolAccept::PreChecks→CheckPeginSubsidyAndMinimumgetrawtransactionEXCLUSIVE_LOCKS_REQUIRED(cs_main, m_pool.cs)CTxMemPool::removeForBlockgetblockheaderAll of these go through
CallMainChainRPC, which applies-mainchainrpctimeout(default900 seconds) to each call, with a fresh blocking TCP connection per call and no caching of
results. (The libevent timeout is per phase — connect and read are timed separately — so the
worst case per call is roughly twice the configured value, plus a blocking DNS lookup if
-mainchainrpchostis a hostname.)If the local mainchain daemon accepts TCP connections but stops answering requests (it is itself
stalled, overloaded, or otherwise wedged), every one of these calls blocks for up to 15 minutes
while the locks are held — freezing block validation, mempool acceptance, and most RPC for the
duration.
ConnectTipeven logs that the chain "will not grow until this is remedied", so thiscoupling is by design; what is missing is a bound on how long a single unresponsive-daemon
episode can stall validation per attempt.
What happens on failure (unchanged by this PR, only sooner)
CheckPeginRipeness(ConnectTip):IsConfirmedBitcoinBlockreturnsfalse→depth_failed→ ConnectTip stalls and retries. This is the deliberate stall-and-retry machinery.
CheckTxInputs): the transaction is rejected (TX_WITNESS_MUTATED,bad-pegin-witness); re-broadcast recovers it.CheckPeginSubsidyAndMinimum): the timeout surfaces asCConnectionFailedthrown out ofAcceptToMemoryPool(caught and logged byProcessMessagesfor P2P, returned as a JSON error for RPC), exactly as it does after 900 s today.
removeForBlock): the peg-in is evicted; re-broadcast recovers it.ConnectBlock(CheckTxInputswith depth checking): a failure is aBLOCK_CONSENSUSfailure and the block is marked
BLOCK_FAILED_VALIDuntilreconsiderblock. This is thepre-existing elementsd stuck syncing at block 2137819 #1198 (daemon goes away between the ripeness check and ConnectBlock). The cap
does not introduce that hazard, but it does turn a mainchain response that takes longer than
the cap (30–900 s today) from a success into a failure on that path. See "Notes for reviewers".
So on every path except
ConnectBlock, blocking for the full 900 s buys nothing: after 30 s thecode lands in exactly the same state it reaches after 900 s today. On
ConnectBlockthe captrades bounded
cs_mainhold time for less tolerance of a slow-but-alive daemon.Change
CallMainChainRPCgains an optionaltimeoutparameter. Negative (the default) preserves thecurrent behavior for every existing caller: read
-mainchainrpctimeout.GetValidationRPCTimeout()returns-mainchainrpctimeoutcapped atMAX_VALIDATION_RPC_TIMEOUT(new constant, 30 seconds).IsConfirmedBitcoinBlock(getblockheader) and thegetrawtransactioncall inCheckPeginSubsidyAndMinimum.IsConfirmedBitcoinBlockalso serves the advisorymaturefield of
createrawpeginand the immature-peg-in warning ofsignrawtransactionwith*, whichfail faster against a hung daemon — harmless and desirable.
MainchainRPCCheckin init.cpp) and all otherCallMainChainRPCusers (wallet RPCs) are unchanged and keep the full
-mainchainrpctimeout.cs_mainhold time per attempt; it does not remove the coupling. The 30 sreverification timer and every block arrival still trigger a fresh (now bounded) attempt.
30 s is generous for the intended deployment (a co-located, trusted
bitcoindansweringgetblockheader/getrawtransaction) and bounds a stall to well under one Liquid block intervalper attempt instead of 15 minutes.
Help text fix (first commit)
The
-mainchainrpctimeouthelp text claims "0 for no timeout". That is not what happens: withthe pinned libevent 2.1.12, a timeout of 0 is treated as unset and libevent substitutes its own
internal defaults (45 s connect, 50 s read/write —
HTTP_CONNECT_TIMEOUT/HTTP_READ_TIMEOUT/HTTP_WRITE_TIMEOUTinhttp-internal.h, applied inhttp.c). There is no way to disable thetimeout through this option. The help text now states the actual behavior (and, in the second
commit, documents the validation cap).
Backward compatibility
-mainchainrpctimeoutto a value ≤ 30 see no change in validation behavior;values above 30 (including the 900 s default) are capped at 30 s for the two validation-path
call sites only.
-mainchainrpctimeout=0: the current head clamps 0 into[1, 30], i.e. a 1 s validationtimeout, which is a regression for anyone who followed the old help text (today they get
libevent's 45/50 s). A fixup making 0 and negative values yield the cap instead is pending.
ConnectBlockpath is reached for slow daemons thatpreviously would have answered within 900 s (see above).
Test
Adds
mainchainrpc_tests.cppcoveringGetValidationRPCTimeout: default capped, values belowthe cap honored, values above the cap capped, boundary value honored, and the 0 case. Nothing
exercises the hung-daemon path end to end; a functional harness (a stub that answers the startup
getnetworkinfo/getblockhashcalls, then accepts and never replies) is feasible and I'm happy toadd it if reviewers want it.
Notes for reviewers
ConnectBlock/ elementsd stuck syncing at block 2137819 #1198: the cleanest outcome is to carrydepth_failedout ofCheckTxInputsso that a mainchain failure inConnectBlockstalls rather than invalidates,which would make the cap unconditionally safe. That is a separate consensus-adjacent change;
I can prepare it as a preceding commit or separate PR if maintainers want the two paired.
Alternatively the cap could be applied only on the retryable paths. Guidance welcome.
bitcoind) while bounding a stall to well under one Liquid block interval; 60 s would workjust as well if preferred.
rejected to keep the option surface minimal; the patch is structured so that swapping the
constant for an argument is a two-line change if maintainers prefer that.
bitcoin-cliin this tree makes-rpcclienttimeout=0mean amulti-year timeout ("Indefinite request timeouts are not possible in libevent-http"). Making 0
behave as documented for the non-validation callers, instead of documenting libevent
internals, is an equally small change if preferred.
elements-29.xandelements-23.xhave both call sites;elements-23.3.xhas noCheckPeginSubsidyAndMinimum, so only theIsConfirmedBitcoinBlockhalf applies there.Edited 2026-10-15: corrected the failure-semantics section (the
ConnectBlockpath invalidates rather than retries — #1198 — and thegetrawtransactiontimeout propagates asCConnectionFailed, not aspegin-subsidy-mainchain-error), noted the-mainchainrpctimeout=0→ 1 s regression in the current head with a fixup pending, corrected the per-input/per-attempt cost and the backport branch names, and added thesignrawtransactionwith*call site and the help-text alternative.