dash/test/functional/feature_llmq_hpmn.py

304 lines
14 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# Copyright (c) 2015-2022 The Dash Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
'''
feature_llmq_hpmn.py
Checks HPMNs
'''
from _decimal import Decimal
from io import BytesIO
from test_framework.mininode import P2PInterface
from test_framework.messages import CBlock, CBlockHeader, CCbTx, CMerkleBlock, FromHex, hash256, msg_getmnlistd, \
QuorumId, ser_uint256
from test_framework.test_framework import DashTestFramework
from test_framework.util import (
assert_equal, p2p_port, wait_until
)
def extract_quorum_members(quorum_info):
return [d['proTxHash'] for d in quorum_info["members"]]
class TestP2PConn(P2PInterface):
def __init__(self):
super().__init__()
self.last_mnlistdiff = None
def on_mnlistdiff(self, message):
self.last_mnlistdiff = message
def wait_for_mnlistdiff(self, timeout=30):
def received_mnlistdiff():
return self.last_mnlistdiff is not None
return wait_until(received_mnlistdiff, timeout=timeout)
def getmnlistdiff(self, baseBlockHash, blockHash):
msg = msg_getmnlistd(baseBlockHash, blockHash)
self.last_mnlistdiff = None
self.send_message(msg)
self.wait_for_mnlistdiff()
return self.last_mnlistdiff
class LLMQHPMNTest(DashTestFramework):
def set_test_params(self):
self.set_dash_test_params(5, 4, fast_dip3_enforcement=True, hpmn_count=7)
self.set_dash_llmq_test_params(4, 4)
def run_test(self):
# Connect all nodes to node1 so that we always have the whole network connected
# Otherwise only masternode connections will be established between nodes, which won't propagate TXs/blocks
# Usually node0 is the one that does this, but in this test we isolate it multiple times
self.test_node = self.nodes[0].add_p2p_connection(TestP2PConn())
null_hash = format(0, "064x")
for i in range(len(self.nodes)):
if i != 0:
self.connect_nodes(i, 0)
self.activate_dip8()
self.nodes[0].sporkupdate("SPORK_17_QUORUM_DKG_ENABLED", 0)
self.wait_for_sporks_same()
expectedUpdated = [mn.proTxHash for mn in self.mninfo]
b_0 = self.nodes[0].getbestblockhash()
self.test_getmnlistdiff(null_hash, b_0, {}, [], expectedUpdated)
self.mine_quorum(llmq_type_name='llmq_test', llmq_type=100)
self.log.info("Test that HPMN registration is rejected before v19")
self.test_hpmn_is_rejected_before_v19()
self.test_masternode_count(expected_mns_count=4, expected_hpmns_count=0)
self.activate_v19(expected_activation_height=900)
self.log.info("Activated v19 at height:" + str(self.nodes[0].getblockcount()))
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()))
(quorum_info_i_0, quorum_info_i_1) = self.mine_cycle_quorum(llmq_type_name='llmq_test_dip0024', llmq_type=103)
hpmn_protxhash_list = list()
for i in range(5):
hpmn_info = self.dynamically_add_masternode(hpmn=True)
hpmn_protxhash_list.append(hpmn_info.proTxHash)
self.nodes[0].generate(8)
self.sync_blocks(self.nodes)
expectedUpdated.append(hpmn_info.proTxHash)
b_i = self.nodes[0].getbestblockhash()
self.test_getmnlistdiff(null_hash, b_i, {}, [], expectedUpdated)
self.test_masternode_count(expected_mns_count=4, expected_hpmns_count=i+1)
fix: check HPMNs duplicate on tx broadcast (#5257) <!-- *** Please remove the following help text before submitting: *** Provide a general summary of your changes in the Title above Pull requests without a rationale and clear improvement may be closed immediately. Please provide clear motivation for your patch and explain how it improves Dash Core user experience or Dash Core developer experience significantly: * Any test improvements or new tests that improve coverage are always welcome. * All other changes should have accompanying unit tests (see `src/test/`) or functional tests (see `test/`). Contributors should note which tests cover modified code. If no tests exist for a region of modified code, new tests should accompany the change. * Bug fixes are most welcome when they come with steps to reproduce or an explanation of the potential issue as well as reasoning for the way the bug was fixed. * Features are welcome, but might be rejected due to design or scope issues. If a feature is based on a lot of dependencies, contributors should first consider building the system outside of Dash Core, if possible. --> ## Issue being fixed or feature implemented <!--- Why is this change required? What problem does it solve? --> <!--- If it fixes an open issue, please link to the issue here. --> Before this fix, uniqueness of HPMN `platformNodeID` was checked only while processing a block containing a `ProRegTx` or a `ProUpServTx`. This is not enough as a `ProRegTx` or `ProUpServTx` containing duplicate HPMN `platformNodeID` must be rejected at tx broadcast level. ## What was done? <!--- Describe your changes in detail --> Checking uniqueness when calling respective RPC and when receiving such txs. ## How Has This Been Tested? <!--- Please describe in detail how you tested your changes. --> <!--- Include details of your testing environment, and the tests you ran to --> <!--- see how your change affects other areas of the code, etc. --> ## Breaking Changes <!--- Please describe any breaking changes your code introduces --> ## Checklist: <!--- Go over all the following points, and put an `x` in all the boxes that apply. --> - [x] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [x] I have added or updated relevant unit/integration/functional/e2e tests - [ ] I have made corresponding changes to the documentation **For repository code-owners and collaborators only** - [x] I have assigned this pull request to a milestone
2023-03-16 17:28:38 +01:00
self.dynamically_hpmn_update_service(hpmn_info)
self.log.info("Test llmq_platform are formed only with HPMNs")
for i in range(3):
quorum_i_hash = self.mine_quorum(llmq_type_name='llmq_test_platform', llmq_type=106, expected_connections=2, expected_members=3, expected_contributions=3, expected_complaints=0, expected_justifications=0, expected_commitments=3 )
self.test_quorum_members_are_high_performance(quorum_i_hash, llmq_type=106)
self.log.info("Test that HPMNs are present in MN list")
self.test_hpmn_protx_are_in_mnlist(hpmn_protxhash_list)
self.log.info("Test that HPMNs are paid 4x blocks in a row")
self.test_hpmn_payments(window_analysis=256)
self.activate_mn_rr()
self.log.info("Activated MN RewardReallocation at height:" + str(self.nodes[0].getblockcount()))
# Generate a few blocks to make HPMN/MN analysis on a pure MN RewardReallocation window
self.bump_mocktime(1)
self.nodes[0].generate(4)
self.sync_blocks()
self.log.info("Test that HPMNs are paid 1 block in a row after MN RewardReallocation activation")
self.test_hpmn_payments(window_analysis=256, v20active=True)
self.log.info(self.nodes[0].masternodelist())
return
def test_hpmn_payments(self, window_analysis, v20active=False):
current_hpmn = None
consecutive_payments = 0
n_payments = 0 if v20active else 4
for i in range(0, window_analysis):
payee = self.get_mn_payee_for_block(self.nodes[0].getbestblockhash())
if payee is not None and payee.hpmn:
if current_hpmn is not None and payee.proTxHash == current_hpmn.proTxHash:
# same HPMN
assert consecutive_payments > 0
if not v20active:
consecutive_payments += 1
consecutive_payments_rpc = self.nodes[0].protx('info', current_hpmn.proTxHash)['state']['consecutivePayments']
assert_equal(consecutive_payments, consecutive_payments_rpc)
else:
# new HPMN
if current_hpmn is not None:
# make sure the old one was paid N times in a row
assert_equal(consecutive_payments, n_payments)
consecutive_payments_rpc = self.nodes[0].protx('info', current_hpmn.proTxHash)['state']['consecutivePayments']
# old HPMN should have its nConsecutivePayments reset to 0
assert_equal(consecutive_payments_rpc, 0)
consecutive_payments_rpc = self.nodes[0].protx('info', payee.proTxHash)['state']['consecutivePayments']
# if hpmn is the one we start "for" loop with,
# we have no idea how many times it was paid before - rely on rpc results here
new_payment_value = 0 if v20active else 1
consecutive_payments = consecutive_payments_rpc if i == 0 and current_hpmn is None else new_payment_value
current_hpmn = payee
assert_equal(consecutive_payments, consecutive_payments_rpc)
else:
# not a HPMN
if current_hpmn is not None:
# make sure the old one was paid N times in a row
assert_equal(consecutive_payments, n_payments)
consecutive_payments_rpc = self.nodes[0].protx('info', current_hpmn.proTxHash)['state']['consecutivePayments']
# old HPMN should have its nConsecutivePayments reset to 0
assert_equal(consecutive_payments_rpc, 0)
current_hpmn = None
consecutive_payments = 0
self.nodes[0].generate(1)
if i % 8 == 0:
self.sync_blocks()
def get_mn_payee_for_block(self, block_hash):
mn_payee_info = self.nodes[0].masternode("payments", block_hash)[0]
mn_payee_protx = mn_payee_info['masternodes'][0]['proTxHash']
mninfos_online = self.mninfo.copy()
for mn_info in mninfos_online:
if mn_info.proTxHash == mn_payee_protx:
return mn_info
return None
def test_quorum_members_are_high_performance(self, quorum_hash, llmq_type):
quorum_info = self.nodes[0].quorum("info", llmq_type, quorum_hash)
quorum_members = extract_quorum_members(quorum_info)
mninfos_online = self.mninfo.copy()
for qm in quorum_members:
found = False
for mn in mninfos_online:
if mn.proTxHash == qm:
assert_equal(mn.hpmn, True)
found = True
break
assert_equal(found, True)
def test_hpmn_protx_are_in_mnlist(self, hpmn_protx_list):
mn_list = self.nodes[0].masternodelist()
for hpmn_protx in hpmn_protx_list:
found = False
for mn in mn_list:
if mn_list.get(mn)['proTxHash'] == hpmn_protx:
found = True
assert_equal(mn_list.get(mn)['type'], "HighPerformance")
assert_equal(found, True)
def test_hpmn_is_rejected_before_v19(self):
bls = self.nodes[0].bls('generate')
collateral_address = self.nodes[0].getnewaddress()
funds_address = self.nodes[0].getnewaddress()
owner_address = self.nodes[0].getnewaddress()
voting_address = self.nodes[0].getnewaddress()
reward_address = self.nodes[0].getnewaddress()
collateral_amount = 4000
outputs = {collateral_address: collateral_amount, funds_address: 1}
collateral_txid = self.nodes[0].sendmany("", outputs)
self.wait_for_instantlock(collateral_txid, self.nodes[0])
tip = self.nodes[0].generate(1)[0]
self.sync_all(self.nodes)
rawtx = self.nodes[0].getrawtransaction(collateral_txid, 1, tip)
assert_equal(rawtx['confirmations'], 1)
collateral_vout = 0
for txout in rawtx['vout']:
if txout['value'] == Decimal(collateral_amount):
collateral_vout = txout['n']
break
assert collateral_vout is not None
ipAndPort = '127.0.0.1:%d' % p2p_port(len(self.nodes))
operatorReward = len(self.nodes)
try:
self.nodes[0].protx('register_hpmn', collateral_txid, collateral_vout, ipAndPort, owner_address, bls['public'], voting_address, operatorReward, reward_address, funds_address, True)
# this should never succeed
assert False
except:
self.log.info("protx_hpmn rejected")
def test_masternode_count(self, expected_mns_count, expected_hpmns_count):
mn_count = self.nodes[0].masternode('count')
assert_equal(mn_count['total'], expected_mns_count + expected_hpmns_count)
detailed_count = mn_count['detailed']
assert_equal(detailed_count['regular']['total'], expected_mns_count)
assert_equal(detailed_count['hpmn']['total'], expected_hpmns_count)
def test_getmnlistdiff(self, baseBlockHash, blockHash, baseMNList, expectedDeleted, expectedUpdated):
d = self.test_getmnlistdiff_base(baseBlockHash, blockHash)
# Assert that the deletedMNs and mnList fields are what we expected
assert_equal(set(d.deletedMNs), set([int(e, 16) for e in expectedDeleted]))
assert_equal(set([e.proRegTxHash for e in d.mnList]), set(int(e, 16) for e in expectedUpdated))
# Build a new list based on the old list and the info from the diff
newMNList = baseMNList.copy()
for e in d.deletedMNs:
newMNList.pop(format(e, '064x'))
for e in d.mnList:
newMNList[format(e.proRegTxHash, '064x')] = e
cbtx = CCbTx()
cbtx.deserialize(BytesIO(d.cbTx.vExtraPayload))
# Verify that the merkle root matches what we locally calculate
hashes = []
for mn in sorted(newMNList.values(), key=lambda mn: ser_uint256(mn.proRegTxHash)):
feat: store protx version in CSimplifiedMNListEntry and use it to ser/deser pubKeyOperator (#5397) ## Issue being fixed or feature implemented Mobile wallets would have to convert 4k+ pubkeys at the V19 fork point and it's a pretty hard job for them that can easily take 10-15 seconds if not more. Also after the HF, if a masternode list is requested from before the HF, the operator keys come in basic scheme, but the merkelroot was calculated with legacy. From mobile team work it wasn't possible to convert all operator keys to legacy and then calculate the correct merkleroot. ~This PR builds on top of ~#5392~ #5403 (changes that belong to this PR: 26f7e966500bdea4c604f1d16716b40b366fc707 and 4b42dc8fcee3354afd82ce7e3a72ebe1659f5f22) and aims to solve both of these issues.~ cc @hashengineering @QuantumExplorer ## What was done? Introduce `nVersion` on p2p level for every CSimplifiedMNListEntry. Set `nVersion` to the same value we have it in CDeterministicMNState i.e. pubkey serialization would not be via basic scheme only after the V19 fork, it would match the way it’s serialized on-chain/in CDeterministicMNState for that specific MN. ## How Has This Been Tested? run tests ## Breaking Changes NOTE: `testnet` is going to re-fork at v19 forkpoint because `merkleRootMNList` is not going to match ## Checklist: - [x] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have added or updated relevant unit/integration/functional/e2e tests - [ ] I have made corresponding changes to the documentation - [ ] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_
2023-06-11 19:29:00 +02:00
hashes.append(hash256(mn.serialize(with_version = False)))
merkleRoot = CBlock.get_merkle_root(hashes)
assert_equal(merkleRoot, cbtx.merkleRootMNList)
return newMNList
def test_getmnlistdiff_base(self, baseBlockHash, blockHash):
hexstr = self.nodes[0].getblockheader(blockHash, False)
header = FromHex(CBlockHeader(), hexstr)
d = self.test_node.getmnlistdiff(int(baseBlockHash, 16), int(blockHash, 16))
assert_equal(d.baseBlockHash, int(baseBlockHash, 16))
assert_equal(d.blockHash, int(blockHash, 16))
# Check that the merkle proof is valid
proof = CMerkleBlock(header, d.merkleProof)
proof = proof.serialize().hex()
assert_equal(self.nodes[0].verifytxoutproof(proof), [d.cbTx.hash])
# Check if P2P messages match with RPCs
d2 = self.nodes[0].protx("diff", baseBlockHash, blockHash)
assert_equal(d2["baseBlockHash"], baseBlockHash)
assert_equal(d2["blockHash"], blockHash)
assert_equal(d2["cbTxMerkleTree"], d.merkleProof.serialize().hex())
assert_equal(d2["cbTx"], d.cbTx.serialize().hex())
assert_equal(set([int(e, 16) for e in d2["deletedMNs"]]), set(d.deletedMNs))
assert_equal(set([int(e["proRegTxHash"], 16) for e in d2["mnList"]]), set([e.proRegTxHash for e in d.mnList]))
assert_equal(set([QuorumId(e["llmqType"], int(e["quorumHash"], 16)) for e in d2["deletedQuorums"]]), set(d.deletedQuorums))
assert_equal(set([QuorumId(e["llmqType"], int(e["quorumHash"], 16)) for e in d2["newQuorums"]]), set([QuorumId(e.llmqType, e.quorumHash) for e in d.newQuorums]))
return d
if __name__ == '__main__':
LLMQHPMNTest().main()