From 3f1693663b83f305733e2df49959bb5d00eacd0f Mon Sep 17 00:00:00 2001 From: random-zebra Date: Sat, 6 Mar 2021 17:29:33 +0100 Subject: [PATCH] [Test] Update tiertwo_mn_compatibility and check winners --- test/functional/test_framework/messages.py | 22 +++ .../test_framework/test_framework.py | 159 +++++++++++++++++- test/functional/test_framework/util.py | 37 +++- test/functional/tiertwo_mn_compatibility.py | 101 +++++++---- 4 files changed, 279 insertions(+), 40 deletions(-) diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py index ffd5d42bfe4ab..2b8e684f4c95d 100644 --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -1372,3 +1372,25 @@ def serialize(self): r += self.block_transactions.serialize(with_witness=True) return r + +# PIVX Classes +class Masternode(object): + def __init__(self, idx, owner_addr, operator_addr, voting_addr, ipport, payout_addr, operator_key): + self.idx = idx + self.owner = owner_addr + self.operator = operator_addr + self.voting = voting_addr + self.ipport = ipport + self.payee = payout_addr + self.operator_key = operator_key + self.proTx = None + self.collateral = None + + def __repr__(self): + return "Masternode(idx=%d, owner=%s, operator=%s, voting=%s, ip=%s, payee=%s, opkey=%s, protx=%s, collateral=%s)" % ( + self.idx, str(self.owner), str(self.operator), str(self.voting), str(self.ipport), + str(self.payee), str(self.operator_key), str(self.proTx), str(self.collateral) + ) + + def __str__(self): + return self.__repr__() diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index 0d159505c8052..4e5ede1697315 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -47,18 +47,19 @@ connect_nodes, connect_nodes_clique, disconnect_nodes, + get_collateral_vout, + lock_utxo, Decimal, DEFAULT_FEE, get_datadir_path, hex_str_to_bytes, bytes_to_hex_str, initialize_datadir, + create_new_dmn, p2p_port, set_node_times, SPORK_ACTIVATION_TIME, SPORK_DEACTIVATION_TIME, - vZC_DENOMS, - wait_until, ) class TestStatus(Enum): @@ -1048,9 +1049,10 @@ def controller_start_masternode(self, mnOwner, masternodeAlias): def send_pings(self, mnodes): for node in mnodes: - sent = node.mnping()["sent"] - if sent != "YES" and "Too early to send Masternode Ping" not in sent: - raise AssertionError("Unable to send ping: \"sent\" = %s" % sent) + try: + node.mnping()["sent"] + except: + pass time.sleep(1) @@ -1068,7 +1070,7 @@ def stake_and_ping(self, node_id, num_blocks, with_ping_mns=[]): if len(with_ping_mns) > 0: self.send_pings(with_ping_mns) - + # !TODO: remove after obsoleting legacy system def setupDMN(self, mnOwner, miner, @@ -1171,6 +1173,149 @@ def setupMasternode(self, return COutPoint(collateralTxId, collateralTxId_n) +### ---------------------- +### ----- DMN setup ------ +### ---------------------- + + def connect_to_all(self, nodePos): + for i in range(self.num_nodes): + if i != nodePos and self.nodes[i] is not None: + connect_nodes(self.nodes[i], nodePos) + + def assert_equal_for_all(self, expected, func_name, *args): + def not_found(): + raise Exception("function %s not found!" % func_name) + + assert_equal([getattr(x, func_name, not_found)(*args) for x in self.nodes], + [expected] * self.num_nodes) + + """ + Create a ProReg tx, which has the collateral as one of its outputs + """ + def protx_register_fund(self, miner, controller, dmn, collateral_addr, lock=True): + # send to the owner the collateral tx + some dust for the ProReg and fee + funding_txid = miner.sendtoaddress(collateral_addr, Decimal('101')) + # confirm and verify reception + miner.generate(1) + self.sync_blocks([miner, controller]) + assert_greater_than(controller.getrawtransaction(funding_txid, True)["confirmations"], 0) + # create and send the ProRegTx funding the collateral + dmn.proTx = controller.protx_register_fund(collateral_addr, dmn.ipport, dmn.owner, + dmn.operator, dmn.voting, dmn.payee) + dmn.collateral = COutPoint(int(dmn.proTx, 16), + get_collateral_vout(controller.getrawtransaction(dmn.proTx, True))) + if lock: + lock_utxo(controller, dmn.collateral) + + """ + Create a ProReg tx, which references an 100 PIV UTXO as collateral. + The controller node owns the collateral and creates the ProReg tx. + """ + def protx_register(self, miner, controller, dmn, collateral_addr, fLock): + # send to the owner the exact collateral tx amount + funding_txid = miner.sendtoaddress(collateral_addr, Decimal('100')) + # send another output to be used for the fee of the proReg tx + miner.sendtoaddress(collateral_addr, Decimal('1')) + # confirm and verify reception + miner.generate(1) + self.sync_blocks([miner, controller]) + json_tx = controller.getrawtransaction(funding_txid, True) + assert_greater_than(json_tx["confirmations"], 0) + # create and send the ProRegTx + dmn.collateral = COutPoint(int(funding_txid, 16), get_collateral_vout(json_tx)) + dmn.proTx = controller.protx_register(funding_txid, dmn.collateral.n, dmn.ipport, dmn.owner, + dmn.operator, dmn.voting, dmn.payee) + if fLock: + lock_utxo(controller, dmn.collateral) + + """ + Create a ProReg tx, referencing a collateral signed externally (eg. HW wallets). + Here the controller node owns the collateral (and signs), but the miner creates the ProReg tx. + """ + def protx_register_ext(self, miner, controller, dmn, outpoint, fSubmit, fLock): + # send to the owner the collateral tx if the outpoint is not specified + if outpoint is None: + funding_txid = miner.sendtoaddress(controller.getnewaddress("collateral"), Decimal('100')) + # confirm and verify reception + miner.generate(1) + self.sync_blocks([miner, controller]) + json_tx = controller.getrawtransaction(funding_txid, True) + assert_greater_than(json_tx["confirmations"], 0) + outpoint = COutPoint(int(funding_txid, 16), get_collateral_vout(json_tx)) + dmn.collateral = outpoint + # Prepare the message to be signed externally by the owner of the collateral (the controller) + reg_tx = miner.protx_register_prepare("%064x" % outpoint.hash, outpoint.n, dmn.ipport, dmn.owner, + dmn.operator, dmn.voting, dmn.payee) + sig = controller.signmessage(reg_tx["collateralAddress"], reg_tx["signMessage"]) + if fSubmit: + if fLock: + lock_utxo(controller, dmn.collateral) + dmn.proTx = miner.protx_register_submit(reg_tx["tx"], sig) + else: + return reg_tx["tx"], sig + + """ Create and register new deterministic masternode + :param idx: (int) index of the (remote) node in self.nodes + miner_idx: (int) index of the miner in self.nodes + controller_idx: (int) index of the controller in self.nodes + strType: (string) "fund"|"internal"|"external" + payout_addr: (string) payee address. If not specified, reuse the collateral address. + outpoint: (COutPoint) collateral outpoint to be used with "external". + It must be owned by the controller (proTx is sent from the miner). + If not provided, a new utxo is created, sending it from the miner. + op_addr_and_key: (list of strings) List with two entries, operator address (0) and private key (1). + If not provided, a new address-key pair is generated. + fLock: (boolean) lock the collateral output + :return: dmn: (Masternode) the deterministic masternode object + """ + def register_new_dmn(self, idx, miner_idx, controller_idx, strType, + payout_addr=None, outpoint=None, op_addr_and_key=None, fLock=True): + # Prepare remote node + assert idx != miner_idx + assert idx != controller_idx + miner_node = self.nodes[miner_idx] + controller_node = self.nodes[controller_idx] + mn_node = self.nodes[idx] + + # Generate ip and addresses/keys + collateral_addr = controller_node.getnewaddress("mncollateral-%d" % idx) + if payout_addr is None: + payout_addr = collateral_addr + dmn = create_new_dmn(idx, controller_node, payout_addr, op_addr_and_key) + + # Create ProRegTx + self.log.info("Creating%s proRegTx for deterministic masternode idx=%d..." % ( + " and funding" if strType == "fund" else "", idx)) + if strType == "fund": + self.protx_register_fund(miner_node, controller_node, dmn, collateral_addr, fLock) + elif strType == "internal": + self.protx_register(miner_node, controller_node, dmn, collateral_addr, fLock) + elif strType == "external": + self.protx_register_ext(miner_node, controller_node, dmn, outpoint, True, fLock) + else: + raise Exception("Type %s not available" % strType) + time.sleep(1) + self.sync_mempools([miner_node, controller_node]) + + # confirm and verify inclusion in list + miner_node.generate(1) + self.sync_blocks(self.nodes) + assert_greater_than(mn_node.getrawtransaction(dmn.proTx, 1)["confirmations"], 0) + assert dmn.proTx in mn_node.protx_list(False) + return dmn + + def check_mn_list_on_node(self, idx, mns): + mnlist = self.nodes[idx].listmasternodes() + if len(mnlist) != len(mns): + raise Exception("Invalid mn list on node %d:\n%s\nExpected:%s" % (idx, str(mnlist), str(mns))) + protxs = [x["proTxHash"] for x in mnlist] + for mn in mns: + if mn.proTx not in protxs: + raise Exception("ProTx for mn %d (%s) not found in the list of node %d", mn.idx, mn.proTx, idx) + + +### ------------------------------------------------------ + class SkipTest(Exception): """This exception is raised to skip a test""" def __init__(self, message): @@ -1180,7 +1325,7 @@ def __init__(self, message): ''' PivxTestFramework extensions ''' - +# !TODO: remove after obsoleting legacy system class PivxTier2TestFramework(PivxTestFramework): def set_test_params(self): diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py index 74d805085f99a..9006cebb57ef2 100644 --- a/test/functional/test_framework/util.py +++ b/test/functional/test_framework/util.py @@ -16,7 +16,7 @@ from subprocess import CalledProcessError import time -from . import coverage +from . import coverage, messages from .authproxy import AuthServiceProxy, JSONRPCException logger = logging.getLogger("TestFramework.utils") @@ -581,3 +581,38 @@ def get_coinstake_address(node, expected_utxos=None): addrs = [a for a in set(addrs) if addrs.count(a) == expected_utxos] assert(len(addrs) > 0) return addrs[0] + +# Deterministic masternodes + +def lock_utxo(node, outpoint): + node.lockunspent(False, [{"txid": "%064x" % outpoint.hash, "vout": outpoint.n}]) + +def get_collateral_vout(json_tx): + funding_txidn = -1 + for o in json_tx["vout"]: + if o["value"] == Decimal('100'): + funding_txidn = o["n"] + break + assert_greater_than(funding_txidn, -1) + return funding_txidn + +# owner and voting keys are created from controller node. +# operator key and address are created, if operator_addr_and_key is None. +def create_new_dmn(idx, controller, payout_addr, operator_addr_and_key): + ipport = "127.0.0.1:" + str(p2p_port(idx)) + owner_addr = controller.getnewaddress("mnowner-%d" % idx) + voting_addr = controller.getnewaddress("mnvoting-%d" % idx) + if operator_addr_and_key is None: + operator_addr = controller.getnewaddress("mnoperator-%d" % idx) + operator_key = controller.dumpprivkey(operator_addr) + else: + operator_addr = operator_addr_and_key[0] + operator_key = operator_addr_and_key[1] + return messages.Masternode(idx, owner_addr, operator_addr, voting_addr, ipport, payout_addr, operator_key) + +def spend_mn_collateral(spender, dmn): + inputs = [{"txid": "%064x" % dmn.collateral.hash, "vout": dmn.collateral.n}] + outputs = {spender.getnewaddress(): Decimal('99.99')} + sig_res = spender.signrawtransaction(spender.createrawtransaction(inputs, outputs)) + assert_equal(sig_res['complete'], True) + return spender.sendrawtransaction(sig_res['hex']) diff --git a/test/functional/tiertwo_mn_compatibility.py b/test/functional/tiertwo_mn_compatibility.py index c0e9f8581f888..b048aabe35cec 100755 --- a/test/functional/tiertwo_mn_compatibility.py +++ b/test/functional/tiertwo_mn_compatibility.py @@ -6,6 +6,7 @@ from test_framework.test_framework import PivxTier2TestFramework from test_framework.util import ( assert_equal, + connect_nodes, ) from decimal import Decimal @@ -59,25 +60,47 @@ def check_mns_status(self, node, txhash): assert_equal(status["dmnstate"]["PoSePenalty"], 0) assert_equal(status["status"], "Ready") + """ + Checks the block at specified height (it must be a v10 block). + Returns the address of the mn paid (in the coinbase), and the json coinstake tx + """ + def get_block_mnwinner(self, height): + blk = self.miner.getblock(self.miner.getblockhash(height), True) + assert_equal(blk['height'], height) + assert_equal(blk['version'], 10) + cbase_tx = self.miner.getrawtransaction(blk['tx'][0], True) + assert_equal(len(cbase_tx['vin']), 1) + cbase_script = height.to_bytes(1 + height // 256, byteorder="little") + cbase_script = len(cbase_script).to_bytes(1, byteorder="little") + cbase_script + bytearray(1) + assert_equal(cbase_tx['vin'][0]['coinbase'], cbase_script.hex()) + assert_equal(len(cbase_tx['vout']), 1) + assert_equal(cbase_tx['vout'][0]['value'], Decimal("3.0")) + return cbase_tx['vout'][0]['scriptPubKey']['addresses'][0], self.miner.getrawtransaction(blk['tx'][1], True) + def check_mn_list(self, node, txHashSet): # check masternode list from node mnlist = node.listmasternodes() - assert_equal(len(mnlist), len(txHashSet)) + if len(mnlist) != len(txHashSet): + raise Exception(str(mnlist)) foundHashes = set([mn["txhash"] for mn in mnlist if mn["txhash"] in txHashSet]) if len(foundHashes) != len(txHashSet): raise Exception(str(mnlist)) for x in mnlist: - self.mn_addresses.add(x["addr"]) - self.log.info("MN address list has %d entries" % len(self.mn_addresses)) + self.mn_addresses[x["txhash"]] = x["addr"] def run_test(self): - self.mn_addresses = set() + self.mn_addresses = {} self.enable_mocktime() self.setup_3_masternodes_network() # add two more nodes to the network self.remoteDMN2 = self.nodes[self.remoteDMN2Pos] self.remoteDMN3 = self.nodes[self.remoteDMN3Pos] + # add more direct connections to the miner + connect_nodes(self.miner, 2) + connect_nodes(self.remoteTwo, 0) + connect_nodes(self.remoteDMN2, 0) + self.sync_all() # check mn list from miner txHashSet = set([self.mnOneCollateral.hash, self.mnTwoCollateral.hash, self.proRegTx1.hash]) @@ -85,11 +108,11 @@ def run_test(self): # check status of masternodes self.check_mns_status_legacy(self.remoteOne, self.mnOneCollateral.hash) - self.log.info("MN1 active") + self.log.info("MN1 active. Pays %s" % self.mn_addresses[self.mnOneCollateral.hash]) self.check_mns_status_legacy(self.remoteTwo, self.mnTwoCollateral.hash) - self.log.info("MN2 active") + self.log.info("MN2 active Pays %s" % self.mn_addresses[self.mnTwoCollateral.hash]) self.check_mns_status(self.remoteDMN1, self.proRegTx1.hash) - self.log.info("DMN1 active") + self.log.info("DMN1 active Pays %s" % self.mn_addresses[self.proRegTx1.hash]) # Create another DMN, this time without funding the collateral. # ProTx references another transaction in the owner's wallet @@ -105,25 +128,14 @@ def run_test(self): txHashSet.add(self.proRegTx2.hash) self.check_mn_list(self.miner, txHashSet) self.check_mns_status(self.remoteDMN2, self.proRegTx2.hash) - self.log.info("DMN2 active") + self.log.info("DMN2 active Pays %s" % self.mn_addresses[self.proRegTx2.hash]) # Check block version and coinbase payment blk_count = self.miner.getblockcount() self.log.info("Checking block version and coinbase payment...") - blk = self.miner.getblock(self.miner.getbestblockhash(), True) - assert_equal(blk['height'], blk_count) - assert_equal(blk['version'], 10) - cbase_tx = self.miner.getrawtransaction(blk['tx'][0], True) - assert_equal(len(cbase_tx['vin']), 1) - cbase_script = blk_count.to_bytes(1 + blk_count//256, byteorder="little") - cbase_script = len(cbase_script).to_bytes(1, byteorder="little") + cbase_script + bytearray(1) - assert_equal(cbase_tx['vin'][0]['coinbase'], cbase_script.hex()) - assert_equal(len(cbase_tx['vout']), 1) - assert_equal(cbase_tx['vout'][0]['value'], Decimal("3.0")) - payee = cbase_tx['vout'][0]['scriptPubKey']['addresses'][0] - if payee not in self.mn_addresses: + payee, cstake_tx = self.get_block_mnwinner(blk_count) + if payee not in [self.mn_addresses[k] for k in self.mn_addresses]: raise Exception("payee %s not found in expected list %s" % (payee, str(self.mn_addresses))) - cstake_tx = self.miner.getrawtransaction(blk['tx'][1], True) assert_equal(len(cstake_tx['vin']), 1) assert_equal(len(cstake_tx['vout']), 2) assert_equal(cstake_tx['vout'][1]['value'], Decimal("497.0")) # 250 + 250 - 3 @@ -138,13 +150,10 @@ def run_test(self): "external", self.mnOneCollateral, ) - self.remoteDMN3.initmasternode(self.dmn3Privkey, "", True) - # The remote node is shutting down the pinging service - try: - self.send_3_pings() - except: - pass + self.send_3_pings() + + self.remoteDMN3.initmasternode(self.dmn3Privkey, "", True) # The legacy masternode must no longer be in the list # and the DMN must have taken its place @@ -154,20 +163,48 @@ def run_test(self): self.check_mn_list(node, txHashSet) self.log.info("Masternode list correctly updated by all nodes.") self.check_mns_status(self.remoteDMN3, self.proRegTx3.hash) - self.log.info("DMN3 active") + self.log.info("DMN3 active Pays %s" % self.mn_addresses[self.proRegTx3.hash]) # Now try to start a legacy MN with a collateral used by a DMN self.log.info("Now trying to start a legacy MN with a collateral of a DMN...") self.controller_start_masternode(self.ownerOne, self.masternodeOneAlias) - try: - self.send_3_pings() - except: - pass + self.send_3_pings() # the masternode list hasn't changed for node in self.nodes: self.check_mn_list(node, txHashSet) self.log.info("Masternode list correctly unchanged in all nodes.") + # stake 30 blocks, sync tiertwo data, and check winners + self.log.info("Staking 30 blocks...") + self.stake(30, [self.remoteTwo]) + self.sync_blocks() + self.wait_until_mnsync_finished() + + # check projection + self.log.info("Checking winners...") + winners = set([x['winner']['address'] for x in self.miner.getmasternodewinners() + if x['winner']['address'] != "Unknown"]) + # all except mn1 must be scheduled + mn_addresses = set([self.mn_addresses[k] for k in self.mn_addresses + if k != self.mnOneCollateral.hash]) + assert_equal(winners, mn_addresses) + + # check mns paid in the last 20 blocks + self.log.info("Checking masternodes paid...") + blk_count = self.miner.getblockcount() + mn_payments = {} # dict address --> payments count + for i in range(blk_count - 20 + 1, blk_count + 1): + winner, _ = self.get_block_mnwinner(i) + if winner not in mn_payments: + mn_payments[winner] = 0 + mn_payments[winner] += 1 + # two full 10-blocks schedule: all mns must be paid at least twice + assert_equal(len(mn_payments), len(mn_addresses)) + assert all([x >= 2 for x in mn_payments.values()]) + self.log.info("All good.") + + + if __name__ == '__main__': MasternodeCompatibilityTest().main()