2019-03-11 09:42:34 +01:00
|
|
|
#!/usr/bin/env python3
|
2023-12-31 01:00:00 +01:00
|
|
|
# Copyright (c) 2015-2024 The Dash Core developers
|
2019-03-11 09:42:34 +01:00
|
|
|
# Distributed under the MIT software license, see the accompanying
|
|
|
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
|
|
|
|
|
|
|
'''
|
2020-07-17 01:44:20 +02:00
|
|
|
feature_llmq_simplepose.py
|
2019-03-11 09:42:34 +01:00
|
|
|
|
|
|
|
Checks simple PoSe system based on LLMQ commitments
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
Merge #13054: tests: Enable automatic detection of undefined names in Python tests scripts. Remove wildcard imports.
68400d8b96 tests: Use explicit imports (practicalswift)
Pull request description:
Enable automatic detection of undefined names in Python tests scripts. Remove wildcard imports.
Wildcard imports make it unclear which names are present in the namespace, confusing both readers and many automated tools.
An additional benefit of not using wildcard imports in tests scripts is that readers of a test script then can infer the rough testing scope just by looking at the imports.
Before this commit:
```
$ contrib/devtools/lint-python.sh | head -10
./test/functional/feature_rbf.py:8:1: F403 'from test_framework.util import *' used; unable to detect undefined names
./test/functional/feature_rbf.py:9:1: F403 'from test_framework.script import *' used; unable to detect undefined names
./test/functional/feature_rbf.py:10:1: F403 'from test_framework.mininode import *' used; unable to detect undefined names
./test/functional/feature_rbf.py:15:12: F405 bytes_to_hex_str may be undefined, or defined from star imports: test_framework.mininode, test_framework.script, test_framework.util
./test/functional/feature_rbf.py:17:58: F405 CScript may be undefined, or defined from star imports: test_framework.mininode, test_framework.script, test_framework.util
./test/functional/feature_rbf.py:25:13: F405 COIN may be undefined, or defined from star imports: test_framework.mininode, test_framework.script, test_framework.util
./test/functional/feature_rbf.py:26:31: F405 satoshi_round may be undefined, or defined from star imports: test_framework.mininode, test_framework.script, test_framework.util
./test/functional/feature_rbf.py:26:60: F405 COIN may be undefined, or defined from star imports: test_framework.mininode, test_framework.script, test_framework.util
./test/functional/feature_rbf.py:30:41: F405 satoshi_round may be undefined, or defined from star imports: test_framework.mininode, test_framework.script, test_framework.util
./test/functional/feature_rbf.py:30:68: F405 COIN may be undefined, or defined from star imports: test_framework.mininode, test_framework.script, test_framework.util
$
```
After this commit:
```
$ contrib/devtools/lint-python.sh | head -10
$
```
Tree-SHA512: 3f826d39cffb6438388e5efcb20a9622ff8238247e882d68f7b38609877421b2a8e10e9229575f8eb6a8fa42dec4256986692e92922c86171f750a0e887438d9
2018-08-13 14:24:43 +02:00
|
|
|
import time
|
|
|
|
|
|
|
|
from test_framework.test_framework import DashTestFramework
|
2020-08-27 08:21:53 +02:00
|
|
|
from test_framework.util import assert_equal, force_finish_mnsync, p2p_port
|
Merge #13054: tests: Enable automatic detection of undefined names in Python tests scripts. Remove wildcard imports.
68400d8b96 tests: Use explicit imports (practicalswift)
Pull request description:
Enable automatic detection of undefined names in Python tests scripts. Remove wildcard imports.
Wildcard imports make it unclear which names are present in the namespace, confusing both readers and many automated tools.
An additional benefit of not using wildcard imports in tests scripts is that readers of a test script then can infer the rough testing scope just by looking at the imports.
Before this commit:
```
$ contrib/devtools/lint-python.sh | head -10
./test/functional/feature_rbf.py:8:1: F403 'from test_framework.util import *' used; unable to detect undefined names
./test/functional/feature_rbf.py:9:1: F403 'from test_framework.script import *' used; unable to detect undefined names
./test/functional/feature_rbf.py:10:1: F403 'from test_framework.mininode import *' used; unable to detect undefined names
./test/functional/feature_rbf.py:15:12: F405 bytes_to_hex_str may be undefined, or defined from star imports: test_framework.mininode, test_framework.script, test_framework.util
./test/functional/feature_rbf.py:17:58: F405 CScript may be undefined, or defined from star imports: test_framework.mininode, test_framework.script, test_framework.util
./test/functional/feature_rbf.py:25:13: F405 COIN may be undefined, or defined from star imports: test_framework.mininode, test_framework.script, test_framework.util
./test/functional/feature_rbf.py:26:31: F405 satoshi_round may be undefined, or defined from star imports: test_framework.mininode, test_framework.script, test_framework.util
./test/functional/feature_rbf.py:26:60: F405 COIN may be undefined, or defined from star imports: test_framework.mininode, test_framework.script, test_framework.util
./test/functional/feature_rbf.py:30:41: F405 satoshi_round may be undefined, or defined from star imports: test_framework.mininode, test_framework.script, test_framework.util
./test/functional/feature_rbf.py:30:68: F405 COIN may be undefined, or defined from star imports: test_framework.mininode, test_framework.script, test_framework.util
$
```
After this commit:
```
$ contrib/devtools/lint-python.sh | head -10
$
```
Tree-SHA512: 3f826d39cffb6438388e5efcb20a9622ff8238247e882d68f7b38609877421b2a8e10e9229575f8eb6a8fa42dec4256986692e92922c86171f750a0e887438d9
2018-08-13 14:24:43 +02:00
|
|
|
|
|
|
|
|
2019-03-11 09:42:34 +01:00
|
|
|
class LLMQSimplePoSeTest(DashTestFramework):
|
2019-09-24 00:57:30 +02:00
|
|
|
def set_test_params(self):
|
2024-08-27 12:30:39 +02:00
|
|
|
self.set_dash_test_params(6, 5)
|
2020-01-07 13:49:51 +01:00
|
|
|
self.set_dash_llmq_test_params(5, 3)
|
2019-03-11 09:42:34 +01:00
|
|
|
|
|
|
|
def run_test(self):
|
|
|
|
|
2024-09-24 00:19:51 +02:00
|
|
|
self.deaf_mns = []
|
2022-06-18 18:52:45 +02:00
|
|
|
self.nodes[0].sporkupdate("SPORK_17_QUORUM_DKG_ENABLED", 0)
|
2019-03-11 09:42:34 +01:00
|
|
|
self.wait_for_sporks_same()
|
|
|
|
|
|
|
|
# check if mining quorums with all nodes being online succeeds without punishment/banning
|
2020-11-28 20:16:31 +01:00
|
|
|
self.test_no_banning()
|
2020-03-30 14:38:35 +02:00
|
|
|
|
|
|
|
# Now lets isolate MNs one by one and verify that punishment/banning happens
|
2024-09-25 15:22:33 +02:00
|
|
|
self.test_banning(self.isolate_mn, 2)
|
2020-03-30 14:38:35 +02:00
|
|
|
|
2020-03-30 16:52:30 +02:00
|
|
|
self.repair_masternodes(False)
|
|
|
|
|
2022-06-18 18:52:45 +02:00
|
|
|
self.nodes[0].sporkupdate("SPORK_21_QUORUM_ALL_CONNECTED", 0)
|
|
|
|
self.nodes[0].sporkupdate("SPORK_23_QUORUM_POSE", 0)
|
2020-03-30 16:52:30 +02:00
|
|
|
self.wait_for_sporks_same()
|
|
|
|
|
|
|
|
self.reset_probe_timeouts()
|
|
|
|
|
|
|
|
# Make sure no banning happens with spork21 enabled
|
2020-11-28 20:16:31 +01:00
|
|
|
self.test_no_banning()
|
2020-03-30 16:52:30 +02:00
|
|
|
|
|
|
|
# Lets restart masternodes with closed ports and verify that they get banned even though they are connected to other MNs (via outbound connections)
|
2024-09-25 15:21:15 +02:00
|
|
|
self.test_banning(self.close_mn_port)
|
2024-09-24 00:19:51 +02:00
|
|
|
self.deaf_mns.clear()
|
2020-03-30 16:52:30 +02:00
|
|
|
|
|
|
|
self.repair_masternodes(True)
|
|
|
|
self.reset_probe_timeouts()
|
|
|
|
|
2020-11-19 12:42:35 +01:00
|
|
|
self.test_banning(self.force_old_mn_proto, 3)
|
|
|
|
|
2021-01-11 04:23:01 +01:00
|
|
|
# With PoSe off there should be no punishing for non-reachable and outdated nodes
|
2022-06-18 18:52:45 +02:00
|
|
|
self.nodes[0].sporkupdate("SPORK_23_QUORUM_POSE", 4070908800)
|
2021-01-11 04:23:01 +01:00
|
|
|
self.wait_for_sporks_same()
|
|
|
|
|
|
|
|
self.repair_masternodes(True)
|
|
|
|
self.force_old_mn_proto(self.mninfo[0])
|
|
|
|
self.test_no_banning(3)
|
|
|
|
|
|
|
|
self.repair_masternodes(True)
|
|
|
|
self.close_mn_port(self.mninfo[0])
|
2024-09-24 00:19:51 +02:00
|
|
|
self.deaf_mns.clear()
|
2021-01-11 04:23:01 +01:00
|
|
|
self.test_no_banning(3)
|
|
|
|
|
2020-11-19 12:42:35 +01:00
|
|
|
def isolate_mn(self, mn):
|
|
|
|
mn.node.setnetworkactive(False)
|
2020-08-27 08:21:53 +02:00
|
|
|
self.wait_until(lambda: mn.node.getconnectioncount() == 0)
|
2022-07-14 20:38:02 +02:00
|
|
|
return True, True
|
2020-11-19 12:42:35 +01:00
|
|
|
|
|
|
|
def close_mn_port(self, mn):
|
2024-09-24 00:19:51 +02:00
|
|
|
self.deaf_mns.append(mn)
|
2020-11-19 12:42:35 +01:00
|
|
|
self.stop_node(mn.node.index)
|
|
|
|
self.start_masternode(mn, ["-listen=0", "-nobind"])
|
2022-09-24 14:36:35 +02:00
|
|
|
self.connect_nodes(mn.node.index, 0)
|
2020-11-19 12:42:35 +01:00
|
|
|
# Make sure the to-be-banned node is still connected well via outbound connections
|
|
|
|
for mn2 in self.mninfo:
|
2024-09-24 00:19:51 +02:00
|
|
|
if self.deaf_mns.count(mn2) == 0:
|
2022-09-24 14:36:35 +02:00
|
|
|
self.connect_nodes(mn.node.index, mn2.node.index)
|
2020-11-19 12:42:35 +01:00
|
|
|
self.reset_probe_timeouts()
|
2022-07-14 20:38:02 +02:00
|
|
|
return False, False
|
2020-03-30 16:52:30 +02:00
|
|
|
|
2020-11-19 12:42:35 +01:00
|
|
|
def force_old_mn_proto(self, mn):
|
|
|
|
self.stop_node(mn.node.index)
|
|
|
|
self.start_masternode(mn, ["-pushversion=70216"])
|
2022-09-24 14:36:35 +02:00
|
|
|
self.connect_nodes(mn.node.index, 0)
|
2020-11-19 12:42:35 +01:00
|
|
|
self.reset_probe_timeouts()
|
2022-07-14 20:38:02 +02:00
|
|
|
return False, True
|
2020-11-19 12:42:35 +01:00
|
|
|
|
2021-01-11 04:23:01 +01:00
|
|
|
def test_no_banning(self, expected_connections=None):
|
2024-09-25 15:17:44 +02:00
|
|
|
for i in range(3):
|
|
|
|
self.log.info(f"Testing no PoSe banning in normal conditions {i + 1}/3")
|
2021-01-11 04:23:01 +01:00
|
|
|
self.mine_quorum(expected_connections=expected_connections)
|
2024-09-25 15:18:29 +02:00
|
|
|
for mn in self.mninfo:
|
|
|
|
assert not self.check_punished(mn) and not self.check_banned(mn)
|
2019-03-11 09:42:34 +01:00
|
|
|
|
2024-09-17 16:46:12 +02:00
|
|
|
def mine_quorum_less_checks(self, expected_good_nodes, mninfos_online):
|
2022-10-12 19:36:17 +02:00
|
|
|
# Unlike in mine_quorum we skip most of the checks and only care about
|
2024-09-17 16:46:12 +02:00
|
|
|
# nodes moving forward from phase to phase correctly and the fact that the quorum is actually mined.
|
|
|
|
self.log.info("Mining a quorum with less checks")
|
2022-10-12 19:36:17 +02:00
|
|
|
nodes = [self.nodes[0]] + [mn.node for mn in mninfos_online]
|
|
|
|
|
|
|
|
# move forward to next DKG
|
|
|
|
skip_count = 24 - (self.nodes[0].getblockcount() % 24)
|
|
|
|
if skip_count != 0:
|
2024-09-13 15:15:01 +02:00
|
|
|
self.bump_mocktime(skip_count, nodes=nodes)
|
2024-10-01 21:22:56 +02:00
|
|
|
self.generate(self.nodes[0], skip_count, sync_fun=self.no_op)
|
2022-10-12 19:36:17 +02:00
|
|
|
self.sync_blocks(nodes)
|
|
|
|
|
|
|
|
q = self.nodes[0].getbestblockhash()
|
|
|
|
self.log.info("Expected quorum_hash: "+str(q))
|
|
|
|
self.log.info("Waiting for phase 1 (init)")
|
|
|
|
self.wait_for_quorum_phase(q, 1, expected_good_nodes, None, 0, mninfos_online)
|
|
|
|
self.move_blocks(nodes, 2)
|
|
|
|
|
|
|
|
self.log.info("Waiting for phase 2 (contribute)")
|
2024-09-17 16:46:12 +02:00
|
|
|
self.wait_for_quorum_phase(q, 2, expected_good_nodes, "receivedContributions", expected_good_nodes, mninfos_online)
|
2022-10-12 19:36:17 +02:00
|
|
|
self.move_blocks(nodes, 2)
|
|
|
|
|
|
|
|
self.log.info("Waiting for phase 3 (complain)")
|
|
|
|
self.wait_for_quorum_phase(q, 3, expected_good_nodes, None, 0, mninfos_online)
|
|
|
|
self.move_blocks(nodes, 2)
|
|
|
|
|
|
|
|
self.log.info("Waiting for phase 4 (justify)")
|
|
|
|
self.wait_for_quorum_phase(q, 4, expected_good_nodes, None, 0, mninfos_online)
|
|
|
|
self.move_blocks(nodes, 2)
|
|
|
|
|
|
|
|
self.log.info("Waiting for phase 5 (commit)")
|
2024-09-17 16:46:12 +02:00
|
|
|
self.wait_for_quorum_phase(q, 5, expected_good_nodes, "receivedPrematureCommitments", expected_good_nodes, mninfos_online)
|
2022-10-12 19:36:17 +02:00
|
|
|
self.move_blocks(nodes, 2)
|
|
|
|
|
|
|
|
self.log.info("Waiting for phase 6 (mining)")
|
|
|
|
self.wait_for_quorum_phase(q, 6, expected_good_nodes, None, 0, mninfos_online)
|
|
|
|
|
|
|
|
self.log.info("Waiting final commitment")
|
|
|
|
self.wait_for_quorum_commitment(q, nodes)
|
|
|
|
|
|
|
|
self.log.info("Mining final commitment")
|
|
|
|
self.bump_mocktime(1, nodes=nodes)
|
|
|
|
self.nodes[0].getblocktemplate() # this calls CreateNewBlock
|
2024-10-01 18:08:19 +02:00
|
|
|
self.generate(self.nodes[0], 1, sync_fun=lambda: self.sync_blocks(nodes))
|
2022-10-12 19:36:17 +02:00
|
|
|
|
|
|
|
self.log.info("Waiting for quorum to appear in the list")
|
|
|
|
self.wait_for_quorum_list(q, nodes)
|
|
|
|
|
|
|
|
new_quorum = self.nodes[0].quorum("list", 1)["llmq_test"][0]
|
|
|
|
assert_equal(q, new_quorum)
|
|
|
|
quorum_info = self.nodes[0].quorum("info", 100, new_quorum)
|
|
|
|
|
|
|
|
# Mine 8 (SIGN_HEIGHT_OFFSET) more blocks to make sure that the new quorum gets eligible for signing sessions
|
2024-09-13 15:15:01 +02:00
|
|
|
self.bump_mocktime(8)
|
2024-10-01 18:08:19 +02:00
|
|
|
self.generate(self.nodes[0], 8, sync_fun=lambda: self.sync_blocks(nodes))
|
2022-10-12 19:36:17 +02:00
|
|
|
self.log.info("New quorum: height=%d, quorumHash=%s, quorumIndex=%d, minedBlock=%s" % (quorum_info["height"], new_quorum, quorum_info["quorumIndex"], quorum_info["minedBlock"]))
|
|
|
|
|
|
|
|
return new_quorum
|
|
|
|
|
2024-09-25 15:21:15 +02:00
|
|
|
def test_banning(self, invalidate_proc, expected_connections=None):
|
2020-11-19 12:42:35 +01:00
|
|
|
mninfos_online = self.mninfo.copy()
|
|
|
|
mninfos_valid = self.mninfo.copy()
|
|
|
|
expected_contributors = len(mninfos_online)
|
2024-09-25 15:17:44 +02:00
|
|
|
for i in range(2):
|
|
|
|
self.log.info(f"Testing PoSe banning due to {invalidate_proc.__name__} {i + 1}/2")
|
2020-11-19 12:42:35 +01:00
|
|
|
mn = mninfos_valid.pop()
|
2022-07-14 20:38:02 +02:00
|
|
|
went_offline, instant_ban = invalidate_proc(mn)
|
2024-09-16 13:38:59 +02:00
|
|
|
expected_complaints = expected_contributors - 1
|
2020-11-19 12:42:35 +01:00
|
|
|
if went_offline:
|
|
|
|
mninfos_online.remove(mn)
|
|
|
|
expected_contributors -= 1
|
2019-03-11 09:42:34 +01:00
|
|
|
|
2022-07-14 20:38:02 +02:00
|
|
|
# NOTE: Min PoSe penalty is 100 (see CDeterministicMNList::CalcMaxPoSePenalty()),
|
|
|
|
# so nodes are PoSe-banned in the same DKG they misbehave without being PoSe-punished first.
|
2022-10-12 19:36:17 +02:00
|
|
|
if instant_ban:
|
2024-09-25 15:21:15 +02:00
|
|
|
assert expected_connections is not None
|
2024-09-25 15:17:44 +02:00
|
|
|
self.log.info("Expecting instant PoSe banning")
|
2022-10-12 19:36:17 +02:00
|
|
|
self.reset_probe_timeouts()
|
2024-09-16 13:38:59 +02:00
|
|
|
self.mine_quorum(expected_connections=expected_connections, expected_members=expected_contributors, expected_contributions=expected_contributors, expected_complaints=expected_complaints, expected_commitments=expected_contributors, mninfos_online=mninfos_online, mninfos_valid=mninfos_valid)
|
2022-10-12 19:36:17 +02:00
|
|
|
else:
|
|
|
|
# It's ok to miss probes/quorum connections up to 5 times.
|
|
|
|
# 6th time is when it should be banned for sure.
|
2024-09-25 15:21:15 +02:00
|
|
|
assert expected_connections is None
|
2024-09-25 15:17:44 +02:00
|
|
|
for j in range(6):
|
|
|
|
self.log.info(f"Accumulating PoSe penalty {j + 1}/6")
|
2022-07-14 20:38:02 +02:00
|
|
|
self.reset_probe_timeouts()
|
2024-09-17 16:46:12 +02:00
|
|
|
self.mine_quorum_less_checks(expected_contributors - 1, mninfos_online)
|
2020-11-19 12:42:35 +01:00
|
|
|
|
2021-08-27 21:03:02 +02:00
|
|
|
assert self.check_banned(mn)
|
2019-03-11 09:42:34 +01:00
|
|
|
|
2020-11-19 12:42:35 +01:00
|
|
|
if not went_offline:
|
|
|
|
# we do not include PoSe banned mns in quorums, so the next one should have 1 contributor less
|
|
|
|
expected_contributors -= 1
|
2019-03-11 09:42:34 +01:00
|
|
|
|
2020-03-30 16:52:30 +02:00
|
|
|
def repair_masternodes(self, restart):
|
2024-09-25 15:17:44 +02:00
|
|
|
self.log.info("Repairing all banned and punished masternodes")
|
2020-03-30 16:52:30 +02:00
|
|
|
for mn in self.mninfo:
|
|
|
|
if self.check_banned(mn) or self.check_punished(mn):
|
|
|
|
addr = self.nodes[0].getnewaddress()
|
|
|
|
self.nodes[0].sendtoaddress(addr, 0.1)
|
|
|
|
self.nodes[0].protx('update_service', mn.proTxHash, '127.0.0.1:%d' % p2p_port(mn.node.index), mn.keyOperator, "", addr)
|
|
|
|
if restart:
|
|
|
|
self.stop_node(mn.node.index)
|
2020-04-17 07:52:06 +02:00
|
|
|
self.start_masternode(mn)
|
2020-03-30 16:52:30 +02:00
|
|
|
else:
|
|
|
|
mn.node.setnetworkactive(True)
|
2024-09-24 00:20:54 +02:00
|
|
|
self.connect_nodes(mn.node.index, 0)
|
2024-02-07 17:13:53 +01:00
|
|
|
|
|
|
|
# syncing blocks only since node 0 has txes waiting to be mined
|
|
|
|
self.sync_blocks()
|
|
|
|
|
|
|
|
# Make sure protxes are "safe" to mine even when InstantSend and ChainLocks are no longer functional
|
|
|
|
self.bump_mocktime(60 * 10 + 1)
|
2024-10-01 21:25:52 +02:00
|
|
|
self.generate(self.nodes[0], 1)
|
2020-03-30 16:52:30 +02:00
|
|
|
|
|
|
|
# Isolate and re-connect all MNs (otherwise there might be open connections with no MNAUTH for MNs which were banned before)
|
|
|
|
for mn in self.mninfo:
|
2024-02-07 17:13:53 +01:00
|
|
|
assert not self.check_banned(mn)
|
2020-03-30 16:52:30 +02:00
|
|
|
mn.node.setnetworkactive(False)
|
2020-08-27 08:21:53 +02:00
|
|
|
self.wait_until(lambda: mn.node.getconnectioncount() == 0)
|
2020-03-30 16:52:30 +02:00
|
|
|
mn.node.setnetworkactive(True)
|
2020-09-11 14:07:34 +02:00
|
|
|
force_finish_mnsync(mn.node)
|
2022-09-24 14:36:35 +02:00
|
|
|
self.connect_nodes(mn.node.index, 0)
|
2020-03-30 16:52:30 +02:00
|
|
|
|
|
|
|
def reset_probe_timeouts(self):
|
|
|
|
# Make sure all masternodes will reconnect/re-probe
|
2022-07-14 20:38:02 +02:00
|
|
|
self.bump_mocktime(10 * 60 + 1)
|
2020-11-19 12:42:35 +01:00
|
|
|
# Sleep a couple of seconds to let mn sync tick to happen
|
|
|
|
time.sleep(2)
|
2020-03-30 16:52:30 +02:00
|
|
|
|
2019-03-11 09:42:34 +01:00
|
|
|
def check_punished(self, mn):
|
|
|
|
info = self.nodes[0].protx('info', mn.proTxHash)
|
|
|
|
if info['state']['PoSePenalty'] > 0:
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
def check_banned(self, mn):
|
|
|
|
info = self.nodes[0].protx('info', mn.proTxHash)
|
|
|
|
if info['state']['PoSeBanHeight'] != -1:
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
LLMQSimplePoSeTest().main()
|