diff --git a/src/budget/budgetmanager.cpp b/src/budget/budgetmanager.cpp index 9cf51d3b9133b..aa02698e51f90 100644 --- a/src/budget/budgetmanager.cpp +++ b/src/budget/budgetmanager.cpp @@ -510,6 +510,31 @@ bool CBudgetManager::GetExpectedPayeeAmount(int chainHeight, CAmount& nAmountRet return GetPayeeAndAmount(chainHeight, payeeRet, nAmountRet); } +const CFinalizedBudget* CBudgetManager::GetBestFinalizedBudget(int chainHeight) const +{ + if (!Params().GetConsensus().IsSuperBlock(chainHeight)) { + return nullptr; + } + int nFivePercent = mnodeman.CountEnabled() / 20; + + const auto highest = GetBudgetWithHighestVoteCount(chainHeight); + if (highest.m_budget_fin == nullptr || highest.m_vote_count <= nFivePercent) { + // No finalization or not enough votes. + return nullptr; + } + return highest.m_budget_fin; +} + +CAmount CBudgetManager::GetFinalizedBudgetTotalPayout(int chainHeight) const +{ + const CFinalizedBudget* pfb = GetBestFinalizedBudget(chainHeight); + if (pfb == nullptr) { + // No finalization or not enough votes. + return 0; + } + return pfb->GetTotalPayout(); +} + bool CBudgetManager::FillBlockPayee(CMutableTransaction& txCoinbase, CMutableTransaction& txCoinstake, const int nHeight, bool fProofOfStake) const { if (nHeight <= 0) return false; @@ -553,6 +578,14 @@ bool CBudgetManager::FillBlockPayee(CMutableTransaction& txCoinbase, CMutableTra return true; } +void CBudgetManager::FillBlockPayees(CMutableTransaction& tx, int height) const +{ + const CFinalizedBudget* pfb = GetBestFinalizedBudget(height); + if (pfb != nullptr) { + pfb->PayAllBudgets(tx); + } +} + void CBudgetManager::VoteOnFinalizedBudgets() { // function called only from initialized masternodes @@ -742,6 +775,18 @@ TrxValidationStatus CBudgetManager::IsTransactionValid(const CTransaction& txNew return fThreshold ? TrxValidationStatus::InValid : TrxValidationStatus::VoteThreshold; } +bool CBudgetManager::IsValidSuperBlockTx(const CTransaction& txNew, int nBlockHeight) const +{ + assert(Params().GetConsensus().IsSuperBlock(nBlockHeight)); + + const CFinalizedBudget* pfb = GetBestFinalizedBudget(nBlockHeight); + if (pfb == nullptr) { + // No finalization or not enough votes. Nothing to check. + return true; + } + return pfb->AllBudgetsPaid(txNew); +} + std::vector CBudgetManager::GetAllProposalsOrdered() { LOCK(cs_proposals); diff --git a/src/budget/budgetmanager.h b/src/budget/budgetmanager.h index cbd395b2a3ae5..d291128f1b69d 100644 --- a/src/budget/budgetmanager.h +++ b/src/budget/budgetmanager.h @@ -136,8 +136,9 @@ class CBudgetManager : public CValidationInterface std::vector GetAllProposalsOrdered(); std::vector GetFinalizedBudgets(); bool GetExpectedPayeeAmount(int chainHeight, CAmount& nAmountRet) const; - bool IsBudgetPaymentBlock(int nBlockHeight) const; - bool IsBudgetPaymentBlock(int nBlockHeight, int& nCountThreshold) const; + CAmount GetFinalizedBudgetTotalPayout(int chainHeight) const; + bool IsBudgetPaymentBlock(int nBlockHeight) const; // legacy (multiple SB) + bool IsBudgetPaymentBlock(int nBlockHeight, int& nCountThreshold) const; // legacy (multiple SB) bool AddProposal(CBudgetProposal& budgetProposal); bool AddFinalizedBudget(CFinalizedBudget& finalizedBudget, CNode* pfrom = nullptr); void ForceAddFinalizedBudget(const uint256& nHash, const uint256& feeTxId, const CFinalizedBudget& finalizedBudget); @@ -145,9 +146,13 @@ class CBudgetManager : public CValidationInterface bool UpdateProposal(const CBudgetVote& vote, CNode* pfrom, std::string& strError); bool UpdateFinalizedBudget(const CFinalizedBudgetVote& vote, CNode* pfrom, std::string& strError); - TrxValidationStatus IsTransactionValid(const CTransaction& txNew, const uint256& nBlockHash, int nBlockHeight) const; + TrxValidationStatus IsTransactionValid(const CTransaction& txNew, const uint256& nBlockHash, int nBlockHeight) const; // legacy (multiple SB) + bool IsValidSuperBlockTx(const CTransaction& txNew, int nBlockHeight) const; // v6.0: single SB + std::string GetRequiredPaymentsString(int nBlockHeight); - bool FillBlockPayee(CMutableTransaction& txCoinbase, CMutableTransaction& txCoinstake, const int nHeight, bool fProofOfStake) const; + const CFinalizedBudget* GetBestFinalizedBudget(int chainHeight) const; + bool FillBlockPayee(CMutableTransaction& txCoinbase, CMutableTransaction& txCoinstake, const int nHeight, bool fProofOfStake) const; // legacy (multiple SB) + void FillBlockPayees(CMutableTransaction& tx, int height) const; // v6.0: single SB // Only initialized masternodes: sign and submit votes on valid finalized budgets void VoteOnFinalizedBudgets(); diff --git a/src/budget/budgetproposal.cpp b/src/budget/budgetproposal.cpp index eae6e4cdc4638..8e9770d028886 100644 --- a/src/budget/budgetproposal.cpp +++ b/src/budget/budgetproposal.cpp @@ -87,7 +87,7 @@ bool CBudgetProposal::CheckStartEnd() { // block start must be a superblock if (nBlockStart < 0 || - nBlockStart % Params().GetConsensus().nBudgetCycleBlocks != 0) { + !Params().GetConsensus().IsSuperBlock(nBlockStart)) { strInvalid = "Invalid nBlockStart"; return false; } diff --git a/src/budget/finalizedbudget.cpp b/src/budget/finalizedbudget.cpp index dc2cf33a50af0..520475e9a0605 100644 --- a/src/budget/finalizedbudget.cpp +++ b/src/budget/finalizedbudget.cpp @@ -5,7 +5,9 @@ #include "budget/finalizedbudget.h" +#include "chainparams.h" #include "masternodeman.h" +#include "utilmoneystr.h" #include "validation.h" CFinalizedBudget::CFinalizedBudget() : @@ -187,7 +189,7 @@ bool CFinalizedBudget::CheckStartEnd() } // Must be the correct block for payment to happen (once a month) - if (nBlockStart % Params().GetConsensus().nBudgetCycleBlocks != 0) { + if (!Params().GetConsensus().IsSuperBlock(nBlockStart)) { strInvalid = "Invalid BlockStart"; return false; } @@ -389,6 +391,45 @@ bool CFinalizedBudget::GetPayeeAndAmount(int64_t nBlockHeight, CScript& payee, C return true; } +bool CFinalizedBudget::AllBudgetsPaid(const CTransaction& tx) const +{ + // make a map for faster lookup and deal with duplicate payees + struct cmp { + bool operator()(const CTxOut& a, const CTxOut& b) const + { + return a.scriptPubKey < b.scriptPubKey || + (a.scriptPubKey == b.scriptPubKey && a.nValue < b.nValue); + } + }; + std::map txouts; + for (const CTxOut& o : tx.vout) { + txouts[o]++; + } + + for (const CTxBudgetPayment& payment : vecBudgetPayments) { + auto it = txouts.find(CTxOut(payment.nAmount, payment.payee)); + if (it == txouts.end() || it->second == 0) { + // Payment not found + CTxDestination addr; + const std::string& payee = ExtractDestination(payment.payee, addr) ? EncodeDestination(addr) : HexStr(payment.payee); + LogPrint(BCLog::MNBUDGET, "Missing payment of %s for %s (proposal hash: %s)\n", + FormatMoney(payment.nAmount), payee, payment.nProposalHash.ToString()); + return false; + } + it->second--; + } + + // all budgets are paid by tx + return true; +} + +void CFinalizedBudget::PayAllBudgets(CMutableTransaction& tx) const +{ + for (const CTxBudgetPayment& payment : vecBudgetPayments) { + tx.vout.emplace_back(payment.nAmount, payment.payee); + } +} + // return broadcast serialization CDataStream CFinalizedBudget::GetBroadcast() const { diff --git a/src/budget/finalizedbudget.h b/src/budget/finalizedbudget.h index dc928c2340f80..a6f8092bab767 100644 --- a/src/budget/finalizedbudget.h +++ b/src/budget/finalizedbudget.h @@ -93,6 +93,12 @@ class CFinalizedBudget bool GetBudgetPaymentByBlock(int64_t nBlockHeight, CTxBudgetPayment& payment) const; bool GetPayeeAndAmount(int64_t nBlockHeight, CScript& payee, CAmount& nAmount) const; + // Check if ALL the budgets are paid by transaction tx + bool AllBudgetsPaid(const CTransaction& tx) const; + + // Add payments for ALL budgets to tx outs + void PayAllBudgets(CMutableTransaction& tx) const; + // Check finalized budget proposals. Masternodes only (when voting on finalized budgets) bool CheckProposals(const std::map& mapWinningProposals) const; // Total amount paid out by this budget diff --git a/src/consensus/params.h b/src/consensus/params.h index bfb6203b24839..037de0478188a 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -243,6 +243,7 @@ struct Params { return (contextHeight - utxoFromBlockHeight >= nStakeMinDepth); } + bool IsSuperBlock(int height) const { return height % nBudgetCycleBlocks == 0; } /* * (Legacy) Zerocoin consensus params diff --git a/src/masternode-payments.cpp b/src/masternode-payments.cpp index 9379a3a503a2a..60a396433ea26 100644 --- a/src/masternode-payments.cpp +++ b/src/masternode-payments.cpp @@ -194,7 +194,7 @@ void DumpMasternodePayments() LogPrint(BCLog::MASTERNODE,"Budget dump finished %dms\n", GetTimeMillis() - nStart); } -bool IsBlockValueValid(int nHeight, CAmount& nExpectedValue, CAmount nMinted, CAmount& nBudgetAmt) +static bool IsBlockValueValid_legacy(int nHeight, CAmount& nExpectedValue, CAmount nMinted, CAmount& nBudgetAmt) { const Consensus::Params& consensus = Params().GetConsensus(); if (!g_tiertwo_sync_state.IsSynced()) { @@ -224,9 +224,31 @@ bool IsBlockValueValid(int nHeight, CAmount& nExpectedValue, CAmount nMinted, CA return nMinted <= nExpectedValue; } -bool IsBlockPayeeValid(const CBlock& block, const CBlockIndex* pindexPrev) +bool IsBlockValueValid(int nHeight, CAmount& nExpectedValue, CAmount nMinted, CAmount& nBudgetAmt) +{ + const Consensus::Params& consensus = Params().GetConsensus(); + if (!consensus.NetworkUpgradeActive(nHeight, Consensus::UPGRADE_V6_0)) { + return IsBlockValueValid_legacy(nHeight, nExpectedValue, nMinted, nBudgetAmt); + } + + if (consensus.IsSuperBlock(nHeight)) { + if (!g_tiertwo_sync_state.IsSynced()) { + // there is no budget data to use to check anything + nExpectedValue += g_budgetman.GetTotalBudget(nHeight); + } else if (sporkManager.IsSporkActive(SPORK_13_ENABLE_SUPERBLOCKS)) { + // we're synced and the superblock spork is enabled + nBudgetAmt = g_budgetman.GetFinalizedBudgetTotalPayout(nHeight); + nExpectedValue += nBudgetAmt; + } + } + + return nMinted >= 0 && nMinted <= nExpectedValue; +} + +bool IsBlockPayeeValid_legacy(const CBlock& block, const CBlockIndex* pindexPrev) { int nBlockHeight = pindexPrev->nHeight + 1; + assert(!Params().GetConsensus().NetworkUpgradeActive(nBlockHeight, Consensus::UPGRADE_V6_0)); TrxValidationStatus transactionStatus = TrxValidationStatus::InValid; if (!g_tiertwo_sync_state.IsSynced()) { //there is no budget data to use to check anything -- find the longest chain @@ -234,8 +256,7 @@ bool IsBlockPayeeValid(const CBlock& block, const CBlockIndex* pindexPrev) return true; } - const bool fPayCoinstake = Params().GetConsensus().NetworkUpgradeActive(nBlockHeight, Consensus::UPGRADE_POS) && - !Params().GetConsensus().NetworkUpgradeActive(nBlockHeight, Consensus::UPGRADE_V6_0); + const bool fPayCoinstake = Params().GetConsensus().NetworkUpgradeActive(nBlockHeight, Consensus::UPGRADE_POS); const CTransaction& txNew = *(fPayCoinstake ? block.vtx[1] : block.vtx[0]); //check if it's a budget block @@ -272,8 +293,38 @@ bool IsBlockPayeeValid(const CBlock& block, const CBlockIndex* pindexPrev) return true; } +bool IsBlockPayeeValid(const CBlock& block, const CBlockIndex* pindexPrev) +{ + const Consensus::Params& consensus = Params().GetConsensus(); + int nBlockHeight = pindexPrev->nHeight + 1; + if (!consensus.NetworkUpgradeActive(nBlockHeight, Consensus::UPGRADE_V6_0)) { + return IsBlockPayeeValid_legacy(block, pindexPrev); + } -void FillBlockPayee(CMutableTransaction& txCoinbase, CMutableTransaction& txCoinstake, const CBlockIndex* pindexPrev, bool fProofOfStake) + if (!g_tiertwo_sync_state.IsSynced()) { // there is no budget data to use to check anything -- find the longest chain + // !TODO: after transition to v6, restrict this to budget-checks only + LogPrint(BCLog::MASTERNODE, "Client not synced, skipping block payee checks\n"); + return true; + } + + const CTransaction& coinbase_tx = *block.vtx[0]; + + // Check masternode payment + if (sporkManager.IsSporkActive(SPORK_8_MASTERNODE_PAYMENT_ENFORCEMENT) && + !masternodePayments.IsTransactionValid(coinbase_tx, pindexPrev)) { + LogPrint(BCLog::MASTERNODE, "Missing required masternode payment\n"); + return false; + } + + // Check budget payments during superblocks + if (sporkManager.IsSporkActive(SPORK_13_ENABLE_SUPERBLOCKS) && consensus.IsSuperBlock(nBlockHeight)) { + return g_budgetman.IsValidSuperBlockTx(coinbase_tx, nBlockHeight); + } + + return true; +} + +static void FillBlockPayee_legacy(CMutableTransaction& txCoinbase, CMutableTransaction& txCoinstake, const CBlockIndex* pindexPrev, bool fProofOfStake) { if (!sporkManager.IsSporkActive(SPORK_13_ENABLE_SUPERBLOCKS) || // if superblocks are not enabled // ... or this is not a superblock @@ -283,6 +334,23 @@ void FillBlockPayee(CMutableTransaction& txCoinbase, CMutableTransaction& txCoin } } +void FillBlockPayee(CMutableTransaction& txCoinbase, CMutableTransaction& txCoinstake, const CBlockIndex* pindexPrev, bool fProofOfStake) +{ + int height = pindexPrev->nHeight + 1; + if (!Params().GetConsensus().NetworkUpgradeActive(height, Consensus::UPGRADE_V6_0)) { + // legacy - !TODO: remove after transition + return FillBlockPayee_legacy(txCoinbase, txCoinstake, pindexPrev, fProofOfStake); + } + + // Add masternode payment + masternodePayments.FillBlockPayee(txCoinbase, txCoinstake, pindexPrev, fProofOfStake); + + // Add budget payments (if superblock, and SPORK_13 is active) + if (sporkManager.IsSporkActive(SPORK_13_ENABLE_SUPERBLOCKS)) { + g_budgetman.FillBlockPayees(txCoinbase, height); + } +} + std::string GetRequiredPaymentsString(int nBlockHeight) { if (sporkManager.IsSporkActive(SPORK_13_ENABLE_SUPERBLOCKS) && g_budgetman.IsBudgetPaymentBlock(nBlockHeight)) { @@ -821,35 +889,25 @@ void CMasternodePayments::RecordWinnerVote(const COutPoint& outMasternode, int n mapMasternodesLastVote[outMasternode] = nBlockHeight; } -bool IsCoinbaseValueValid(const CTransactionRef& tx, CAmount nBudgetAmt, CValidationState& _state) +bool IsCoinbaseValueValid(const int nHeight, const CTransactionRef& tx, CAmount nBudgetAmt, CValidationState& _state) { assert(tx->IsCoinBase()); if (g_tiertwo_sync_state.IsSynced()) { - const CAmount nCBaseOutAmt = tx->GetValueOut(); - if (nBudgetAmt > 0) { - // Superblock - if (nCBaseOutAmt != nBudgetAmt) { - const std::string strError = strprintf("%s: invalid coinbase payment for budget (%s vs expected=%s)", - __func__, FormatMoney(nCBaseOutAmt), FormatMoney(nBudgetAmt)); - return _state.DoS(100, error(strError.c_str()), REJECT_INVALID, "bad-superblock-cb-amt"); - } - return true; - } else { - // regular block - int nHeight = mnodeman.GetBestHeight(); - CAmount nMnAmt = GetMasternodePayment(nHeight); - // if enforcement is disabled, there could be no masternode payment - bool sporkEnforced = sporkManager.IsSporkActive(SPORK_8_MASTERNODE_PAYMENT_ENFORCEMENT); - const std::string strError = strprintf("%s: invalid coinbase payment for masternode (%s vs expected=%s)", - __func__, FormatMoney(nCBaseOutAmt), FormatMoney(nMnAmt)); - if (sporkEnforced && nCBaseOutAmt != nMnAmt) { - return _state.DoS(100, error(strError.c_str()), REJECT_INVALID, "bad-cb-amt"); - } - if (!sporkEnforced && nCBaseOutAmt > nMnAmt) { - return _state.DoS(100, error(strError.c_str()), REJECT_INVALID, "bad-cb-amt-spork8-disabled"); - } - return true; + const CAmount paid = tx->GetValueOut(); + const CAmount expected = GetMasternodePayment(nHeight) + nBudgetAmt; + // if enforcement is disabled, there could be no masternode payment + bool sporkEnforced = sporkManager.IsSporkActive(SPORK_8_MASTERNODE_PAYMENT_ENFORCEMENT); + + const std::string strError = strprintf("%s: invalid coinbase payment (%s vs expected=%s)", + __func__, FormatMoney(paid), FormatMoney(expected)); + std::string rej_reason = (nBudgetAmt > 0 ? "bad-superblock-cb-amt" : "bad-cb-amt"); + if (sporkEnforced && paid != expected) { + return _state.DoS(100, error(strError.c_str()), REJECT_INVALID, rej_reason); + } + if (!sporkEnforced && paid > expected) { + return _state.DoS(100, error(strError.c_str()), REJECT_INVALID, rej_reason + "-spork8-disabled"); } + return true; } return true; } diff --git a/src/masternode-payments.h b/src/masternode-payments.h index 17be8863b7152..5974f8d96eeb7 100644 --- a/src/masternode-payments.h +++ b/src/masternode-payments.h @@ -34,7 +34,7 @@ void FillBlockPayee(CMutableTransaction& txCoinbase, CMutableTransaction& txCoin * Check coinbase output value for blocks after v6.0 enforcement. * It must pay the masternode for regular blocks and a proposal during superblocks. */ -bool IsCoinbaseValueValid(const CTransactionRef& tx, CAmount nBudgetAmt, CValidationState& _state); +bool IsCoinbaseValueValid(const int nHeight, const CTransactionRef& tx, CAmount nBudgetAmt, CValidationState& _state); void DumpMasternodePayments(); diff --git a/src/test/budget_tests.cpp b/src/test/budget_tests.cpp index a530a9110d3e6..50f56ee44a2fc 100644 --- a/src/test/budget_tests.cpp +++ b/src/test/budget_tests.cpp @@ -372,30 +372,30 @@ BOOST_FIXTURE_TEST_CASE(IsCoinbaseValueValid_test, TestingSetup) // Exact CMutableTransaction cbase = NewCoinBase(1, mnAmt, cbaseScript); - BOOST_CHECK(IsCoinbaseValueValid(MakeTransactionRef(cbase), 0, state)); + BOOST_CHECK(IsCoinbaseValueValid(nHeight, MakeTransactionRef(cbase), 0, state)); cbase.vout[0].nValue /= 2; cbase.vout.emplace_back(cbase.vout[0]); - BOOST_CHECK(IsCoinbaseValueValid(MakeTransactionRef(cbase), 0, state)); + BOOST_CHECK(IsCoinbaseValueValid(nHeight, MakeTransactionRef(cbase), 0, state)); // Underpaying with SPORK_8 disabled (good) cbase.vout.clear(); cbase.vout.emplace_back(mnAmt - 1, cbaseScript); - BOOST_CHECK(IsCoinbaseValueValid(MakeTransactionRef(cbase), 0, state)); + BOOST_CHECK(IsCoinbaseValueValid(nHeight, MakeTransactionRef(cbase), 0, state)); cbase.vout[0].nValue = mnAmt/2; cbase.vout.emplace_back(cbase.vout[0]); cbase.vout[1].nValue = mnAmt/2 - 1; - BOOST_CHECK(IsCoinbaseValueValid(MakeTransactionRef(cbase), 0, state)); + BOOST_CHECK(IsCoinbaseValueValid(nHeight, MakeTransactionRef(cbase), 0, state)); // Overpaying with SPORK_8 disabled cbase.vout.clear(); cbase.vout.emplace_back(mnAmt + 1, cbaseScript); - BOOST_CHECK(!IsCoinbaseValueValid(MakeTransactionRef(cbase), 0, state)); + BOOST_CHECK(!IsCoinbaseValueValid(nHeight, MakeTransactionRef(cbase), 0, state)); BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-cb-amt-spork8-disabled"); state = CValidationState(); cbase.vout[0].nValue = mnAmt/2; cbase.vout.emplace_back(cbase.vout[0]); cbase.vout[1].nValue = mnAmt/2 + 1; - BOOST_CHECK(!IsCoinbaseValueValid(MakeTransactionRef(cbase), 0, state)); + BOOST_CHECK(!IsCoinbaseValueValid(nHeight, MakeTransactionRef(cbase), 0, state)); BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-cb-amt-spork8-disabled"); state = CValidationState(); @@ -408,26 +408,26 @@ BOOST_FIXTURE_TEST_CASE(IsCoinbaseValueValid_test, TestingSetup) // Underpaying with SPORK_8 enabled cbase.vout.clear(); cbase.vout.emplace_back(mnAmt - 1, cbaseScript); - BOOST_CHECK(!IsCoinbaseValueValid(MakeTransactionRef(cbase), 0, state)); + BOOST_CHECK(!IsCoinbaseValueValid(nHeight, MakeTransactionRef(cbase), 0, state)); BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-cb-amt"); state = CValidationState(); cbase.vout[0].nValue = mnAmt/2; cbase.vout.emplace_back(cbase.vout[0]); cbase.vout[1].nValue = mnAmt/2 - 1; - BOOST_CHECK(!IsCoinbaseValueValid(MakeTransactionRef(cbase), 0, state)); + BOOST_CHECK(!IsCoinbaseValueValid(nHeight, MakeTransactionRef(cbase), 0, state)); BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-cb-amt"); state = CValidationState(); // Overpaying with SPORK_8 enabled cbase.vout.clear(); cbase.vout.emplace_back(mnAmt + 1, cbaseScript); - BOOST_CHECK(!IsCoinbaseValueValid(MakeTransactionRef(cbase), 0, state)); + BOOST_CHECK(!IsCoinbaseValueValid(nHeight, MakeTransactionRef(cbase), 0, state)); BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-cb-amt"); state = CValidationState(); cbase.vout[0].nValue = mnAmt/2; cbase.vout.emplace_back(cbase.vout[0]); cbase.vout[1].nValue = mnAmt/2 + 1; - BOOST_CHECK(!IsCoinbaseValueValid(MakeTransactionRef(cbase), 0, state)); + BOOST_CHECK(!IsCoinbaseValueValid(nHeight, MakeTransactionRef(cbase), 0, state)); BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-cb-amt"); state = CValidationState(); @@ -437,35 +437,50 @@ BOOST_FIXTURE_TEST_CASE(IsCoinbaseValueValid_test, TestingSetup) // Exact cbase.vout.clear(); + cbase.vout.emplace_back(mnAmt, cbaseScript); cbase.vout.emplace_back(budgAmt, cbaseScript); - BOOST_CHECK(IsCoinbaseValueValid(MakeTransactionRef(cbase), budgAmt, state)); - cbase.vout[0].nValue /= 2; - cbase.vout.emplace_back(cbase.vout[0]); - BOOST_CHECK(IsCoinbaseValueValid(MakeTransactionRef(cbase), budgAmt, state)); + BOOST_CHECK(IsCoinbaseValueValid(nHeight, MakeTransactionRef(cbase), budgAmt, state)); + cbase.vout[1].nValue /= 2; + cbase.vout.emplace_back(cbase.vout[1]); + BOOST_CHECK(IsCoinbaseValueValid(nHeight, MakeTransactionRef(cbase), budgAmt, state)); // Underpaying cbase.vout.clear(); + cbase.vout.emplace_back(mnAmt, cbaseScript); cbase.vout.emplace_back(budgAmt - 1, cbaseScript); - BOOST_CHECK(!IsCoinbaseValueValid(MakeTransactionRef(cbase), budgAmt, state)); + BOOST_CHECK(!IsCoinbaseValueValid(nHeight, MakeTransactionRef(cbase), budgAmt, state)); BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-superblock-cb-amt"); state = CValidationState(); - cbase.vout[0].nValue = budgAmt/2; - cbase.vout.emplace_back(cbase.vout[0]); - cbase.vout[1].nValue = budgAmt/2 - 1; - BOOST_CHECK(!IsCoinbaseValueValid(MakeTransactionRef(cbase), budgAmt, state)); + cbase.vout[1].nValue = budgAmt / 2; + cbase.vout.emplace_back(cbase.vout[1]); + cbase.vout[2].nValue = budgAmt / 2 - 1; + BOOST_CHECK(!IsCoinbaseValueValid(nHeight, MakeTransactionRef(cbase), budgAmt, state)); + state = CValidationState(); + cbase.vout.clear(); + cbase.vout.emplace_back(mnAmt - 1, cbaseScript); + cbase.vout.emplace_back(budgAmt, cbaseScript); + BOOST_CHECK(!IsCoinbaseValueValid(nHeight, MakeTransactionRef(cbase), budgAmt, state)); + BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-superblock-cb-amt"); BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-superblock-cb-amt"); state = CValidationState(); // Overpaying cbase.vout.clear(); + cbase.vout.emplace_back(mnAmt, cbaseScript); cbase.vout.emplace_back(budgAmt + 1, cbaseScript); - BOOST_CHECK(!IsCoinbaseValueValid(MakeTransactionRef(cbase), budgAmt, state)); + BOOST_CHECK(!IsCoinbaseValueValid(nHeight, MakeTransactionRef(cbase), budgAmt, state)); BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-superblock-cb-amt"); state = CValidationState(); - cbase.vout[0].nValue = budgAmt/2; - cbase.vout.emplace_back(cbase.vout[0]); - cbase.vout[1].nValue = budgAmt/2 + 1; - BOOST_CHECK(!IsCoinbaseValueValid(MakeTransactionRef(cbase), budgAmt, state)); + cbase.vout[1].nValue = budgAmt / 2; + cbase.vout.emplace_back(cbase.vout[1]); + cbase.vout[2].nValue = budgAmt / 2 + 1; + BOOST_CHECK(!IsCoinbaseValueValid(nHeight, MakeTransactionRef(cbase), budgAmt, state)); + BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-superblock-cb-amt"); + state = CValidationState(); + cbase.vout.clear(); + cbase.vout.emplace_back(mnAmt + 1, cbaseScript); + cbase.vout.emplace_back(budgAmt, cbaseScript); + BOOST_CHECK(!IsCoinbaseValueValid(nHeight, MakeTransactionRef(cbase), budgAmt, state)); BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-superblock-cb-amt"); } diff --git a/src/validation.cpp b/src/validation.cpp index afa1e005a916e..0ba784cdf068c 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1624,7 +1624,7 @@ static bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockInd nExpectedMint += nFees; //Check that the block does not overmint - CAmount nBudgetAmt = 0; // If this is a superblock, amount to be paid to the winning proposal, otherwise 0 + CAmount nBudgetAmt = 0; // If this is a superblock, amount to be paid to the winning proposals, otherwise 0 if (!IsBlockValueValid(pindex->nHeight, nExpectedMint, nMint, nBudgetAmt)) { return state.DoS(100, error("%s: reward pays too much (actual=%s vs limit=%s)", __func__, FormatMoney(nMint), FormatMoney(nExpectedMint)), @@ -1641,7 +1641,7 @@ static bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockInd } // After v6 enforcement: Check that the coinbase pays the exact amount - if (isPoSBlock && isV6UpgradeEnforced && !IsCoinbaseValueValid(block.vtx[0], nBudgetAmt, state)) { + if (isPoSBlock && isV6UpgradeEnforced && !IsCoinbaseValueValid(pindex->nHeight, block.vtx[0], nBudgetAmt, state)) { // pass the state returned by the function above return false; } diff --git a/test/functional/test_framework/budget_util.py b/test/functional/test_framework/budget_util.py new file mode 100755 index 0000000000000..cd135c44dc69b --- /dev/null +++ b/test/functional/test_framework/budget_util.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +# Copyright (c) 2020-2021 The PIVX Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://www.opensource.org/licenses/mit-license.php. + +from .util import ( + assert_equal, + assert_greater_than_or_equal, + assert_true, + satoshi_round, +) + + +class Proposal: + def __init__(self, name, link, cycles, payment_addr, amount_per_cycle): + self.name = name + self.link = link + self.cycles = cycles + self.paymentAddr = payment_addr + self.amountPerCycle = amount_per_cycle + self.feeTxId = "" + self.proposalHash = "" + + +def get_proposal_obj(Name, URL, Hash, FeeHash, BlockStart, BlockEnd, + TotalPaymentCount, RemainingPaymentCount, PaymentAddress, + Ratio, Yeas, Nays, Abstains, TotalPayment, MonthlyPayment, + IsEstablished, IsValid, Allotted, TotalBudgetAllotted, IsInvalidReason = ""): + obj = {} + obj["Name"] = Name + obj["URL"] = URL + obj["Hash"] = Hash + obj["FeeHash"] = FeeHash + obj["BlockStart"] = BlockStart + obj["BlockEnd"] = BlockEnd + obj["TotalPaymentCount"] = TotalPaymentCount + obj["RemainingPaymentCount"] = RemainingPaymentCount + obj["PaymentAddress"] = PaymentAddress + obj["Ratio"] = Ratio + obj["Yeas"] = Yeas + obj["Nays"] = Nays + obj["Abstains"] = Abstains + obj["TotalPayment"] = TotalPayment + obj["MonthlyPayment"] = MonthlyPayment + obj["IsEstablished"] = IsEstablished + obj["IsValid"] = IsValid + if IsInvalidReason != "": + obj["IsInvalidReason"] = IsInvalidReason + obj["Allotted"] = Allotted + obj["TotalBudgetAllotted"] = TotalBudgetAllotted + return obj + + +def get_proposal(prop, block_start, alloted, total_budget_alloted, positive_votes): + blockEnd = block_start + prop.cycles * 145 + total_payment = prop.amountPerCycle * prop.cycles + return get_proposal_obj(prop.name, prop.link, prop.proposalHash, prop.feeTxId, block_start, + blockEnd, prop.cycles, prop.cycles, prop.paymentAddr, 1, + positive_votes, 0, 0, satoshi_round(total_payment), satoshi_round(prop.amountPerCycle), + True, True, satoshi_round(alloted), satoshi_round(total_budget_alloted)) + + +def check_mns_status_legacy(node, txhash): + status = node.getmasternodestatus() + assert_equal(status["txhash"], txhash) + assert_equal(status["message"], "Masternode successfully started") + + +def check_mns_status(node, txhash): + status = node.getmasternodestatus() + assert_equal(status["proTxHash"], txhash) + assert_equal(status["dmnstate"]["PoSePenalty"], 0) + assert_equal(status["status"], "Ready") + + +def check_mn_list(node, txHashSet): + # check masternode list from node + mnlist = node.listmasternodes() + assert_equal(len(mnlist), len(txHashSet)) + foundHashes = set([mn["txhash"] for mn in mnlist if mn["txhash"] in txHashSet]) + assert_equal(len(foundHashes), len(txHashSet)) + + +def check_budget_finalization_sync(nodes, votesCount, status): + for i in range(0, len(nodes)): + node = nodes[i] + budFin = node.mnfinalbudget("show") + assert_greater_than_or_equal(len(budFin), 1) + budget = budFin[next(iter(budFin))] + assert_equal(budget["VoteCount"], votesCount) + assert_equal(budget["Status"], status) + + +def check_proposal_existence(nodes, proposalName, proposalHash): + for node in nodes: + proposals = node.getbudgetinfo(proposalName) + assert (len(proposals) > 0) + assert_equal(proposals[0]["Hash"], proposalHash) + + +def check_vote_existence(nodes, proposalName, mnCollateralHash, voteType, voteValid): + for i in range(0, len(nodes)): + node = nodes[i] + node.syncwithvalidationinterfacequeue() + votesInfo = node.getbudgetvotes(proposalName) + assert (len(votesInfo) > 0) + found = False + for voteInfo in votesInfo: + if (voteInfo["mnId"].split("-")[0] == mnCollateralHash) : + assert_equal(voteInfo["Vote"], voteType) + assert_equal(voteInfo["fValid"], voteValid) + found = True + assert_true(found, "Error checking vote existence in node " + str(i)) + + +def check_budgetprojection(nodes, expected, log): + for i in range(len(nodes)): + assert_equal(nodes[i].getbudgetprojection(), expected) + log.info("Budget projection valid for node %d" % i) + + +def create_proposals_tx(miner, props): + nextSuperBlockHeight = miner.getnextsuperblock() + for entry in props: + proposalFeeTxId = miner.preparebudget( + entry.name, + entry.link, + entry.cycles, + nextSuperBlockHeight, + entry.paymentAddr, + entry.amountPerCycle) + entry.feeTxId = proposalFeeTxId + return props + + +def propagate_proposals(miner, props): + nextSuperBlockHeight = miner.getnextsuperblock() + for entry in props: + proposalHash = miner.submitbudget( + entry.name, + entry.link, + entry.cycles, + nextSuperBlockHeight, + entry.paymentAddr, + entry.amountPerCycle, + entry.feeTxId) + entry.proposalHash = proposalHash + return props diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index e521d24c2d37f..26251ff61f1f1 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -1623,43 +1623,44 @@ def mine_quorum(self, invalidate_func=None, invalidated_idx=None, skip_bad_membe # !TODO: remove after obsoleting legacy system class PivxTier2TestFramework(PivxTestFramework): - def set_test_params(self): + def set_test_params(self, v6_enforcement_height=250): self.setup_clean_chain = True self.num_nodes = 6 self.enable_mocktime() - self.ownerOnePos = 0 + self.ownerPos = 0 self.remoteOnePos = 1 - self.ownerTwoPos = 2 - self.remoteTwoPos = 3 - self.minerPos = 4 - self.remoteDMN1Pos = 5 - - self.extra_args = [["-nuparams=v5_shield:249", "-nuparams=PIVX_v5.5:250", "-nuparams=v6_evo:250", "-whitelist=127.0.0.1"]] * self.num_nodes + self.remoteTwoPos = 2 + self.minerPos = 3 + self.remoteDMN1Pos = 4 + self.remoteThreePos = 5 + self.extra_args = [["-nuparams=v5_shield:249", "-nuparams=v6_evo:"+str(v6_enforcement_height), "-whitelist=127.0.0.1"]] * self.num_nodes for i in [self.remoteOnePos, self.remoteTwoPos, self.remoteDMN1Pos]: self.extra_args[i] += ["-listen", "-externalip=127.0.0.1"] self.extra_args[self.minerPos].append("-sporkkey=932HEevBSujW2ud7RfB1YF91AFygbBRQj3de3LyaCRqNzKKgWXi") self.masternodeOneAlias = "mnOne" self.masternodeTwoAlias = "mntwo" + self.masternodeThreeAlias = "mnThree" self.mnOnePrivkey = "9247iC59poZmqBYt9iDh9wDam6v9S1rW5XekjLGyPnDhrDkP4AK" self.mnTwoPrivkey = "92Hkebp3RHdDidGZ7ARgS4orxJAGyFUPDXNqtsYsiwho1HGVRbF" + self.mnThreePrivkey = "91qP855JNR3aWv6Z71BcFjqhkeizchSDjKSi7BdqMSSirEVDTEk" - # Updated in setup_3_masternodes_network() to be called at the start of run_test - self.ownerOne = None # self.nodes[self.ownerOnePos] + # Updated in setup_masternodes_network() to be called at the start of run_test + self.owner = None # self.nodes[self.ownerPos] self.remoteOne = None # self.nodes[self.remoteOnePos] - self.ownerTwo = None # self.nodes[self.ownerTwoPos] self.remoteTwo = None # self.nodes[self.remoteTwoPos] self.miner = None # self.nodes[self.minerPos] - self.remoteDMN1 = None # self.nodes[self.remoteDMN1Pos] + self.remoteDMN1 = None # self.nodes[self.remoteDMN1Pos] + self.remoteThree = None # self.nodes[self.remoteThreePos] self.mnOneCollateral = COutPoint() self.mnTwoCollateral = COutPoint() + self.mnThreeCollateral = COutPoint() self.proRegTx1 = None # hash of provider-register-tx - def send_3_pings(self): - mns = [self.remoteOne, self.remoteTwo] + mns = [self.remoteOne, self.remoteTwo, self.remoteThree] self.advance_mocktime(30) self.send_pings(mns) self.stake(1, mns) @@ -1671,14 +1672,17 @@ def stake(self, num_blocks, with_ping_mns=[]): self.stake_and_ping(self.minerPos, num_blocks, with_ping_mns) def controller_start_all_masternodes(self): - self.controller_start_masternode(self.ownerOne, self.masternodeOneAlias) - self.controller_start_masternode(self.ownerTwo, self.masternodeTwoAlias) + self.controller_start_masternode(self.owner, self.masternodeOneAlias) + self.controller_start_masternode(self.owner, self.masternodeTwoAlias) + self.controller_start_masternode(self.owner, self.masternodeThreeAlias) self.wait_until_mn_preenabled(self.mnOneCollateral.hash, 40) self.wait_until_mn_preenabled(self.mnTwoCollateral.hash, 40) - self.log.info("masternodes started, waiting until both get enabled..") + self.wait_until_mn_preenabled(self.mnThreeCollateral.hash, 40) + self.log.info("masternodes started, waiting until they get enabled..") self.send_3_pings() - self.wait_until_mn_enabled(self.mnOneCollateral.hash, 120, [self.remoteOne, self.remoteTwo]) - self.wait_until_mn_enabled(self.mnTwoCollateral.hash, 120, [self.remoteOne, self.remoteTwo]) + self.wait_until_mn_enabled(self.mnOneCollateral.hash, 120, [self.remoteOne, self.remoteTwo, self.remoteThree]) + self.wait_until_mn_enabled(self.mnTwoCollateral.hash, 120, [self.remoteOne, self.remoteTwo, self.remoteThree]) + self.wait_until_mn_enabled(self.mnThreeCollateral.hash, 120, [self.remoteOne, self.remoteTwo, self.remoteThree]) self.log.info("masternodes enabled and running properly!") def advance_mocktime_and_stake(self, secs_to_add): @@ -1686,15 +1690,14 @@ def advance_mocktime_and_stake(self, secs_to_add): self.mocktime = self.generate_pos(self.minerPos, self.mocktime) time.sleep(2) - def setup_3_masternodes_network(self): - self.ownerOne = self.nodes[self.ownerOnePos] + def setup_masternodes_network(self, setup_dmn=True): + self.owner = self.nodes[self.ownerPos] self.remoteOne = self.nodes[self.remoteOnePos] - self.ownerTwo = self.nodes[self.ownerTwoPos] self.remoteTwo = self.nodes[self.remoteTwoPos] + self.remoteThree = self.nodes[self.remoteThreePos] self.miner = self.nodes[self.minerPos] self.remoteDMN1 = self.nodes[self.remoteDMN1Pos] - ownerOneDir = os.path.join(self.options.tmpdir, "node%d" % self.ownerOnePos) - ownerTwoDir = os.path.join(self.options.tmpdir, "node%d" % self.ownerTwoPos) + ownerDir = os.path.join(self.options.tmpdir, "node%d" % self.ownerPos) self.log.info("generating 256 blocks..") # First mine 250 PoW blocks @@ -1707,27 +1710,36 @@ def setup_3_masternodes_network(self): self.log.info("masternodes setup..") # setup first masternode node, corresponding to nodeOne self.mnOneCollateral = self.setupMasternode( - self.ownerOne, + self.owner, self.miner, self.masternodeOneAlias, - os.path.join(ownerOneDir, "regtest"), + os.path.join(ownerDir, "regtest"), self.remoteOnePos, self.mnOnePrivkey) # setup second masternode node, corresponding to nodeTwo self.mnTwoCollateral = self.setupMasternode( - self.ownerTwo, + self.owner, self.miner, self.masternodeTwoAlias, - os.path.join(ownerTwoDir, "regtest"), + os.path.join(ownerDir, "regtest"), self.remoteTwoPos, self.mnTwoPrivkey) - # setup deterministic masternode - self.proRegTx1, self.dmn1Privkey = self.setupDMN( - self.ownerOne, + # setup third masternode node, corresponding to nodeTwo + self.mnThreeCollateral = self.setupMasternode( + self.owner, self.miner, - self.remoteDMN1Pos, - "fund" - ) + self.masternodeThreeAlias, + os.path.join(ownerDir, "regtest"), + self.remoteThreePos, + self.mnThreePrivkey) + if setup_dmn: + # setup deterministic masternode + self.proRegTx1, self.dmn1Privkey = self.setupDMN( + self.owner, + self.miner, + self.remoteDMN1Pos, + "fund" + ) self.log.info("masternodes setup completed, initializing them..") @@ -1737,9 +1749,12 @@ def setup_3_masternodes_network(self): self.advance_mocktime(10) remoteOnePort = p2p_port(self.remoteOnePos) remoteTwoPort = p2p_port(self.remoteTwoPos) + remoteThreePort = p2p_port(self.remoteThreePos) self.remoteOne.initmasternode(self.mnOnePrivkey, "127.0.0.1:"+str(remoteOnePort)) self.remoteTwo.initmasternode(self.mnTwoPrivkey, "127.0.0.1:"+str(remoteTwoPort)) - self.remoteDMN1.initmasternode(self.dmn1Privkey) + self.remoteThree.initmasternode(self.mnThreePrivkey, "127.0.0.1:"+str(remoteThreePort)) + if setup_dmn: + self.remoteDMN1.initmasternode(self.dmn1Privkey) # wait until mnsync complete on all nodes self.stake(1) diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 99f6a0479af35..58f1f24f3243f 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -161,6 +161,7 @@ TIERTWO_SCRIPTS = [ # Longest test should go first, to favor running tests in parallel 'tiertwo_governance_sync_basic.py', # ~ 1160 sec + 'tiertwo_budget.py', # ~ 500 sec 'tiertwo_dkg_errors.py', # ~ 486 sec 'tiertwo_dkg_pose.py', # ~ 444 sec 'tiertwo_mn_compatibility.py', # ~ 413 sec diff --git a/test/functional/tiertwo_budget.py b/test/functional/tiertwo_budget.py new file mode 100755 index 0000000000000..a57efa64e4ac6 --- /dev/null +++ b/test/functional/tiertwo_budget.py @@ -0,0 +1,311 @@ +#!/usr/bin/env python3 +# Copyright (c) 2023 The PIVX developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://www.opensource.org/licenses/mit-license.php. +""" +Test checking: + 1) pre-v6 multi-block payments + 2) different payment ordering validation (proposals paid in different order) + 3) duplicated payments validation --> wrong path + 4) post-v6 single block payments + 5) single block payments ordering validation (proposals paid in different order) + 6) duplicated payments validation --> wrong path +""" + +import time + +from decimal import Decimal +from test_framework.test_framework import PivxTier2TestFramework +from test_framework.util import ( + assert_equal +) +from test_framework.budget_util import ( + check_budget_finalization_sync, + create_proposals_tx, + check_budgetprojection, + check_proposal_existence, + check_mn_list, + check_mns_status_legacy, + check_vote_existence, + get_proposal, + Proposal, + propagate_proposals +) +from test_framework.messages import ( + COIN +) + + +class BudgetTest(PivxTier2TestFramework): + + def set_test_params(self): + super().set_test_params(300) + + def broadcastbudgetfinalization(self, node, with_ping_mns=[]): + self.log.info("suggesting the budget finalization..") + assert (node.mnfinalbudgetsuggest() is not None) + + self.log.info("confirming the budget finalization..") + time.sleep(1) + self.stake(4, with_ping_mns) + + self.log.info("broadcasting the budget finalization..") + return node.mnfinalbudgetsuggest() + + def submit_proposals(self, props): + props = create_proposals_tx(self.miner, props) + # generate 3 blocks to confirm the tx (and update the mnping) + self.stake(3, [self.remoteOne, self.remoteTwo, self.remoteThree]) + # check fee tx existence + for entry in props: + txinfo = self.miner.gettransaction(entry.feeTxId) + assert_equal(txinfo['amount'], -50.00) + # propagate proposals + props = propagate_proposals(self.miner, props) + # let's wait a little bit and see if all nodes are sync + time.sleep(1) + for entry in props: + check_proposal_existence(self.nodes, entry.name, entry.proposalHash) + self.log.info("proposal %s broadcast successful!" % entry.name) + return props + + def vote_legacy(self, node_voter, proposal, vote_direction, mn_voter_alias): + self.log.info("Voting with " + mn_voter_alias + ", for: " + proposal.name) + voteResult = node_voter.mnbudgetvote("alias", proposal.proposalHash, vote_direction, mn_voter_alias, True) + assert_equal(voteResult["detail"][0]["result"], "success") + time.sleep(1) + + def vote(self, node_voter, proposal, vote_direction, pro_reg_tx): + self.log.info("Voting with DMN " + pro_reg_tx + ", for: " + proposal.name) + voteResult = node_voter.mnbudgetvote("alias", proposal.proposalHash, vote_direction, pro_reg_tx) + assert_equal(voteResult["detail"][0]["result"], "success") + time.sleep(1) + + def vote_finalization(self, voting_node, budget_fin_hash, legacy): + voteResult = voting_node.mnfinalbudget("vote" if legacy else "vote", budget_fin_hash, legacy) + assert_equal(voteResult["detail"][0]["result"], "success") + + def check_address_balance(self, addr, expected_balance, has_balance=True): + addrInfo = self.nodes[self.ownerPos].listreceivedbyaddress(0, False, False, addr) + if has_balance: + assert_equal(addrInfo[0]["amount"], expected_balance) + else: + assert_equal(len(addrInfo), 0) + + def check_block_proposal_payment(self, block_hash, expected_to_address, expected_to_value, expected_out_index, is_v6_active): + block = self.miner.getblock(block_hash) + if is_v6_active: + # Get the coinbase second output that is the proposal payment + coinbase_tx = self.miner.getrawtransaction(block["tx"][0], True) + proposal_out = coinbase_tx["vout"][expected_out_index] + assert_equal(proposal_out["value"], expected_to_value) + assert_equal(proposal_out["scriptPubKey"]["addresses"][0], expected_to_address) + else: + # Get the coinstake third output + coinstake_tx = self.miner.getrawtransaction(block["tx"][1], True) + proposal_out = coinstake_tx["vout"][expected_out_index] + assert_equal(proposal_out["value"], expected_to_value) + assert_equal(proposal_out["scriptPubKey"]["addresses"][0], expected_to_address) + + def finalize_and_vote_budget(self): + # suggest the budget finalization and confirm the tx (+4 blocks). + budgetFinHash = self.broadcastbudgetfinalization(self.miner, + with_ping_mns=[self.remoteOne, self.remoteTwo, self.remoteThree]) + assert (budgetFinHash != "") + time.sleep(2) + self.log.info("voting budget finalization..") + for node in [self.remoteOne, self.remoteTwo, self.remoteThree]: + self.vote_finalization(node, budgetFinHash, True) + time.sleep(2) # wait a bit + check_budget_finalization_sync(self.nodes, 3, "OK") + + def run_test(self): + self.enable_mocktime() + self.setup_masternodes_network(setup_dmn=False) + txHashSet = set([self.mnOneCollateral.hash, self.mnTwoCollateral.hash, self.mnThreeCollateral.hash]) + # check mn list from miner + check_mn_list(self.miner, txHashSet) + + # check status of masternodes + check_mns_status_legacy(self.remoteOne, self.mnOneCollateral.hash) + check_mns_status_legacy(self.remoteTwo, self.mnTwoCollateral.hash) + check_mns_status_legacy(self.remoteThree, self.mnThreeCollateral.hash) + + # activate sporks + self.activate_spork(self.minerPos, "SPORK_8_MASTERNODE_PAYMENT_ENFORCEMENT") + self.activate_spork(self.minerPos, "SPORK_9_MASTERNODE_BUDGET_ENFORCEMENT") + self.activate_spork(self.minerPos, "SPORK_13_ENABLE_SUPERBLOCKS") + nextSuperBlockHeight = self.miner.getnextsuperblock() + + # Submit first proposal + self.log.info("preparing budget proposal..") + # Create 15 more proposals to have a higher tier two net gossip movement + props = [] + for i in range(16): + props.append(Proposal("prop_"+str(i), + "https://link_"+str(i)+".com", + 4, + self.nodes[self.ownerPos].getnewaddress(), + 11 * (i + 1))) + self.submit_proposals(props) + + # Proposals are established after 5 minutes. Mine 7 blocks + # Proposal needs to be on the chain > 5 min. + self.stake(7, [self.remoteOne, self.remoteTwo, self.remoteThree]) + # Check proposals existence + for i in range(self.num_nodes): + assert_equal(len(self.nodes[i].getbudgetinfo()), 16) + + # now let's vote for the two first proposals + expected_budget = [] + blockStart = nextSuperBlockHeight + alloted = 0 + for i in range(2): + prop = props[i] + self.vote_legacy(self.owner, prop, "yes", self.masternodeOneAlias) + check_vote_existence(self.nodes, prop.name, self.mnOneCollateral.hash, "YES", True) + self.vote_legacy(self.owner, prop, "yes", self.masternodeTwoAlias) + check_vote_existence(self.nodes, prop.name, self.mnTwoCollateral.hash, "YES", True) + if i < 1: + self.vote_legacy(self.owner, prop, "yes", self.masternodeThreeAlias) + check_vote_existence(self.nodes, prop.name, self.mnThreeCollateral.hash, "YES", True) + alloted += prop.amountPerCycle + expected_budget.append(get_proposal(prop, blockStart, prop.amountPerCycle, alloted, 3 - i)) + + # Now check the budget + check_budgetprojection(self.nodes, expected_budget, self.log) + + # Quick block count check. + assert_equal(self.owner.getblockcount(), 272) + self.stake(10, [self.remoteOne, self.remoteTwo, self.remoteThree]) + # Finalize budget + self.finalize_and_vote_budget() + self.stake(2, [self.remoteOne, self.remoteTwo, self.remoteThree]) + + # Check first proposal payment + prop1 = props[0] + self.check_block_proposal_payment(self.miner.getbestblockhash(), prop1.paymentAddr, prop1.amountPerCycle, 2, False) + self.check_address_balance(prop1.paymentAddr, prop1.amountPerCycle) + + # Check second proposal payment + prop2 = props[1] + assert prop2.paymentAddr is not prop1.paymentAddr + self.check_address_balance(prop2.paymentAddr, 0, has_balance=False) + self.stake(1, [self.remoteOne, self.remoteTwo, self.remoteThree]) + self.check_block_proposal_payment(self.miner.getbestblockhash(), prop2.paymentAddr, prop2.amountPerCycle, 2, False) + self.check_address_balance(prop2.paymentAddr, prop2.amountPerCycle) + + # Check that the proposal info returns updated payment count + expected_budget[0]["RemainingPaymentCount"] -= 1 + expected_budget[1]["RemainingPaymentCount"] -= 1 + check_budgetprojection(self.nodes, expected_budget, self.log) + self.stake(1, [self.remoteOne, self.remoteTwo, self.remoteThree]) + + self.log.info("pre-v6 budget proposal paid, all good. Testing enforcement now..") + + ################################################################## + self.log.info("checking post enforcement coinstake value..") + # Now test post enforcement, active from block 300 + for _ in range(4): + self.miner.generate(30) + self.stake_and_ping(self.minerPos, 1, [self.remoteOne, self.remoteTwo, self.remoteThree]) + + # Post-v6 enforcement + # Get the coinstake and check that the input value is equal to + # the output value + block reward - MN payment. + BLOCK_REWARD = Decimal(250 * COIN) + MN_BLOCK_REWARD = Decimal(3 * COIN) + tx_coinstake_id = self.miner.getblock(self.miner.getbestblockhash(), True)["tx"][1] + tx_coinstake = self.miner.getrawtransaction(tx_coinstake_id, True) + tx_coinstake_out_value = Decimal(tx_coinstake["vout"][1]["value"]) * COIN + tx_coinstake_vin = tx_coinstake["vin"][0] + tx_coinstake_input = self.miner.getrawtransaction(tx_coinstake_vin["txid"], True) + tx_coinstake_input_value = Decimal(tx_coinstake_input["vout"][int(tx_coinstake_vin["vout"])]["value"]) * COIN + assert (tx_coinstake_out_value == tx_coinstake_input_value + BLOCK_REWARD - MN_BLOCK_REWARD) + + ############################################################## + # Check single block payments + self.log.info("mining until next superblock..") + next_super_block = self.miner.getnextsuperblock() + block_count = self.miner.getblockcount() + self.stake_and_ping(self.minerPos, next_super_block - block_count - 6, [self.remoteOne, self.remoteTwo, self.remoteThree]) + assert_equal(self.owner.getblockcount(), 426) + # Finalize budget + self.finalize_and_vote_budget() + self.stake(2, [self.remoteOne, self.remoteTwo, self.remoteThree]) + self.log.info("checking single block payments..") + assert_equal(self.owner.getblockcount(), 432) + self.check_block_proposal_payment(self.miner.getbestblockhash(), prop1.paymentAddr, prop1.amountPerCycle, 1, True) + self.check_block_proposal_payment(self.miner.getbestblockhash(), prop2.paymentAddr, prop2.amountPerCycle, 2, True) + + # Check that the proposal info returns updated payment count + expected_budget[0]["RemainingPaymentCount"] -= 1 + expected_budget[1]["RemainingPaymentCount"] -= 1 + check_budgetprojection(self.nodes, expected_budget, self.log) + self.stake(1, [self.remoteOne, self.remoteTwo, self.remoteThree]) + self.log.info("post-v6 budget proposal paid, all good.") + + ################################################################## + self.log.info("Now test proposal with duplicate script and value") + + self.proRegTx1, self.dmn1Privkey = self.setupDMN( + self.owner, + self.miner, + self.remoteDMN1Pos, + "fund" + ) + self.stake(1, [self.remoteOne, self.remoteTwo, self.remoteThree]) + time.sleep(3) + self.advance_mocktime(10) + self.remoteDMN1.initmasternode(self.dmn1Privkey) + self.stake(1, [self.remoteOne, self.remoteTwo, self.remoteThree]) + + # Now test creating a new proposal paying to the same script and value as prop1 + blockStart = self.miner.getnextsuperblock() + prop17 = Proposal("prop_17", "https://link_"+str(17)+".com", 2, prop1.paymentAddr, 11) + self.submit_proposals([prop17]) + self.stake(5, [self.remoteOne, self.remoteTwo, self.remoteThree]) + for i in range(self.num_nodes): + assert_equal(len(self.nodes[i].getbudgetinfo()), 17) + # vote prop17 + self.vote_legacy(self.owner, prop17, "yes", self.masternodeOneAlias) + check_vote_existence(self.nodes, prop17.name, self.mnOneCollateral.hash, "YES", True) + self.vote_legacy(self.owner, prop17, "yes", self.masternodeTwoAlias) + check_vote_existence(self.nodes, prop17.name, self.mnTwoCollateral.hash, "YES", True) + self.vote_legacy(self.owner, prop17, "yes", self.masternodeThreeAlias) + check_vote_existence(self.nodes, prop17.name, self.mnThreeCollateral.hash, "YES", True) + self.vote(self.owner, prop17, "yes", self.proRegTx1) + check_vote_existence(self.nodes, prop17.name, self.proRegTx1, "YES", True) + + alloted += prop17.amountPerCycle + expected_budget.insert(0, get_proposal(prop17, blockStart, prop17.amountPerCycle, prop17.amountPerCycle, 4)) + expected_budget[1]["TotalBudgetAllotted"] = Decimal('22.00000000') + expected_budget[2]["TotalBudgetAllotted"] = Decimal('44.00000000') + # Now check the budget + check_budgetprojection(self.nodes, expected_budget, self.log) + + for _ in range(4): + self.miner.generate(30) + self.stake_and_ping(self.minerPos, 1, [self.remoteOne, self.remoteTwo, self.remoteThree]) + next_super_block = self.miner.getnextsuperblock() + block_count = self.miner.getblockcount() + self.stake_and_ping(self.minerPos, next_super_block - block_count - 6, [self.remoteOne, self.remoteTwo, self.remoteThree]) + assert_equal(self.owner.getblockcount(), 570) + + # Finalize budget + self.finalize_and_vote_budget() + self.stake(2, [self.remoteOne, self.remoteTwo, self.remoteThree]) + self.check_address_balance(prop1.paymentAddr, prop1.amountPerCycle * 4) # prop1.amountPerCycle * 3 + prop17.amountPerCycle + self.check_address_balance(prop2.paymentAddr, prop2.amountPerCycle * 3) + + # Check that the proposal info returns updated payment count + expected_budget[0]["RemainingPaymentCount"] -= 1 + expected_budget[1]["RemainingPaymentCount"] -= 1 + expected_budget[2]["RemainingPaymentCount"] -= 1 + check_budgetprojection(self.nodes, expected_budget, self.log) + self.stake(1, [self.remoteOne, self.remoteTwo, self.remoteThree]) + self.log.info("post-v6 duplicate proposals payouts paid.") + + +if __name__ == '__main__': + BudgetTest().main() diff --git a/test/functional/tiertwo_governance_sync_basic.py b/test/functional/tiertwo_governance_sync_basic.py index 0be6354985d72..3c046c9bfcfe9 100755 --- a/test/functional/tiertwo_governance_sync_basic.py +++ b/test/functional/tiertwo_governance_sync_basic.py @@ -15,9 +15,21 @@ from test_framework.messages import COutPoint from test_framework.test_framework import PivxTier2TestFramework +from test_framework.budget_util import ( + check_budget_finalization_sync, + create_proposals_tx, + check_budgetprojection, + check_proposal_existence, + check_mn_list, + check_mns_status_legacy, + check_mns_status, + check_vote_existence, + get_proposal_obj, + Proposal, + propagate_proposals +) from test_framework.util import ( assert_equal, - assert_true, connect_nodes, get_datadir_path, satoshi_round @@ -25,45 +37,9 @@ import shutil import os -class Proposal: - def __init__(self, name, link, cycles, payment_addr, amount_per_cycle): - self.name = name - self.link = link - self.cycles = cycles - self.paymentAddr = payment_addr - self.amountPerCycle = amount_per_cycle - self.feeTxId = "" - self.proposalHash = "" class MasternodeGovernanceBasicTest(PivxTier2TestFramework): - def check_mns_status_legacy(self, node, txhash): - status = node.getmasternodestatus() - assert_equal(status["txhash"], txhash) - assert_equal(status["message"], "Masternode successfully started") - - def check_mns_status(self, node, txhash): - status = node.getmasternodestatus() - assert_equal(status["proTxHash"], txhash) - assert_equal(status["dmnstate"]["PoSePenalty"], 0) - assert_equal(status["status"], "Ready") - - def check_mn_list(self, node, txHashSet): - # check masternode list from node - mnlist = node.listmasternodes() - assert_equal(len(mnlist), 3) - foundHashes = set([mn["txhash"] for mn in mnlist if mn["txhash"] in txHashSet]) - assert_equal(len(foundHashes), len(txHashSet)) - - def check_budget_finalization_sync(self, votesCount, status): - for i in range(0, len(self.nodes)): - node = self.nodes[i] - budFin = node.mnfinalbudget("show") - assert_true(len(budFin) == 1, "MN budget finalization not synced in node" + str(i)) - budget = budFin[next(iter(budFin))] - assert_equal(budget["VoteCount"], votesCount) - assert_equal(budget["Status"], status) - def broadcastbudgetfinalization(self, node, with_ping_mns=[]): self.log.info("suggesting the budget finalization..") assert (node.mnfinalbudgetsuggest() is not None) @@ -75,120 +51,42 @@ def broadcastbudgetfinalization(self, node, with_ping_mns=[]): self.log.info("broadcasting the budget finalization..") return node.mnfinalbudgetsuggest() - def check_proposal_existence(self, proposalName, proposalHash): - for node in self.nodes: - proposals = node.getbudgetinfo(proposalName) - assert(len(proposals) > 0) - assert_equal(proposals[0]["Hash"], proposalHash) - - def check_vote_existence(self, proposalName, mnCollateralHash, voteType, voteValid): - for i in range(0, len(self.nodes)): - node = self.nodes[i] - node.syncwithvalidationinterfacequeue() - votesInfo = node.getbudgetvotes(proposalName) - assert(len(votesInfo) > 0) - found = False - for voteInfo in votesInfo: - if (voteInfo["mnId"].split("-")[0] == mnCollateralHash) : - assert_equal(voteInfo["Vote"], voteType) - assert_equal(voteInfo["fValid"], voteValid) - found = True - assert_true(found, "Error checking vote existence in node " + str(i)) - - def get_proposal_obj(self, Name, URL, Hash, FeeHash, BlockStart, BlockEnd, - TotalPaymentCount, RemainingPaymentCount, PaymentAddress, - Ratio, Yeas, Nays, Abstains, TotalPayment, MonthlyPayment, - IsEstablished, IsValid, Allotted, TotalBudgetAllotted, IsInvalidReason = ""): - obj = {} - obj["Name"] = Name - obj["URL"] = URL - obj["Hash"] = Hash - obj["FeeHash"] = FeeHash - obj["BlockStart"] = BlockStart - obj["BlockEnd"] = BlockEnd - obj["TotalPaymentCount"] = TotalPaymentCount - obj["RemainingPaymentCount"] = RemainingPaymentCount - obj["PaymentAddress"] = PaymentAddress - obj["Ratio"] = Ratio - obj["Yeas"] = Yeas - obj["Nays"] = Nays - obj["Abstains"] = Abstains - obj["TotalPayment"] = TotalPayment - obj["MonthlyPayment"] = MonthlyPayment - obj["IsEstablished"] = IsEstablished - obj["IsValid"] = IsValid - if IsInvalidReason != "": - obj["IsInvalidReason"] = IsInvalidReason - obj["Allotted"] = Allotted - obj["TotalBudgetAllotted"] = TotalBudgetAllotted - return obj - - def check_budgetprojection(self, expected): - for i in range(self.num_nodes): - assert_equal(self.nodes[i].getbudgetprojection(), expected) - self.log.info("Budget projection valid for node %d" % i) - def connect_nodes_bi(self, nodes, a, b): connect_nodes(nodes[a], b) connect_nodes(nodes[b], a) - def create_proposals_tx(self, props): - nextSuperBlockHeight = self.miner.getnextsuperblock() - for entry in props: - proposalFeeTxId = self.miner.preparebudget( - entry.name, - entry.link, - entry.cycles, - nextSuperBlockHeight, - entry.paymentAddr, - entry.amountPerCycle) - entry.feeTxId = proposalFeeTxId - return props - - def propagate_proposals(self, props): - nextSuperBlockHeight = self.miner.getnextsuperblock() - for entry in props: - proposalHash = self.miner.submitbudget( - entry.name, - entry.link, - entry.cycles, - nextSuperBlockHeight, - entry.paymentAddr, - entry.amountPerCycle, - entry.feeTxId) - entry.proposalHash = proposalHash - return props - def submit_proposals(self, props): - props = self.create_proposals_tx(props) + props = create_proposals_tx(self.miner, props) # generate 3 blocks to confirm the tx (and update the mnping) - self.stake(3, [self.remoteOne, self.remoteTwo]) + self.stake(3, [self.remoteOne, self.remoteTwo,self.remoteThree]) # check fee tx existence for entry in props: txinfo = self.miner.gettransaction(entry.feeTxId) assert_equal(txinfo['amount'], -50.00) # propagate proposals - props = self.propagate_proposals(props) + props = propagate_proposals(self.miner, props) # let's wait a little bit and see if all nodes are sync time.sleep(1) for entry in props: - self.check_proposal_existence(entry.name, entry.proposalHash) + check_proposal_existence(self.nodes, entry.name, entry.proposalHash) self.log.info("proposal %s broadcast successful!" % entry.name) return props def run_test(self): self.enable_mocktime() - self.setup_3_masternodes_network() - txHashSet = set([self.mnOneCollateral.hash, self.mnTwoCollateral.hash, self.proRegTx1]) + self.setup_masternodes_network() + txHashSet = set([self.mnOneCollateral.hash, self.mnTwoCollateral.hash, self.mnThreeCollateral.hash, self.proRegTx1]) # check mn list from miner - self.check_mn_list(self.miner, txHashSet) + check_mn_list(self.miner, txHashSet) # check status of masternodes - self.check_mns_status_legacy(self.remoteOne, self.mnOneCollateral.hash) + check_mns_status_legacy(self.remoteOne, self.mnOneCollateral.hash) self.log.info("MN1 active") - self.check_mns_status_legacy(self.remoteTwo, self.mnTwoCollateral.hash) + check_mns_status_legacy(self.remoteTwo, self.mnTwoCollateral.hash) self.log.info("MN2 active") - self.check_mns_status(self.remoteDMN1, self.proRegTx1) + check_mns_status_legacy(self.remoteThree, self.mnThreeCollateral.hash) + self.log.info("MN3 active") + check_mns_status(self.remoteDMN1, self.proRegTx1) self.log.info("DMN1 active") # activate sporks @@ -220,53 +118,53 @@ def run_test(self): # Proposals are established after 5 minutes. Mine 7 blocks # Proposal needs to be on the chain > 5 min. - self.stake(7, [self.remoteOne, self.remoteTwo]) + self.stake(7, [self.remoteOne, self.remoteTwo,self.remoteThree]) # Check proposals existence for i in range(self.num_nodes): assert_equal(len(self.nodes[i].getbudgetinfo()), 16) # now let's vote for the proposal with the first MN self.log.info("Voting with MN1...") - voteResult = self.ownerOne.mnbudgetvote("alias", firstProposal.proposalHash, "yes", self.masternodeOneAlias, True) + voteResult = self.owner.mnbudgetvote("alias", firstProposal.proposalHash, "yes", self.masternodeOneAlias, True) assert_equal(voteResult["detail"][0]["result"], "success") # check that the vote was accepted everywhere - self.stake(1, [self.remoteOne, self.remoteTwo]) - self.check_vote_existence(firstProposal.name, self.mnOneCollateral.hash, "YES", True) + self.stake(1, [self.remoteOne, self.remoteTwo,self.remoteThree]) + check_vote_existence(self.nodes, firstProposal.name, self.mnOneCollateral.hash, "YES", True) self.log.info("all good, MN1 vote accepted everywhere!") - # before broadcast the second vote, let's drop the budget data of ownerOne. + # before broadcast the second vote, let's drop the budget data of owner. # so the node is forced to send a single proposal sync when the, now orphan, proposal vote is received. self.log.info("Testing single proposal re-sync based on an orphan vote, dropping budget data...") - self.ownerOne.cleanbudget(try_sync=False) - assert_equal(self.ownerOne.getbudgetprojection(), []) # empty - assert_equal(self.ownerOne.getbudgetinfo(), []) + self.owner.cleanbudget(try_sync=False) + assert_equal(self.owner.getbudgetprojection(), []) # empty + assert_equal(self.owner.getbudgetinfo(), []) # now let's vote for the proposal with the second MN self.log.info("Voting with MN2...") - voteResult = self.ownerTwo.mnbudgetvote("alias", firstProposal.proposalHash, "yes", self.masternodeTwoAlias, True) + voteResult = self.remoteTwo.mnbudgetvote("local", firstProposal.proposalHash, "yes", "", True) assert_equal(voteResult["detail"][0]["result"], "success") # check orphan vote proposal re-sync self.log.info("checking orphan vote based proposal re-sync...") - time.sleep(5) # wait a bit before check it - self.check_proposal_existence(firstProposal.name, firstProposal.proposalHash) - self.check_vote_existence(firstProposal.name, self.mnOneCollateral.hash, "YES", True) + time.sleep(5) # wait a bit before check it + check_proposal_existence(self.nodes, firstProposal.name, firstProposal.proposalHash) + check_vote_existence(self.nodes, firstProposal.name, self.mnOneCollateral.hash, "YES", True) self.log.info("all good, orphan vote based proposal re-sync succeeded") # check that the vote was accepted everywhere - self.stake(1, [self.remoteOne, self.remoteTwo]) - self.check_vote_existence(firstProposal.name, self.mnTwoCollateral.hash, "YES", True) + self.stake(1, [self.remoteOne, self.remoteTwo,self.remoteThree]) + check_vote_existence(self.nodes, firstProposal.name, self.mnTwoCollateral.hash, "YES", True) self.log.info("all good, MN2 vote accepted everywhere!") # now let's vote for the proposal with the first DMN self.log.info("Voting with DMN1...") - voteResult = self.ownerOne.mnbudgetvote("alias", firstProposal.proposalHash, "yes", self.proRegTx1) + voteResult = self.owner.mnbudgetvote("alias", firstProposal.proposalHash, "yes", self.proRegTx1) assert_equal(voteResult["detail"][0]["result"], "success") # check that the vote was accepted everywhere - self.stake(1, [self.remoteOne, self.remoteTwo]) - self.check_vote_existence(firstProposal.name, self.proRegTx1, "YES", True) + self.stake(1, [self.remoteOne, self.remoteTwo,self.remoteThree]) + check_vote_existence(self.nodes, firstProposal.name, self.proRegTx1, "YES", True) self.log.info("all good, DMN1 vote accepted everywhere!") # Now check the budget @@ -275,22 +173,21 @@ def run_test(self): TotalPayment = firstProposal.amountPerCycle * firstProposal.cycles Allotted = firstProposal.amountPerCycle RemainingPaymentCount = firstProposal.cycles - expected_budget = [ - self.get_proposal_obj(firstProposal.name, firstProposal.link, firstProposal.proposalHash, firstProposal.feeTxId, blockStart, + expected_budget = [get_proposal_obj(firstProposal.name, firstProposal.link, firstProposal.proposalHash, firstProposal.feeTxId, blockStart, blockEnd, firstProposal.cycles, RemainingPaymentCount, firstProposal.paymentAddr, 1, 3, 0, 0, satoshi_round(TotalPayment), satoshi_round(firstProposal.amountPerCycle), True, True, satoshi_round(Allotted), satoshi_round(Allotted)) ] - self.check_budgetprojection(expected_budget) + check_budgetprojection(self.nodes, expected_budget, self.log) # Quick block count check. - assert_equal(self.ownerOne.getblockcount(), 279) + assert_equal(self.owner.getblockcount(), 280) self.log.info("starting budget finalization sync test..") - self.stake(2, [self.remoteOne, self.remoteTwo]) + self.stake(2, [self.remoteOne, self.remoteTwo,self.remoteThree]) # assert that there is no budget finalization first. - assert_equal(len(self.ownerOne.mnfinalbudget("show")), 0) + assert_equal(len(self.owner.mnfinalbudget("show")), 0) # suggest the budget finalization and confirm the tx (+4 blocks). budgetFinHash = self.broadcastbudgetfinalization(self.miner, @@ -299,41 +196,43 @@ def run_test(self): time.sleep(2) self.log.info("checking budget finalization sync..") - self.check_budget_finalization_sync(0, "OK") + check_budget_finalization_sync(self.nodes, 0, "OK") self.log.info("budget finalization synced!, now voting for the budget finalization..") # Connecting owner to all the other nodes. - self.connect_to_all(self.ownerOnePos) + self.connect_to_all(self.ownerPos) - voteResult = self.ownerOne.mnfinalbudget("vote-many", budgetFinHash, True) + voteResult = self.remoteOne.mnfinalbudget("vote", budgetFinHash, True) + assert_equal(voteResult["detail"][0]["result"], "success") + time.sleep(2) # wait a bit + voteResult = self.remoteTwo.mnfinalbudget("vote", budgetFinHash, True) assert_equal(voteResult["detail"][0]["result"], "success") - time.sleep(2) # wait a bit - self.stake(2, [self.remoteOne, self.remoteTwo]) - self.check_budget_finalization_sync(1, "OK") + self.stake(2, [self.remoteOne, self.remoteTwo,self.remoteThree]) + check_budget_finalization_sync(self.nodes, 2, "OK") self.log.info("Remote One voted successfully.") - # before broadcast the second finalization vote, let's drop the budget data of remoteOne. + # before broadcast the third finalization vote, let's drop the budget data of remoteOne. # so the node is forced to send a single fin sync when the, now orphan, vote is received. self.log.info("Testing single fin re-sync based on an orphan vote, dropping budget data...") self.remoteOne.cleanbudget(try_sync=False) - assert_equal(self.remoteOne.getbudgetprojection(), []) # empty + assert_equal(self.remoteOne.getbudgetprojection(), []) # empty assert_equal(self.remoteOne.getbudgetinfo(), []) # vote for finalization with MN2 and the DMN - voteResult = self.ownerTwo.mnfinalbudget("vote-many", budgetFinHash, True) + voteResult = self.remoteThree.mnfinalbudget("vote", budgetFinHash, True) assert_equal(voteResult["detail"][0]["result"], "success") self.log.info("Remote Two voted successfully.") voteResult = self.remoteDMN1.mnfinalbudget("vote", budgetFinHash) assert_equal(voteResult["detail"][0]["result"], "success") self.log.info("DMN voted successfully.") - time.sleep(2) # wait a bit - self.stake(2, [self.remoteOne, self.remoteTwo]) + time.sleep(2) # wait a bit + self.stake(2, [self.remoteOne, self.remoteTwo,self.remoteThree]) self.log.info("checking finalization votes..") - self.check_budget_finalization_sync(3, "OK") + check_budget_finalization_sync(self.nodes, 4, "OK") self.log.info("orphan vote based finalization re-sync succeeded") - self.stake(6, [self.remoteOne, self.remoteTwo]) + self.stake(6, [self.remoteOne, self.remoteTwo,self.remoteThree]) addrInfo = self.miner.listreceivedbyaddress(0, False, False, firstProposal.paymentAddr) assert_equal(addrInfo[0]["amount"], firstProposal.amountPerCycle) @@ -341,20 +240,20 @@ def run_test(self): # Check that the proposal info returns updated payment count expected_budget[0]["RemainingPaymentCount"] -= 1 - self.check_budgetprojection(expected_budget) + check_budgetprojection(self.nodes, expected_budget, self.log) - self.stake(1, [self.remoteOne, self.remoteTwo]) + self.stake(1, [self.remoteOne, self.remoteTwo,self.remoteThree]) self.log.info("checking resync (1): cleaning budget data only..") # now let's drop budget data and try to re-sync it. self.remoteOne.cleanbudget(True) assert_equal(self.remoteOne.mnsync("status")["RequestedMasternodeAssets"], 0) - assert_equal(self.remoteOne.getbudgetprojection(), []) # empty + assert_equal(self.remoteOne.getbudgetprojection(), []) # empty assert_equal(self.remoteOne.getbudgetinfo(), []) self.log.info("budget cleaned, starting resync") self.wait_until_mnsync_finished() - self.check_budgetprojection(expected_budget) + check_budgetprojection(self.nodes, expected_budget, self.log) for i in range(self.num_nodes): assert_equal(len(self.nodes[i].getbudgetinfo()), 16) @@ -362,17 +261,17 @@ def run_test(self): self.log.info("checking resync (2): stop node, delete chain data and resync from scratch..") # stop and remove everything - self.stop_node(self.ownerTwoPos) - ownerTwoDir = os.path.join(get_datadir_path(self.options.tmpdir, self.ownerTwoPos), "regtest") + self.stop_node(self.ownerPos) + ownerDir = os.path.join(get_datadir_path(self.options.tmpdir, self.ownerPos), "regtest") for entry in ['chainstate', 'blocks', 'sporks', 'evodb', 'zerocoin', "mncache.dat", "budget.dat", "mnpayments.dat", "peers.dat"]: - rem_path = os.path.join(ownerTwoDir, entry) + rem_path = os.path.join(ownerDir, entry) shutil.rmtree(rem_path) if os.path.isdir(rem_path) else os.remove(rem_path) self.log.info("restarting node..") - self.start_node(self.ownerTwoPos) - self.ownerTwo.setmocktime(self.mocktime) - self.connect_to_all(self.ownerTwoPos) - self.stake(2, [self.remoteOne, self.remoteTwo]) + self.start_node(self.ownerPos) + self.owner.setmocktime(self.mocktime) + self.connect_to_all(self.ownerPos) + self.stake(2, [self.remoteOne, self.remoteTwo,self.remoteThree]) self.log.info("syncing node..") self.wait_until_mnsync_finished() @@ -385,54 +284,55 @@ def run_test(self): # (this is done once every 28 blocks on regtest). self.log.info("Testing incremental sync from peers, dropping budget data...") self.remoteDMN1.cleanbudget(try_sync=False) - assert_equal(self.remoteDMN1.getbudgetprojection(), []) # empty + assert_equal(self.remoteDMN1.getbudgetprojection(), []) # empty assert_equal(self.remoteDMN1.getbudgetinfo(), []) self.log.info("Generating blocks until someone syncs the node..") - self.stake(40, [self.remoteOne, self.remoteTwo]) - time.sleep(5) # wait a little bit + self.stake(40, [self.remoteOne, self.remoteTwo,self.remoteThree]) + time.sleep(5) # wait a little bit self.log.info("Checking budget sync..") for i in range(self.num_nodes): assert_equal(len(self.nodes[i].getbudgetinfo()), 16) - self.check_vote_existence(firstProposal.name, self.mnOneCollateral.hash, "YES", True) - self.check_vote_existence(firstProposal.name, self.mnTwoCollateral.hash, "YES", True) - self.check_vote_existence(firstProposal.name, self.proRegTx1, "YES", True) - self.check_budget_finalization_sync(3, "OK") + check_vote_existence(self.nodes, firstProposal.name, self.mnOneCollateral.hash, "YES", True) + check_vote_existence(self.nodes, firstProposal.name, self.mnTwoCollateral.hash, "YES", True) + check_vote_existence(self.nodes, firstProposal.name, self.proRegTx1, "YES", True) + check_budget_finalization_sync(self.nodes, 4, "OK") self.log.info("Remote incremental sync succeeded") # now let's verify that votes expire properly. # Drop one MN and one DMN self.log.info("expiring MN1..") - self.spend_collateral(self.ownerOne, self.mnOneCollateral, self.miner) + self.spend_collateral(self.owner, self.mnOneCollateral, self.miner) self.wait_until_mn_vinspent(self.mnOneCollateral.hash, 30, [self.remoteTwo]) - self.stake(15, [self.remoteTwo]) # create blocks to remove staled votes - time.sleep(2) # wait a little bit - self.check_vote_existence(firstProposal.name, self.mnOneCollateral.hash, "YES", False) - self.check_budget_finalization_sync(2, "OK") # budget finalization vote removal + self.stake(15, [self.remoteTwo, self.remoteThree]) # create blocks to remove staled votes + time.sleep(2) # wait a little bit + check_vote_existence(self.nodes, firstProposal.name, self.mnOneCollateral.hash, "YES", False) + check_budget_finalization_sync(self.nodes, 3, "OK") # budget finalization vote removal self.log.info("MN1 vote expired after collateral spend, all good") self.log.info("expiring DMN1..") - lm = self.ownerOne.listmasternodes(self.proRegTx1)[0] - self.spend_collateral(self.ownerOne, COutPoint(lm["collateralHash"], lm["collateralIndex"]), self.miner) + lm = self.owner.listmasternodes(self.proRegTx1)[0] + self.spend_collateral(self.owner, COutPoint(lm["collateralHash"], lm["collateralIndex"]), self.miner) self.wait_until_mn_vinspent(self.proRegTx1, 30, [self.remoteTwo]) - self.stake(15, [self.remoteTwo]) # create blocks to remove staled votes - time.sleep(2) # wait a little bit - self.check_vote_existence(firstProposal.name, self.proRegTx1, "YES", False) - self.check_budget_finalization_sync(1, "OK") # budget finalization vote removal + self.stake(15, [self.remoteTwo, self.remoteThree]) # create blocks to remove staled votes + time.sleep(2) # wait a little bit + check_vote_existence(self.nodes, firstProposal.name, self.proRegTx1, "YES", False) + check_budget_finalization_sync(self.nodes, 2, "OK") # budget finalization vote removal self.log.info("DMN vote expired after collateral spend, all good") # Check that the budget is removed 200 blocks after the last payment assert_equal(len(self.miner.mnfinalbudget("show")), 1) blocks_to_mine = nextSuperBlockHeight + 200 - self.miner.getblockcount() self.log.info("Mining %d more blocks to check expired budget removal..." % blocks_to_mine) - self.stake(blocks_to_mine - 1, [self.remoteTwo]) + self.stake(blocks_to_mine - 1, [self.remoteTwo, self.remoteThree]) # finalized budget must still be there self.miner.checkbudgets() assert_equal(len(self.miner.mnfinalbudget("show")), 1) # after one more block it must be removed - self.stake(1, [self.remoteTwo]) + self.stake(1, [self.remoteTwo, self.remoteThree]) self.miner.checkbudgets() assert_equal(len(self.miner.mnfinalbudget("show")), 0) self.log.info("All good.") + if __name__ == '__main__': MasternodeGovernanceBasicTest().main() diff --git a/test/functional/tiertwo_masternode_activation.py b/test/functional/tiertwo_masternode_activation.py index 1ce26a98ca6d5..9383f3fb68075 100755 --- a/test/functional/tiertwo_masternode_activation.py +++ b/test/functional/tiertwo_masternode_activation.py @@ -68,7 +68,7 @@ def wait_until_mn_expired(self, _timeout, removed=False): def run_test(self): self.enable_mocktime() - self.setup_3_masternodes_network() + self.setup_masternodes_network() # check masternode expiration self.log.info("testing expiration now.") @@ -90,7 +90,7 @@ def run_test(self): self.reconnect_and_restart_masternodes() self.advance_mocktime(30) self.log.info("spending the collateral now..") - self.spend_collateral(self.ownerOne, self.mnOneCollateral, self.miner) + self.spend_collateral(self.owner, self.mnOneCollateral, self.miner) self.sync_blocks() self.log.info("checking mn status..") time.sleep(3) # wait a little bit diff --git a/test/functional/tiertwo_mn_compatibility.py b/test/functional/tiertwo_mn_compatibility.py index aeb7f30604e27..3fa1dff7348c0 100755 --- a/test/functional/tiertwo_mn_compatibility.py +++ b/test/functional/tiertwo_mn_compatibility.py @@ -16,39 +16,16 @@ class MasternodeCompatibilityTest(PivxTier2TestFramework): - def set_test_params(self): - self.setup_clean_chain = True - self.num_nodes = 7 - self.enable_mocktime() - - self.minerPos = 0 - self.ownerOnePos = self.ownerTwoPos = 1 - self.remoteOnePos = 2 - self.remoteTwoPos = 3 - self.remoteDMN1Pos = 4 - self.remoteDMN2Pos = 5 - self.remoteDMN3Pos = 6 - - self.masternodeOneAlias = "mnOne" - self.masternodeTwoAlias = "mntwo" - + super().set_test_params() + self.num_nodes = 8 + self.remoteDMN2Pos = 6 + self.remoteDMN3Pos = 7 self.extra_args = [["-nuparams=v5_shield:249", "-nuparams=v6_evo:250", "-whitelist=127.0.0.1"]] * self.num_nodes - for i in [self.remoteOnePos, self.remoteTwoPos, self.remoteDMN1Pos, self.remoteDMN2Pos, self.remoteDMN3Pos]: + for i in [self.remoteOnePos, self.remoteTwoPos, self.remoteDMN1Pos]: self.extra_args[i] += ["-listen", "-externalip=127.0.0.1"] self.extra_args[self.minerPos].append("-sporkkey=932HEevBSujW2ud7RfB1YF91AFygbBRQj3de3LyaCRqNzKKgWXi") - self.mnOnePrivkey = "9247iC59poZmqBYt9iDh9wDam6v9S1rW5XekjLGyPnDhrDkP4AK" - self.mnTwoPrivkey = "92Hkebp3RHdDidGZ7ARgS4orxJAGyFUPDXNqtsYsiwho1HGVRbF" - - self.miner = None - self.ownerOne = self.ownerTwo = None - self.remoteOne = None - self.remoteTwo = None - self.remoteDMN1 = None - self.remoteDMN2 = None - self.remoteDMN3 = None - def check_mns_status_legacy(self, node, txhash): status = node.getmasternodestatus() assert_equal(status["txhash"], txhash) @@ -96,10 +73,10 @@ def check_mn_list(self, node, txHashSet): def run_test(self): self.mn_addresses = {} self.enable_mocktime() - self.setup_3_masternodes_network() + self.setup_masternodes_network() - # start with 3 masternodes (2 legacy + 1 DMN) - self.check_mn_enabled_count(3, 3) + # start with 4 masternodes (4 legacy + 1 DMN) + self.check_mn_enabled_count(4, 4) # add two more nodes to the network self.remoteDMN2 = self.nodes[self.remoteDMN2Pos] @@ -111,7 +88,7 @@ def run_test(self): self.sync_all() # check mn list from miner - txHashSet = set([self.mnOneCollateral.hash, self.mnTwoCollateral.hash, self.proRegTx1]) + txHashSet = set([self.mnOneCollateral.hash, self.mnTwoCollateral.hash, self.mnThreeCollateral.hash, self.proRegTx1]) self.check_mn_list(self.miner, txHashSet) # check status of masternodes @@ -119,13 +96,15 @@ def run_test(self): 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 Pays %s" % self.mn_addresses[self.mnTwoCollateral.hash]) + self.check_mns_status_legacy(self.remoteThree, self.mnThreeCollateral.hash) + self.log.info("MN3 active Pays %s" % self.mn_addresses[self.mnThreeCollateral.hash]) self.check_mns_status(self.remoteDMN1, self.proRegTx1) self.log.info("DMN1 active Pays %s" % self.mn_addresses[self.proRegTx1]) # Create another DMN, this time without funding the collateral. # ProTx references another transaction in the owner's wallet self.proRegTx2, self.dmn2Privkey = self.setupDMN( - self.ownerOne, + self.owner, self.miner, self.remoteDMN2Pos, "internal" @@ -133,7 +112,7 @@ def run_test(self): self.remoteDMN2.initmasternode(self.dmn2Privkey) # check list and status - self.check_mn_enabled_count(4, 4) # 2 legacy + 2 DMN + self.check_mn_enabled_count(5, 5) # 3 legacy + 2 DMN txHashSet.add(self.proRegTx2) self.check_mn_list(self.miner, txHashSet) self.check_mns_status(self.remoteDMN2, self.proRegTx2) @@ -153,7 +132,7 @@ def run_test(self): # Now create a DMN, reusing the collateral output of a legacy MN self.log.info("Creating a DMN reusing the collateral of a legacy MN...") self.proRegTx3, self.dmn3Privkey = self.setupDMN( - self.ownerOne, + self.owner, self.miner, self.remoteDMN3Pos, "external", @@ -166,7 +145,7 @@ def run_test(self): # The legacy masternode must no longer be in the list # and the DMN must have taken its place - self.check_mn_enabled_count(4, 4) # 1 legacy + 3 DMN + self.check_mn_enabled_count(5, 5) # 2 legacy + 3 DMN txHashSet.remove(self.mnOneCollateral.hash) txHashSet.add(self.proRegTx3) for node in self.nodes: @@ -177,11 +156,11 @@ def run_test(self): # 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) + self.controller_start_masternode(self.owner, self.masternodeOneAlias) self.send_3_pings() # the masternode list hasn't changed - self.check_mn_enabled_count(4, 4) + self.check_mn_enabled_count(5, 5) for node in self.nodes: self.check_mn_list(node, txHashSet) self.log.info("Masternode list correctly unchanged in all nodes.")