diff --git a/test/functional/feature_llmq_is_cl_conflicts.py b/test/functional/feature_llmq_is_cl_conflicts.py index 70177051186f..c0821db3adca 100755 --- a/test/functional/feature_llmq_is_cl_conflicts.py +++ b/test/functional/feature_llmq_is_cl_conflicts.py @@ -51,8 +51,8 @@ def on_getdata(self, message): class LLMQ_IS_CL_Conflicts(DashTestFramework): def set_test_params(self): - self.set_dash_test_params(4, 3, fast_dip3_enforcement=True) - #disable_mocktime() + self.set_dash_test_params(5, 4, fast_dip3_enforcement=True) + self.set_dash_llmq_test_params(4, 4) def run_test(self): self.activate_dip8() @@ -72,6 +72,14 @@ def run_test(self): self.test_chainlock_overrides_islock(True, False) self.test_chainlock_overrides_islock(True, True) self.test_chainlock_overrides_islock_overrides_nonchainlock(False) + self.activate_dip0024() + self.log.info("Activated DIP0024 at height:" + str(self.nodes[0].getblockcount())) + self.test_chainlock_overrides_islock_overrides_nonchainlock(False) + # At this point, we need to move forward 3 cycles (3 x 24 blocks) so the first 3 quarters can be created (without DKG sessions) + self.move_to_next_cycle() + self.move_to_next_cycle() + self.move_to_next_cycle() + self.mine_cycle_quorum() self.test_chainlock_overrides_islock_overrides_nonchainlock(True) def test_chainlock_overrides_islock(self, test_block_conflict, mine_confllicting=False): @@ -320,7 +328,7 @@ def create_block(self, node, vtx=[]): coinbase.calc_sha256() - block = create_block(int(tip_hash, 16), coinbase, ntime=bt['curtime']) + block = create_block(int(tip_hash, 16), coinbase, ntime=bt['curtime'], version=bt['version']) block.vtx += vtx # Add quorum commitments from template diff --git a/test/functional/feature_llmq_is_migration.py b/test/functional/feature_llmq_is_migration.py index 213a16c4bc2e..68ff79291c6b 100755 --- a/test/functional/feature_llmq_is_migration.py +++ b/test/functional/feature_llmq_is_migration.py @@ -6,7 +6,7 @@ from test_framework.messages import CTransaction, FromHex, hash256, ser_compact_size, ser_string from test_framework.test_framework import DashTestFramework -from test_framework.util import wait_until, connect_nodes, sync_blocks +from test_framework.util import wait_until, connect_nodes ''' feature_llmq_is_migration.py @@ -91,7 +91,7 @@ def run_test(self): self.move_to_next_cycle() self.log.info("Cycle H+2C height:" + str(self.nodes[0].getblockcount())) - (quorum_info_0_0, quorum_info_0_1) = self.mine_cycle_quorum("llmq_test_dip0024", 103) + (quorum_info_0_0, quorum_info_0_1) = self.mine_cycle_quorum() q_list = self.nodes[0].quorum("list") self.log.info(q_list) @@ -118,20 +118,5 @@ def run_test(self): assert not n.quorum("hasrecsig", 104, request_id2, txid2) - def move_to_next_cycle(self): - cycle_length = 24 - mninfos_online = self.mninfo.copy() - nodes = [self.nodes[0]] + [mn.node for mn in mninfos_online] - cur_block = self.nodes[0].getblockcount() - - # move forward to next DKG - skip_count = cycle_length - (cur_block % cycle_length) - if skip_count != 0: - self.bump_mocktime(1, nodes=nodes) - self.nodes[0].generate(skip_count) - sync_blocks(nodes) - time.sleep(1) - self.log.info('Moved from block %d to %d' % (cur_block, self.nodes[0].getblockcount())) - if __name__ == '__main__': LLMQISMigrationTest().main() diff --git a/test/functional/feature_llmq_rotation.py b/test/functional/feature_llmq_rotation.py index 18f574a0e872..6e737ed49b3e 100755 --- a/test/functional/feature_llmq_rotation.py +++ b/test/functional/feature_llmq_rotation.py @@ -9,7 +9,6 @@ Checks LLMQs Quorum Rotation ''' -import time from test_framework.test_framework import DashTestFramework from test_framework.util import ( assert_equal, @@ -128,21 +127,6 @@ def run_test(self): wait_until(lambda: self.nodes[0].getbestblockhash() == new_quorum_blockhash, sleep=1) assert_equal(self.nodes[0].quorum("list", llmq_type), new_quorum_list) - def move_to_next_cycle(self): - cycle_length = 24 - mninfos_online = self.mninfo.copy() - nodes = [self.nodes[0]] + [mn.node for mn in mninfos_online] - cur_block = self.nodes[0].getblockcount() - - # move forward to next DKG - skip_count = cycle_length - (cur_block % cycle_length) - if skip_count != 0: - self.bump_mocktime(1, nodes=nodes) - self.nodes[0].generate(skip_count) - sync_blocks(nodes) - time.sleep(1) - self.log.info('Moved from block %d to %d' % (cur_block, self.nodes[0].getblockcount())) - if __name__ == '__main__': LLMQQuorumRotationTest().main() diff --git a/test/functional/interface_zmq_dash.py b/test/functional/interface_zmq_dash.py index 4410ae53b12a..dc5caaf4b974 100755 --- a/test/functional/interface_zmq_dash.py +++ b/test/functional/interface_zmq_dash.py @@ -28,6 +28,7 @@ msg_clsig, msg_inv, msg_isdlock, + msg_islock, msg_tx, ser_string, uint256_from_str, @@ -52,24 +53,39 @@ class ZMQPublisher(Enum): raw_recovered_sig = "rawrecoveredsig" +class ZMQSubscriber: + def __init__(self, socket, topic): + self.socket = socket + self.topic = topic + + import zmq + self.socket.setsockopt(zmq.SUBSCRIBE, self.topic) + + def receive(self, flags=0): + topic, body, seq = self.socket.recv_multipart(flags) + # Topic should match the subscriber topic. + assert_equal(topic, self.topic) + return io.BytesIO(body) + + class TestP2PConn(P2PInterface): def __init__(self): super().__init__() self.islocks = {} self.txes = {} - def send_islock(self, islock): + def send_islock(self, islock, deterministic): hash = uint256_from_str(hash256(islock.serialize())) self.islocks[hash] = islock - inv = msg_inv([CInv(30, hash)]) + inv = msg_inv([CInv(31 if deterministic else 30, hash)]) self.send_message(inv) - def send_tx(self, tx): + def send_tx(self, tx, deterministic): hash = uint256_from_str(hash256(tx.serialize())) self.txes[hash] = tx - inv = msg_inv([CInv(30, hash)]) + inv = msg_inv([CInv(31 if deterministic else 30, hash)]) self.send_message(inv) def on_getdata(self, message): @@ -89,7 +105,10 @@ def set_test_params(self): node0_extra_args.append("-whitelist=127.0.0.1") node0_extra_args.append("-watchquorums") # have to watch quorums to receive recsigs and trigger zmq - self.set_dash_test_params(4, 3, fast_dip3_enforcement=True, extra_args=[node0_extra_args, [], [], []]) + extra_args = [[]] * 5 + extra_args[0] = node0_extra_args + self.set_dash_test_params(5, 4, fast_dip3_enforcement=True, extra_args=extra_args) + self.set_dash_llmq_test_params(4, 4) def skip_test_if_missing_module(self): self.skip_if_no_py3_zmq() @@ -97,16 +116,15 @@ def skip_test_if_missing_module(self): self.skip_if_no_wallet() def run_test(self): + self.subscribers = {} # Check that dashd has been built with ZMQ enabled. config = configparser.ConfigParser() config.read_file(open(self.options.configfile)) import zmq try: - # Setup the ZMQ subscriber socket + # Setup the ZMQ subscriber context self.zmq_context = zmq.Context() - self.socket = self.zmq_context.socket(zmq.SUB) - self.socket.connect(self.address) # Initialize the network self.activate_dip8() self.nodes[0].spork("SPORK_17_QUORUM_DKG_ENABLED", 0) @@ -122,30 +140,41 @@ def run_test(self): # Test all dash related ZMQ publisher self.test_recovered_signature_publishers() self.test_chainlock_publishers() - self.test_instantsend_publishers() self.test_governance_publishers() self.test_getzmqnotifications() + self.test_instantsend_publishers(False) + self.activate_dip0024() + self.wait_for_chainlocked_block_all_nodes(self.nodes[0].getbestblockhash()) + self.log.info("Activated DIP0024 at height:" + str(self.nodes[0].getblockcount())) + self.test_instantsend_publishers(False) + # At this point, we need to move forward 3 cycles (3 x 24 blocks) so the first 3 quarters can be created (without DKG sessions) + self.move_to_next_cycle() + self.test_instantsend_publishers(False) + self.move_to_next_cycle() + self.test_instantsend_publishers(False) + self.move_to_next_cycle() + self.test_instantsend_publishers(False) + self.mine_cycle_quorum() + self.test_instantsend_publishers(True) finally: # Destroy the ZMQ context. self.log.debug("Destroying ZMQ context") self.zmq_context.destroy(linger=None) def subscribe(self, publishers): + import zmq + # Setup the ZMQ subscriber socket + socket = self.zmq_context.socket(zmq.SUB) + socket.set(zmq.RCVTIMEO, 60000) + socket.connect(self.address) # Subscribe to a list of ZMQPublishers for pub in publishers: - self.socket.subscribe(pub.value) + self.subscribers[pub] = ZMQSubscriber(socket, pub.value.encode()) def unsubscribe(self, publishers): # Unsubscribe from a list of ZMQPublishers for pub in publishers: - self.socket.unsubscribe(pub.value) - - def receive(self, publisher, flags=0): - # Receive a ZMQ message and validate it's sent from the correct ZMQPublisher - topic, body, seq = self.socket.recv_multipart(flags) - # Topic should match the publisher value - assert_equal(topic.decode(), publisher.value) - return io.BytesIO(body) + del self.subscribers[pub] def test_recovered_signature_publishers(self): @@ -153,11 +182,11 @@ def validate_recovered_sig(request_id, msg_hash): # Make sure the recovered sig exists by RPC rpc_recovered_sig = self.get_recovered_sig(request_id, msg_hash) # Validate hashrecoveredsig - zmq_recovered_sig_hash = self.receive(ZMQPublisher.hash_recovered_sig).read(32).hex() + zmq_recovered_sig_hash = self.subscribers[ZMQPublisher.hash_recovered_sig].receive().read(32).hex() assert_equal(zmq_recovered_sig_hash, msg_hash) # Validate rawrecoveredsig zmq_recovered_sig_raw = CRecoveredSig() - zmq_recovered_sig_raw.deserialize(self.receive(ZMQPublisher.raw_recovered_sig)) + zmq_recovered_sig_raw.deserialize(self.subscribers[ZMQPublisher.raw_recovered_sig].receive()) assert_equal(zmq_recovered_sig_raw.llmqType, rpc_recovered_sig['llmqType']) assert_equal(uint256_to_string(zmq_recovered_sig_raw.quorumHash), rpc_recovered_sig['quorumHash']) assert_equal(uint256_to_string(zmq_recovered_sig_raw.id), rpc_recovered_sig['id']) @@ -207,15 +236,15 @@ def test_chainlock_publishers(self): rpc_chain_lock_hash = rpc_chain_locked_block["hash"] assert_equal(generated_hash, rpc_chain_lock_hash) # Validate hashchainlock - zmq_chain_lock_hash = self.receive(ZMQPublisher.hash_chain_lock).read(32).hex() + zmq_chain_lock_hash = self.subscribers[ZMQPublisher.hash_chain_lock].receive().read(32).hex() assert_equal(zmq_chain_lock_hash, rpc_best_chain_lock_hash) # Validate rawchainlock zmq_chain_locked_block = CBlock() - zmq_chain_locked_block.deserialize(self.receive(ZMQPublisher.raw_chain_lock)) + zmq_chain_locked_block.deserialize(self.subscribers[ZMQPublisher.raw_chain_lock].receive()) assert zmq_chain_locked_block.is_valid() assert_equal(zmq_chain_locked_block.hash, rpc_chain_lock_hash) # Validate rawchainlocksig - zmq_chain_lock_sig_stream = self.receive(ZMQPublisher.raw_chain_lock_sig) + zmq_chain_lock_sig_stream = self.subscribers[ZMQPublisher.raw_chain_lock_sig].receive() zmq_chain_locked_block = CBlock() zmq_chain_locked_block.deserialize(zmq_chain_lock_sig_stream) assert zmq_chain_locked_block.is_valid() @@ -228,7 +257,7 @@ def test_chainlock_publishers(self): # Unsubscribe from ChainLock messages self.unsubscribe(chain_lock_publishers) - def test_instantsend_publishers(self): + def test_instantsend_publishers(self, deterministic): import zmq instantsend_publishers = [ ZMQPublisher.hash_tx_lock, @@ -251,57 +280,57 @@ def test_instantsend_publishers(self): rpc_raw_tx_1_hash = self.nodes[0].sendrawtransaction(rpc_raw_tx_1['hex']) self.wait_for_instantlock(rpc_raw_tx_1_hash, self.nodes[0]) # Validate hashtxlock - zmq_tx_lock_hash = self.receive(ZMQPublisher.hash_tx_lock).read(32).hex() + zmq_tx_lock_hash = self.subscribers[ZMQPublisher.hash_tx_lock].receive().read(32).hex() assert_equal(zmq_tx_lock_hash, rpc_raw_tx_1['txid']) # Validate rawtxlock zmq_tx_lock_raw = CTransaction() - zmq_tx_lock_raw.deserialize(self.receive(ZMQPublisher.raw_tx_lock)) + zmq_tx_lock_raw.deserialize(self.subscribers[ZMQPublisher.raw_tx_lock].receive()) assert zmq_tx_lock_raw.is_valid() assert_equal(zmq_tx_lock_raw.hash, rpc_raw_tx_1['txid']) # Validate rawtxlocksig - zmq_tx_lock_sig_stream = self.receive(ZMQPublisher.raw_tx_lock_sig) + zmq_tx_lock_sig_stream = self.subscribers[ZMQPublisher.raw_tx_lock_sig].receive() zmq_tx_lock_tx = CTransaction() zmq_tx_lock_tx.deserialize(zmq_tx_lock_sig_stream) assert zmq_tx_lock_tx.is_valid() assert_equal(zmq_tx_lock_tx.hash, rpc_raw_tx_1['txid']) - zmq_tx_lock = msg_isdlock() + zmq_tx_lock = msg_isdlock() if deterministic else msg_islock() zmq_tx_lock.deserialize(zmq_tx_lock_sig_stream) assert_equal(uint256_to_string(zmq_tx_lock.txid), rpc_raw_tx_1['txid']) # Try to send the second transaction. This must throw an RPC error because it conflicts with rpc_raw_tx_1 # which already got the InstantSend lock. assert_raises_rpc_error(-26, "tx-txlock-conflict", self.nodes[0].sendrawtransaction, rpc_raw_tx_2['hex']) # Validate hashinstantsenddoublespend - zmq_double_spend_hash2 = self.receive(ZMQPublisher.hash_instantsend_doublespend).read(32).hex() - zmq_double_spend_hash1 = self.receive(ZMQPublisher.hash_instantsend_doublespend).read(32).hex() + zmq_double_spend_hash2 = self.subscribers[ZMQPublisher.hash_instantsend_doublespend].receive().read(32).hex() + zmq_double_spend_hash1 = self.subscribers[ZMQPublisher.hash_instantsend_doublespend].receive().read(32).hex() assert_equal(zmq_double_spend_hash2, rpc_raw_tx_2['txid']) assert_equal(zmq_double_spend_hash1, rpc_raw_tx_1['txid']) # Validate rawinstantsenddoublespend zmq_double_spend_tx_2 = CTransaction() - zmq_double_spend_tx_2.deserialize(self.receive(ZMQPublisher.raw_instantsend_doublespend)) + zmq_double_spend_tx_2.deserialize(self.subscribers[ZMQPublisher.raw_instantsend_doublespend].receive()) assert zmq_double_spend_tx_2.is_valid() assert_equal(zmq_double_spend_tx_2.hash, rpc_raw_tx_2['txid']) zmq_double_spend_tx_1 = CTransaction() - zmq_double_spend_tx_1.deserialize(self.receive(ZMQPublisher.raw_instantsend_doublespend)) + zmq_double_spend_tx_1.deserialize(self.subscribers[ZMQPublisher.raw_instantsend_doublespend].receive()) assert zmq_double_spend_tx_1.is_valid() assert_equal(zmq_double_spend_tx_1.hash, rpc_raw_tx_1['txid']) # No islock notifications when tx is not received yet self.nodes[0].generate(1) rpc_raw_tx_3 = self.create_raw_tx(self.nodes[0], self.nodes[0], 1, 1, 100) - islock = self.create_islock(rpc_raw_tx_3['hex']) - self.test_node.send_islock(islock) + islock = self.create_islock(rpc_raw_tx_3['hex'], deterministic) + self.test_node.send_islock(islock, deterministic) # Validate NO hashtxlock time.sleep(1) try: - self.receive(ZMQPublisher.hash_tx_lock, zmq.NOBLOCK) + self.subscribers[ZMQPublisher.hash_tx_lock].receive(zmq.NOBLOCK) assert False except zmq.ZMQError: # this is expected pass # Now send the tx itself - self.test_node.send_tx(FromHex(msg_tx(), rpc_raw_tx_3['hex'])) + self.test_node.send_tx(FromHex(msg_tx(), rpc_raw_tx_3['hex']), deterministic) self.wait_for_instantlock(rpc_raw_tx_3['txid'], self.nodes[0]) # Validate hashtxlock - zmq_tx_lock_hash = self.receive(ZMQPublisher.hash_tx_lock).read(32).hex() + zmq_tx_lock_hash = self.subscribers[ZMQPublisher.hash_tx_lock].receive().read(32).hex() assert_equal(zmq_tx_lock_hash, rpc_raw_tx_3['txid']) # Drop test node connection self.nodes[0].disconnect_p2ps() @@ -337,10 +366,11 @@ def test_governance_publishers(self): self.sync_blocks() rpc_proposal_hash = self.nodes[0].gobject("submit", "0", proposal_rev, proposal_time, proposal_hex, collateral) # Validate hashgovernanceobject - zmq_governance_object_hash = self.receive(ZMQPublisher.hash_governance_object).read(32).hex() + zmq_governance_object_hash = self.subscribers[ZMQPublisher.hash_governance_object].receive().read(32).hex() assert_equal(zmq_governance_object_hash, rpc_proposal_hash) + # Validate rawgovernanceobject zmq_governance_object_raw = CGovernanceObject() - zmq_governance_object_raw.deserialize(self.receive(ZMQPublisher.raw_governance_object)) + zmq_governance_object_raw.deserialize(self.subscribers[ZMQPublisher.raw_governance_object].receive()) assert_equal(zmq_governance_object_raw.nHashParent, 0) assert_equal(zmq_governance_object_raw.nRevision, proposal_rev) assert_equal(zmq_governance_object_raw.nTime, proposal_time) @@ -365,11 +395,11 @@ def test_governance_publishers(self): self.nodes[0].gobject("vote-many", rpc_proposal_hash, map_vote_signals[1], map_vote_outcomes[1]) rpc_proposal_votes = self.nodes[0].gobject('getcurrentvotes', rpc_proposal_hash) # Validate hashgovernancevote - zmq_governance_vote_hash = self.receive(ZMQPublisher.hash_governance_vote).read(32).hex() + zmq_governance_vote_hash = self.subscribers[ZMQPublisher.hash_governance_vote].receive().read(32).hex() assert zmq_governance_vote_hash in rpc_proposal_votes # Validate rawgovernancevote zmq_governance_vote_raw = CGovernanceVote() - zmq_governance_vote_raw.deserialize(self.receive(ZMQPublisher.raw_governance_vote)) + zmq_governance_vote_raw.deserialize(self.subscribers[ZMQPublisher.raw_governance_vote].receive()) assert_equal(uint256_to_string(zmq_governance_vote_raw.nParentHash), rpc_proposal_hash) rpc_vote_parts = rpc_proposal_votes[zmq_governance_vote_hash].split(':') rpc_outpoint_parts = rpc_vote_parts[0].split('-') diff --git a/test/functional/rpc_verifyislock.py b/test/functional/rpc_verifyislock.py index 7c503fee9725..e16a8f694abd 100755 --- a/test/functional/rpc_verifyislock.py +++ b/test/functional/rpc_verifyislock.py @@ -82,7 +82,7 @@ def run_test(self): break assert selected_hash == oldest_quorum_hash # Create the ISLOCK, then mine a quorum to move the signing quorum out of the active set - islock = self.create_islock(rawtx, True) + islock = self.create_islock(rawtx, False) # Mine one block to trigger the "signHeight + dkgInterval" verification for the ISLOCK self.mine_quorum() # Verify the ISLOCK for a transaction that is not yet known by the node diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index a0a589db9256..c7c53ae4875b 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -1038,13 +1038,14 @@ def create_islock(self, hextx, deterministic=False): request_id = hash256(request_id_buf)[::-1].hex() message_hash = tx.hash + llmq_type = 103 if deterministic else 104 quorum_member = None for mn in self.mninfo: - res = mn.node.quorum('sign', 104, request_id, message_hash) + res = mn.node.quorum('sign', llmq_type, request_id, message_hash) if (res and quorum_member is None): quorum_member = mn - rec_sig = self.get_recovered_sig(request_id, message_hash, node=quorum_member.node, llmq_type=104) + rec_sig = self.get_recovered_sig(request_id, message_hash, node=quorum_member.node, llmq_type=llmq_type) if deterministic: block_count = quorum_member.node.getblockcount() @@ -1330,7 +1331,7 @@ def mine_quorum(self, llmq_type_name="llmq_test", llmq_type=100, expected_connec return new_quorum - def mine_cycle_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_cycle_quorum(self, llmq_type_name="llmq_test_dip0024", llmq_type=103, expected_connections=None, expected_members=None, expected_contributions=None, expected_complaints=0, expected_justifications=0, expected_commitments=None, mninfos_online=None, mninfos_valid=None): 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 @@ -1467,6 +1468,21 @@ def mine_cycle_quorum(self, llmq_type_name="llmq_test", llmq_type=100, expected return (quorum_info_0, quorum_info_1) + def move_to_next_cycle(self): + cycle_length = 24 + mninfos_online = self.mninfo.copy() + nodes = [self.nodes[0]] + [mn.node for mn in mninfos_online] + cur_block = self.nodes[0].getblockcount() + + # move forward to next DKG + skip_count = cycle_length - (cur_block % cycle_length) + if skip_count != 0: + self.bump_mocktime(1, nodes=nodes) + self.nodes[0].generate(skip_count) + sync_blocks(nodes) + time.sleep(1) + self.log.info('Moved from block %d to %d' % (cur_block, self.nodes[0].getblockcount())) + def get_recovered_sig(self, rec_sig_id, rec_sig_msg_hash, llmq_type=100, node=None): # Note: recsigs aren't relayed to regular nodes by default, # make sure to pick a mn as a node to query for recsigs.