Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion test/functional/feature_asset_locks.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from test_framework.wallet_util import bytes_to_wif

llmq_type_test = 106 # LLMQType::LLMQ_TEST_PLATFORM
MNEHF_SIGNAL_TX_TYPE = 7 # TRANSACTION_MNHF_SIGNAL
tiny_amount = int(Decimal("0.0007") * COIN)
blocks_in_one_day = 100
HEIGHT_DIFF_EXPIRING = 48
Expand Down Expand Up @@ -175,9 +176,13 @@ def validate_credit_pool_balance(self, expected = None, block_hash = None):
return expected

def check_mempool_size(self):
# Masternodes submit the MnEHF signal transaction on their own as soon as a quorum
# able to sign it exists, so it is not part of what this test puts in the mempool.
self.sync_mempools()
for node in self.nodes:
assert_equal(node.getmempoolinfo()['size'], self.mempool_size)
own = [txid for txid in node.getrawmempool()
if node.getrawtransaction(txid, 1)['type'] != MNEHF_SIGNAL_TX_TYPE]
assert_equal(len(own), self.mempool_size)

def check_mempool_result(self, result_expected, tx):
"""Wrapper to check result of testmempoolaccept on node_0's mempool"""
Expand Down
37 changes: 22 additions & 15 deletions test/functional/test_framework/test_framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -2151,20 +2151,13 @@ def check_dkg_session():

self.wait_until(check_dkg_session, timeout=timeout, sleep=sleep)

def node_has_quorum_commitment(self, node, quorum_hash, llmq_type):
def node_quorum_commitment_types(self, node, quorum_hash):
s = node.quorum("dkgstatus")
if "minableCommitments" not in s:
return False
commits = s["minableCommitments"]
for c in commits:
if c["llmqType"] != llmq_type:
continue
if c["quorumHash"] != quorum_hash:
continue
if c["quorumPublicKey"] == '0' * 96:
continue
return True
return False
return {c["llmqType"] for c in s.get("minableCommitments", [])
if c["quorumHash"] == quorum_hash and c["quorumPublicKey"] != '0' * 96}

def node_has_quorum_commitment(self, node, quorum_hash, llmq_type):
return llmq_type in self.node_quorum_commitment_types(node, quorum_hash)

def wait_for_quorum_commitment(self, quorum_hash, mninfos, llmq_type=100, timeout=15):
def check_dkg_comitments():
Expand All @@ -2175,6 +2168,20 @@ def check_dkg_comitments():

self.wait_until(check_dkg_comitments, timeout=timeout)

def wait_for_quorum_commitments_on_miner(self, quorum_hash, mninfos, timeout=15):
# The final-commitment block carries a commitment for every llmq type whose mining
# window is open, so a commitment that has not reached the mining node in time is
# mined as a null one and its quorum is silently skipped for the whole cycle. Only
# commitments the masternodes actually produced are awaited, so a type whose DKG
# legitimately produced nothing does not hold this up.
def check_miner_commitments():
expected = set()
for mn in mninfos:
expected |= self.node_quorum_commitment_types(mn.get_node(self), quorum_hash)
return expected <= self.node_quorum_commitment_types(self.nodes[0], quorum_hash)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Wait for auxiliary DKG handlers to finalize

When an undriven LLMQ type's independent phase-handler thread is slower than the driven type, none of the queried masternodes may advertise that auxiliary commitment on the first poll. In that case expected contains only the driven commitment—which the miner already has—so this subset check succeeds immediately; the following block can still be mined before the slower handler adds and relays its real commitment, recreating the null-commitment/skipped-cycle behavior this change is intended to prevent. Establish that the auxiliary handlers have finished, or otherwise stabilize the expected type set, before accepting this condition.

AGENTS.md reference: AGENTS.md:L163-L165

Useful? React with 👍 / 👎.

Comment on lines +2177 to +2181

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Stabilize auxiliary commitment discovery before mining

This predicate can succeed before an auxiliary LLMQ handler has finalized. The preceding wait_for_quorum_commitment() only waits for the explicitly driven type, so the first poll here may find that type alone on every masternode and immediately confirm that the miner has it. Each LLMQ type runs in an independent phase-handler thread (src/llmq/net_dkg.cpp:605-610), and reaching phase 6 does not close the race: WaitForNextPhase() publishes the phase before control returns to FinalizeCommitments() and AddMineableCommitment() at src/llmq/net_dkg.cpp:788-792. A slower auxiliary handler can therefore add its real commitment just after this subset check returns, while the following block has already synthesized a null commitment for that type. Establish that all relevant auxiliary handlers have completed finalization before deriving the expected set, while retaining an explicit way to represent a completed DKG that legitimately produced no commitment.

source: ['codex']


self.wait_until(check_miner_commitments, timeout=timeout)

def wait_for_quorum_list(self, quorum_hash, nodes, timeout=15, llmq_type_name="llmq_test"):
def wait_func():
return quorum_hash in self.nodes[0].quorum('list')[llmq_type_name]
Expand Down Expand Up @@ -2285,8 +2292,8 @@ def mine_quorum(self, llmq_type_name="llmq_test", llmq_type=100, expected_connec
self.log.info("Waiting final commitment")
self.wait_for_quorum_commitment(q, mninfos_online, llmq_type=llmq_type)

self.log.info("Waiting final commitment on mining node")
self.wait_until(lambda: self.node_has_quorum_commitment(self.nodes[0], q, llmq_type), timeout=15)
self.log.info("Waiting final commitments on mining node")
self.wait_for_quorum_commitments_on_miner(q, mninfos_online)

self.log.info("Mining final commitment")
self.bump_mocktime(1)
Expand Down
Loading