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
69 changes: 66 additions & 3 deletions test/functional/feature_llmq_dkgerrors.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,17 @@
# 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.messages import (
CBlock,
CFinalCommitmentPayload,
from_hex,
)

from test_framework.test_framework import DashTestFramework
from test_framework.util import assert_equal
'''
feature_llmq_dkgerrors.py

Expand All @@ -20,8 +29,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:]
Expand Down Expand Up @@ -70,6 +80,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])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we have enum class LLMQType constants mirrored in Python and have this expressed as subtractions of known-good values? Not a dealbreaker but worth considering since it would convey what is a valid value and what we did to make it invalid.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

we don't have it currently.

Either constant is defined when it's used multiple times over test:

test/functional/feature_asset_locks.py:llmq_type_test = 106 # LLMQType::LLMQ_TEST_PLATFORM

Or just directly specified llmq_type:

test/functional/feature_llmq_evo.py:            self.test_quorum_members_are_evo_nodes(quorum_i_hash, llmq_type=106)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If there's additional iteration on this PR or work on resolving the TODOs in a future PR, do consider defining them and adjusting tests to use them

# 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']:
Expand Down
28 changes: 28 additions & 0 deletions test/functional/test_framework/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("<H", f.read(2))[0]
self.nHeight = struct.unpack("<I", f.read(4))[0]
self.commitment = CFinalCommitment()
self.commitment.deserialize(f)

def serialize(self):
r = b""
r += struct.pack("<H", self.nVersion)
r += struct.pack("<I", self.nHeight)
r += self.commitment.serialize()
return r

def __repr__(self):
return f"CFinalCommitmentPayload(nVersion={self.nVersion} nHeight={self.nHeight} commitment={self.commitment})"

class CGovernanceObject:
__slots__ = ("nHashParent", "nRevision", "nTime", "nCollateralHash", "vchData", "nObjectType",
"masternodeOutpoint", "vchSig")
Expand Down
9 changes: 5 additions & 4 deletions test/functional/test_framework/test_framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -2185,10 +2185,11 @@ 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"]))
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)
Expand Down
Loading