Merge #6430: feat: dynamically register MN in functional tests without IS

37fbdee1d9 feat: do not mine extra quorum in feature_dip3_v19.py (Konstantin Akimov)
70ba007f1a feat: do not mine extra quorum in feature_llmq_evo.py (Konstantin Akimov)
129385d349 feat: remove regular masternodes from feature_asset_locks tests (Konstantin Akimov)
8ea45bbf69 fix: make helper get_merkle_root works with no masternodes (Konstantin Akimov)
7e0c2ca5a5 fix: make dynamic masternode register even without is-quorum (Konstantin Akimov)

Pull request description:

  This PR has important fixes to implement "single-node quorum" feature.

  ## Issue being fixed or feature implemented
  Without IS-quorum you can not dynamically add a new MN or Evo node.
  For evo nodes adding it dynamically is the only way.

  ## What was done?
  Fixed a function that dynamically adds a masternode without Instant Send quorums; use it in a functional test feature_asset_locks.py

  As side effect it improves performance of functional tests which do not wait more IS lock significantly; for feature_asset_locks.py it gave ~20 seconds per run.

  ## How Has This Been Tested?
  See updates in `feature_asset_locks.py`, `feature_llmq_evo.py`, `feature_dip3_v19.py`

  ## Breaking Changes
  N/A

  ## Checklist:
  - [x] I have performed a self-review of my own code
  - [x] I have commented my code, particularly in hard-to-understand areas
  - [x] I have added or updated relevant unit/integration/functional/e2e tests
  - [x] I have made corresponding changes to the documentation
  - [x] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_

ACKs for top commit:
  UdjinM6:
    utACK 37fbdee1d9
  PastaPastaPasta:
    utACK 37fbdee1d9

Tree-SHA512: 2da036ccd9842946f20450606621108fa8cd8fae31927ac2d1fc5f02bf9c2f6955a67e23ef76141a02299510c8cbf206fcd4947b0ec41b776fe80f70947afc14
This commit is contained in:
pasta 2024-12-01 22:46:34 -06:00
commit f6edb8a85c
No known key found for this signature in database
GPG Key ID: E2F3D7916E722D38
5 changed files with 24 additions and 47 deletions

View File

