diff --git a/src/blockassembler.cpp b/src/blockassembler.cpp index 9df83ef0ce30..7d3e2fe3b49f 100644 --- a/src/blockassembler.cpp +++ b/src/blockassembler.cpp @@ -171,7 +171,8 @@ std::unique_ptr BlockAssembler::CreateNewBlock(const CScript& sc CWallet* pwallet, bool fProofOfStake, std::vector* availableCoins, - bool fNoMempoolTx) + bool fNoMempoolTx, + bool fTestValidity) { resetBlock(); @@ -242,7 +243,8 @@ std::unique_ptr BlockAssembler::CreateNewBlock(const CScript& sc if (chainActive.Tip() != pindexPrev) return nullptr; // new block came in, move on CValidationState state; - if (!TestBlockValidity(state, *pblock, pindexPrev, false, false, false)) { + if (fTestValidity && + !TestBlockValidity(state, *pblock, pindexPrev, false, false, false)) { throw std::runtime_error( strprintf("%s: TestBlockValidity failed: %s", __func__, FormatStateMessage(state))); } diff --git a/src/blockassembler.h b/src/blockassembler.h index d927feca2b76..a805a52bcbc2 100644 --- a/src/blockassembler.h +++ b/src/blockassembler.h @@ -69,7 +69,8 @@ class BlockAssembler CWallet* pwallet = nullptr, bool fProofOfStake = false, std::vector* availableCoins = nullptr, - bool fNoMempoolTx = false); + bool fNoMempoolTx = false, + bool fTestValidity = true); private: // utility functions diff --git a/src/budget/budgetmanager.cpp b/src/budget/budgetmanager.cpp index 5d6f513a69bd..f6092b4dc363 100644 --- a/src/budget/budgetmanager.cpp +++ b/src/budget/budgetmanager.cpp @@ -191,7 +191,7 @@ std::string CBudgetManager::GetFinalizedBudgetStatus(const uint256& nHash) const return retBadHashes + " -- " + retBadPayeeOrAmount; } -bool CBudgetManager::AddFinalizedBudget(CFinalizedBudget& finalizedBudget) +bool CBudgetManager::AddFinalizedBudget(CFinalizedBudget& finalizedBudget, CNode* pfrom) { AssertLockNotHeld(cs_budgets); // need to lock cs_main here (CheckCollateral) const uint256& nHash = finalizedBudget.GetHash(); @@ -222,6 +222,25 @@ bool CBudgetManager::AddFinalizedBudget(CFinalizedBudget& finalizedBudget) return false; } + // Compare budget payments with existent proposals, don't care on the order, just verify proposals existence. + std::vector vBudget = GetBudget(); + std::map mapWinningProposals; + for (const CBudgetProposal& p: vBudget) { mapWinningProposals.emplace(p.GetHash(), p); } + if (!finalizedBudget.CheckProposals(mapWinningProposals)) { + finalizedBudget.SetStrInvalid("Invalid proposals"); + LogPrint(BCLog::MNBUDGET,"%s: Budget finalization does not match with winning proposals\n", __func__); + // just for now (until v6), request proposals sync in case we are missing one of them. + if (pfrom) { + for (const auto& propId : finalizedBudget.GetProposalsHashes()) { + if (!g_budgetman.HaveProposal(propId)) { + pfrom->PushInventory(CInv(MSG_BUDGET_PROPOSAL, propId)); + } + } + } + return false; + } + + // Add budget finalization. SetBudgetProposalsStr(finalizedBudget); ForceAddFinalizedBudget(nHash, feeTxId, finalizedBudget); @@ -632,19 +651,19 @@ TrxValidationStatus CBudgetManager::IsTransactionValid(const CTransaction& txNew bool fThreshold = false; { LOCK(cs_budgets); - for (const auto& it: mapFinalizedBudgets) { - const CFinalizedBudget* pfb = &(it.second); - const int nVoteCount = pfb->GetVoteCount(); - LogPrint(BCLog::MNBUDGET,"%s: checking %s (%s): votes %d (threshold %d)\n", - __func__, pfb->GetName(), pfb->GetProposalsStr(), nVoteCount, nCountThreshold); - if (nVoteCount > nCountThreshold) { + // Get the finalized budget with the highest amount of votes.. + const CFinalizedBudget* highestVotesBudget = GetBudgetWithHighestVoteCount(nBlockHeight); + if (highestVotesBudget) { + // Need to surpass the threshold + if (highestVotesBudget->GetVoteCount() > nCountThreshold) { fThreshold = true; - if (pfb->IsTransactionValid(txNew, nBlockHash, nBlockHeight) == TrxValidationStatus::Valid) { + if (highestVotesBudget->IsTransactionValid(txNew, nBlockHash, nBlockHeight) == + TrxValidationStatus::Valid) { return TrxValidationStatus::Valid; } - // tx not valid. keep looking. - LogPrint(BCLog::MNBUDGET, "%s: ignoring budget. Out of range or tx not valid.\n", __func__); } + // tx not valid + LogPrint(BCLog::MNBUDGET, "%s: ignoring budget. Out of range or tx not valid.\n", __func__); } } @@ -1049,7 +1068,7 @@ bool CBudgetManager::ProcessProposalVote(CBudgetVote& vote, CNode* pfrom, CValid return true; } -int CBudgetManager::ProcessFinalizedBudget(CFinalizedBudget& finalbudget) +int CBudgetManager::ProcessFinalizedBudget(CFinalizedBudget& finalbudget, CNode* pfrom) { const uint256& nHash = finalbudget.GetHash(); @@ -1057,7 +1076,7 @@ int CBudgetManager::ProcessFinalizedBudget(CFinalizedBudget& finalbudget) masternodeSync.AddedBudgetItem(nHash); return 0; } - if (!AddFinalizedBudget(finalbudget)) { + if (!AddFinalizedBudget(finalbudget, pfrom)) { return 0; } finalbudget.Relay(); @@ -1195,7 +1214,7 @@ int CBudgetManager::ProcessMessageInner(CNode* pfrom, std::string& strCommand, C if (!finalbudget.ParseBroadcast(vRecv)) { return 20; } - return ProcessFinalizedBudget(finalbudget); + return ProcessFinalizedBudget(finalbudget, pfrom); } if (strCommand == NetMsgType::FINALBUDGETVOTE) { diff --git a/src/budget/budgetmanager.h b/src/budget/budgetmanager.h index 72202db8bfdc..b2dc8081c263 100644 --- a/src/budget/budgetmanager.h +++ b/src/budget/budgetmanager.h @@ -99,7 +99,7 @@ class CBudgetManager int ProcessBudgetVoteSync(const uint256& nProp, CNode* pfrom); int ProcessProposal(CBudgetProposal& proposal); - int ProcessFinalizedBudget(CFinalizedBudget& finalbudget); + int ProcessFinalizedBudget(CFinalizedBudget& finalbudget, CNode* pfrom); bool ProcessProposalVote(CBudgetVote& proposal, CNode* pfrom, CValidationState& state); bool ProcessFinalizedBudgetVote(CFinalizedBudgetVote& vote, CNode* pfrom, CValidationState& state); @@ -121,7 +121,7 @@ class CBudgetManager bool IsBudgetPaymentBlock(int nBlockHeight) const; bool IsBudgetPaymentBlock(int nBlockHeight, int& nCountThreshold) const; bool AddProposal(CBudgetProposal& budgetProposal); - bool AddFinalizedBudget(CFinalizedBudget& finalizedBudget); + bool AddFinalizedBudget(CFinalizedBudget& finalizedBudget, CNode* pfrom = nullptr); void ForceAddFinalizedBudget(const uint256& nHash, const uint256& feeTxId, const CFinalizedBudget& finalizedBudget); uint256 SubmitFinalBudget(); diff --git a/src/rpc/budget.cpp b/src/rpc/budget.cpp index 377f5ae710f6..c7757d655430 100644 --- a/src/rpc/budget.cpp +++ b/src/rpc/budget.cpp @@ -769,6 +769,93 @@ UniValue mnfinalbudgetsuggest(const JSONRPCRequest& request) return (budgetHash.IsNull()) ? NullUniValue : budgetHash.ToString(); } +UniValue createrawmnfinalbudget(const JSONRPCRequest& request) +{ + if (request.fHelp || request.params.size() > 4) + throw std::runtime_error( + "createrawmnfinalbudget\n" + "\nTry to submit the raw budget finalization\n" + "returns the budget hash if it was broadcasted sucessfully" + "\nArguments:\n" + "1. \"budgetname\" (string, required) finalization name\n" + "2. \"blockstart\" (numeric, required) superblock height\n" + "3. \"proposals\" (string, required) A json array of json objects\n" + " [\n" + " {\n" + " \"proposalid\":\"id\", (string, required) The proposal id\n" + " \"payee\":n, (hex, required) The payee script\n" + " \"amount\":n (numeric, optional) The payee amount\n" + " }\n" + " ,...\n" + " ]\n" + "4. \"feetxid\" (string, optional) the transaction fee hash\n" + "" + "\nResult:\n" + "{\n" + "\"result\" (string) Budget suggest broadcast or error\n" + "\"id\" (string) id of the fee tx or the finalized budget\n" + "}\n" + ); // future: add examples. + + if (!Params().IsRegTestNet()) { + throw JSONRPCError(RPC_MISC_ERROR, "command available only for RegTest network"); + } + + // future: add inputs error checking.. + std::string budName = request.params[0].get_str(); + int nBlockStart = request.params[1].get_int(); + std::vector vecTxBudgetPayments; + UniValue budgetVec = request.params[2].get_array(); + for (unsigned int idx = 0; idx < budgetVec.size(); idx++) { + const UniValue& prop = budgetVec[idx].get_obj(); + uint256 propId = ParseHashO(prop, "proposalid"); + std::vector scriptData(ParseHexO(prop, "payee")); + CScript payee = CScript(scriptData.begin(), scriptData.end()); + CAmount amount = AmountFromValue(find_value(prop, "amount")); + vecTxBudgetPayments.emplace_back(propId, payee, amount); + } + + Optional txFeeId = nullopt; + if (request.params.size() > 3) { + txFeeId = ParseHashV(request.params[3], "parameter 4"); + } + + if (!txFeeId) { + CFinalizedBudget tempBudget(budName, nBlockStart, vecTxBudgetPayments, UINT256_ZERO); + const uint256& budgetHash = tempBudget.GetHash(); + + // create fee tx + CTransactionRef wtx; + CReserveKey keyChange(vpwallets[0]); + if (!vpwallets[0]->CreateBudgetFeeTX(wtx, budgetHash, keyChange, true)) { + throw std::runtime_error("Can't make collateral transaction"); + } + // Send the tx to the network + const CWallet::CommitResult& res = vpwallets[0]->CommitTransaction(wtx, keyChange, g_connman.get()); + UniValue ret(UniValue::VOBJ); + if (res.status == CWallet::CommitStatus::OK) { + ret.pushKV("result", "tx_fee_sent"); + ret.pushKV("id", wtx->GetHash().ToString()); + } else { + ret.pushKV("result", "error"); + } + return ret; + } + + UniValue ret(UniValue::VOBJ); + // Collateral tx already exists, see if it's mature enough. + CFinalizedBudget fb(budName, nBlockStart, vecTxBudgetPayments, *txFeeId); + if (g_budgetman.AddFinalizedBudget(fb)) { + fb.Relay(); + ret.pushKV("result", "fin_budget_sent"); + ret.pushKV("id", fb.GetHash().ToString()); + } else { + // future: add proper error + ret.pushKV("result", "error"); + } + return ret; +} + UniValue mnfinalbudget(const JSONRPCRequest& request) { std::string strCommand; @@ -879,6 +966,7 @@ static const CRPCCommand commands[] = /* Not shown in help */ { "hidden", "mnfinalbudgetsuggest", &mnfinalbudgetsuggest, true, {} }, + { "hidden", "createrawmnfinalbudget", &createrawmnfinalbudget, true, {"budgetname", "blockstart", "proposals", "feetxid"} }, }; diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 157a2adf8fc1..d294b07ad044 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -37,6 +37,8 @@ static const CRPCConvertParam vRPCConvertParams[] = { { "createrawtransaction", 0, "inputs" }, { "createrawtransaction", 1, "outputs" }, { "createrawtransaction", 2, "locktime" }, + {"createrawmnfinalbudget", 1, "blockstart"}, + {"createrawmnfinalbudget", 2, "proposals"}, { "delegatestake", 1, "amount" }, { "delegatestake", 3, "ext_owner" }, { "delegatestake", 4, "include_delegated" }, diff --git a/src/test/budget_tests.cpp b/src/test/budget_tests.cpp index 759a57bbb07c..efaea9bb788e 100644 --- a/src/test/budget_tests.cpp +++ b/src/test/budget_tests.cpp @@ -4,11 +4,11 @@ #include "test_pivx.h" -#include "test/util/blocksutil.h" #include "budget/budgetmanager.h" #include "masternode-payments.h" #include "masternode-sync.h" #include "spork.h" +#include "test/util/blocksutil.h" #include "tinyformat.h" #include "utilmoneystr.h" #include "validation.h" @@ -127,40 +127,190 @@ BOOST_FIXTURE_TEST_CASE(block_value, TestnetSetup) BOOST_CHECK_EQUAL(nBudgetAmtRet, 0); } + +/** + * 1) Create two proposals and two budget finalizations with a different proposal payment order: + BudA pays propA and propB, BudB pays propB and propA. + 2) Vote both finalization budgets, adding more votes to budA (so it becomes the most voted one). + */ +void forceAddFakeProposals(const CTxOut& payee1, const CTxOut& payee2) +{ + const CTxIn mnVin(GetRandHash(), 0); + const uint256& propHash = GetRandHash(), finTxId = GetRandHash(); + const CTxBudgetPayment txBudgetPayment(propHash, payee1.scriptPubKey, payee1.nValue); + + const CTxIn mnVin2(GetRandHash(), 0); + const uint256& propHash2 = GetRandHash(), finTxId2 = GetRandHash(); + const CTxBudgetPayment txBudgetPayment2(propHash2, payee2.scriptPubKey, payee2.nValue); + + // Create first finalization + CFinalizedBudget fin("main (test)", 144, {txBudgetPayment, txBudgetPayment2}, finTxId); + const CFinalizedBudgetVote fvote(mnVin, fin.GetHash()); + const CFinalizedBudgetVote fvote1_a({GetRandHash(), 0}, fin.GetHash()); + const CFinalizedBudgetVote fvote1_b({GetRandHash(), 0}, fin.GetHash()); + std::string strError; + BOOST_CHECK(fin.AddOrUpdateVote(fvote, strError)); + BOOST_CHECK(fin.AddOrUpdateVote(fvote1_a, strError)); + BOOST_CHECK(fin.AddOrUpdateVote(fvote1_b, strError)); + g_budgetman.ForceAddFinalizedBudget(fin.GetHash(), fin.GetFeeTXHash(), fin); + + // Create second finalization + CFinalizedBudget fin2("main2 (test)", 144, {txBudgetPayment2, txBudgetPayment}, finTxId2); + const CFinalizedBudgetVote fvote2(mnVin2, fin2.GetHash()); + const CFinalizedBudgetVote fvote2_a({GetRandHash(), 0}, fin2.GetHash()); + BOOST_CHECK(fin2.AddOrUpdateVote(fvote2, strError)); + BOOST_CHECK(fin2.AddOrUpdateVote(fvote2_a, strError)); + g_budgetman.ForceAddFinalizedBudget(fin2.GetHash(), fin2.GetFeeTXHash(), fin2); +} + BOOST_FIXTURE_TEST_CASE(budget_blocks_payee_test, TestChain100Setup) { // Regtest superblock is every 144 blocks. for (int i=0; i<43; i++) CreateAndProcessBlock({}, coinbaseKey); enableMnSyncAndSuperblocksPayment(); + g_budgetman.Clear(); BOOST_CHECK_EQUAL(WITH_LOCK(cs_main, return chainActive.Height();), 143); + BOOST_ASSERT(g_budgetman.GetFinalizedBudgets().size() == 0); // Now we are at the superblock height, let's add a proposal to pay. - const CTxIn mnVin(GetRandHash(), 0); - const CScript payee = GetScriptForDestination(CKeyID(uint160(ParseHex("816115944e077fe7c803cfa57f29b36bf87c1d35")))); - const CAmount propAmt = 100 * COIN; - const uint256& propHash = GetRandHash(), finTxId = GetRandHash(); - const CTxBudgetPayment txBudgetPayment(propHash, payee, propAmt); - CFinalizedBudget fin("main (test)", 144, {txBudgetPayment}, finTxId); - const CFinalizedBudgetVote fvote(mnVin, fin.GetHash()); - std::string strError; - BOOST_CHECK(fin.AddOrUpdateVote(fvote, strError)); - g_budgetman.ForceAddFinalizedBudget(fin.GetHash(), fin.GetFeeTXHash(), fin); + const CScript payee1 = GetScriptForDestination(CKeyID(uint160(ParseHex("816115944e077fe7c803cfa57f29b36bf87c1d35")))); + const CAmount propAmt1 = 100 * COIN; + const CScript payee2 = GetScriptForDestination(CKeyID(uint160(ParseHex("8d5b4f83212214d6ef693e02e6d71969fddad976")))); + const CAmount propAmt2 = propAmt1; + forceAddFakeProposals({propAmt1, payee1}, {propAmt2, payee2}); CBlock block = CreateBlock({}, coinbaseKey); // Check payee validity: CTxOut payeeOut = block.vtx[0]->vout[1]; - BOOST_CHECK_EQUAL(payeeOut.nValue, propAmt); - BOOST_CHECK(payeeOut.scriptPubKey == payee); + BOOST_CHECK_EQUAL(payeeOut.nValue, propAmt1); + BOOST_CHECK(payeeOut.scriptPubKey == payee1); + + // Good tx + CMutableTransaction goodMtx(*block.vtx[0]); // Modify payee CMutableTransaction mtx(*block.vtx[0]); mtx.vout[1].scriptPubKey = GetScriptForDestination(CKeyID(uint160(ParseHex("8c988f1a4a4de2161e0f50aac7f17e7f9555caa4")))); block.vtx[0] = MakeTransactionRef(mtx); std::shared_ptr pblock = FinalizeBlock(std::make_shared(block)); - BOOST_CHECK(block.vtx[0]->vout[1].scriptPubKey != payee); + BOOST_CHECK(block.vtx[0]->vout[1].scriptPubKey != payee1); // Verify block rejection reason. ProcessBlockAndCheckRejectionReason(pblock, "bad-cb-payee", 143); + + // Try to overmint, valid payee --> bad amount. + mtx = goodMtx; // reset + mtx.vout[1].nValue *= 2; // invalid amount + block.vtx[0] = MakeTransactionRef(mtx); + pblock = FinalizeBlock(std::make_shared(block)); + BOOST_CHECK(block.vtx[0]->vout[1].scriptPubKey == payee1); + BOOST_CHECK(block.vtx[0]->vout[1].nValue == payeeOut.nValue * 2); + ProcessBlockAndCheckRejectionReason(pblock, "bad-blk-amount", 143); + + // Try to send less to a valid payee --> bad amount. + mtx = goodMtx; // reset + mtx.vout[1].nValue /= 2; + block.vtx[0] = MakeTransactionRef(mtx); + pblock = FinalizeBlock(std::make_shared(block)); + BOOST_CHECK(block.vtx[0]->vout[1].scriptPubKey == payee1); + BOOST_CHECK(block.vtx[0]->vout[1].nValue == payeeOut.nValue / 2); + ProcessBlockAndCheckRejectionReason(pblock, "bad-cb-payee", 143); + + // Context, this has: + // 1) Two proposals and two budget finalizations with a different proposal payment order (read `forceAddFakeProposals()` description): + // BudA pays propA and propB, BudB pays propB and propA. + // 2) Voted both budgets, adding more votes to budA (so it becomes the most voted one). + // 3) Now: in the superblock, pay to budB order (the less voted finalization) --> which will fail. + + // Try to pay proposals in different order + mtx = goodMtx; // reset + std::vector vecFin = g_budgetman.GetFinalizedBudgets(); + CFinalizedBudget* secondFin{nullptr}; + for (auto fin : vecFin) { + if (!secondFin || fin->GetVoteCount() < secondFin->GetVoteCount()) { + secondFin = fin; + } + } + secondFin->GetPayeeAndAmount(144, mtx.vout[1].scriptPubKey, mtx.vout[1].nValue); + BOOST_CHECK(mtx.vout[1].scriptPubKey != goodMtx.vout[1].scriptPubKey); + BOOST_CHECK(mtx.vout[1].nValue == goodMtx.vout[1].nValue); + block.vtx[0] = MakeTransactionRef(mtx); + pblock = FinalizeBlock(std::make_shared(block)); + ProcessBlockAndCheckRejectionReason(pblock, "bad-cb-payee", 143); + + // Now create the good block + block.vtx[0] = MakeTransactionRef(goodMtx); + pblock = FinalizeBlock(std::make_shared(block)); + CValidationState state; + ProcessNewBlock(state, pblock, nullptr); + BOOST_CHECK_EQUAL(WITH_LOCK(cs_main, return chainActive.Tip()->GetBlockHash();), pblock->GetHash()); +} + +BOOST_FIXTURE_TEST_CASE(budget_blocks_reorg_test, TestChain100Setup) +{ + // Regtest superblock is every 144 blocks. + for (int i=0; i<43; i++) CreateAndProcessBlock({}, coinbaseKey); + enableMnSyncAndSuperblocksPayment(); + BOOST_CHECK_EQUAL(WITH_LOCK(cs_main, return chainActive.Height();), 143); + + // Now we are at the superblock height, let's add a proposal to pay. + const CScript payee = GetScriptForDestination(CKeyID(uint160(ParseHex("816115944e077fe7c803cfa57f29b36bf87c1d35")))); + const CAmount propAmt = 100 * COIN; + const CScript payee2 = GetScriptForDestination(CKeyID(uint160(ParseHex("816115944e077fe7c803cfa57f29b36bf87c1d35")))); + const CAmount propAmt2 = propAmt * 2; + forceAddFakeProposals({propAmt, payee}, {propAmt2, payee2}); + + // This will: + // 1) Create a proposal to be paid at block 144 (first superblock). + // 1) create blocksA and blockB at block 144 (paying for the proposal). + // 2) Process and connect blockA. + // 3) Create blockC on top of BlockA and blockD on top of blockB. At height 145. + // 4) Process and connect blockC. + // 5) Now force the reorg: + // a) Process blockB and blockD. + // b) Create and process blockE on top of blockD. + // 6) Verify that tip is at blockE. + + CScript forkCoinbaseScript = GetScriptForDestination(CKeyID(uint160(ParseHex("8c988f1a4a4de2161e0f50aac7f17e7f9555caa4")))); + CBlock blockA = CreateBlock({}, coinbaseKey, false); + CBlock blockB = CreateBlock({}, forkCoinbaseScript, false); + BOOST_CHECK(blockA.GetHash() != blockB.GetHash()); + // Check blocks payee validity: + CTxOut payeeOut = blockA.vtx[0]->vout[1]; + BOOST_CHECK_EQUAL(payeeOut.nValue, propAmt); + BOOST_CHECK(payeeOut.scriptPubKey == payee); + payeeOut = blockB.vtx[0]->vout[1]; + BOOST_CHECK_EQUAL(payeeOut.nValue, propAmt); + BOOST_CHECK(payeeOut.scriptPubKey == payee); + + // Now let's process BlockA: + CValidationState stateA; + auto pblockA = std::make_shared(blockA); + ProcessNewBlock(stateA, pblockA, nullptr); + BOOST_CHECK(WITH_LOCK(cs_main, return chainActive.Tip()->GetBlockHash()) == blockA.GetHash()); + + // Now let's create blockC on top of BlockA, blockD on top of blockB + // and process blockC to expand the chain. + CBlock blockC = CreateBlock({}, coinbaseKey, false); + BOOST_CHECK(blockC.hashPrevBlock == blockA.GetHash()); + CBlock blockD = CreateBlock({}, forkCoinbaseScript, false); + + // Process and connect blockC + ProcessNewBlock(stateA, std::make_shared(blockC), nullptr); + BOOST_CHECK(WITH_LOCK(cs_main, return chainActive.Tip()->GetBlockHash()) == blockC.GetHash()); + + // Now let's process the secondary chain + blockD.hashPrevBlock = blockB.GetHash(); + std::shared_ptr pblockD = FinalizeBlock(std::make_shared(blockD)); + + CValidationState stateB; + ProcessNewBlock(stateB, std::make_shared(blockB), nullptr); + ProcessNewBlock(stateB, pblockD, nullptr); + CBlock blockE = CreateBlock({}, forkCoinbaseScript, false); + blockE.hashPrevBlock = pblockD->GetHash(); + std::shared_ptr pblockE = FinalizeBlock(std::make_shared(blockE)); + ProcessNewBlock(stateB, pblockE, nullptr); + BOOST_CHECK(WITH_LOCK(cs_main, return chainActive.Tip()->GetBlockHash()) == pblockE->GetHash()); } static CScript GetRandomP2PKH() diff --git a/src/test/test_pivx.cpp b/src/test/test_pivx.cpp index b56d27c6757f..b4981a94808d 100644 --- a/src/test/test_pivx.cpp +++ b/src/test/test_pivx.cpp @@ -164,10 +164,18 @@ CBlock TestChainSetup::CreateAndProcessBlock(const std::vector& txns, const CScript& scriptPubKey, bool fNoMempoolTx) +CBlock TestChainSetup::CreateBlock(const std::vector& txns, + const CScript& scriptPubKey, + bool fNoMempoolTx, + bool fTestBlockValidity) { std::unique_ptr pblocktemplate = BlockAssembler( - Params(), DEFAULT_PRINTPRIORITY).CreateNewBlock(scriptPubKey, nullptr, false, nullptr, fNoMempoolTx); + Params(), DEFAULT_PRINTPRIORITY).CreateNewBlock(scriptPubKey, + nullptr, // wallet + false, // fProofOfStake + nullptr, // availableCoins + fNoMempoolTx, + fTestBlockValidity); std::shared_ptr pblock = std::make_shared(pblocktemplate->block); // Add passed-in txns: @@ -185,10 +193,11 @@ CBlock TestChainSetup::CreateBlock(const std::vector& txns, return *pblock; } -CBlock TestChainSetup::CreateBlock(const std::vector& txns, const CKey& scriptKey) +CBlock TestChainSetup::CreateBlock(const std::vector& txns, const CKey& scriptKey, + bool fTestBlockValidity) { CScript scriptPubKey = CScript() << ToByteVector(scriptKey.GetPubKey()) << OP_CHECKSIG; - return CreateBlock(txns, scriptPubKey); + return CreateBlock(txns, scriptPubKey, fTestBlockValidity); } std::shared_ptr FinalizeBlock(std::shared_ptr pblock) diff --git a/src/test/test_pivx.h b/src/test/test_pivx.h index b93e0fb8f39e..03d1f8511a0f 100644 --- a/src/test/test_pivx.h +++ b/src/test/test_pivx.h @@ -93,8 +93,11 @@ struct TestChainSetup : public TestingSetup // Include given transactions, and, if fNoMempoolTx=true, remove transactions coming from the mempool. CBlock CreateAndProcessBlock(const std::vector& txns, const CScript& scriptPubKey, bool fNoMempoolTx = true); CBlock CreateAndProcessBlock(const std::vector& txns, const CKey& scriptKey); - CBlock CreateBlock(const std::vector& txns, const CScript& scriptPubKey, bool fNoMempoolTx = true); - CBlock CreateBlock(const std::vector& txns, const CKey& scriptKey); + CBlock CreateBlock(const std::vector& txns, + const CScript& scriptPubKey, + bool fNoMempoolTx = true, + bool fTestBlockValidity = true); + CBlock CreateBlock(const std::vector& txns, const CKey& scriptKey, bool fTestBlockValidity = true); std::vector coinbaseTxns; // For convenience, coinbase transactions CKey coinbaseKey; // private/public key needed to spend coinbase transactions diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index fb1491f04ec1..571046d4a122 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -151,6 +151,7 @@ 'tiertwo_masternode_activation.py', # ~ 352 sec 'tiertwo_masternode_ping.py', # ~ 293 sec 'tiertwo_reorg_mempool.py', # ~ 97 sec + 'tiertwo_governance_invalid_budget.py', ] SAPLING_SCRIPTS = [ diff --git a/test/functional/tiertwo_governance_invalid_budget.py b/test/functional/tiertwo_governance_invalid_budget.py new file mode 100755 index 000000000000..fb653153f496 --- /dev/null +++ b/test/functional/tiertwo_governance_invalid_budget.py @@ -0,0 +1,141 @@ +#!/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, + p2p_port, +) + +import os +import time + +class GovernanceInvalidBudgetTest(PivxTestFramework): + + def set_test_params(self): + self.setup_clean_chain = True + # 3 nodes: + # - 1 miner/mncontroller + # - 2 remote mns + self.num_nodes = 3 + self.extra_args = [["-sporkkey=932HEevBSujW2ud7RfB1YF91AFygbBRQj3de3LyaCRqNzKKgWXi"], + [], + [], + ] + self.enable_mocktime() + + self.minerAPos = 0 + self.remoteOnePos = 1 + self.remoteTwoPos = 2 + + self.masternodeOneAlias = "mnOne" + self.masternodeTwoAlias = "mntwo" + + self.mnOnePrivkey = "9247iC59poZmqBYt9iDh9wDam6v9S1rW5XekjLGyPnDhrDkP4AK" + self.mnTwoPrivkey = "92Hkebp3RHdDidGZ7ARgS4orxJAGyFUPDXNqtsYsiwho1HGVRbF" + + def run_test(self): + self.minerA = self.nodes[self.minerAPos] # also controller of mn1 and mn2 + self.mn1 = self.nodes[self.remoteOnePos] + self.mn2 = self.nodes[self.remoteTwoPos] + self.setupContext() + + # Create a valid proposal and vote on it + next_superblock = self.minerA.getnextsuperblock() + payee = self.minerA.getnewaddress() + self.log.info("Creating a proposal to be paid at block %d" % next_superblock) + proposalFeeTxId = self.minerA.preparebudget("test1", "https://test1.org", 2, + next_superblock, payee, 300) + self.stake_and_ping(self.minerAPos, 3, [self.mn1, self.mn2]) + proposalHash = self.minerA.submitbudget("test1", "https://test1.org", 2, + next_superblock, payee, 300, proposalFeeTxId) + time.sleep(1) + self.stake_and_ping(self.minerAPos, 7, [self.mn1, self.mn2]) + self.log.info("Vote for the proposal and check projection...") + self.minerA.mnbudgetvote("alias", proposalHash, "yes", self.masternodeOneAlias) + self.minerA.mnbudgetvote("alias", proposalHash, "yes", self.masternodeTwoAlias) + time.sleep(1) + self.stake_and_ping(self.minerAPos, 1, [self.mn1, self.mn2]) + projection = self.mn1.getbudgetprojection()[0] + assert_equal(projection["Name"], "test1") + assert_equal(projection["Hash"], proposalHash) + assert_equal(projection["Yeas"], 2) + + # Try to create an invalid finalized budget, paying to an nonexistent proposal + self.log.info("Creating invalid budget finalization...") + self.stake_and_ping(self.minerAPos, 5, [self.mn1, self.mn2]) + + budgetname = "invalid finalization" + blockstart = self.minerA.getnextsuperblock() + proposals = [] + badPropId = "aa0061d705de36385c37701e7632408bd9d2876626b1299a17f7dc818c0ad285" + badPropPayee = "8c988f1a4a4de2161e0f50aac7f17e7f9555caa4" + badPropAmount = 500 + proposals.append({"proposalid": badPropId, "payee": badPropPayee, "amount": badPropAmount}) + res = self.minerA.createrawmnfinalbudget(budgetname, blockstart, proposals) + assert(res["result"] == "tx_fee_sent") + feeBudgetId = res["id"] + time.sleep(1) + self.stake_and_ping(self.minerAPos, 4, [self.mn1, self.mn2]) + res = self.minerA.createrawmnfinalbudget(budgetname, blockstart, proposals, feeBudgetId) + assert(res["result"] == "error") # not accepted + + self.log.info("Good, invalid budget not accepted.") + + 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 setupContext(self): + # First mine 250 PoW blocks (250 with minerA) + self.log.info("Generating 259 blocks...") + for _ in range(250): + 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.mnOneCollateral = self.setupMasternode(self.minerA, self.minerA, self.masternodeOneAlias, + ownerdir, self.remoteOnePos, self.mnOnePrivkey) + self.mnTwoCollateral = self.setupMasternode(self.minerA, self.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) + self.mn1.initmasternode(self.mnOnePrivkey, "127.0.0.1:"+str(remoteOnePort)) + self.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(self.minerA, self.masternodeOneAlias) + self.controller_start_masternode(self.minerA, self.masternodeTwoAlias) + self.wait_until_mn_preenabled(self.mnOneCollateral.hash, 40) + self.wait_until_mn_preenabled(self.mnOneCollateral.hash, 40) + self.send_3_pings([self.mn1, self.mn2]) + self.wait_until_mn_enabled(self.mnOneCollateral.hash, 120, [self.mn1, self.mn2]) + self.wait_until_mn_enabled(self.mnOneCollateral.hash, 120, [self.mn1, self.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") + + +if __name__ == '__main__': + GovernanceInvalidBudgetTest().main() \ No newline at end of file