Skip to content

Commit cd3c3d9

Browse files
Merge #7240: fix: initialize quorum connections on startup
5a43003 test: verify post-restart IS lock on all nodes, not just sender (UdjinM6) ecd5c90 refactor: defer active_ctx block tip init until after nodeman->Init() (UdjinM6) c25c61a test: add InstantSend after full restart test (UdjinM6) 5e79dbe fix: initialize quorum connections at startup on idle chains (UdjinM6) Pull request description: ## Issue being fixed or feature implemented After PR dashpay/dash#7063 (commit dashpay/dash@1360d9d), `CQuorumManager::UpdatedBlockTip` was moved from `CDSNotificationInterface::UpdatedBlockTip` (which is called directly at startup via `InitializeCurrentBlockTip`) to the `ActiveContext`/`ObserverContext` `CValidationInterface` subscribers. These subscribers only receive `UpdatedBlockTip` via `GetMainSignals()` broadcast, which is never triggered at startup on idle chains — `ActivateBestChain` early-returns when `pindexMostWork == m_chain.Tip()`. This means that after a full restart with no new blocks: - `QuorumObserver::UpdatedBlockTip` never fires - `CheckQuorumConnections` is never called - Quorum connections are never re-established - MNs cannot exchange signing shares - InstantSend and ChainLock signing fails ## What was done? Fix by adding `InitializeCurrentBlockTip()` to `ActiveContext` and `ObserverContext`, called from the `loadblk` thread right after `CDSNotificationInterface::InitializeCurrentBlockTip()`. This method also calls the new `QuorumObserver::InitializeQuorumConnections()`, which bypasses the `IsBlockchainSynced()` guard (mnsync is not yet complete at that point in the startup sequence). ## How Has This Been Tested? See new "IS after MNs restart" test which fails on develop and passes with the patch applied ## Breaking Changes n/a ## Checklist: - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have added or updated relevant unit/integration/functional/e2e tests - [ ] I have made corresponding changes to the documentation - [ ] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_ ACKs for top commit: PastaPastaPasta: utACK 5a43003 knst: utACK 5a43003 Tree-SHA512: 3c91e7cde81d9359e0b55a501eb230a9abcc80212da5050d10d73f23cab076a36bd0f2bd10b2bab3015cd7f64d4156429a03247381e1b96f424b7cd75205a662
2 parents 65366a4 + 5a43003 commit cd3c3d9

8 files changed

Lines changed: 124 additions & 1 deletion

File tree

src/active/context.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,14 @@ void ActiveContext::SetCJServer(gsl::not_null<CCoinJoinServer*> cj_server)
9797
m_cj_server = cj_server;
9898
}
9999