@ -49,11 +49,11 @@ HEIGHT_DIFF_EXPIRING = 48
class AssetLocksTest(DashTestFramework): class AssetLocksTest(DashTestFramework):
def set_test_params(self): def set_test_params(self):
self.set_dash_test_params(4, 2, [[ self.set_dash_test_params(2, 0, [[
"-whitelist=127.0.0.1", "-whitelist=127.0.0.1",
"-llmqtestinstantsenddip0024=llmq_test_instantsend", "-llmqtestinstantsenddip0024=llmq_test_instantsend",
"-testactivationheight=mn_rr@1400", "-testactivationheight=mn_rr@1400",
]] * 4, evo_count=2) ]] * 2, evo_count=2)
def skip_test_if_missing_module(self): def skip_test_if_missing_module(self):
self.skip_if_no_wallet() self.skip_if_no_wallet()
@ -231,15 +231,14 @@ class AssetLocksTest(DashTestFramework):
self.log.info(f"Generating batch of blocks {count} left") self.log.info(f"Generating batch of blocks {count} left")
batch = min(50, count) batch = min(50, count)
count -= batch count -= batch
self.bump_mocktime(batch) self.bump_mocktime(10 * 60 + 1)
self.generate(self.nodes[1], batch) self.generate(self.nodes[1], batch)
# This functional test intentionally setup only 2 MN and only 2 Evo nodes # This functional test intentionally setup only 2 MN and only 2 Evo nodes
# to ensure that corner case of quorum with minimum amount of nodes as possible # to ensure that corner case of quorum with minimum amount of nodes as possible
# does not cause any issues in Dash Core # does not cause any issues in Dash Core
def mine_quorum_2_nodes(self, llmq_type_name, llmq_type): def mine_quorum_2_nodes(self):
self.mine_quorum(llmq_type_name=llmq_type_name, expected_members=2, expected_connections=1, expected_contributions=2, expected_commitments=2, llmq_type=llmq_type) self.mine_quorum(llmq_type_name='llmq_test_platform', expected_members=2, expected_connections=1, expected_contributions=2, expected_commitments=2, llmq_type=106)
def run_test(self): def run_test(self):
node_wallet = self.nodes[0] node_wallet = self.nodes[0]
@ -250,17 +249,9 @@ class AssetLocksTest(DashTestFramework):
self.activate_v20(expected_activation_height=900) self.activate_v20(expected_activation_height=900)
self.log.info("Activated v20 at height:" + str(node.getblockcount())) self.log.info("Activated v20 at height:" + str(node.getblockcount()))
self.nodes[0].sporkupdate("SPORK_2_INSTANTSEND_ENABLED", 0)
self.wait_for_sporks_same()
self.mine_quorum_2_nodes(llmq_type_name='llmq_test_instantsend', llmq_type=104)
for _ in range(2): for _ in range(2):
self.dynamically_add_masternode(evo=True) self.dynamically_add_masternode(evo=True)
self.generate(node, 8, sync_fun=lambda: self.sync_blocks())
self.set_sporks()
self.generate(node, 1)
self.mempool_size = 0 self.mempool_size = 0
key = ECKey() key = ECKey()
@ -328,7 +319,7 @@ class AssetLocksTest(DashTestFramework):
self.create_and_check_block([extra_lock_tx], expected_error = 'bad-cbtx-assetlocked-amount') self.create_and_check_block([extra_lock_tx], expected_error = 'bad-cbtx-assetlocked-amount')
self.log.info("Mine a quorum...") self.log.info("Mine a quorum...")
self.mine_quorum_2_nodes(llmq_type_name='llmq_test_platform', llmq_type=106) self.mine_quorum_2_nodes()
self.validate_credit_pool_balance(locked_1) self.validate_credit_pool_balance(locked_1)
@ -350,6 +341,10 @@ class AssetLocksTest(DashTestFramework):
asset_unlock_tx_duplicate_index.vout[0].nValue += COIN asset_unlock_tx_duplicate_index.vout[0].nValue += COIN
too_late_height = node.getblockcount() + HEIGHT_DIFF_EXPIRING too_late_height = node.getblockcount() + HEIGHT_DIFF_EXPIRING
self.log.info("Mine block to empty mempool")
self.bump_mocktime(10 * 60 + 1)
self.generate(self.nodes[0], 1)
self.check_mempool_result(tx=asset_unlock_tx, result_expected={'allowed': True, 'fees': {'base': Decimal(str(tiny_amount / COIN))}}) self.check_mempool_result(tx=asset_unlock_tx, result_expected={'allowed': True, 'fees': {'base': Decimal(str(tiny_amount / COIN))}})
self.check_mempool_result(tx=asset_unlock_tx_too_big_fee, self.check_mempool_result(tx=asset_unlock_tx_too_big_fee,
result_expected={'allowed': False, 'reject-reason' : 'max-fee-exceeded'}) result_expected={'allowed': False, 'reject-reason' : 'max-fee-exceeded'})
@ -417,7 +412,7 @@ class AssetLocksTest(DashTestFramework):
reason = "double copy") reason = "double copy")
self.log.info("Mining next quorum to check tx 'asset_unlock_tx_late' is still valid...") self.log.info("Mining next quorum to check tx 'asset_unlock_tx_late' is still valid...")
self.mine_quorum_2_nodes(llmq_type_name='llmq_test_platform', llmq_type=106) self.mine_quorum_2_nodes()
self.log.info("Checking credit pool amount is same...") self.log.info("Checking credit pool amount is same...")
self.validate_credit_pool_balance(locked - 1 * COIN) self.validate_credit_pool_balance(locked - 1 * COIN)
self.check_mempool_result(tx=asset_unlock_tx_late, result_expected={'allowed': True, 'fees': {'base': Decimal(str(tiny_amount / COIN))}}) self.check_mempool_result(tx=asset_unlock_tx_late, result_expected={'allowed': True, 'fees': {'base': Decimal(str(tiny_amount / COIN))}})
@ -435,7 +430,7 @@ class AssetLocksTest(DashTestFramework):
result_expected={'allowed': False, 'reject-reason' : 'bad-assetunlock-too-late'}) result_expected={'allowed': False, 'reject-reason' : 'bad-assetunlock-too-late'})
self.log.info("Checking that two quorums later it is too late because quorum is not active...") self.log.info("Checking that two quorums later it is too late because quorum is not active...")
self.mine_quorum_2_nodes(llmq_type_name='llmq_test_platform', llmq_type=106) self.mine_quorum_2_nodes()
self.log.info("Expecting new reject-reason...") self.log.info("Expecting new reject-reason...")
assert not softfork_active(self.nodes[0], 'withdrawals') assert not softfork_active(self.nodes[0], 'withdrawals')
self.check_mempool_result(tx=asset_unlock_tx_too_late, self.check_mempool_result(tx=asset_unlock_tx_too_late,
@ -513,7 +508,7 @@ class AssetLocksTest(DashTestFramework):
self.log.info("Fast forward to the next day to reset all current unlock limits...") self.log.info("Fast forward to the next day to reset all current unlock limits...")
self.generate_batch(blocks_in_one_day) self.generate_batch(blocks_in_one_day)
self.mine_quorum_2_nodes(llmq_type_name='llmq_test_platform', llmq_type=106) self.mine_quorum_2_nodes()
total = self.get_credit_pool_balance() total = self.get_credit_pool_balance()
coins = node_wallet.listunspent() coins = node_wallet.listunspent()
@ -669,12 +664,12 @@ class AssetLocksTest(DashTestFramework):
while quorumHash_str != node_wallet.quorum('list')['llmq_test_platform'][-1]: while quorumHash_str != node_wallet.quorum('list')['llmq_test_platform'][-1]:
self.log.info("Generate one more quorum until signing quorum becomes the last one in the list") self.log.info("Generate one more quorum until signing quorum becomes the last one in the list")
self.mine_quorum_2_nodes(llmq_type_name="llmq_test_platform", llmq_type=106) self.mine_quorum_2_nodes()
self.check_mempool_result(tx=asset_unlock_tx, result_expected={'allowed': True, 'fees': {'base': Decimal(str(tiny_amount / COIN))}}) self.check_mempool_result(tx=asset_unlock_tx, result_expected={'allowed': True, 'fees': {'base': Decimal(str(tiny_amount / COIN))}})
self.log.info("Generate one more quorum after which signing quorum is gone but Asset Unlock tx is still valid") self.log.info("Generate one more quorum after which signing quorum is gone but Asset Unlock tx is still valid")
assert quorumHash_str in node_wallet.quorum('list')['llmq_test_platform'] assert quorumHash_str in node_wallet.quorum('list')['llmq_test_platform']
self.mine_quorum_2_nodes(llmq_type_name="llmq_test_platform", llmq_type=106) self.mine_quorum_2_nodes()
assert quorumHash_str not in node_wallet.quorum('list')['llmq_test_platform'] assert quorumHash_str not in node_wallet.quorum('list')['llmq_test_platform']
if asset_unlock_tx_payload.requestedHeight + HEIGHT_DIFF_EXPIRING > node_wallet.getblockcount(): if asset_unlock_tx_payload.requestedHeight + HEIGHT_DIFF_EXPIRING > node_wallet.getblockcount():
@ -686,7 +681,7 @@ class AssetLocksTest(DashTestFramework):
index += 1 index += 1
self.log.info("Generate one more quorum after which signing quorum becomes too old") self.log.info("Generate one more quorum after which signing quorum becomes too old")
self.mine_quorum_2_nodes(llmq_type_name="llmq_test_platform", llmq_type=106) self.mine_quorum_2_nodes()
self.check_mempool_result(tx=asset_unlock_tx, result_expected={'allowed': False, 'reject-reason': 'bad-assetunlock-too-old-quorum'}) self.check_mempool_result(tx=asset_unlock_tx, result_expected={'allowed': False, 'reject-reason': 'bad-assetunlock-too-old-quorum'})
asset_unlock_tx = self.create_assetunlock(520, 2000 * COIN + 1, pubkey) asset_unlock_tx = self.create_assetunlock(520, 2000 * COIN + 1, pubkey)

View File

@ -75,18 +75,8 @@ class DIP3V19Test(DashTestFramework):
self.log.info("pubkeyoperator should still be shown using legacy scheme") self.log.info("pubkeyoperator should still be shown using legacy scheme")
assert_equal(pubkeyoperator_list_before, pubkeyoperator_list_after) assert_equal(pubkeyoperator_list_before, pubkeyoperator_list_after)
self.move_to_next_cycle()
self.log.info("Cycle H height:" + str(self.nodes[0].getblockcount()))
self.move_to_next_cycle()
self.log.info("Cycle H+C height:" + str(self.nodes[0].getblockcount()))
self.move_to_next_cycle()
self.log.info("Cycle H+2C height:" + str(self.nodes[0].getblockcount()))
self.mine_cycle_quorum(llmq_type_name='llmq_test_dip0024', llmq_type=103)
evo_info_0 = self.dynamically_add_masternode(evo=True, rnd=7) evo_info_0 = self.dynamically_add_masternode(evo=True, rnd=7)
assert evo_info_0 is not None assert evo_info_0 is not None
self.generate(self.nodes[0], 8, sync_fun=lambda: self.sync_blocks())
self.log.info("Checking that protxs with duplicate EvoNodes fields are rejected") self.log.info("Checking that protxs with duplicate EvoNodes fields are rejected")
evo_info_1 = self.dynamically_add_masternode(evo=True, rnd=7, should_be_rejected=True) evo_info_1 = self.dynamically_add_masternode(evo=True, rnd=7, should_be_rejected=True)
@ -96,7 +86,6 @@ class DIP3V19Test(DashTestFramework):
assert evo_info_2 is None assert evo_info_2 is None
evo_info_3 = self.dynamically_add_masternode(evo=True, rnd=9) evo_info_3 = self.dynamically_add_masternode(evo=True, rnd=9)
assert evo_info_3 is not None assert evo_info_3 is not None
self.generate(self.nodes[0], 8, sync_fun=lambda: self.sync_blocks())
self.dynamically_evo_update_service(evo_info_0, 9, should_be_rejected=True) self.dynamically_evo_update_service(evo_info_0, 9, should_be_rejected=True)
revoke_protx = self.mninfo[-1].proTxHash revoke_protx = self.mninfo[-1].proTxHash
@ -123,12 +112,12 @@ class DIP3V19Test(DashTestFramework):
def test_revoke_protx(self, node_idx, revoke_protx, revoke_keyoperator): def test_revoke_protx(self, node_idx, revoke_protx, revoke_keyoperator):
funds_address = self.nodes[0].getnewaddress() funds_address = self.nodes[0].getnewaddress()
fund_txid = self.nodes[0].sendtoaddress(funds_address, 1) fund_txid = self.nodes[0].sendtoaddress(funds_address, 1)
self.wait_for_instantlock(fund_txid, self.nodes[0]) self.bump_mocktime(10 * 60 + 1) # to make tx safe to include in block
tip = self.generate(self.nodes[0], 1)[0] tip = self.generate(self.nodes[0], 1)[0]
assert_equal(self.nodes[0].getrawtransaction(fund_txid, 1, tip)['confirmations'], 1) assert_equal(self.nodes[0].getrawtransaction(fund_txid, 1, tip)['confirmations'], 1)
protx_result = self.nodes[0].protx('revoke', revoke_protx, revoke_keyoperator, 1, funds_address) protx_result = self.nodes[0].protx('revoke', revoke_protx, revoke_keyoperator, 1, funds_address)
self.wait_for_instantlock(protx_result, self.nodes[0]) self.bump_mocktime(10 * 60 + 1) # to make tx safe to include in block
tip = self.generate(self.nodes[0], 1, sync_fun=self.no_op)[0] tip = self.generate(self.nodes[0], 1, sync_fun=self.no_op)[0]
assert_equal(self.nodes[0].getrawtransaction(protx_result, 1, tip)['confirmations'], 1) assert_equal(self.nodes[0].getrawtransaction(protx_result, 1, tip)['confirmations'], 1)
# Revoking a MN results in disconnects. Wait for disconnects to actually happen # Revoking a MN results in disconnects. Wait for disconnects to actually happen

View File

@ -76,15 +76,6 @@ class LLMQEvoNodesTest(DashTestFramework):
self.nodes[0].sporkupdate("SPORK_2_INSTANTSEND_ENABLED", 0) self.nodes[0].sporkupdate("SPORK_2_INSTANTSEND_ENABLED", 0)
self.wait_for_sporks_same() self.wait_for_sporks_same()
self.move_to_next_cycle()
self.log.info("Cycle H height:" + str(self.nodes[0].getblockcount()))
self.move_to_next_cycle()
self.log.info("Cycle H+C height:" + str(self.nodes[0].getblockcount()))
self.move_to_next_cycle()
self.log.info("Cycle H+2C height:" + str(self.nodes[0].getblockcount()))
self.mine_cycle_quorum(llmq_type_name='llmq_test_dip0024', llmq_type=103)
evo_protxhash_list = list() evo_protxhash_list = list()
for i in range(self.evo_count): for i in range(self.evo_count):
evo_info = self.dynamically_add_masternode(evo=True) evo_info = self.dynamically_add_masternode(evo=True)

View File

@ -640,6 +640,8 @@ class CBlock(CBlockHeader):
# Calculate the merkle root given a vector of transaction hashes # Calculate the merkle root given a vector of transaction hashes
@staticmethod @staticmethod
def get_merkle_root(hashes): def get_merkle_root(hashes):
if len(hashes) == 0:
return 0
while len(hashes) > 1: while len(hashes) > 1:
newhashes = [] newhashes = []
for i in range(0, len(hashes), 2): for i in range(0, len(hashes), 2):

View File

@ -1313,7 +1313,7 @@ class DashTestFramework(BitcoinTestFramework):
collateral_amount = EVONODE_COLLATERAL if evo else MASTERNODE_COLLATERAL collateral_amount = EVONODE_COLLATERAL if evo else MASTERNODE_COLLATERAL
outputs = {collateral_address: collateral_amount, funds_address: 1} outputs = {collateral_address: collateral_amount, funds_address: 1}
collateral_txid = self.nodes[0].sendmany("", outputs) collateral_txid = self.nodes[0].sendmany("", outputs)
self.wait_for_instantlock(collateral_txid, self.nodes[0]) self.bump_mocktime(10 * 60 + 1) # to make tx safe to include in block
tip = self.generate(self.nodes[0], 1)[0] tip = self.generate(self.nodes[0], 1)[0]
rawtx = self.nodes[0].getrawtransaction(collateral_txid, 1, tip) rawtx = self.nodes[0].getrawtransaction(collateral_txid, 1, tip)
@ -1334,7 +1334,7 @@ class DashTestFramework(BitcoinTestFramework):
else: else:
protx_result = self.nodes[0].protx("register", collateral_txid, collateral_vout, ipAndPort, owner_address, bls['public'], voting_address, operatorReward, reward_address, funds_address, True) protx_result = self.nodes[0].protx("register", collateral_txid, collateral_vout, ipAndPort, owner_address, bls['public'], voting_address, operatorReward, reward_address, funds_address, True)
self.wait_for_instantlock(protx_result, self.nodes[0]) self.bump_mocktime(10 * 60 + 1) # to make tx safe to include in block
tip = self.generate(self.nodes[0], 1)[0] tip = self.generate(self.nodes[0], 1)[0]
assert_equal(self.nodes[0].getrawtransaction(protx_result, 1, tip)['confirmations'], 1) assert_equal(self.nodes[0].getrawtransaction(protx_result, 1, tip)['confirmations'], 1)
@ -1356,14 +1356,14 @@ class DashTestFramework(BitcoinTestFramework):
platform_http_port = '%d' % (r + 2) platform_http_port = '%d' % (r + 2)
fund_txid = self.nodes[0].sendtoaddress(funds_address, 1) fund_txid = self.nodes[0].sendtoaddress(funds_address, 1)
self.wait_for_instantlock(fund_txid, self.nodes[0]) self.bump_mocktime(10 * 60 + 1) # to make tx safe to include in block
tip = self.generate(self.nodes[0], 1)[0] tip = self.generate(self.nodes[0], 1)[0]
assert_equal(self.nodes[0].getrawtransaction(fund_txid, 1, tip)['confirmations'], 1) assert_equal(self.nodes[0].getrawtransaction(fund_txid, 1, tip)['confirmations'], 1)
protx_success = False protx_success = False
try: try:
protx_result = self.nodes[0].protx('update_service_evo', evo_info.proTxHash, evo_info.addr, evo_info.keyOperator, platform_node_id, platform_p2p_port, platform_http_port, operator_reward_address, funds_address) protx_result = self.nodes[0].protx('update_service_evo', evo_info.proTxHash, evo_info.addr, evo_info.keyOperator, platform_node_id, platform_p2p_port, platform_http_port, operator_reward_address, funds_address)
self.wait_for_instantlock(protx_result, self.nodes[0]) self.bump_mocktime(10 * 60 + 1) # to make tx safe to include in block
tip = self.generate(self.nodes[0], 1)[0] tip = self.generate(self.nodes[0], 1)[0]
assert_equal(self.nodes[0].getrawtransaction(protx_result, 1, tip)['confirmations'], 1) assert_equal(self.nodes[0].getrawtransaction(protx_result, 1, tip)['confirmations'], 1)
self.log.info("Updated EvoNode %s: platformNodeID=%s, platformP2PPort=%s, platformHTTPPort=%s" % (evo_info.proTxHash, platform_node_id, platform_p2p_port, platform_http_port)) self.log.info("Updated EvoNode %s: platformNodeID=%s, platformP2PPort=%s, platformHTTPPort=%s" % (evo_info.proTxHash, platform_node_id, platform_p2p_port, platform_http_port))