From b619a4f9c7762b037be3b331cb7aad59a28b30ca Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 11 Nov 2026 18:26:57 -0600 Subject: [PATCH] fix(llmq): retry pending EHF signals on new tips Allow members to re-sign pending EHF signals after incomplete signing sessions expire. Exercise recovery with the same quorum after isolation and timeout in both connection modes. --- src/llmq/ehf_signals.cpp | 2 +- test/functional/feature_mnehf.py | 61 +++++++++++++++++++++++++------- test/functional/test_runner.py | 1 + 3 files changed, 51 insertions(+), 13 deletions(-) diff --git a/src/llmq/ehf_signals.cpp b/src/llmq/ehf_signals.cpp index f156b6f8c2c2..ebc617911420 100644 --- a/src/llmq/ehf_signals.cpp +++ b/src/llmq/ehf_signals.cpp @@ -94,7 +94,7 @@ void CEHFSignalsHandler::trySignEHFSignal(int bit, const CBlockIndex* const pind const uint256 msgHash = mnhfPayload.PrepareTx().GetHash(); WITH_LOCK(cs, ids.insert(requestId)); - shareman.AsyncSignIfMember(llmqType, requestId, msgHash, quorum->qc->quorumHash, false, true); + shareman.AsyncSignIfMember(llmqType, requestId, msgHash, quorum->qc->quorumHash, true, true); } RecoveredSigResult CEHFSignalsHandler::HandleNewRecoveredSig(const CRecoveredSig& recoveredSig) diff --git a/test/functional/feature_mnehf.py b/test/functional/feature_mnehf.py index 851607a8e5bb..487a86f83069 100755 --- a/test/functional/feature_mnehf.py +++ b/test/functional/feature_mnehf.py @@ -26,13 +26,14 @@ class MnehfTest(DashTestFramework): def add_options(self, parser): self.add_wallet_options(parser) + parser.add_argument("--spork21", default=False, action="store_true", + help="Test with all quorum members connected") def set_test_params(self): # testdummy starts as NEVER_ACTIVE, run_test opens its signalling window later - extra_args = [["-vbparams=testdummy:-2:999999999999:0:4:4:4:5:1", "-persistmempool=0"]] * 2 - # One masternode is enough: the EHF signal only needs a recovered signature of any quorum - self.set_dash_test_params(2, 1, extra_args=extra_args) - self.set_dash_llmq_test_params(1, 1) + extra_args = [["-vbparams=testdummy:-2:999999999999:0:4:4:4:5:1", "-persistmempool=0"]] * 4 + self.set_dash_test_params(4, 3, extra_args=extra_args) + self.set_dash_llmq_test_params(3, 3) def skip_test_if_missing_module(self): self.skip_if_no_wallet() @@ -91,6 +92,8 @@ def set_sporks(self): self.nodes[0].sporkupdate("SPORK_17_QUORUM_DKG_ENABLED", spork_enabled) self.nodes[0].sporkupdate("SPORK_19_CHAINLOCKS_ENABLED", spork_disabled) self.nodes[0].sporkupdate("SPORK_2_INSTANTSEND_ENABLED", spork_disabled) + if self.options.spork21: + self.nodes[0].sporkupdate("SPORK_21_QUORUM_ALL_CONNECTED", spork_enabled) self.wait_for_sporks_same() def check_fork(self, expected): @@ -123,11 +126,45 @@ def test_no_signing_outside_window(self): self.check_no_ehf_signing("before its start time") self.log.info("Check that masternodes sign testdummy once its start time is reached") + request_id = hash256(ser_string(b"mnhf") + struct.pack("