100+
void ActiveContext::InitializeCurrentBlockTip(const CBlockIndex* tip, bool ibd)
101+
{
102+
UpdatedBlockTip(tip, nullptr, ibd);
103+
if (tip) {
104+
qman_handler->InitializeQuorumConnections(tip);
105+
}
106+
}
107+
100108
void ActiveContext::UpdatedBlockTip(const CBlockIndex* pindexNew, const CBlockIndex* pindexFork, bool fInitialDownload)
101109
{
102110
if (fInitialDownload || pindexNew == pindexFork) // In IBD or blocks were disconnected without any new ones

src/active/context.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ struct ActiveContext final : public CValidationInterface {
7474

7575
void Start(CConnman& connman, PeerManager& peerman, int16_t worker_count);
7676
void Stop();
77+
void InitializeCurrentBlockTip(const CBlockIndex* tip, bool ibd);
7778

7879
CCoinJoinServer& GetCJServer() const;
7980
void SetCJServer(gsl::not_null<CCoinJoinServer*> cj_server);

src/init.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2417,6 +2417,15 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
24172417
// but don't call it directly to prevent triggering of other listeners like zmq etc.
24182418
// GetMainSignals().UpdatedBlockTip(::ChainActive().Tip());
24192419
g_ds_notification_interface->InitializeCurrentBlockTip();
2420+
{
2421+
const CBlockIndex* tip = WITH_LOCK(::cs_main, return chainman.ActiveTip());
2422+
const bool ibd = chainman.ActiveChainstate().IsInitialBlockDownload();
2423+
if (node.observer_ctx && !node.active_ctx) {
2424+
node.observer_ctx->InitializeCurrentBlockTip(tip, ibd);
2425+
}
2426+
// Note: active_ctx initialization is deferred until after nodeman->Init()
2427+
// so that GetProTxHash() is available for quorum connection setup.
2428+
}
24202429

24212430
{
24222431
// Get all UTXOs for each MN collateral in one go so that we can fill coin cache early
@@ -2481,6 +2490,13 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
24812490

24822491
if (node.active_ctx) {
24832492
node.active_ctx->nodeman->Init(chainman.ActiveTip());
2493+
// Initialize current block tip after nodeman->Init() so that
2494+
// GetProTxHash() is available for quorum connection setup.
2495+
// Without this ordering, EnsureQuorumConnections returns early
2496+
// because the null proTxHash makes the MN appear as a non-member.
2497+
const CBlockIndex* tip = WITH_LOCK(::cs_main, return chainman.ActiveTip());
2498+
const bool ibd = chainman.ActiveChainstate().IsInitialBlockDownload();
2499+
node.active_ctx->InitializeCurrentBlockTip(tip, ibd);
24842500
}
24852501
});
24862502
#ifdef ENABLE_WALLET

src/llmq/observer/context.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ void ObserverContext::Stop()
4747
qman_handler->Stop();
4848
}
4949

