mirror of
https://github.com/dashpay/dash.git
synced 2024-12-26 20:42:59 +01:00
a760e33236
## 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)_
290 lines
13 KiB
Python
Executable File
290 lines
13 KiB
Python
Executable File
#!/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)
|
|
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.log.info(self.nodes[0].masternodelist())
|
|
|
|
return
|
|
|
|
def test_hpmn_payments(self, window_analysis):
|
|
current_hpmn = None
|
|
consecutive_payments = 0
|
|
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
|
|
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 4 times in a row
|
|
assert_equal(consecutive_payments, 4)
|
|
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
|
|
consecutive_payments = consecutive_payments_rpc if i == 0 and current_hpmn is None else 1
|
|
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 4 times in a row
|
|
assert_equal(consecutive_payments, 4)
|
|
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)):
|
|
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()
|