From bb0b8b00032c1f37ab7d15c0eef675204036d748 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Mon, 16 Jun 2025 18:15:45 +0700 Subject: [PATCH 1/4] test: add serialization/deserialization of CFinalCommitmentPayload --- test/functional/test_framework/messages.py | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py index 95023f840c5b..5892c7c0d1ac 100755 --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -1407,6 +1407,34 @@ def __repr__(self): .format(self.nVersion, self.llmqType, self.quorumHash, self.quorumIndex, repr(self.signers), repr(self.validMembers), self.quorumPublicKey.hex(), self.quorumVvecHash, self.quorumSig.hex(), self.membersSig.hex()) + +class CFinalCommitmentPayload: + __slots__ = ("nVersion", "nHeight", "commitment") + + def __init__(self): + self.set_null() + + def set_null(self): + self.nVersion = 0 + self.nHeight = 0 + self.commitment = CFinalCommitment() + + def deserialize(self, f): + self.nVersion = struct.unpack(" Date: Mon, 16 Jun 2025 21:12:10 +0700 Subject: [PATCH 2/4] test: added functional tests for invalid CQuorumCommitment --- test/functional/feature_llmq_dkgerrors.py | 68 ++++++++++++++++++- .../test_framework/test_framework.py | 7 +- 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/test/functional/feature_llmq_dkgerrors.py b/test/functional/feature_llmq_dkgerrors.py index 803294c48a3f..f46161caf1f4 100755 --- a/test/functional/feature_llmq_dkgerrors.py +++ b/test/functional/feature_llmq_dkgerrors.py @@ -3,8 +3,16 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -from test_framework.test_framework import DashTestFramework +import copy +from io import BytesIO +from test_framework.test_framework import DashTestFramework +from test_framework.messages import ( + CBlock, + CFinalCommitmentPayload, + from_hex, +) +from test_framework.util import assert_equal ''' feature_llmq_dkgerrors.py @@ -20,8 +28,9 @@ def run_test(self): self.nodes[0].sporkupdate("SPORK_17_QUORUM_DKG_ENABLED", 0) self.wait_for_sporks_same() - self.log.info("Mine one quorum without simulating any errors") - qh = self.mine_quorum() + qh = self.test_qc() + + self.log.info("Mine one regular quorum with no invalid members is mined at this point") self.assert_member_valid(qh, self.mninfo[0].proTxHash, True) mninfos_valid = self.mninfo.copy()[1:] @@ -70,6 +79,59 @@ def run_test(self): qh = self.mine_quorum(expected_contributions=3, expected_complaints=0, expected_justifications=0, expected_commitments=2, mninfos_valid=mninfos_valid) self.assert_member_valid(qh, self.mninfo[0].proTxHash, True) + def test_qc(self): + quorumHash = self.mine_quorum(skip_maturity=True) + best = self.nodes[0].getbestblockhash() + block_hex = self.nodes[0].getblock(best, 0) + + block = from_hex(CBlock(), block_hex) + + self.nodes[0].invalidateblock(best) + + self.test_invalid(block, 'bad-qc-commitment-type', lambda qc : (setattr(qc, 'llmqType', 77), qc)[1]) + self.test_invalid(block, 'bad-qc-invalid', lambda qc : (setattr(qc, 'llmqType', 106), qc)[1]) + # TODO: test quorumIndex for rotation quorums + # self.test_invalid(block, 'bad-qc-invalid', lambda qc : (setattr(qc, 'quorumIndex', 2), qc)[1]) + self.test_invalid(block, 'bad-qc-invalid', lambda qc : (setattr(qc, 'quorumSig', getattr(qc, 'membersSig')), qc)[1]) + self.test_invalid(block, 'bad-qc-invalid', lambda qc : (setattr(qc, 'membersSig', getattr(qc, 'quorumSig')), qc)[1]) + self.test_invalid(block, 'bad-qc-invalid', lambda qc : (setattr(qc, 'quorumPublicKey', b'\x00' * 48), qc)[1]) + # TODO: test quorumVvecHash + # TODO: test signers + # TODO: test validMembers + + self.nodes[0].reconsiderblock(best) + # Mine 8 (SIGN_HEIGHT_OFFSET) more blocks to make sure that the new quorum gets eligible for signing sessions + self.generate(self.nodes[0], 8, sync_fun=lambda: self.sync_blocks(self.nodes)) + + return quorumHash + + def test_invalid(self, original_block, error, transform): + node = self.nodes[0] + + block = copy.deepcopy(original_block) + + for tx in block.vtx: + if tx.nType == 6: + qc_payload = CFinalCommitmentPayload() + qc_payload.deserialize(BytesIO(tx.vExtraPayload)) + # test not only quorum 100, rotation quorums also and single-node quorum + if qc_payload.commitment.llmqType == 100: + qc_payload.commitment = transform(qc_payload.commitment) + + tx.vExtraPayload = qc_payload.serialize() + tx.rehash() + + def assert_submitblock(block, result_str_1, result_str_2=None): + block.hashMerkleRoot = block.calc_merkle_root() + block.solve() + result_str_2 = result_str_2 or 'duplicate-invalid' + assert_equal(result_str_1, node.submitblock(hexdata=block.serialize().hex())) + assert_equal(result_str_2, node.submitblock(hexdata=block.serialize().hex())) + + # TODO: implement similar validation not only for submitblock but for p2p message + assert_submitblock(block, error) + + def assert_member_valid(self, quorumHash, proTxHash, expectedValid): q = self.nodes[0].quorum('info', 100, quorumHash, True) for m in q['members']: diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index ca98429fac64..a05434eb972a 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -2108,7 +2108,7 @@ def move_blocks(self, nodes, num_blocks): self.bump_mocktime(1, nodes=nodes) self.generate(self.nodes[0], num_blocks, sync_fun=lambda: self.sync_blocks(nodes)) - def mine_quorum(self, llmq_type_name="llmq_test", llmq_type=100, expected_connections=None, expected_members=None, expected_contributions=None, expected_complaints=0, expected_justifications=0, expected_commitments=None, mninfos_online=None, mninfos_valid=None): + def mine_quorum(self, llmq_type_name="llmq_test", llmq_type=100, expected_connections=None, expected_members=None, expected_contributions=None, expected_complaints=0, expected_justifications=0, expected_commitments=None, mninfos_online=None, mninfos_valid=None, skip_maturity=False): spork21_active = self.nodes[0].spork('show')['SPORK_21_QUORUM_ALL_CONNECTED'] <= 1 spork23_active = self.nodes[0].spork('show')['SPORK_23_QUORUM_POSE'] <= 1 @@ -2185,8 +2185,9 @@ def mine_quorum(self, llmq_type_name="llmq_test", llmq_type=100, expected_connec assert_equal(q, new_quorum) quorum_info = self.nodes[0].quorum("info", llmq_type, new_quorum) - # Mine 8 (SIGN_HEIGHT_OFFSET) more blocks to make sure that the new quorum gets eligible for signing sessions - self.generate(self.nodes[0], 8, sync_fun=lambda: self.sync_blocks(nodes)) + if not skip_maturity: + # Mine 8 (SIGN_HEIGHT_OFFSET) more blocks to make sure that the new quorum gets eligible for signing sessions + self.generate(self.nodes[0], 8, sync_fun=lambda: self.sync_blocks(nodes)) self.log.info("New quorum: height=%d, quorumHash=%s, quorumIndex=%d, minedBlock=%s" % (quorum_info["height"], new_quorum, quorum_info["quorumIndex"], quorum_info["minedBlock"])) From 59060b54ad98e2aa9f69c6c217f41b1a55e5197b Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Wed, 2 Jul 2025 01:56:10 +0700 Subject: [PATCH 3/4] fmt: order imports and fix gap in feature_llmq_dkgerrors.py --- test/functional/feature_llmq_dkgerrors.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/functional/feature_llmq_dkgerrors.py b/test/functional/feature_llmq_dkgerrors.py index f46161caf1f4..187bea3b8bc8 100755 --- a/test/functional/feature_llmq_dkgerrors.py +++ b/test/functional/feature_llmq_dkgerrors.py @@ -6,12 +6,13 @@ import copy from io import BytesIO -from test_framework.test_framework import DashTestFramework from test_framework.messages import ( - CBlock, - CFinalCommitmentPayload, - from_hex, + CBlock, + CFinalCommitmentPayload, + from_hex, ) + +from test_framework.test_framework import DashTestFramework from test_framework.util import assert_equal ''' feature_llmq_dkgerrors.py From c9ef70a38a4b5de4bcce9a01230f4ed69224ef3c Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Wed, 2 Jul 2025 01:56:39 +0700 Subject: [PATCH 4/4] tests: add is_mature for quorum generation logs --- test/functional/test_framework/test_framework.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index a05434eb972a..2ab88cfabcf3 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -2189,7 +2189,7 @@ def mine_quorum(self, llmq_type_name="llmq_test", llmq_type=100, expected_connec # Mine 8 (SIGN_HEIGHT_OFFSET) more blocks to make sure that the new quorum gets eligible for signing sessions self.generate(self.nodes[0], 8, sync_fun=lambda: self.sync_blocks(nodes)) - self.log.info("New quorum: height=%d, quorumHash=%s, quorumIndex=%d, minedBlock=%s" % (quorum_info["height"], new_quorum, quorum_info["quorumIndex"], quorum_info["minedBlock"])) + self.log.info(f"New quorum: height={quorum_info['height']}, quorumHash={new_quorum}, is_mature={not skip_maturity} quorumIndex={quorum_info['quorumIndex']}, minedBlock={quorum_info['minedBlock']}") for mn in mninfos_valid: assert not check_punished(self.nodes[0], mn)