50+
void ObserverContext::InitializeCurrentBlockTip(const CBlockIndex* tip, bool ibd)
51+
{
52+
UpdatedBlockTip(tip, nullptr, ibd);
53+
if (tip) {
54+
qman_handler->InitializeQuorumConnections(tip);
55+
}
56+
}
57+
5058
void ObserverContext::UpdatedBlockTip(const CBlockIndex* pindexNew, const CBlockIndex* pindexFork, bool fInitialDownload)
5159
{
5260
if (fInitialDownload || pindexNew == pindexFork) // In IBD or blocks were disconnected without any new ones

src/llmq/observer/context.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ struct ObserverContext final : public CValidationInterface {
4949

5050
void Start(int16_t worker_count);
5151
void Stop();
52+
void InitializeCurrentBlockTip(const CBlockIndex* tip, bool ibd);
5253

5354
protected:
5455
// CValidationInterface

src/llmq/observer/quorums.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ void QuorumObserver::Stop()
5959
workerPool.stop(true);
6060
}
6161

62+
void QuorumObserver::InitializeQuorumConnections(gsl::not_null<const CBlockIndex*> pindexNew) const
63+
{
64+
for (const auto& params : Params().GetConsensus().llmqs) {
65+
CheckQuorumConnections(params, pindexNew);
66+
}
67+
}
68+
6269
void QuorumObserver::UpdatedBlockTip(const CBlockIndex* pindexNew, bool fInitialDownload) const
6370
{
6471
if (!pindexNew) return;

src/llmq/observer/quorums.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ class QuorumObserver
9696
void Stop();
9797

9898
void UpdatedBlockTip(const CBlockIndex* pindexNew, bool fInitialDownload) const;
99+
void InitializeQuorumConnections(gsl::not_null<const CBlockIndex*> pindexNew) const;
99100

100101
public:
101102
virtual bool SetQuorumSecretKeyShare(CQuorum& quorum, Span<CBLSSecretKey> skContributions) const;

test/functional/p2p_instantsend.py

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
55

66
from test_framework.test_framework import DashTestFramework
7-
from test_framework.util import assert_equal, assert_raises_rpc_error
7+
from test_framework.util import assert_equal, assert_raises_rpc_error, force_finish_mnsync
88

99
'''
1010
p2p_instantsend.py
@@ -36,6 +36,7 @@ def run_test(self):
3636

3737
self.test_mempool_doublespend()
3838
self.test_block_doublespend()
39+
self.test_instantsend_after_restart()
3940

4041
def test_block_doublespend(self):
4142
sender = self.nodes[self.sender_idx]
@@ -143,5 +144,85 @@ def test_mempool_doublespend(self):
143144
# mine more blocks
144145
self.generate(self.nodes[0], 2)
145146

147+
def test_instantsend_after_restart(self):
148+
self.log.info("Testing InstantSend works after full restart without new blocks")
149+
150+
# fund sender with confirmed coins
151+
sender = self.nodes[self.sender_idx]
152+
receiver = self.nodes[self.receiver_idx]
153+
sender_addr = sender.getnewaddress()
154+
fund_id = self.nodes[0].sendtoaddress(sender_addr, 1)
155+
self.bump_mocktime(30)
156+
self.sync_mempools()
157+
for node in self.nodes:
158+
self.wait_for_instantlock(fund_id, node)
159+
tip = self.generate(self.nodes[0], 2)[-1]
160+
self.bump_mocktime(30)
161+
self.wait_for_chainlocked_block_all_nodes(tip)
162+
self.sync_blocks()
163+
assert sender.getbalance() >= 0.5
164+
165+
receiver_addr = receiver.getnewaddress()
166+
167+
# restart all nodes without mining new blocks
168+
self.log.info("Restarting all nodes")
169+
num_simple_nodes = self.num_nodes - self.mn_count
170+
self.stop_nodes()
171+
172+
for i in range(num_simple_nodes):
173+
self.start_node(i)
174+
for mn_info in self.mninfo:
175+
self.start_masternode(mn_info)
176+
177+
# reconnect: simple nodes to node 0, MNs to node 0 only.
178+
# Quorum connections between MNs must be re-established automatically
179+
# via InitializeCurrentBlockTip → EnsureQuorumConnections, NOT via
180+
# manual connect_nodes between MN pairs.
181+
for i in range(1, num_simple_nodes):
182+
self.connect_nodes(i, 0)
183+
for mn_info in self.mninfo:
184+
self.connect_nodes(mn_info.nodeIdx, 0)
185+
for i in range(num_simple_nodes):
186+
force_finish_mnsync(self.nodes[i])
187+
188+
# bump past WAIT_FOR_ISLOCK_TIMEOUT so txFirstSeenTime loss doesn't
189+
# block chainlock signing for TXs mined before restart
190+
self.bump_mocktime(10 * 60 + 1)
191+
self.sync_blocks()
192+
193+
# Verify that MNs formed quorum connections to other MNs after restart.
194+
# InitializeCurrentBlockTip → EnsureQuorumConnections must populate
195+
# masternodeQuorumNodes so ThreadOpenMasternodeConnections establishes
196+
# MN-to-MN links beyond the manual connections to node 0.
197+
self.log.info("Verifying MN-to-MN quorum connections formed after restart")
198+
for mn_info in self.mninfo:
199+
mn_node = self.nodes[mn_info.nodeIdx]
200+
201+
def check_mn_peers(node=mn_node, my_hash=mn_info.proTxHash):
202+
peers = node.getpeerinfo()
203+
mn_peers = set(p['verified_proregtx_hash'] for p in peers
204+
if p.get('verified_proregtx_hash', '') != '')
205+
other_mn_peers = mn_peers - {my_hash}
206+
return len(other_mn_peers) > 0
207+
self.wait_until(check_mn_peers, timeout=30)
208+
209+
# re-grab references after restart
210+
sender = self.nodes[self.sender_idx]
211+
receiver = self.nodes[self.receiver_idx]
212+
213+
# send a TX — needs IS lock from all restarted MNs, no new blocks mined
214+
is_id = sender.sendtoaddress(receiver_addr, 0.5)
215+
self.bump_mocktime(30)
216+
self.sync_mempools()
217+
for node in self.nodes:
218+
self.wait_for_instantlock(is_id, node)
219+
self.log.info("InstantSend lock succeeded after full restart")
220+
221+
# clean up
222+
receiver.sendtoaddress(self.nodes[0].getnewaddress(), 0.5, "", "", True)
223+
self.bump_mocktime(30)
224+
self.sync_mempools()
225+
self.generate(self.nodes[0], 2)
226+
146227
if __name__ == '__main__':
147228
InstantSendTest().main()

0 commit comments

Comments
 (0)