From 0374dbebaff3706043bd08aca24cd2fe7711cf4b Mon Sep 17 00:00:00 2001 From: random-zebra Date: Thu, 17 Jun 2021 20:44:12 +0200 Subject: [PATCH 1/2] [QA] Add tiertwo_governance_reorg functional test >>> Backports 88e3993b7b547120522da264a6d2a7b15ec8df5f (#2436) --- .../test_framework/test_framework.py | 19 +- test/functional/test_runner.py | 1 + test/functional/tiertwo_governance_reorg.py | 209 ++++++++++++++++++ 3 files changed, 224 insertions(+), 5 deletions(-) create mode 100755 test/functional/tiertwo_governance_reorg.py diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index 46805f7d01f1..85e8a17f2bc7 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -43,6 +43,7 @@ PortSeed, assert_equal, assert_greater_than, + assert_greater_than_or_equal, check_json_precision, connect_nodes, connect_nodes_clique, @@ -1081,26 +1082,34 @@ def setupMasternode(self, collateralTxId = miner.sendtoaddress(mnAddress, Decimal('10000')) # confirm and verify reception self.stake_and_sync(self.nodes.index(miner), 1) - assert_equal(mnOwner.getbalance(), Decimal('10000')) + assert_greater_than_or_equal(mnOwner.getbalance(), Decimal('10000')) assert_greater_than(mnOwner.getrawtransaction(collateralTxId, 1)["confirmations"], 0) self.log.info("all good, creating masternode " + masternodeAlias + "..") # get the collateral output using the RPC command - mnCollateralOutput = mnOwner.getmasternodeoutputs()[0] - assert_equal(mnCollateralOutput["txhash"], collateralTxId) - mnCollateralOutputIndex = mnCollateralOutput["outputidx"] + mnCollateralOutputIndex = -1 + for mnc in mnOwner.getmasternodeoutputs(): + if collateralTxId == mnc["txhash"]: + mnCollateralOutputIndex = mnc["outputidx"] + break + assert_greater_than(mnCollateralOutputIndex, -1) self.log.info("collateral accepted for "+ masternodeAlias +". Updating masternode.conf...") # verify collateral confirmed - confData = masternodeAlias + " 127.0.0.1:" + str(p2p_port(mnRemotePos)) + " " + str(masternodePrivKey) + " " + str(mnCollateralOutput["txhash"]) + " " + str(mnCollateralOutputIndex) + confData = "%s %s %s %s %d" % ( + masternodeAlias, "127.0.0.1:" + str(p2p_port(mnRemotePos)), + masternodePrivKey, collateralTxId, mnCollateralOutputIndex) destinationDirPath = mnOwnerDirPath destPath = os.path.join(destinationDirPath, "masternode.conf") with open(destPath, "a+") as file_object: file_object.write("\n") file_object.write(confData) + # lock the collateral + mnOwner.lockunspent(False, [{"txid": collateralTxId, "vout": mnCollateralOutputIndex}]) + # return the collateral id return collateralTxId diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index af87ac550cf6..35270af321f5 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -141,6 +141,7 @@ TIERTWO_SCRIPTS = [ # Longest test should go first, to favor running tests in parallel 'tiertwo_governance_sync_basic.py', + 'tiertwo_governance_reorg.py', # ~ 361 sec 'tiertwo_masternode_activation.py', 'tiertwo_masternode_ping.py', ] diff --git a/test/functional/tiertwo_governance_reorg.py b/test/functional/tiertwo_governance_reorg.py new file mode 100755 index 000000000000..5c5d412e6161 --- /dev/null +++ b/test/functional/tiertwo_governance_reorg.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The PIVX developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://www.opensource.org/licenses/mit-license.php. + +from test_framework.test_framework import PivxTestFramework +from test_framework.util import ( + assert_equal, + connect_nodes, + disconnect_nodes, + p2p_port, + set_node_times, +) + +from decimal import Decimal +import os +import time + +class GovernanceReorgTest(PivxTestFramework): + + def set_test_params(self): + self.setup_clean_chain = True + # 4 nodes: + # - 1 miner/mncontroller + # - 2 remote mns + # - 1 other node to stake a forked chain + self.num_nodes = 4 + self.extra_args = [["-sporkkey=932HEevBSujW2ud7RfB1YF91AFygbBRQj3de3LyaCRqNzKKgWXi"], + [], + ["-listen", "-externalip=127.0.0.1"], + ["-listen", "-externalip=127.0.0.1"], + ] + self.enable_mocktime() + + self.minerAPos = 0 + self.minerBPos = 1 + self.remoteOnePos = 1 + self.remoteTwoPos = 2 + + self.masternodeOneAlias = "mnOne" + self.masternodeTwoAlias = "mntwo" + + self.mnOnePrivkey = "9247iC59poZmqBYt9iDh9wDam6v9S1rW5XekjLGyPnDhrDkP4AK" + self.mnTwoPrivkey = "92Hkebp3RHdDidGZ7ARgS4orxJAGyFUPDXNqtsYsiwho1HGVRbF" + + def run_test(self): + minerA = self.nodes[self.minerAPos] # also controller of mn1 and mn2 + minerB = self.nodes[self.minerBPos] + mn1 = self.nodes[self.remoteOnePos] + mn2 = self.nodes[self.remoteTwoPos] + + # First mine 250 PoW blocks (50 with minerB, 200 with minerA) + self.log.info("Generating 259 blocks...") + for _ in range(2): + for _ in range(25): + self.mocktime = self.generate_pow(self.minerBPos, self.mocktime) + self.sync_blocks() + for _ in range(100): + self.mocktime = self.generate_pow(self.minerAPos, self.mocktime) + self.sync_blocks() + # Then stake 9 blocks with minerA + self.stake_and_ping(self.minerAPos, 9, []) + for n in self.nodes: + assert_equal(n.getblockcount(), 259) + + # Setup Masternodes + self.log.info("Masternodes setup...") + ownerdir = os.path.join(self.options.tmpdir, "node%d" % self.minerAPos, "regtest") + self.mnOneTxHash = self.setupMasternode(minerA, minerA, self.masternodeOneAlias, + ownerdir, self.remoteOnePos, self.mnOnePrivkey) + self.mnTwoTxHash = self.setupMasternode(minerA, minerA, self.masternodeTwoAlias, + ownerdir, self.remoteTwoPos, self.mnTwoPrivkey) + + # Activate masternodes + self.log.info("Masternodes activation...") + self.stake_and_ping(self.minerAPos, 1, []) + time.sleep(3) + self.advance_mocktime(10) + remoteOnePort = p2p_port(self.remoteOnePos) + remoteTwoPort = p2p_port(self.remoteTwoPos) + mn1.initmasternode(self.mnOnePrivkey, "127.0.0.1:"+str(remoteOnePort)) + mn2.initmasternode(self.mnTwoPrivkey, "127.0.0.1:"+str(remoteTwoPort)) + self.stake_and_ping(self.minerAPos, 1, []) + self.wait_until_mnsync_finished() + self.controller_start_masternode(minerA, self.masternodeOneAlias) + self.controller_start_masternode(minerA, self.masternodeTwoAlias) + self.wait_until_mn_preenabled(self.mnOneTxHash, 40) + self.wait_until_mn_preenabled(self.mnTwoTxHash, 40) + self.send_3_pings([mn1, mn2]) + self.wait_until_mn_enabled(self.mnOneTxHash, 120, [mn1, mn2]) + self.wait_until_mn_enabled(self.mnTwoTxHash, 120, [mn1, mn2]) + + # activate sporks + self.log.info("Masternodes enabled. Activating sporks.") + self.activate_spork(self.minerAPos, "SPORK_8_MASTERNODE_PAYMENT_ENFORCEMENT") + self.activate_spork(self.minerAPos, "SPORK_9_MASTERNODE_BUDGET_ENFORCEMENT") + self.activate_spork(self.minerAPos, "SPORK_13_ENABLE_SUPERBLOCKS") + + # Create a proposal and vote on it + next_superblock = minerA.getnextsuperblock() + payee = minerA.getnewaddress() + self.log.info("Creating a proposal to be paid at block %d" % next_superblock) + proposalFeeTxId = minerA.preparebudget("test1", "https://test1.org", 2, + next_superblock, payee, 300) + self.stake_and_ping(self.minerAPos, 3, [mn1, mn2]) + proposalHash = minerA.submitbudget("test1", "https://test1.org", 2, + next_superblock, payee, 300, proposalFeeTxId) + time.sleep(1) + self.stake_and_ping(self.minerAPos, 7, [mn1, mn2]) + self.log.info("Vote for the proposal and check projection...") + minerA.mnbudgetvote("alias", proposalHash, "yes", self.masternodeOneAlias) + minerA.mnbudgetvote("alias", proposalHash, "yes", self.masternodeTwoAlias) + time.sleep(1) + self.stake_and_ping(self.minerAPos, 1, [mn1, mn2]) + projection = minerB.getbudgetprojection()[0] + assert_equal(projection["Name"], "test1") + assert_equal(projection["Hash"], proposalHash) + assert_equal(projection["Yeas"], 2) + + # Create the finalized budget and vote on it + self.log.info("Finalizing the budget...") + self.stake_and_ping(self.minerAPos, 5, [mn1, mn2]) + assert (minerA.mnfinalbudgetsuggest() is not None) + time.sleep(1) + self.stake_and_ping(self.minerAPos, 4, [mn1, mn2]) + budgetFinHash = minerA.mnfinalbudgetsuggest() + assert (budgetFinHash != "") + time.sleep(1) + minerA.mnfinalbudget("vote-many", budgetFinHash) + self.stake_and_ping(self.minerAPos, 2, [mn1, mn2]) + budFin = minerB.mnfinalbudget("show") + budget = budFin[next(iter(budFin))] + assert_equal(budget["VoteCount"], 2) + + # Stake up until the block before the superblock. + skip_blocks = next_superblock - minerA.getblockcount() - 1 + self.stake_and_ping(self.minerAPos, skip_blocks, [mn1, mn2]) + + # Split the network. + self.log.info("Splitting the chain at block %d" % minerA.getblockcount()) + self.split_network() + + # --- Chain A --- + self.nodes.pop(self.minerBPos) + # mine the superblock and check payment + self.log.info("Checking superblock on chain A...") + self.create_and_check_superblock(minerA, next_superblock, payee) + # Add 10 blocks on top + self.log.info("Staking 10 blocks...") + self.stake_and_ping(self.nodes.index(minerA), 10, [mn1, mn2]) + + # --- Chain B --- + other_nodes = self.nodes.copy() + self.nodes = [minerB] + # mine the superblock and check payment + self.log.info("Checking superblock on chain B...") + self.create_and_check_superblock(minerB, next_superblock, payee) + # Add 1 single block on top + self.log.info("Staking 1 block...") + self.stake_and_ping(self.nodes.index(minerB), 1, []) + + # --- Reconnect nodes -- + self.log.info("Reconnecting and re-organizing blocks...") + self.nodes = other_nodes + self.nodes.insert(self.minerBPos, minerB) + set_node_times(self.nodes, self.mocktime) + self.reconnect_nodes() + # !TODO: fix me + #self.sync_all() + #assert_equal(minerB.getblockcount(), next_superblock + 10) + #assert_equal(minerB.getbestblockhash(), minerA.getbestblockhash()) + + self.log.info("All good.") + + + def send_3_pings(self, mn_list): + self.advance_mocktime(30) + self.send_pings(mn_list) + self.stake_and_ping(self.minerAPos, 1, mn_list) + self.advance_mocktime(30) + self.send_pings(mn_list) + time.sleep(2) + + def split_network(self): + for i in range(self.num_nodes): + if i != self.minerBPos: + disconnect_nodes(self.nodes[i], self.minerBPos) + disconnect_nodes(self.nodes[self.minerBPos], i) + # by-pass ring connection + assert self.minerBPos > 0 + connect_nodes(self.nodes[self.minerBPos-1], self.minerBPos+1) + + def reconnect_nodes(self): + for i in range(self.num_nodes): + if i != self.minerBPos: + connect_nodes(self.nodes[i], self.minerBPos) + connect_nodes(self.nodes[self.minerBPos], i) + + def create_and_check_superblock(self, node, next_superblock, payee): + self.stake_and_ping(self.nodes.index(node), 1, []) + assert_equal(node.getblockcount(), next_superblock) + coinstake = node.getrawtransaction(node.getblock(node.getbestblockhash())["tx"][1], True) + budget_payment_out = coinstake["vout"][-1] + assert_equal(budget_payment_out["value"], Decimal("300")) + assert_equal(budget_payment_out["scriptPubKey"]["addresses"][0], payee) + + +if __name__ == '__main__': + GovernanceReorgTest().main() \ No newline at end of file From 43686138e9d46f66784eeb8736560797a3e53d60 Mon Sep 17 00:00:00 2001 From: random-zebra Date: Thu, 17 Jun 2021 20:52:11 +0200 Subject: [PATCH 2/2] [BUG] Check masternode/budget payments during block connection Partial backport of f6aa6add3df43f660e9b92d32cbef1afab49b2a9 (#2345): only moving IsBlockPayeeValid --- src/validation.cpp | 21 ++++++++------------- test/functional/tiertwo_governance_reorg.py | 7 +++---- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/validation.cpp b/src/validation.cpp index ee6b90377b8a..7514c8c85f59 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1636,6 +1636,14 @@ static bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockInd REJECT_INVALID, "bad-cb-amount"); } + if (!fInitialBlockDownload) { + // check masternode/budget payment + if (!IsBlockPayeeValid(block, pindex->nHeight)) { + mapRejectedBlocks.emplace(block.GetHash(), GetTime()); + return state.DoS(0, false, REJECT_INVALID, "bad-cb-payee", false, "Couldn't find masternode/budget payment"); + } + } + if (!control.Wait()) return state.DoS(100, error("%s: CheckQueue failed", __func__), REJECT_INVALID, "block-validation-failed"); int64_t nTime2 = GetTimeMicros(); @@ -2711,11 +2719,6 @@ bool CheckBlock(const CBlock& block, CValidationState& state, bool fCheckPOW, bo } // PIVX - // It is entierly possible that we don't have enough data and this could fail - // (i.e. the block could indeed be valid). Store the block for later consideration - // but issue an initial reject message. - // The case also exists that the sending peer could not have enough data to see - // that this block is invalid, so don't issue an outright ban. if (nHeight != 0 && !IsInitialBlockDownload()) { // Last output of Cold-Stake is not abused if (IsPoS && !CheckColdStakeFreeOutput(*(block.vtx[1]), nHeight)) { @@ -2726,13 +2729,6 @@ bool CheckBlock(const CBlock& block, CValidationState& state, bool fCheckPOW, bo // set Cold Staking Spork fColdStakingActive = !sporkManager.IsSporkActive(SPORK_19_COLDSTAKING_MAINTENANCE); - // check masternode/budget payment - if (!IsBlockPayeeValid(block, nHeight)) { - mapRejectedBlocks.emplace(block.GetHash(), GetTime()); - return state.DoS(0, false, REJECT_INVALID, "bad-cb-payee", false, "Couldn't find masternode/budget payment"); - } - } else { - LogPrintf("%s: Masternode/Budget payment checks skipped on sync\n", __func__); } } @@ -3263,7 +3259,6 @@ bool ProcessNewBlock(CValidationState& state, CNode* pfrom, const std::shared_pt // Preliminary checks int64_t nStartTime = GetTimeMillis(); - const Consensus::Params& consensus = Params().GetConsensus(); int newHeight = 0; { diff --git a/test/functional/tiertwo_governance_reorg.py b/test/functional/tiertwo_governance_reorg.py index 5c5d412e6161..8ca1bf13c52c 100755 --- a/test/functional/tiertwo_governance_reorg.py +++ b/test/functional/tiertwo_governance_reorg.py @@ -165,10 +165,9 @@ def run_test(self): self.nodes.insert(self.minerBPos, minerB) set_node_times(self.nodes, self.mocktime) self.reconnect_nodes() - # !TODO: fix me - #self.sync_all() - #assert_equal(minerB.getblockcount(), next_superblock + 10) - #assert_equal(minerB.getbestblockhash(), minerA.getbestblockhash()) + self.sync_all() + assert_equal(minerB.getblockcount(), next_superblock + 10) + assert_equal(minerB.getbestblockhash(), minerA.getbestblockhash()) self.log.info("All good.")