diff --git a/src/activemasternode.cpp b/src/activemasternode.cpp index ed1539fc94c5c..c152de8db0fe4 100644 --- a/src/activemasternode.cpp +++ b/src/activemasternode.cpp @@ -484,6 +484,7 @@ void CActiveMasternode::GetKeys(CKey& _privKeyMasternode, CPubKey& _pubKeyMaster _pubKeyMasternode = pubKeyMasternode; } +// TODO: Remove after full transition to DMNs bool GetActiveDMNKeys(CBLSSecretKey& key, CTxIn& vin) { if (activeMasternodeManager == nullptr) { @@ -498,6 +499,7 @@ bool GetActiveDMNKeys(CBLSSecretKey& key, CTxIn& vin) return true; } +// TODO: Remove after full transition to DMNs bool GetActiveMasternodeKeys(CTxIn& vin, Optional& key, CBLSSecretKey& blsKey) { if (activeMasternodeManager != nullptr) { @@ -520,3 +522,44 @@ bool GetActiveMasternodeKeys(CTxIn& vin, Optional& key, CBLSSecretKey& bls blsKey.Reset(); return true; } + +// Difference with the other function: here the vin returned is the one used for voting, i.e the outpoint is: (proRegTxHash; 0) +// WHICH IS OF COURSE (IN GENERAL) DIFFERENT FROM THE OUTPOINT OF THE COLLATERAL +bool GetActiveVotingDMNKeys(CBLSSecretKey& key, CTxIn& vin) +{ + if (activeMasternodeManager == nullptr) { + return error("%s: Active Masternode not initialized", __func__); + } + CDeterministicMNCPtr dmn; + auto res = activeMasternodeManager->GetOperatorKey(key, dmn); + if (!res) { + return error("%s: %s", __func__, res.getError()); + } + vin = CTxIn(COutPoint(dmn->proTxHash, 0)); + return true; +} + +// Difference with the other function: here the vin returned is the one used for voting, i.e the outpoint is: (proRegTxHash; 0) +// WHICH IS OF COURSE (IN GENERAL) DIFFERENT FROM THE OUTPOINT OF THE COLLATERAL +bool GetActiveMasternodeVotingKeys(CTxIn& vin, Optional& key, CBLSSecretKey& blsKey) +{ + if (activeMasternodeManager != nullptr) { + // deterministic mn + key = nullopt; + return GetActiveVotingDMNKeys(blsKey, vin); + } + // legacy mn + if (activeMasternode.vin == nullopt) { + return error("%s: Active Masternode not initialized", __func__); + } + if (activeMasternode.GetStatus() != ACTIVE_MASTERNODE_STARTED) { + return error("%s: MN not started (%s)", __func__, activeMasternode.GetStatusMessage()); + } + vin = *activeMasternode.vin; + CKey sk; + CPubKey pk; + activeMasternode.GetKeys(sk, pk); + key = Optional(sk); + blsKey.Reset(); + return true; +} diff --git a/src/activemasternode.h b/src/activemasternode.h index f6c84cc92e811..2bf351b192c2a 100644 --- a/src/activemasternode.h +++ b/src/activemasternode.h @@ -112,9 +112,16 @@ class CActiveMasternode void GetKeys(CKey& privKeyMasternode, CPubKey& pubKeyMasternode) const; }; -// Compatibility code: get vin and keys for either legacy or deterministic masternode +// Compatibility code: get vin OF THE COLLATERAL and keys for either legacy or deterministic masternode +// TODO: Remove after full transition to DMNs bool GetActiveMasternodeKeys(CTxIn& vin, Optional& key, CBLSSecretKey& blsKey); -// Get active masternode BLS operator keys for DMN +// Get active masternode BLS operator keys for DMN and vin OF THE COLLATERAL +// TODO: Remove after full transition to DMNs bool GetActiveDMNKeys(CBLSSecretKey& key, CTxIn& vin); +// Compatibility code: get vin FOR VOTING and keys for either legacy or deterministic masternode +bool GetActiveMasternodeVotingKeys(CTxIn& vin, Optional& key, CBLSSecretKey& blsKey); +// Get active masternode BLS operator keys for DMN and vin FOR VOTING +bool GetActiveVotingDMNKeys(CBLSSecretKey& key, CTxIn& vin); + #endif diff --git a/src/budget/budgetmanager.cpp b/src/budget/budgetmanager.cpp index 491ca5839aa99..a1db63156b5bb 100644 --- a/src/budget/budgetmanager.cpp +++ b/src/budget/budgetmanager.cpp @@ -572,7 +572,7 @@ void CBudgetManager::VoteOnFinalizedBudgets() CTxIn mnVin; Optional mnKey{nullopt}; CBLSSecretKey blsKey; - if (!GetActiveMasternodeKeys(mnVin, mnKey, blsKey)) { + if (!GetActiveMasternodeVotingKeys(mnVin, mnKey, blsKey)) { return; } @@ -885,8 +885,8 @@ void CBudgetManager::RemoveStaleVotesOnProposal(CBudgetProposal* prop) auto it = prop->mapVotes.begin(); while (it != prop->mapVotes.end()) { auto mnList = deterministicMNManager->GetListAtChainTip(); - auto dmn = mnList.GetMNByCollateral(it->first); - if (dmn) { + auto dmn = mnList.GetMN(it->first.hash); + if (dmn && it->first.n == 0) { (*it).second.SetValid(!dmn->IsPoSeBanned()); } else { // -- Legacy System (!TODO: remove after enforcement) -- @@ -909,8 +909,8 @@ void CBudgetManager::RemoveStaleVotesOnFinalBudget(CFinalizedBudget* fbud) auto it = fbud->mapVotes.begin(); while (it != fbud->mapVotes.end()) { auto mnList = deterministicMNManager->GetListAtChainTip(); - auto dmn = mnList.GetMNByCollateral(it->first); - if (dmn) { + auto dmn = mnList.GetMN(it->first.hash); + if (dmn && it->first.n == 0) { (*it).second.SetValid(!dmn->IsPoSeBanned()); } else { // -- Legacy System (!TODO: remove after enforcement) -- @@ -1101,10 +1101,16 @@ bool CBudgetManager::ProcessProposalVote(CBudgetVote& vote, CNode* pfrom, CValid // See if this vote was signed with a deterministic masternode auto mnList = deterministicMNManager->GetListAtChainTip(); - auto dmn = mnList.GetMNByCollateral(voteVin.prevout); + auto dmn = mnList.GetMN(voteVin.prevout.hash); if (dmn) { const std::string& mn_protx_id = dmn->proTxHash.ToString(); + if (voteVin.prevout.n != 0) { + err = strprintf("masternode (%s) not valid prevout.n (0 != %d)", mn_protx_id, voteVin.prevout.n); + // TODO: ban the node in this case? + return state.DoS(0, false, REJECT_INVALID, "bad-prevout", false, err); + } + if (dmn->IsPoSeBanned()) { err = strprintf("masternode (%s) not valid or PoSe banned", mn_protx_id); return state.DoS(0, false, REJECT_INVALID, "bad-mvote", false, err); @@ -1210,10 +1216,16 @@ bool CBudgetManager::ProcessFinalizedBudgetVote(CFinalizedBudgetVote& vote, CNod // See if this vote was signed with a deterministic masternode auto mnList = deterministicMNManager->GetListAtChainTip(); - auto dmn = mnList.GetMNByCollateral(voteVin.prevout); + auto dmn = mnList.GetMN(voteVin.prevout.hash); if (dmn) { const std::string& mn_protx_id = dmn->proTxHash.ToString(); + if (voteVin.prevout.n != 0) { + err = strprintf("masternode (%s) not valid prevout.n (0 != %d)", mn_protx_id, voteVin.prevout.n); + // TODO: ban the node in this case? + return state.DoS(0, false, REJECT_INVALID, "bad-prevout", false, err); + } + if (dmn->IsPoSeBanned()) { err = strprintf("masternode (%s) not valid or PoSe banned", mn_protx_id); return state.DoS(0, false, REJECT_INVALID, "bad-fbvote", false, err); diff --git a/src/budget/budgetutil.cpp b/src/budget/budgetutil.cpp index e7281f5706ecf..03424454ff0f2 100644 --- a/src/budget/budgetutil.cpp +++ b/src/budget/budgetutil.cpp @@ -40,20 +40,18 @@ static UniValue packVoteReturnValue(const UniValue& details, int success, int fa struct MnKeyData { std::string mnAlias; - const COutPoint* collateralOut; + const COutPoint collateralOut; MnKeyData() = delete; - MnKeyData(const std::string& _mnAlias, const COutPoint* _collateralOut, const CKey& _key): - mnAlias(_mnAlias), - collateralOut(_collateralOut), - key(_key), - use_bls(false) + MnKeyData(const std::string& _mnAlias, const COutPoint _collateralOut, const CKey& _key) : mnAlias(_mnAlias), + collateralOut(_collateralOut), + key(_key), + use_bls(false) {} - MnKeyData(const std::string& _mnAlias, const COutPoint* _collateralOut, const CBLSSecretKey& _key): - mnAlias(_mnAlias), - collateralOut(_collateralOut), - blsKey(_key), - use_bls(true) + MnKeyData(const std::string& _mnAlias, const COutPoint _collateralOut, const CBLSSecretKey& _key) : mnAlias(_mnAlias), + collateralOut(_collateralOut), + blsKey(_key), + use_bls(true) {} bool Sign(CSignedMessage* msg) const @@ -75,7 +73,7 @@ static UniValue voteProposal(const uint256& propHash, const CBudgetVote::VoteDir { int success = 0; for (const auto& k : mnKeys) { - CBudgetVote vote(CTxIn(*k.collateralOut), propHash, nVote); + CBudgetVote vote(CTxIn(k.collateralOut), propHash, nVote); if (!k.Sign(&vote)) { resultsObj.push_back(packErrorRetStatus(k.mnAlias, "Failure to sign.")); failed++; @@ -99,7 +97,7 @@ static UniValue voteFinalBudget(const uint256& budgetHash, { int success = 0; for (const auto& k : mnKeys) { - CFinalizedBudgetVote vote(CTxIn(*k.collateralOut), budgetHash); + CFinalizedBudgetVote vote(CTxIn(k.collateralOut), budgetHash); if (!k.Sign(&vote)) { resultsObj.push_back(packErrorRetStatus(k.mnAlias, "Failure to sign.")); failed++; @@ -138,7 +136,7 @@ static mnKeyList getMNKeys(const Optional& mnAliasFilter, failed++; continue; } - mnKeys.emplace_back(mnAlias, &pmn->vin.prevout, mnKey); + mnKeys.emplace_back(mnAlias, pmn->vin.prevout, mnKey); } return mnKeys; } @@ -162,7 +160,7 @@ static mnKeyList getMNKeysForActiveMasternode(UniValue& resultsObj) return mnKeyList(); } - return {MnKeyData("local", &pmn->vin.prevout, mnKey)}; + return {MnKeyData("local", pmn->vin.prevout, mnKey)}; } // Deterministic masternodes @@ -198,7 +196,7 @@ static mnKeyList getDMNVotingKeys(CWallet* const pwallet, const Optionalcs_wallet); CKey mnKey; if (pwallet->GetKey(dmn->pdmnState->keyIDVoting, mnKey)) { - mnKeys.emplace_back(dmn->proTxHash.ToString(), &dmn->collateralOutpoint, mnKey); + mnKeys.emplace_back(dmn->proTxHash.ToString(), COutPoint(dmn->proTxHash, 0), mnKey); } else if (filtered) { resultsObj.push_back(packErrorRetStatus(*mnAliasFilter, strprintf( "Private key for voting address %s not known by this wallet", @@ -226,7 +224,7 @@ static mnKeyList getDMNKeysForActiveMasternode(UniValue& resultsObj) return {}; } - return {MnKeyData("local", &dmn->collateralOutpoint, sk)}; + return {MnKeyData("local", COutPoint(dmn->proTxHash, 0), sk)}; } // vote on proposal (finalized budget, if fFinal=true) with all possible keys or a single mn (mnAliasFilter) diff --git a/src/budget/budgetvote.h b/src/budget/budgetvote.h index 6414ae0ad327f..34ba386b7f1a9 100644 --- a/src/budget/budgetvote.h +++ b/src/budget/budgetvote.h @@ -30,6 +30,9 @@ class CBudgetVote : public CSignedMessage uint256 nProposalHash; VoteDirection nVote; int64_t nTime; + // NOTE: SDMNs don't have a public collateral outpoint since they point to a shield note. + // Therefore from v6.0+ the prevout of the vin instead of being the masternode collateral outpoint will become the quantity Coutpoint(proRegTxHash, 0). + // Sadly to keep backward compatibility we cannot change this CTxIn to a more simple uint256. CTxIn vin; public: diff --git a/src/budget/finalizedbudgetvote.h b/src/budget/finalizedbudgetvote.h index fdad71f39071a..b1233c06e4eb9 100644 --- a/src/budget/finalizedbudgetvote.h +++ b/src/budget/finalizedbudgetvote.h @@ -21,6 +21,9 @@ class CFinalizedBudgetVote : public CSignedMessage private: bool fValid; //if the vote is currently valid / counted bool fSynced; //if we've sent this to our peers + // NOTE: SDMNs don't have a public collateral outpoint since they point to a shield note. + // Therefore from v6.0+ the prevout of the vin instead of being the masternode collateral outpoint will become the quantity Coutpoint(proRegTxHash, 0). + // Sadly to keep backward compatibility we cannot change this CTxIn to a more simple uint256. CTxIn vin; uint256 nBudgetHash; int64_t nTime; diff --git a/src/evo/deterministicmns.cpp b/src/evo/deterministicmns.cpp index e426be85c75f6..c4e41c8350e26 100644 --- a/src/evo/deterministicmns.cpp +++ b/src/evo/deterministicmns.cpp @@ -7,17 +7,19 @@ #include "bls/key_io.h" #include "chain.h" -#include "coins.h" #include "chainparams.h" +#include "coins.h" #include "consensus/upgrades.h" #include "consensus/validation.h" #include "core_io.h" -#include "key_io.h" #include "guiinterface.h" +#include "key_io.h" #include "masternodeman.h" // for mnodeman (!TODO: remove) +#include "primitives/transaction.h" #include "script/standard.h" #include "spork.h" #include "sync.h" +#include "uint256.h" #include @@ -77,7 +79,7 @@ uint64_t CDeterministicMN::GetInternalId() const std::string CDeterministicMN::ToString() const { - return strprintf("CDeterministicMN(proTxHash=%s, collateralOutpoint=%s, nOperatorReward=%f, state=%s", proTxHash.ToString(), collateralOutpoint.ToStringShort(), (double)nOperatorReward / 100, pdmnState->ToString()); + return strprintf("CDeterministicMN(proTxHash=%s, collateralOutpoint=%s, nullifier=%s, nOperatorReward=%f, state=%s", proTxHash.ToString(), collateralOutpoint.ToStringShort(), nullifier.ToString(), (double)nOperatorReward / 100, pdmnState->ToString()); } void CDeterministicMN::ToJson(UniValue& obj) const @@ -91,6 +93,7 @@ void CDeterministicMN::ToJson(UniValue& obj) const obj.pushKV("proTxHash", proTxHash.ToString()); obj.pushKV("collateralHash", collateralOutpoint.hash.ToString()); obj.pushKV("collateralIndex", (int)collateralOutpoint.n); + obj.pushKV("nullifier", nullifier.ToString()); obj.pushKV("operatorReward", (double)nOperatorReward / 100); obj.pushKV("dmnstate", stateObj); } @@ -125,6 +128,7 @@ CDeterministicMNCPtr CDeterministicMNList::GetMNByOperatorKey(const CBLSPublicKe CDeterministicMNCPtr CDeterministicMNList::GetMNByCollateral(const COutPoint& collateralOutpoint) const { + assert(!collateralOutpoint.IsNull()); return GetUniquePropertyMN(collateralOutpoint); } @@ -137,6 +141,12 @@ CDeterministicMNCPtr CDeterministicMNList::GetValidMNByCollateral(const COutPoin return dmn; } +CDeterministicMNCPtr CDeterministicMNList::GetMNByNullifier(const uint256& nullifier) const +{ + assert(nullifier != UINT256_ZERO); + return GetUniquePropertyMN(nullifier); +} + CDeterministicMNCPtr CDeterministicMNList::GetMNByService(const CService& service) const { return GetUniquePropertyMN(service); @@ -393,7 +403,12 @@ void CDeterministicMNList::AddMN(const CDeterministicMNCPtr& dmn, bool fBumpTota mnMap = mnMap.set(dmn->proTxHash, dmn); mnInternalIdMap = mnInternalIdMap.set(dmn->GetInternalId(), dmn->proTxHash); - AddUniqueProperty(dmn, dmn->collateralOutpoint); + if (!dmn->collateralOutpoint.IsNull()) { + AddUniqueProperty(dmn, dmn->collateralOutpoint); + } + if (dmn->nullifier != UINT256_ZERO) { + AddUniqueProperty(dmn, dmn->nullifier); + } if (dmn->pdmnState->addr != CService()) { AddUniqueProperty(dmn, dmn->pdmnState->addr); } @@ -448,7 +463,12 @@ void CDeterministicMNList::RemoveMN(const uint256& proTxHash) if (!dmn) { throw(std::runtime_error(strprintf("%s: Can't find a masternode with proTxHash=%s", __func__, proTxHash.ToString()))); } - DeleteUniqueProperty(dmn, dmn->collateralOutpoint); + if (!dmn->collateralOutpoint.IsNull()) { + DeleteUniqueProperty(dmn, dmn->collateralOutpoint); + } + if (dmn->nullifier != UINT256_ZERO) { + DeleteUniqueProperty(dmn, dmn->nullifier); + } if (dmn->pdmnState->addr != CService()) { DeleteUniqueProperty(dmn, dmn->pdmnState->addr); } @@ -610,30 +630,34 @@ bool CDeterministicMNManager::BuildNewListFromBlock(const CBlock& block, const C auto dmn = std::make_shared(newList.GetTotalRegisteredCount()); dmn->proTxHash = tx.GetHash(); - // collateralOutpoint is either pointing to an external collateral or to the ProRegTx itself - dmn->collateralOutpoint = pl.collateralOutpoint.hash.IsNull() ? COutPoint(tx.GetHash(), pl.collateralOutpoint.n) - : pl.collateralOutpoint; - - // if the collateral outpoint appears in the legacy masternode list, remove the old node - // !TODO: remove this when the transition to DMN is complete - CMasternode* old_mn = mnodeman.Find(dmn->collateralOutpoint); - if (old_mn) { - old_mn->SetSpent(); - mnodeman.CheckAndRemove(); + bool isShieldDmn = !pl.shieldCollateral.IsNull(); + dmn->nullifier = pl.shieldCollateral.input.nullifier; + if (!isShieldDmn) { + // collateralOutpoint is either pointing to an external collateral or to the ProRegTx itself + dmn->collateralOutpoint = pl.collateralOutpoint.hash.IsNull() ? COutPoint(tx.GetHash(), pl.collateralOutpoint.n) : pl.collateralOutpoint; + + // if the collateral outpoint appears in the legacy masternode list, remove the old node + // !TODO: remove this when the transition to DMN is complete + CMasternode* old_mn = mnodeman.Find(dmn->collateralOutpoint); + if (old_mn) { + old_mn->SetSpent(); + mnodeman.CheckAndRemove(); + } + } else { + // In case of shield DMn we set the outpoint to the default valute + dmn->collateralOutpoint = COutPoint(UINT256_ZERO, (uint32_t)-1); } - - auto replacedDmn = newList.GetMNByCollateral(dmn->collateralOutpoint); + auto replacedDmn = isShieldDmn ? newList.GetMNByNullifier(dmn->nullifier) : newList.GetMNByCollateral(dmn->collateralOutpoint); if (replacedDmn != nullptr) { // This might only happen with a ProRegTx that refers an external collateral // In that case the new ProRegTx will replace the old one. This means the old one is removed // and the new one is added like a completely fresh one, which is also at the bottom of the payment list newList.RemoveMN(replacedDmn->proTxHash); if (debugLogs) { - LogPrintf("CDeterministicMNManager::%s -- MN %s removed from list because collateral was used for a new ProRegTx. collateralOutpoint=%s, nHeight=%d, mapCurMNs.allMNsCount=%d\n", - __func__, replacedDmn->proTxHash.ToString(), dmn->collateralOutpoint.ToStringShort(), nHeight, newList.GetAllMNsCount()); + LogPrintf("CDeterministicMNManager::%s -- MN %s removed from list because collateral was used for a new ProRegTx. collateralOutpoint=%s, nullifier=%s, nHeight=%d, mapCurMNs.allMNsCount=%d\n", + __func__, replacedDmn->proTxHash.ToString(), dmn->collateralOutpoint.ToStringShort(), dmn->nullifier.ToString(), nHeight, newList.GetAllMNsCount()); } } - if (newList.HasUniqueProperty(pl.addr)) { return _state.DoS(100, false, REJECT_DUPLICATE, "bad-protx-dup-IP-address"); } @@ -788,6 +812,17 @@ bool CDeterministicMNManager::BuildNewListFromBlock(const CBlock& block, const C } } } + if (!tx.hasSaplingData()) continue; + for (const auto& in : (*tx.sapData).vShieldedSpend) { + auto dmn = newList.GetMNByNullifier(in.nullifier); + if (dmn && dmn->nullifier == in.nullifier) { + newList.RemoveMN(dmn->proTxHash); + if (debugLogs) { + LogPrintf("CDeterministicMNManager::%s -- MN %s removed from list because collateral was spent. nullifier=%s, nHeight=%d, mapCurMNs.allMNsCount=%d\n", + __func__, dmn->proTxHash.ToString(), dmn->nullifier.ToString(), nHeight, newList.GetAllMNsCount()); + } + } + } } // The payee for the current block was determined by the previous block's list but it might have disappeared in the diff --git a/src/evo/deterministicmns.h b/src/evo/deterministicmns.h index 93b2d73bf921e..acddbfbeba80d 100644 --- a/src/evo/deterministicmns.h +++ b/src/evo/deterministicmns.h @@ -13,6 +13,7 @@ #include "evo/providertx.h" #include "llmq/quorums_commitment.h" #include "saltedhasher.h" +#include "serialize.h" #include "sync.h" #include @@ -201,6 +202,7 @@ class CDeterministicMN uint256 proTxHash; COutPoint collateralOutpoint; + uint256 nullifier; uint16_t nOperatorReward; CDeterministicMNStateCPtr pdmnState; @@ -210,6 +212,7 @@ class CDeterministicMN READWRITE(obj.proTxHash); READWRITE(VARINT(obj.internalId)); READWRITE(obj.collateralOutpoint); + READWRITE(obj.nullifier); READWRITE(obj.nOperatorReward); READWRITE(obj.pdmnState); } @@ -365,6 +368,7 @@ class CDeterministicMNList CDeterministicMNCPtr GetMNByOperatorKey(const CBLSPublicKey& pubKey); CDeterministicMNCPtr GetMNByCollateral(const COutPoint& collateralOutpoint) const; CDeterministicMNCPtr GetValidMNByCollateral(const COutPoint& collateralOutpoint) const; + CDeterministicMNCPtr GetMNByNullifier(const uint256& nullifier) const; CDeterministicMNCPtr GetMNByService(const CService& service) const; CDeterministicMNCPtr GetMNByInternalId(uint64_t internalId) const; CDeterministicMNCPtr GetMNPayee() const; diff --git a/src/evo/providertx.cpp b/src/evo/providertx.cpp index ca836cb848948..c1a5b1423e6c3 100644 --- a/src/evo/providertx.cpp +++ b/src/evo/providertx.cpp @@ -7,6 +7,8 @@ #include "bls/key_io.h" #include "key_io.h" +#include "primitives/transaction.h" +#include "uint256.h" std::string ProRegPL::MakeSignString() const { @@ -39,6 +41,7 @@ void ProRegPL::ToJson(UniValue& obj) const obj.pushKV("version", nVersion); obj.pushKV("collateralHash", collateralOutpoint.hash.ToString()); obj.pushKV("collateralIndex", (int)collateralOutpoint.n); + obj.pushKV("nullifier", shieldCollateral.input.nullifier.ToString()); obj.pushKV("service", addr.ToString()); obj.pushKV("ownerAddress", EncodeDestination(keyIDOwner)); obj.pushKV("operatorPubKey", bls::EncodePublic(Params(), pubKeyOperator)); @@ -119,6 +122,37 @@ void ProUpRevPL::ToJson(UniValue& obj) const obj.pushKV("inputsHash", inputsHash.ToString()); } +bool IsShieldProReg(const CTransactionRef& tx) +{ + if (tx == nullptr) { + return false; + } + if (!tx->IsSpecialTx() || tx->nType != CTransaction::TxType::PROREG) { + return false; + } + ProRegPL pl; + if (!GetTxPayload(*tx, pl)) { + return false; + } + return !pl.shieldCollateral.IsNull(); +} + +bool GetProRegNullifier(const CTransactionRef& tx, uint256& outNullifier) +{ + if (tx == nullptr) { + return false; + } + if (!tx->IsSpecialTx() || tx->nType != CTransaction::TxType::PROREG) { + return false; + } + ProRegPL pl; + if (!GetTxPayload(*tx, pl)) { + return false; + } + outNullifier = pl.shieldCollateral.input.nullifier; + return true; +} + bool GetProRegCollateral(const CTransactionRef& tx, COutPoint& outRet) { if (tx == nullptr) { diff --git a/src/evo/providertx.h b/src/evo/providertx.h index 4cc1c3fdef432..10733cd83e132 100644 --- a/src/evo/providertx.h +++ b/src/evo/providertx.h @@ -6,23 +6,61 @@ #define PIVX_PROVIDERTX_H #include "bls/bls_wrapper.h" -#include "primitives/transaction.h" #include "netaddress.h" +#include "primitives/transaction.h" +#include "sapling/sapling_transaction.h" +#include "serialize.h" #include +// Proof of ownership/validity/unspentness for the collateral of a shield DMn + +class ShieldDMNProof +{ +public: + SpendDescription input; + CTxOut output; + std::vector bindingSig; + + ShieldDMNProof() + { + SetNull(); + } + + void SetNull() + { + input = SpendDescription(); + output = CTxOut(); + bindingSig.clear(); + } + + bool IsNull() + { + return output.IsNull() && bindingSig.empty() && input == SpendDescription(); + } + + SERIALIZE_METHODS(ShieldDMNProof, obj) + { + READWRITE(obj.input); + READWRITE(obj.output); + READWRITE(obj.bindingSig); + } +}; + + // Provider-Register tx payload class ProRegPL { public: - static const uint16_t CURRENT_VERSION = 1; + static const uint16_t CURRENT_VERSION = 2; public: uint16_t nVersion{CURRENT_VERSION}; // message version uint16_t nType{0}; // only 0 supported for now uint16_t nMode{0}; // only 0 supported for now COutPoint collateralOutpoint{UINT256_ZERO, (uint32_t)-1}; // if hash is null, we refer to a ProRegTx output + ShieldDMNProof shieldCollateral{ShieldDMNProof()}; // if null the ProRegTx refers to a transparent DMNs CService addr; CKeyID keyIDOwner; CBLSPublicKey pubKeyOperator; @@ -41,6 +79,7 @@ class ProRegPL READWRITE(obj.nType); READWRITE(obj.nMode); READWRITE(obj.collateralOutpoint); + READWRITE(obj.shieldCollateral); READWRITE(obj.addr); READWRITE(obj.keyIDOwner); READWRITE(obj.pubKeyOperator); @@ -160,5 +199,9 @@ class ProUpRevPL // If tx is a ProRegTx, return the collateral outpoint in outRet. bool GetProRegCollateral(const CTransactionRef& tx, COutPoint& outRet); +// return true is the tx is a ProReg and corresponds to a shield masternode +bool IsShieldProReg(const CTransactionRef& tx); +// If tx is a ProRegTx, return the corresponding shield nullifier. +bool GetProRegNullifier(const CTransactionRef& tx, uint256& outNullifier); #endif //PIVX_PROVIDERTX_H diff --git a/src/evo/specialtx_validation.cpp b/src/evo/specialtx_validation.cpp index 64ab69de08f1c..6491fbb37dbf1 100644 --- a/src/evo/specialtx_validation.cpp +++ b/src/evo/specialtx_validation.cpp @@ -6,18 +6,20 @@ #include "evo/specialtx_validation.h" #include "chain.h" -#include "coins.h" #include "chainparams.h" #include "clientversion.h" +#include "coins.h" #include "consensus/validation.h" #include "evo/deterministicmns.h" #include "evo/providertx.h" #include "llmq/quorums_blockprocessor.h" #include "messagesigner.h" -#include "primitives/transaction.h" #include "primitives/block.h" +#include "primitives/transaction.h" +#include "sapling/sapling_validation.h" #include "script/standard.h" #include "spork.h" +#include /* -- Helper static functions -- */ @@ -105,6 +107,17 @@ static bool CheckCollateralOut(const CTxOut& out, const ProRegPL& pl, CValidatio return true; } +// This checks if the provided note value is valid +static bool CheckShieldDMnCollateralValidity(const CBlockIndex* pindexPrev, const CCoinsViewCache* view, CValidationState& state, CTransaction& tx) +{ + assert(pindexPrev); + + if (!SaplingValidation::ContextualCheckTransaction(tx, state, Params(), pindexPrev->nHeight + 1, false, false)) { + return false; + } + return true; +} + // Provider Register Payload static bool CheckProRegTx(const CTransaction& tx, const CBlockIndex* pindexPrev, const CCoinsViewCache* view, CValidationState& state) { @@ -114,6 +127,7 @@ static bool CheckProRegTx(const CTransaction& tx, const CBlockIndex* pindexPrev, if (!GetTxPayload(tx, pl)) { return state.DoS(100, false, REJECT_INVALID, "bad-protx-payload"); } + bool isShieldDMn = !pl.shieldCollateral.IsNull(); if (pl.nVersion == 0 || pl.nVersion > ProRegPL::CURRENT_VERSION) { return state.DoS(100, false, REJECT_INVALID, "bad-protx-version"); @@ -161,46 +175,96 @@ static bool CheckProRegTx(const CTransaction& tx, const CBlockIndex* pindexPrev, return state.DoS(10, false, REJECT_INVALID, "bad-protx-operator-reward"); } - if (pl.collateralOutpoint.hash.IsNull()) { - // collateral included in the proReg tx - if (pl.collateralOutpoint.n >= tx.vout.size()) { - return state.DoS(10, false, REJECT_INVALID, "bad-protx-collateral-index"); - } - CTxDestination collateralTxDest; - if (!CheckCollateralOut(tx.vout[pl.collateralOutpoint.n], pl, state, collateralTxDest)) { - // pass the state returned by the function above - return false; - } - // collateral is part of this ProRegTx, so we know the collateral is owned by the issuer - if (!pl.vchSig.empty()) { - return state.DoS(100, false, REJECT_INVALID, "bad-protx-sig"); - } - } else if (pindexPrev != nullptr) { - assert(view != nullptr); + if (pl.collateralOutpoint.IsNull() && pl.shieldCollateral.IsNull()) { + return state.DoS(10, false, REJECT_INVALID, "bad-protx-collaterals-empty"); + } - // Referenced external collateral. - // This is checked only when pindexPrev is not null (thus during ConnectBlock-->CheckSpecialTx), - // because this is a contextual check: we need the updated utxo set, to verify that - // the coin exists and it is unspent. - Coin coin; - if (!view->GetUTXOCoin(pl.collateralOutpoint, coin)) { - return state.DoS(10, false, REJECT_INVALID, "bad-protx-collateral"); - } - CTxDestination collateralTxDest; - if (!CheckCollateralOut(coin.out, pl, state, collateralTxDest)) { - // pass the state returned by the function above - return false; - } - // Extract key from collateral. This only works for P2PK and P2PKH collaterals and will fail for P2SH. - // Issuer of this ProRegTx must prove ownership with this key by signing the ProRegTx - const CKeyID* keyForPayloadSig = boost::get(&collateralTxDest); - if (!keyForPayloadSig) { - return state.DoS(10, false, REJECT_INVALID, "bad-protx-collateral-pkh"); + if (!pl.collateralOutpoint.IsNull() && !pl.shieldCollateral.IsNull()) { + return state.DoS(10, false, REJECT_INVALID, "bad-protx-collaterals-non-empty"); + } + + if (!isShieldDMn) { + if (pl.collateralOutpoint.hash.IsNull()) { + // collateral included in the proReg tx + if (pl.collateralOutpoint.n >= tx.vout.size()) { + return state.DoS(10, false, REJECT_INVALID, "bad-protx-collateral-index"); + } + CTxDestination collateralTxDest; + if (!CheckCollateralOut(tx.vout[pl.collateralOutpoint.n], pl, state, collateralTxDest)) { + // pass the state returned by the function above + return false; + } + // collateral is part of this ProRegTx, so we know the collateral is owned by the issuer + if (!pl.vchSig.empty()) { + return state.DoS(100, false, REJECT_INVALID, "bad-protx-sig"); + } + } else if (pindexPrev != nullptr) { + assert(view != nullptr); + + // Referenced external collateral. + // This is checked only when pindexPrev is not null (thus during ConnectBlock-->CheckSpecialTx), + // because this is a contextual check: we need the updated utxo set, to verify that + // the coin exists and it is unspent. + Coin coin; + if (!view->GetUTXOCoin(pl.collateralOutpoint, coin)) { + return state.DoS(10, false, REJECT_INVALID, "bad-protx-collateral"); + } + CTxDestination collateralTxDest; + if (!CheckCollateralOut(coin.out, pl, state, collateralTxDest)) { + // pass the state returned by the function above + return false; + } + // Extract key from collateral. This only works for P2PK and P2PKH collaterals and will fail for P2SH. + // Issuer of this ProRegTx must prove ownership with this key by signing the ProRegTx + const CKeyID* keyForPayloadSig = boost::get(&collateralTxDest); + if (!keyForPayloadSig) { + return state.DoS(10, false, REJECT_INVALID, "bad-protx-collateral-pkh"); + } + // collateral is not part of this ProRegTx, so we must verify ownership of the collateral + if (!CheckStringSig(pl, *keyForPayloadSig, state)) { + // pass the state returned by the function above + return false; + } } - // collateral is not part of this ProRegTx, so we must verify ownership of the collateral - if (!CheckStringSig(pl, *keyForPayloadSig, state)) { - // pass the state returned by the function above - return false; + } else { + if (pindexPrev != nullptr) { + assert(view != nullptr); + // ShieldDmns are disabled when the legacy system is still active + // TODO: remove after complete transition to DMN + if (!deterministicMNManager->LegacyMNObsolete(pindexPrev->nHeight + 1) && isShieldDMn) { + return state.DoS(10, false, REJECT_INVALID, "spork-21-inactive"); + } + + // Create the tx proof + CMutableTransaction txMut = CMutableTransaction(); + txMut.nVersion = 3; + txMut.vout.push_back(pl.shieldCollateral.output); + (*txMut.sapData).vShieldedSpend.push_back(pl.shieldCollateral.input); + (*txMut.sapData).valueBalance = Params().GetConsensus().nMNCollateralAmt; + if (pl.shieldCollateral.bindingSig.size() != (*txMut.sapData).bindingSig.size()) { + return state.DoS(100, false, REJECT_INVALID, "bad-protx-bindingSig-len"); + } + if (pl.shieldCollateral.output.nValue != Params().GetConsensus().nMNCollateralAmt) { + return state.DoS(100, false, REJECT_INVALID, "bad-protx-proofCollateralOutput-amt"); + } + std::copy(pl.shieldCollateral.bindingSig.begin(), pl.shieldCollateral.bindingSig.end(), (*txMut.sapData).bindingSig.begin()); + + // Check that the proof is valid + CTransaction tx = txMut; + if (!CheckShieldDMnCollateralValidity(pindexPrev, view, state, tx)) { + // pass the state returned by the function above + return false; + } + + // Check that the nullifier is still unspent + if (!view->HaveShieldedRequirements(tx)) { + return state.DoS(100, false, REJECT_INVALID, "bad-txns-shielded-requirements-not-met"); + } + + // Check that the signature is empty + if (!pl.vchSig.empty()) { + return state.DoS(100, false, REJECT_INVALID, "bad-protx-sig"); + } } } @@ -210,9 +274,15 @@ static bool CheckProRegTx(const CTransaction& tx, const CBlockIndex* pindexPrev, if (pindexPrev) { auto mnList = deterministicMNManager->GetListForBlock(pindexPrev); - // only allow reusing of addresses when it's for the same collateral (which replaces the old MN) - if (mnList.HasUniqueProperty(pl.addr) && mnList.GetUniquePropertyMN(pl.addr)->collateralOutpoint != pl.collateralOutpoint) { - return state.DoS(10, false, REJECT_DUPLICATE, "bad-protx-dup-IP-address"); + // only allow reusing of addresses when it's for the same collateral/nullifier (which replaces the old MN) + if (!isShieldDMn) { + if (mnList.HasUniqueProperty(pl.addr) && mnList.GetUniquePropertyMN(pl.addr)->collateralOutpoint != pl.collateralOutpoint) { + return state.DoS(10, false, REJECT_DUPLICATE, "bad-protx-dup-IP-address"); + } + } else { + if (mnList.HasUniqueProperty(pl.addr) && mnList.GetUniquePropertyMN(pl.addr)->nullifier != pl.shieldCollateral.input.nullifier) { + return state.DoS(10, false, REJECT_DUPLICATE, "bad-protx-dup-IP-address"); + } } // never allow duplicate keys, even if this ProTx would replace an existing MN if (mnList.HasUniqueProperty(pl.keyIDOwner)) { @@ -222,7 +292,6 @@ static bool CheckProRegTx(const CTransaction& tx, const CBlockIndex* pindexPrev, return state.DoS(10, false, REJECT_DUPLICATE, "bad-protx-dup-operator-key"); } } - return true; } diff --git a/src/masternode-payments.cpp b/src/masternode-payments.cpp index 9379a3a503a2a..b1a8d41ec70b0 100644 --- a/src/masternode-payments.cpp +++ b/src/masternode-payments.cpp @@ -440,6 +440,7 @@ bool CMasternodePayments::ProcessMessageMasternodePayments(CNode* pfrom, std::st return true; } +// This function is not compatible with shield DMNs, but it's not a problem since they will activated after the transition bool CMasternodePayments::ProcessMNWinner(CMasternodePaymentWinner& winner, CNode* pfrom, CValidationState& state) { int nHeight = mnodeman.GetBestHeight(); @@ -464,7 +465,7 @@ bool CMasternodePayments::ProcessMNWinner(CMasternodePaymentWinner& winner, CNod // See if the mnw signer exists, and whether it's a legacy or DMN masternode const CMasternode* pmn{nullptr}; - auto dmn = deterministicMNManager->GetListAtChainTip().GetMNByCollateral(winner.vinMasternode.prevout); + auto dmn = winner.vinMasternode.prevout.IsNull() ? nullptr : deterministicMNManager->GetListAtChainTip().GetMNByCollateral(winner.vinMasternode.prevout); if (dmn == nullptr) { // legacy masternode pmn = mnodeman.Find(winner.vinMasternode.prevout); diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 05ad24f006ab8..5bede01fc98c4 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -126,6 +126,10 @@ static const CRPCConvertParam vRPCConvertParams[] = { { "preparebudget", 2, "npayments" }, { "preparebudget", 3, "start" }, { "preparebudget", 5, "montly_payment" }, + { "protx_register", 1, "collateralIndex" }, + { "protx_register", 2, "transparent" }, + { "protx_register_prepare", 1, "collateralIndex" }, + { "protx_register_prepare", 2, "transparent" }, { "prioritisetransaction", 1, "fee_delta" }, { "quorumdkgsimerror", 1, "rate" }, { "quorumdkgstatus", 0, "detail_level" }, diff --git a/src/rpc/masternode.cpp b/src/rpc/masternode.cpp index 38c6312562f35..19f6d51efcd3a 100644 --- a/src/rpc/masternode.cpp +++ b/src/rpc/masternode.cpp @@ -146,16 +146,9 @@ static inline bool filter(const std::string& str, const std::string& strFilter) return str.find(strFilter) != std::string::npos; } -static inline bool filterMasternode(const UniValue& dmno, const std::string& strFilter, bool fEnabled) +static inline bool filterMasternode(const UniValue& dmno, const std::string& strFilter, bool fEnabled, bool isShield) { - return strFilter.empty() || (filter("ENABLED", strFilter) && fEnabled) - || (filter("POSE_BANNED", strFilter) && !fEnabled) - || (filter(dmno["proTxHash"].get_str(), strFilter)) - || (filter(dmno["collateralHash"].get_str(), strFilter)) - || (filter(dmno["collateralAddress"].get_str(), strFilter)) - || (filter(dmno["dmnstate"]["ownerAddress"].get_str(), strFilter)) - || (filter(dmno["dmnstate"]["operatorPubKey"].get_str(), strFilter)) - || (filter(dmno["dmnstate"]["votingAddress"].get_str(), strFilter)); + return strFilter.empty() || (filter("ENABLED", strFilter) && fEnabled) || (filter("POSE_BANNED", strFilter) && !fEnabled) || (filter("SHIELD", strFilter) && isShield) || (filter(dmno["proTxHash"].get_str(), strFilter)) || (filter(dmno["collateralHash"].get_str(), strFilter)) || (!isShield && filter(dmno["collateralAddress"].get_str(), strFilter)) || (filter(dmno["dmnstate"]["ownerAddress"].get_str(), strFilter)) || (filter(dmno["dmnstate"]["operatorPubKey"].get_str(), strFilter)) || (filter(dmno["dmnstate"]["votingAddress"].get_str(), strFilter)); } UniValue listmasternodes(const JSONRPCRequest& request) @@ -198,7 +191,7 @@ UniValue listmasternodes(const JSONRPCRequest& request) auto mnList = deterministicMNManager->GetListAtChainTip(); mnList.ForEachMN(false, [&](const CDeterministicMNCPtr& dmn) { UniValue obj = DmnToJson(dmn); - if (filterMasternode(obj, strFilter, !dmn->IsPoSeBanned())) { + if (filterMasternode(obj, strFilter, !dmn->IsPoSeBanned(), !dmn->nullifier.IsNull())) { ret.push_back(obj); } }); @@ -224,7 +217,7 @@ UniValue listmasternodes(const JSONRPCRequest& request) if (dmn) { UniValue obj = DmnToJson(dmn); bool fEnabled = !dmn->IsPoSeBanned(); - if (filterMasternode(obj, strFilter, fEnabled)) { + if (filterMasternode(obj, strFilter, fEnabled, false)) { // Added for backward compatibility with legacy masternodes obj.pushKV("type", "deterministic"); obj.pushKV("txhash", obj["proTxHash"].get_str()); diff --git a/src/rpc/rpcevo.cpp b/src/rpc/rpcevo.cpp index df6890e723182..eeda8520f9021 100644 --- a/src/rpc/rpcevo.cpp +++ b/src/rpc/rpcevo.cpp @@ -4,25 +4,30 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include "activemasternode.h" -#include "bls/key_io.h" #include "bls/bls_wrapper.h" +#include "bls/key_io.h" +#include "chainparams.h" #include "core_io.h" #include "destination_io.h" #include "evo/deterministicmns.h" -#include "evo/specialtx_validation.h" #include "evo/providertx.h" +#include "evo/specialtx_validation.h" #include "key_io.h" #include "masternode.h" #include "messagesigner.h" #include "netbase.h" #include "operationresult.h" #include "policy/policy.h" +#include "primitives/transaction.h" #include "pubkey.h" // COMPACT_SIGNATURE_SIZE +#include "rpc/protocol.h" #include "rpc/server.h" +#include "sapling/transaction_builder.h" #include "script/sign.h" #include "tiertwo/masternode_meta_manager.h" #include "util/validation.h" #include "utilmoneystr.h" +#include "validation.h" #ifdef ENABLE_WALLET #include "coincontrol.h" @@ -37,6 +42,7 @@ enum ProRegParam { collateralAddress, collateralHash, collateralIndex, + transparent, ipAndPort_register, ipAndPort_update, operatorPubKey_register, @@ -56,85 +62,68 @@ enum ProRegParam { }; static const std::map mapParamHelp = { - {collateralAddress, - "%d. \"collateralAddress\" (string, required) The PIVX address to send the collateral to.\n" - }, - {collateralHash, - "%d. \"collateralHash\" (string, required) The collateral transaction hash.\n" - }, - {collateralIndex, - "%d. collateralIndex (numeric, required) The collateral transaction output index.\n" - }, - {ipAndPort_register, - "%d. \"ipAndPort\" (string, required) IP and port in the form \"IP:PORT\".\n" - " Must be unique on the network. Can be set to 0, which will require a ProUpServTx afterwards.\n" - }, - {ipAndPort_update, - "%d. \"ipAndPort\" (string, required) IP and port in the form \"IP:PORT\".\n" - " If set to an empty string, the currently active ip is reused.\n" - }, - {operatorPubKey_register, - "%d. \"operatorPubKey\" (string, required) The operator BLS public key. The BLS private key does not have to be known.\n" - " It has to match the BLS private key which is later used when operating the masternode.\n" - }, - {operatorPubKey_update, - "%d. \"operatorPubKey\" (string, required) The operator BLS public key. The BLS private key does not have to be known.\n" - " It has to match the BLS private key which is later used when operating the masternode.\n" - " If set to an empty string, the currently active operator BLS public key is reused.\n" - }, - {operatorKey, - "%d. \"operatorKey\" (string, optional) The operator BLS private key associated with the\n" - " registered operator public key. If not specified, or set to an empty string, then this command must\n" - " be performed on the active masternode with the corresponding operator key.\n" - }, - {operatorPayoutAddress_register, - "%d. \"operatorPayoutAddress\" (string, optional) The address used for operator reward payments.\n" - " Only allowed when the ProRegTx had a non-zero operatorReward value.\n" - " If set to an empty string, the operatorPubKey is used.\n" - }, - {operatorPayoutAddress_update, - "%d. \"operatorPayoutAddress\" (string, optional) The address used for operator reward payments.\n" - " Only allowed when the ProRegTx had a non-zero operatorReward value.\n" - " If set to an empty string, the currently active one is reused.\n" - }, - {operatorReward, - "%d. \"operatorReward\" (numeric, optional) The fraction in %% to share with the operator. The value must be\n" - " between 0.00 and 100.00. If not set, it takes the default value of 0.0\n" - }, - {ownerAddress, - "%d. \"ownerAddress\" (string, required) The PIVX address to use for payee updates and proposal voting.\n" - " The private key belonging to this address must be known in your wallet, in order to send updates.\n" - " The address must not be already registered, and must differ from the collateralAddress\n" - }, - {ownerKey, - "%d. \"ownerKey\" (string, optional) The owner key associated with the operator address of the masternode.\n" - " If not specified, or set to an empty string, then the mn key must be known by your wallet, in order to sign the tx.\n" - }, - {payoutAddress_register, - "%d. \"payoutAddress\" (string, required) The PIVX address to use for masternode reward payments.\n" - }, - {payoutAddress_update, - "%d. \"payoutAddress\" (string, required) The PIVX address to use for masternode reward payments.\n" - " If set to an empty string, the currently active payout address is reused.\n" - }, - {proTxHash, - "%d. \"proTxHash\" (string, required) The hash of the initial ProRegTx.\n" - }, - {revocationReason, - "%d. reason (numeric, optional) The reason for masternode service revocation. Default: 0.\n" - " 0=not_specified, 1=service_termination, 2=compromised_keys, 3=keys_change.\n" - }, - {votingAddress_register, - "%d. \"votingAddress\" (string, required) The voting key address. The private key does not have to be known by your wallet.\n" - " It has to match the private key which is later used when voting on proposals.\n" - " If set to an empty string, ownerAddress will be used.\n" - }, - {votingAddress_update, - "%d. \"votingAddress\" (string, required) The voting key address. The private key does not have to be known by your wallet.\n" - " It has to match the private key which is later used when voting on proposals.\n" - " If set to an empty string, the currently active voting key address is reused.\n" - }, - }; + {collateralAddress, + "%d. \"collateralAddress\" (string, required) The PIVX address to send the collateral to.\n"}, + {collateralHash, + "%d. \"collateralHash\" (string, required) The collateral transaction hash.\n"}, + {collateralIndex, + "%d. collateralIndex (numeric, required) The collateral transaction output index.\n"}, + {transparent, + "%d. transparent (bool, required) Whether you want to create a shield masternode or a transparent one.\n"}, + {ipAndPort_register, + "%d. \"ipAndPort\" (string, required) IP and port in the form \"IP:PORT\".\n" + " Must be unique on the network. Can be set to 0, which will require a ProUpServTx afterwards.\n"}, + {ipAndPort_update, + "%d. \"ipAndPort\" (string, required) IP and port in the form \"IP:PORT\".\n" + " If set to an empty string, the currently active ip is reused.\n"}, + {operatorPubKey_register, + "%d. \"operatorPubKey\" (string, required) The operator BLS public key. The BLS private key does not have to be known.\n" + " It has to match the BLS private key which is later used when operating the masternode.\n"}, + {operatorPubKey_update, + "%d. \"operatorPubKey\" (string, required) The operator BLS public key. The BLS private key does not have to be known.\n" + " It has to match the BLS private key which is later used when operating the masternode.\n" + " If set to an empty string, the currently active operator BLS public key is reused.\n"}, + {operatorKey, + "%d. \"operatorKey\" (string, optional) The operator BLS private key associated with the\n" + " registered operator public key. If not specified, or set to an empty string, then this command must\n" + " be performed on the active masternode with the corresponding operator key.\n"}, + {operatorPayoutAddress_register, + "%d. \"operatorPayoutAddress\" (string, optional) The address used for operator reward payments.\n" + " Only allowed when the ProRegTx had a non-zero operatorReward value.\n" + " If set to an empty string, the operatorPubKey is used.\n"}, + {operatorPayoutAddress_update, + "%d. \"operatorPayoutAddress\" (string, optional) The address used for operator reward payments.\n" + " Only allowed when the ProRegTx had a non-zero operatorReward value.\n" + " If set to an empty string, the currently active one is reused.\n"}, + {operatorReward, + "%d. \"operatorReward\" (numeric, optional) The fraction in %% to share with the operator. The value must be\n" + " between 0.00 and 100.00. If not set, it takes the default value of 0.0\n"}, + {ownerAddress, + "%d. \"ownerAddress\" (string, required) The PIVX address to use for payee updates and proposal voting.\n" + " The private key belonging to this address must be known in your wallet, in order to send updates.\n" + " The address must not be already registered, and must differ from the collateralAddress\n"}, + {ownerKey, + "%d. \"ownerKey\" (string, optional) The owner key associated with the operator address of the masternode.\n" + " If not specified, or set to an empty string, then the mn key must be known by your wallet, in order to sign the tx.\n"}, + {payoutAddress_register, + "%d. \"payoutAddress\" (string, required) The PIVX address to use for masternode reward payments.\n"}, + {payoutAddress_update, + "%d. \"payoutAddress\" (string, required) The PIVX address to use for masternode reward payments.\n" + " If set to an empty string, the currently active payout address is reused.\n"}, + {proTxHash, + "%d. \"proTxHash\" (string, required) The hash of the initial ProRegTx.\n"}, + {revocationReason, + "%d. reason (numeric, optional) The reason for masternode service revocation. Default: 0.\n" + " 0=not_specified, 1=service_termination, 2=compromised_keys, 3=keys_change.\n"}, + {votingAddress_register, + "%d. \"votingAddress\" (string, required) The voting key address. The private key does not have to be known by your wallet.\n" + " It has to match the private key which is later used when voting on proposals.\n" + " If set to an empty string, ownerAddress will be used.\n"}, + {votingAddress_update, + "%d. \"votingAddress\" (string, required) The voting key address. The private key does not have to be known by your wallet.\n" + " It has to match the private key which is later used when voting on proposals.\n" + " If set to an empty string, the currently active voting key address is reused.\n"}, +}; std::string GetHelpString(int nParamNum, ProRegParam p) { @@ -441,6 +430,39 @@ static ProRegPL ParseProRegPLParams(const UniValue& params, unsigned int paramId return pl; } +static bool CreateProRegShieldProof(std::string& error, ProRegPL& pl, SaplingNoteEntry& note, CWallet* pwallet) +{ + TransactionBuilder txBuilder(Params().GetConsensus(), pwallet); + txBuilder.SetFee(0); + libzcash::SaplingExtendedSpendingKey sk; + if (!pwallet->GetSaplingExtendedSpendingKey(note.address, sk)) { + error = strprintf("Cannot fetch the SaplingExtendedSpendingKey"); + return false; + } + uint256 anchor; + std::vector> witnesses; + std::vector noteop; + noteop.emplace_back(note.op); + pwallet->GetSaplingScriptPubKeyMan()->GetSaplingNoteWitnesses(noteop, witnesses, anchor); + txBuilder.AddSaplingSpend(sk.expsk, note.note, anchor, witnesses[0].get()); + auto newAddress = pwallet->getNewAddress(""); + if (!newAddress.getRes()) { + error = newAddress.getError(); + return false; + } + txBuilder.AddTransparentOutput(*newAddress.getObjResult(), Params().GetConsensus().nMNCollateralAmt); + auto txTrial = txBuilder.Build(); + if (txTrial.IsError()) { + error = strprintf("Could not build shield proof"); + return false; + } + auto txFinal = *txTrial.GetTx(); + pl.shieldCollateral.input = (*txFinal.sapData).vShieldedSpend[0]; + pl.shieldCollateral.output = txFinal.vout[0]; + copy(begin((*txFinal.sapData).bindingSig), end((*txFinal.sapData).bindingSig), back_inserter(pl.shieldCollateral.bindingSig)); + return true; +} + // handles protx_register, and protx_register_prepare static UniValue ProTxRegister(const JSONRPCRequest& request, bool fSignAndSend) { @@ -449,45 +471,32 @@ static UniValue ProTxRegister(const JSONRPCRequest& request, bool fSignAndSend) if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) return NullUniValue; - if (request.fHelp || request.params.size() < 7 || request.params.size() > 9) { + if (request.fHelp || request.params.size() < 8 || request.params.size() > 10) { throw std::runtime_error( - (fSignAndSend ? - "protx_register \"collateralHash\" collateralIndex \"ipAndPort\" \"ownerAddress\" \"operatorPubKey\" \"votingAddress\" \"payoutAddress\" (operatorReward \"operatorPayoutAddress\")\n" + (fSignAndSend ? + "protx_register \"collateralHash\" collateralIndex transparent \"ipAndPort\" \"ownerAddress\" \"operatorPubKey\" \"votingAddress\" \"payoutAddress\" (operatorReward \"operatorPayoutAddress\")\n" "The collateral is specified through \"collateralHash\" and \"collateralIndex\" and must be an unspent\n" - "transaction output spendable by this wallet. It must also not be used by any other masternode.\n" - : - "protx_register_prepare \"collateralHash\" collateralIndex \"ipAndPort\" \"ownerAddress\" \"operatorPubKey\" \"votingAddress\" \"payoutAddress\" (operatorReward \"operatorPayoutAddress\")\n" + "transaction output spendable by this wallet. It must also not be used by any other masternode.\n" : + "protx_register_prepare \"collateralHash\" collateralIndex transparent \"ipAndPort\" \"ownerAddress\" \"operatorPubKey\" \"votingAddress\" \"payoutAddress\" (operatorReward \"operatorPayoutAddress\")\n" "\nCreates an unsigned ProTx and returns it. The ProTx must be signed externally with the collateral\n" - "key and then passed to \"protx_register_submit\".\n" - "The collateral is specified through \"collateralHash\" and \"collateralIndex\" and must be an unspent transaction output.\n" - ) - + HelpRequiringPassphrase(pwallet) + "\n" - "\nArguments:\n" - + GetHelpString(1, collateralHash) - + GetHelpString(2, collateralIndex) - + GetHelpString(3, ipAndPort_register) - + GetHelpString(4, ownerAddress) - + GetHelpString(5, operatorPubKey_register) - + GetHelpString(6, votingAddress_register) - + GetHelpString(7, payoutAddress_register) - + GetHelpString(8, operatorReward) - + GetHelpString(9, operatorPayoutAddress_register) + - "\nResult:\n" + - (fSignAndSend ? ( - "\"txid\" (string) The transaction id.\n" - "\nExamples:\n" - + HelpExampleCli("protx_register", "\"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\" 0 \"168.192.1.100:51472\" \"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\" \"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\" \"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\" \"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\"") - ) : ( - "{ (json object)\n" - " \"tx\" : (string) The serialized ProTx in hex format.\n" - " \"collateralAddress\" : (string) The collateral address.\n" - " \"signMessage\" : (string) The string message that needs to be signed with the collateral key\n" - "}\n" - "\nExamples:\n" - + HelpExampleCli("protx_register_prepare", "\"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\" 0 \"168.192.1.100:51472\" \"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\" \"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\" \"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\" \"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\"") - ) - ) - ); + "key (if the masternode is transparent) and then passed to \"protx_register_submit\".\n" + "The collateral is specified through \"collateralHash\" and \"collateralIndex\" and must be an unspent transaction output/ unspent shield outpoint.\n") + + HelpRequiringPassphrase(pwallet) + "\n" + "\nArguments:\n" + + GetHelpString(1, collateralHash) + GetHelpString(2, collateralIndex) + GetHelpString(3, transparent) + GetHelpString(4, ipAndPort_register) + GetHelpString(5, ownerAddress) + GetHelpString(6, operatorPubKey_register) + GetHelpString(7, votingAddress_register) + GetHelpString(8, payoutAddress_register) + GetHelpString(9, operatorReward) + GetHelpString(10, operatorPayoutAddress_register) + + "\nResult:\n" + + (fSignAndSend ? ( + "\"txid\" (string) The transaction id.\n" + "\nExamples:\n" + + HelpExampleCli("protx_register", "\"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\" 0 false \"168.192.1.100:51472\" \"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\" \"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\" \"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\" \"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\"")) : + ( + "{ (json object)\n" + " \"tx\" : (string) The serialized ProTx in hex format.\n" + " \"collateralAddress\" : (string) The collateral address.\n" + " \"signMessage\" : (string) The string message that needs to be signed with the collateral key\n" + "}\n" + "\nExamples:\n" + + HelpExampleCli("protx_register_prepare", "\"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\" 0 false \"168.192.1.100:51472\" \"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\" \"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\" \"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\" \"DMJRSsuU9zfyrvxVaAEFQqK4MxZg6vgeS6\"")))); } if (fSignAndSend) CheckEvoUpgradeEnforcement(); @@ -501,10 +510,9 @@ static UniValue ProTxRegister(const JSONRPCRequest& request, bool fSignAndSend) if (collateralIndex < 0) { throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("invalid collateral index (negative): %d", collateralIndex)); } - - ProRegPL pl = ParseProRegPLParams(request.params, 2); + const bool transparent = request.params[2].get_bool(); + ProRegPL pl = ParseProRegPLParams(request.params, 3); pl.nVersion = ProRegPL::CURRENT_VERSION; - pl.collateralOutpoint = COutPoint(collateralHash, (uint32_t)collateralIndex); CMutableTransaction tx; tx.nVersion = CTransaction::TxVersion::SAPLING; @@ -512,12 +520,36 @@ static UniValue ProTxRegister(const JSONRPCRequest& request, bool fSignAndSend) // referencing unspent collateral outpoint Coin coin; - if (!WITH_LOCK(cs_main, return pcoinsTip->GetUTXOCoin(pl.collateralOutpoint, coin); )) { - throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("collateral not found: %s-%d", collateralHash.ToString(), collateralIndex)); - } - if (coin.out.nValue != Params().GetConsensus().nMNCollateralAmt) { - throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("collateral %s-%d with invalid value %d", collateralHash.ToString(), collateralIndex, coin.out.nValue)); + if (transparent) { + pl.collateralOutpoint = COutPoint(collateralHash, (uint32_t)collateralIndex); + if (!WITH_LOCK(cs_main, return pcoinsTip->GetUTXOCoin(pl.collateralOutpoint, coin);)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("collateral not found: %s-%d", collateralHash.ToString(), collateralIndex)); + } + if (coin.out.nValue != Params().GetConsensus().nMNCollateralAmt) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("collateral %s-%d with invalid value %d", collateralHash.ToString(), collateralIndex, coin.out.nValue)); + } + } else { + const SaplingOutPoint op(collateralHash, collateralIndex); + std::vector note; + pwallet->GetSaplingScriptPubKeyMan()->GetNotes({op}, note); + if (note.size() == 0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Cannot find sapling note indexed by %s-%d", collateralHash.ToString(), collateralIndex)); + } + std::string errStr; + if (!CreateProRegShieldProof(errStr, pl, note[0], pwallet)) { + throw JSONRPCError(RPC_INTERNAL_ERROR, errStr); + } + FundSpecialTx(pwallet, tx, pl); + SetTxPayload(tx, pl); + if (fSignAndSend) { + return SignAndSendSpecialTx(pwallet, tx, pl); + } else { + UniValue ret(UniValue::VOBJ); + ret.pushKV("tx", EncodeHexTx(tx)); + return ret; + } } + CTxDestination txDest; ExtractDestination(coin.out.scriptPubKey, txDest); const CKeyID* keyID = boost::get(&txDest); @@ -566,20 +598,19 @@ UniValue protx_register_submit(const JSONRPCRequest& request) if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) return NullUniValue; - if (request.fHelp || request.params.size() != 2) { + if (request.fHelp || (request.params.size() != 1 && request.params.size() != 2)) { throw std::runtime_error( - "protx_register_submit \"tx\" \"sig\"\n" - "\nSubmits the specified ProTx to the network. This command will also sign the inputs of the transaction\n" - "which were previously added by \"protx_register_prepare\" to cover transaction fees\n" - + HelpRequiringPassphrase(pwallet) + "\n" - "\nArguments:\n" - "1. \"tx\" (string, required) The serialized transaction previously returned by \"protx_register_prepare\"\n" - "2. \"sig\" (string, required) The signature signed with the collateral key. Must be in base64 format.\n" - "\nResult:\n" - "\"txid\" (string) The transaction id.\n" - "\nExamples:\n" - + HelpExampleCli("protx_register_submit", "\"tx\" \"sig\"") - ); + "protx_register_submit \"tx\" \"sig\"\n" + "\nSubmits the specified ProTx to the network. This command will also sign the inputs of the transaction\n" + "which were previously added by \"protx_register_prepare\" to cover transaction fees (note: the signature is not needed for shield masternodes)\n" + + HelpRequiringPassphrase(pwallet) + "\n" + "\nArguments:\n" + "1. \"tx\" (string, required) The serialized transaction previously returned by \"protx_register_prepare\"\n" + "2. \"sig\" (string, required only for transparent masternodes) The signature signed with the collateral key. Must be in base64 format.\n" + "\nResult:\n" + "\"txid\" (string) The transaction id.\n" + "\nExamples:\n" + + HelpExampleCli("protx_register_submit", "\"tx\" \"sig\"")); } CheckEvoUpgradeEnforcement(); @@ -603,7 +634,13 @@ UniValue protx_register_submit(const JSONRPCRequest& request) throw JSONRPCError(RPC_INVALID_PARAMETER, "payload signature not empty"); } - pl.vchSig = DecodeBase64(request.params[1].get_str().c_str()); + bool isShield = !pl.shieldCollateral.IsNull(); + if (!isShield) { + if (request.params.size() != 2) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "missing signature"); + } + pl.vchSig = DecodeBase64(request.params[1].get_str().c_str()); + } // check the payload, add the tx inputs sigs, and send the tx. return SignAndSendSpecialTx(pwallet, tx, pl); @@ -1067,9 +1104,9 @@ static const CRPCCommand commands[] = { "evo", "generateblskeypair", &generateblskeypair, true, {} }, { "evo", "protx_list", &protx_list, true, {"detailed","wallet_only","valid_only","height"} }, #ifdef ENABLE_WALLET - { "evo", "protx_register", &protx_register, true, {"collateralHash","collateralIndex","ipAndPort","ownerAddress","operatorPubKey","votingAddress","payoutAddress","operatorReward","operatorPayoutAddress"} }, + { "evo", "protx_register", &protx_register, true, {"collateralHash","collateralIndex", "transparent", "ipAndPort","ownerAddress","operatorPubKey","votingAddress","payoutAddress","operatorReward","operatorPayoutAddress"} }, { "evo", "protx_register_fund", &protx_register_fund, true, {"collateralAddress","ipAndPort","ownerAddress","operatorPubKey","votingAddress","payoutAddress","operatorReward","operatorPayoutAddress"} }, - { "evo", "protx_register_prepare", &protx_register_prepare, true, {"collateralHash","collateralIndex","ipAndPort","ownerAddress","operatorPubKey","votingAddress","payoutAddress","operatorReward","operatorPayoutAddress"} }, + { "evo", "protx_register_prepare", &protx_register_prepare, true, {"collateralHash","collateralIndex", "transparent", "ipAndPort","ownerAddress","operatorPubKey","votingAddress","payoutAddress","operatorReward","operatorPayoutAddress"} }, { "evo", "protx_register_submit", &protx_register_submit, true, {"tx","sig"} }, { "evo", "protx_revoke", &protx_revoke, true, {"proTxHash","operatorKey","reason"} }, { "evo", "protx_update_registrar", &protx_update_registrar, true, {"proTxHash","operatorPubKey","votingAddress","payoutAddress","ownerKey"} }, diff --git a/src/sapling/transaction_builder.cpp b/src/sapling/transaction_builder.cpp index 09ba15c20a638..113256e929e32 100644 --- a/src/sapling/transaction_builder.cpp +++ b/src/sapling/transaction_builder.cpp @@ -5,10 +5,11 @@ #include "sapling/transaction_builder.h" -#include "script/sign.h" -#include "utilmoneystr.h" #include "consensus/upgrades.h" #include "policy/policy.h" +#include "primitives/transaction.h" +#include "script/sign.h" +#include "utilmoneystr.h" #include "validation.h" #include @@ -447,3 +448,9 @@ TransactionBuilderResult TransactionBuilder::Build(bool fDummySig) return fDummySig ? AddDummySignatures() : ProveAndSign(); } + +// WARNING: This function must be used only for testing +TransactionBuilderResult TransactionBuilder::BuildWithoutConstraints() +{ + return ProveAndSign(); +} diff --git a/src/sapling/transaction_builder.h b/src/sapling/transaction_builder.h index 6de366cc4eb13..62d8b9601461d 100644 --- a/src/sapling/transaction_builder.h +++ b/src/sapling/transaction_builder.h @@ -137,6 +137,9 @@ class TransactionBuilder void SendChangeTo(const CTxDestination& changeAddr); + // WARNING: This function must be used only for testing + TransactionBuilderResult BuildWithoutConstraints(); + TransactionBuilderResult Build(bool fDummySig = false); // Add Sapling Spend/Output descriptions, binding sig, and transparent signatures TransactionBuilderResult ProveAndSign(); diff --git a/src/test/evo_deterministicmns_tests.cpp b/src/test/evo_deterministicmns_tests.cpp index 13083f936deb0..7112241d28b12 100644 --- a/src/test/evo_deterministicmns_tests.cpp +++ b/src/test/evo_deterministicmns_tests.cpp @@ -3,13 +3,18 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. -#include "test/test_pivx.h" + +#include "chainparams.h" +#include "consensus/validation.h" +#include "sapling/transaction_builder.h" +#include "sync.h" +#include "wallet/test/pos_test_fixture.h" #include "blockassembler.h" #include "consensus/merkle.h" #include "consensus/params.h" -#include "evo/specialtx_validation.h" #include "evo/deterministicmns.h" +#include "evo/specialtx_validation.h" #include "llmq/quorums_blockprocessor.h" #include "llmq/quorums_commitment.h" #include "llmq/quorums_utils.h" @@ -28,6 +33,14 @@ #include typedef std::map> SimpleUTXOMap; +class CWallet; + +enum class MasternodeType { + MN_EMPTY, + MN_TRANSPARENT, + MN_SHIELD, + MN_BOTH +}; // static 0.1 PIV fee used for the special txes in these tests static const CAmount fee = 10000000; @@ -135,18 +148,65 @@ static CBLSSecretKey GetRandomBLSKey() return sk; } +// Create a good/fake ProRegShieldProof +static bool CreateProRegShieldProof(ProRegPL& pl, const SaplingNoteEntry& note, CWallet* pwallet, CAmount amount, bool useRogueBuilder, bool emptySig) +{ + TransactionBuilder txBuilder(Params().GetConsensus(), pwallet); + txBuilder.SetFee(0); + libzcash::SaplingExtendedSpendingKey sk; + if (!pwallet->GetSaplingExtendedSpendingKey(note.address, sk)) { + return false; + } + uint256 anchor; + std::vector> witnesses; + std::vector noteop; + noteop.emplace_back(note.op); + pwallet->GetSaplingScriptPubKeyMan()->GetSaplingNoteWitnesses(noteop, witnesses, anchor); + txBuilder.AddSaplingSpend(sk.expsk, note.note, anchor, witnesses[0].get()); + auto newAddress = pwallet->getNewAddress(""); + if (!newAddress.getRes()) { + return false; + } + txBuilder.AddTransparentOutput(*newAddress.getObjResult(), amount); + auto txTrial = useRogueBuilder ? txBuilder.BuildWithoutConstraints() : txBuilder.Build(); + if (txTrial.IsError()) { + return false; + } + auto txFinal = *txTrial.GetTx(); + pl.shieldCollateral.input = (*txFinal.sapData).vShieldedSpend[0]; + pl.shieldCollateral.output = txFinal.vout[0]; + if (!emptySig) copy(begin((*txFinal.sapData).bindingSig), end((*txFinal.sapData).bindingSig), back_inserter(pl.shieldCollateral.bindingSig)); + return true; +} + // Creates a ProRegTx. // - if optCollateralOut is nullopt, generate a new collateral in the first output of the tx // - otherwise reference *optCollateralOut as external collateral static CMutableTransaction CreateProRegTx(Optional optCollateralOut, - SimpleUTXOMap& utxos, int port, const CScript& scriptPayout, const CKey& coinbaseKey, - const CKey& ownerKey, - const CBLSPublicKey& operatorPubKey, - uint16_t operatorReward = 0, - bool fInvalidCollateral = false) + SimpleUTXOMap& utxos, + int port, + const CScript& scriptPayout, + const CKey& coinbaseKey, + const CKey& ownerKey, + const CBLSPublicKey& operatorPubKey, + uint16_t operatorReward = 0, + bool fInvalidCollateral = false, + MasternodeType masternodeType = MasternodeType::MN_TRANSPARENT, + const SaplingNoteEntry* note = nullptr, + CWallet* pwallet = nullptr, + CAmount amount = Params().GetConsensus().nMNCollateralAmt, + bool useRogueBuilder = true, + bool emptySig = false) { ProRegPL pl; - pl.collateralOutpoint = (optCollateralOut ? *optCollateralOut : COutPoint(UINT256_ZERO, 0)); + if (masternodeType == MasternodeType::MN_TRANSPARENT) { + pl.collateralOutpoint = (optCollateralOut ? *optCollateralOut : COutPoint(UINT256_ZERO, 0)); + } else if (masternodeType == MasternodeType::MN_SHIELD) { + BOOST_CHECK(CreateProRegShieldProof(pl, *note, pwallet, amount, useRogueBuilder, emptySig)); + } else if (masternodeType == MasternodeType::MN_BOTH) { + pl.collateralOutpoint = (optCollateralOut ? *optCollateralOut : COutPoint(UINT256_ZERO, 0)); + BOOST_CHECK(CreateProRegShieldProof(pl, *note, pwallet, amount, useRogueBuilder, emptySig)); + } pl.addr = LookupNumeric("1.1.1.1", port); pl.keyIDOwner = ownerKey.GetPubKey().GetID(); pl.pubKeyOperator = operatorPubKey; @@ -157,10 +217,15 @@ static CMutableTransaction CreateProRegTx(Optional optCollateralOut, CMutableTransaction tx; tx.nVersion = CTransaction::TxVersion::SAPLING; tx.nType = CTransaction::TxType::PROREG; - FundTransaction(tx, utxos, scriptPayout, - GetScriptForDestination(coinbaseKey.GetPubKey().GetID()), - (optCollateralOut ? 0 : Params().GetConsensus().nMNCollateralAmt - (fInvalidCollateral ? 1 : 0))); - + if (masternodeType == MasternodeType::MN_TRANSPARENT) { + FundTransaction(tx, utxos, scriptPayout, + GetScriptForDestination(coinbaseKey.GetPubKey().GetID()), + (optCollateralOut ? 0 : Params().GetConsensus().nMNCollateralAmt - (fInvalidCollateral ? 1 : 0))); + } else { + FundTransaction(tx, utxos, scriptPayout, + GetScriptForDestination(coinbaseKey.GetPubKey().GetID()), + 0); + } pl.inputsHash = CalcTxInputsHash(tx); SetTxPayload(tx, pl); SignTransaction(tx, coinbaseKey); @@ -913,6 +978,294 @@ BOOST_FIXTURE_TEST_CASE(dip3_protx, TestChain400Setup) UpdateNetworkUpgradeParameters(Consensus::UPGRADE_V6_0, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); } +void ProcessBlockAndUpdateUtxos(std::shared_ptr pblock, SimpleUTXOMap& utxos) +{ + BOOST_CHECK(ProcessNewBlock(pblock, nullptr)); + for (auto& vtx : pblock.get()->vtx) { + for (auto& vin : vtx->vin) { + int count = utxos.count(vin.prevout); + if (count > 0) { + BOOST_ASSERT(count == 1); + utxos.erase(vin.prevout); + } + } + } +} + +BOOST_FIXTURE_TEST_CASE(dip3_shield, TestPoSChainSetup) +{ + // Verify that we are at block 251 + int nHeight = WITH_LOCK(cs_main, return chainActive.Tip()->nHeight); + BOOST_CHECK_EQUAL(nHeight, 250); + SyncWithValidationInterfaceQueue(); + + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_V5_0, nHeight + 1); + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_V6_0, nHeight + 2); + + auto utxos = BuildSimpleUtxoMap(coinbaseTxns); + // Disable masterode sync so we won't have problems with missing mn payments for now + g_tiertwo_sync_state.SetCurrentSyncPhase(MASTERNODE_SYNC_INITIAL); + // Mint the block before v6 + { + std::shared_ptr pblock = CreateBlockInternal(pwalletMain.get()); + ProcessBlockAndUpdateUtxos(pblock, utxos); + nHeight += 1; + } + + // Create a pro reg tx and check its validity + int port = 1; + const CKey& ownerKeyTransp = GetRandomKey(); + const CBLSSecretKey& operatorKeyTransp = GetRandomBLSKey(); + auto tx = CreateProRegTx(nullopt, utxos, port++, GenerateRandomAddress(), coinbaseKey, ownerKeyTransp, operatorKeyTransp.GetPublicKey()); + + CValidationState dummyState; + CBlockIndex* chainTip = chainActive.Tip(); + CCoinsViewCache* view = pcoinsTip.get(); + + BOOST_CHECK(WITH_LOCK(cs_main, return CheckSpecialTx(tx, chainTip, view, dummyState);)); + BOOST_CHECK(CheckTransactionSignature(tx)); + + // Commit the transaction, mint another block and the DMN should be on chain! + CReserveKey reservekey(&*pwalletMain); + pwalletMain->CommitTransaction(MakeTransactionRef(tx), reservekey, nullptr); + std::shared_ptr pblock = CreateBlockInternal(pwalletMain.get(), {tx}, nullptr, {}, true, true); + ProcessBlockAndUpdateUtxos(pblock, utxos); + BOOST_CHECK(deterministicMNManager->GetListAtChainTip().HasMN(tx.GetHash())); + nHeight += 1; + + // force mnsync complete and enable spork 8 + { + g_tiertwo_sync_state.SetCurrentSyncPhase(MASTERNODE_SYNC_FINISHED); + int64_t nTime = GetTime() - 10; + const CSporkMessage& sporkMnPayment = CSporkMessage(SPORK_8_MASTERNODE_PAYMENT_ENFORCEMENT, nTime + 1, nTime); + sporkManager.AddOrUpdateSporkMessage(sporkMnPayment); + BOOST_CHECK(sporkManager.IsSporkActive(SPORK_8_MASTERNODE_PAYMENT_ENFORCEMENT)); + } + + // Let's create a few more PoS blocks + for (int i = 0; i < 10; i++) { + std::shared_ptr pblock = CreateBlockInternal(pwalletMain.get()); + ProcessBlockAndUpdateUtxos(pblock, utxos); + nHeight += 1; + } + + // Create a bunch of sapling notes, with which we will create our SDMNs + for (int i = 0; i < 2; i++) { + SaplingOperation operation = CreateOperationAndBuildTx(pwalletMain, Params().GetConsensus().nMNCollateralAmt, true); + pwalletMain->CommitTransaction(operation.getFinalTxRef(), reservekey, nullptr); + } + std::shared_ptr shieldBlockNormal = CreateBlockInternal(pwalletMain.get(), {}, nullptr, {}, false, true); + ProcessBlockAndUpdateUtxos(shieldBlockNormal, utxos); + nHeight += 1; + { + SaplingOperation operation = CreateOperationAndBuildTx(pwalletMain, Params().GetConsensus().nMNCollateralAmt * 2, true); + pwalletMain->CommitTransaction(operation.getFinalTxRef(), reservekey, nullptr); + } + std::shared_ptr shieldBlockBig = CreateBlockInternal(pwalletMain.get(), {}, nullptr, {}, false, true); + ProcessBlockAndUpdateUtxos(shieldBlockBig, utxos); + nHeight += 1; + { + SaplingOperation operation = CreateOperationAndBuildTx(pwalletMain, Params().GetConsensus().nMNCollateralAmt * 0.5, true); + pwalletMain->CommitTransaction(operation.getFinalTxRef(), reservekey, nullptr); + } + std::shared_ptr shieldBlockSmall = CreateBlockInternal(pwalletMain.get(), {}, nullptr, {}, false, true); + ProcessBlockAndUpdateUtxos(shieldBlockSmall, utxos); + nHeight += 1; + + // Sanity check on the blocks created + BOOST_CHECK(shieldBlockNormal->vtx.size() == 4); + BOOST_CHECK(shieldBlockNormal->vtx[2]->sapData->vShieldedOutput.size() == 1 && shieldBlockNormal->vtx[3]->sapData->vShieldedOutput.size() == 1); + BOOST_CHECK(shieldBlockBig->vtx.size() == 3); + BOOST_CHECK(shieldBlockBig->vtx[2]->sapData->vShieldedOutput.size() == 1); + BOOST_CHECK(shieldBlockSmall->vtx.size() == 3); + BOOST_CHECK(shieldBlockSmall->vtx[2]->sapData->vShieldedOutput.size() == 1); + + // Finally create a SDMN, or at least try! spork21 is not active yet + const CKey& ownerKeyShield = GetRandomKey(); + const CBLSSecretKey& operatorKeyShield = GetRandomBLSKey(); + std::vector collateralNote1; + pwalletMain.get()->GetSaplingScriptPubKeyMan()->GetNotes({SaplingOutPoint(shieldBlockNormal->vtx[2]->GetHash(), 0)}, collateralNote1); + BOOST_CHECK(collateralNote1.size() == 1); + + std::vector collateralNote2; + pwalletMain.get()->GetSaplingScriptPubKeyMan()->GetNotes({SaplingOutPoint(shieldBlockNormal->vtx[3]->GetHash(), 0)}, collateralNote2); + BOOST_CHECK(collateralNote2.size() == 1); + CMutableTransaction txShield; + { + txShield = CreateProRegTx(nullopt, utxos, port++, GenerateRandomAddress(), coinbaseKey, ownerKeyShield, operatorKeyShield.GetPublicKey(), 0, false, MasternodeType::MN_SHIELD, &collateralNote1[0], pwalletMain.get()); + CValidationState state; + CBlockIndex* chainTip = chainActive.Tip(); + CCoinsViewCache* view = pcoinsTip.get(); + WITH_LOCK(cs_main, return CheckSpecialTx(txShield, chainTip, view, state);); + BOOST_CHECK_EQUAL(state.GetRejectReason(), "spork-21-inactive"); + + // Finally activate spork 21 and recheck the tx + int64_t nTime = GetTime() - 10; + const CSporkMessage& sporkLegacyObs = CSporkMessage(SPORK_21_LEGACY_MNS_MAX_HEIGHT, nHeight, nTime); + sporkManager.AddOrUpdateSporkMessage(sporkLegacyObs); + BOOST_CHECK(sporkManager.IsSporkActive(SPORK_21_LEGACY_MNS_MAX_HEIGHT)); + + state = CValidationState(); + BOOST_CHECK(WITH_LOCK(cs_main, return CheckSpecialTx(txShield, chainTip, view, dummyState);)); + BOOST_CHECK(CheckTransactionSignature(txShield)); + + pwalletMain->CommitTransaction(MakeTransactionRef(txShield), reservekey, nullptr); + std::shared_ptr pblock = CreateBlockInternal(pwalletMain.get(), {txShield}, nullptr, {}, true, true); + ProcessBlockAndUpdateUtxos(pblock, utxos); + nHeight += 1; + BOOST_CHECK(deterministicMNManager->GetListAtChainTip().HasMN(txShield.GetHash())); + } + + // Next, verify payments in this mixture of transparent and shield masternodes: + std::map mapPayments; + for (size_t i = 0; i < 20; i++) { + auto mnList = deterministicMNManager->GetListAtChainTip(); + BOOST_CHECK_EQUAL(mnList.GetValidMNsCount(), 2); + BOOST_CHECK_EQUAL(mnList.GetHeight(), nHeight); + + // get next payee + auto dmnExpectedPayee = mnList.GetMNPayee(); + std::shared_ptr block = CreateBlockInternal(pwalletMain.get()); + ProcessBlockAndUpdateUtxos(block, utxos); + chainTip = chainActive.Tip(); + BOOST_ASSERT(!block.get()->vtx.empty()); + BOOST_CHECK(IsMNPayeeInBlock(*block.get(), dmnExpectedPayee->pdmnState->scriptPayout)); + mapPayments[dmnExpectedPayee->proTxHash]++; + BOOST_CHECK_EQUAL(chainTip->nHeight, ++nHeight); + } + // 20 blocks, 2 masternodes. Must have been paid 10 times each. + CheckPayments(mapPayments, 2, 10); + + // Try to register another SDMN with the same address of the first one: + { + auto tx = CreateProRegTx(nullopt, utxos, port - 1, GenerateRandomAddress(), coinbaseKey, GetRandomKey(), GetRandomBLSKey().GetPublicKey(), 0, false, MasternodeType::MN_SHIELD, &collateralNote2[0], pwalletMain.get()); + CValidationState state; + CBlockIndex* chainTip = chainActive.Tip(); + CCoinsViewCache* view = pcoinsTip.get(); + WITH_LOCK(cs_main, return CheckSpecialTx(tx, chainTip, view, state);); + BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-protx-dup-IP-address"); + } + + // Try to register another SDMN with the same operator key of the first one: + { + auto tx = CreateProRegTx(nullopt, utxos, port, GenerateRandomAddress(), coinbaseKey, GetRandomKey(), operatorKeyShield.GetPublicKey(), 0, false, MasternodeType::MN_SHIELD, &collateralNote2[0], pwalletMain.get()); + CValidationState state; + CBlockIndex* chainTip = chainActive.Tip(); + CCoinsViewCache* view = pcoinsTip.get(); + WITH_LOCK(cs_main, return CheckSpecialTx(tx, chainTip, view, state);); + BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-protx-dup-operator-key"); + } + + // Try to register another SDMN with the same owner key of the first one: + { + auto tx = CreateProRegTx(nullopt, utxos, port, GenerateRandomAddress(), coinbaseKey, ownerKeyShield, GetRandomBLSKey().GetPublicKey(), 0, false, MasternodeType::MN_SHIELD, &collateralNote2[0], pwalletMain.get()); + CValidationState state; + CBlockIndex* chainTip = chainActive.Tip(); + CCoinsViewCache* view = pcoinsTip.get(); + WITH_LOCK(cs_main, return CheckSpecialTx(tx, chainTip, view, state);); + BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-protx-dup-owner-key"); + } + + // Try to create a SDMN with incorrect input note (note value too big) + std::vector collateralNoteBig1; + pwalletMain.get()->GetSaplingScriptPubKeyMan()->GetNotes({SaplingOutPoint(shieldBlockBig->vtx[2]->GetHash(), 0)}, collateralNoteBig1); + BOOST_CHECK(collateralNoteBig1.size() == 1); + { + auto tx = CreateProRegTx(nullopt, utxos, port, GenerateRandomAddress(), coinbaseKey, GetRandomKey(), GetRandomBLSKey().GetPublicKey(), 0, false, MasternodeType::MN_SHIELD, &collateralNoteBig1[0], pwalletMain.get(), Params().GetConsensus().nMNCollateralAmt); + CValidationState state; + CBlockIndex* chainTip = chainActive.Tip(); + CCoinsViewCache* view = pcoinsTip.get(); + WITH_LOCK(cs_main, return CheckSpecialTx(tx, chainTip, view, state);); + BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-txns-sapling-spend-description-invalid"); + } + + // Try to create a SDMN with correct input note, but broken proof (wrong output amount) + { + auto tx = CreateProRegTx(nullopt, utxos, port, GenerateRandomAddress(), coinbaseKey, GetRandomKey(), GetRandomBLSKey().GetPublicKey(), 0, false, MasternodeType::MN_SHIELD, &collateralNote2[0], pwalletMain.get(), Params().GetConsensus().nMNCollateralAmt - 1); + CValidationState state; + CBlockIndex* chainTip = chainActive.Tip(); + CCoinsViewCache* view = pcoinsTip.get(); + WITH_LOCK(cs_main, return CheckSpecialTx(tx, chainTip, view, state);); + BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-protx-proofCollateralOutput-amt"); + } + + // Try to create a SDMN with incorrect input note (note value too small) + std::vector collateralNoteSmall1; + pwalletMain.get()->GetSaplingScriptPubKeyMan()->GetNotes({SaplingOutPoint(shieldBlockSmall->vtx[2]->GetHash(), 0)}, collateralNoteSmall1); + BOOST_CHECK(collateralNoteSmall1.size() == 1); + { + auto tx = CreateProRegTx(nullopt, utxos, port, GenerateRandomAddress(), coinbaseKey, GetRandomKey(), GetRandomBLSKey().GetPublicKey(), 0, false, MasternodeType::MN_SHIELD, &collateralNoteSmall1[0], pwalletMain.get(), Params().GetConsensus().nMNCollateralAmt); + CValidationState state; + CBlockIndex* chainTip = chainActive.Tip(); + CCoinsViewCache* view = pcoinsTip.get(); + WITH_LOCK(cs_main, return CheckSpecialTx(tx, chainTip, view, state);); + BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-txns-sapling-spend-description-invalid"); + } + + // Try to create a SDMN by using a big note as input but this time get a shield change as additional output (200 -> 100 + 100) + { + auto tx = CreateProRegTx(nullopt, utxos, port, GenerateRandomAddress(), coinbaseKey, GetRandomKey(), GetRandomBLSKey().GetPublicKey(), 0, false, MasternodeType::MN_SHIELD, &collateralNoteBig1[0], pwalletMain.get(), Params().GetConsensus().nMNCollateralAmt, false); + CValidationState state; + CBlockIndex* chainTip = chainActive.Tip(); + CCoinsViewCache* view = pcoinsTip.get(); + WITH_LOCK(cs_main, return CheckSpecialTx(tx, chainTip, view, state);); + BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-txns-sapling-spend-description-invalid"); + } + + // Try to create a SDMN with a wrong bindig sig length: + { + auto tx = CreateProRegTx(nullopt, utxos, port, GenerateRandomAddress(), coinbaseKey, GetRandomKey(), GetRandomBLSKey().GetPublicKey(), 0, false, MasternodeType::MN_SHIELD, &collateralNote2[0], pwalletMain.get(), Params().GetConsensus().nMNCollateralAmt, false, true); + CValidationState state; + CBlockIndex* chainTip = chainActive.Tip(); + CCoinsViewCache* view = pcoinsTip.get(); + WITH_LOCK(cs_main, return CheckSpecialTx(tx, chainTip, view, state);); + BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-protx-bindingSig-len"); + } + + // Try to create a Masternode with both normal collateral and shield collateral non-empty: + { + auto tx = CreateProRegTx(nullopt, utxos, port, GenerateRandomAddress(), coinbaseKey, GetRandomKey(), GetRandomBLSKey().GetPublicKey(), 0, false, MasternodeType::MN_BOTH, &collateralNote2[0], pwalletMain.get(), Params().GetConsensus().nMNCollateralAmt, false, true); + CValidationState state; + CBlockIndex* chainTip = chainActive.Tip(); + CCoinsViewCache* view = pcoinsTip.get(); + WITH_LOCK(cs_main, return CheckSpecialTx(tx, chainTip, view, state);); + BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-protx-collaterals-non-empty"); + } + + // Try to create a Masternode with both normal collateral and shield collateral empty: + { + auto tx = CreateProRegTx(nullopt, utxos, port, GenerateRandomAddress(), coinbaseKey, GetRandomKey(), GetRandomBLSKey().GetPublicKey(), 0, false, MasternodeType::MN_EMPTY, &collateralNote2[0], pwalletMain.get(), Params().GetConsensus().nMNCollateralAmt, false, true); + CValidationState state; + CBlockIndex* chainTip = chainActive.Tip(); + CCoinsViewCache* view = pcoinsTip.get(); + WITH_LOCK(cs_main, return CheckSpecialTx(tx, chainTip, view, state);); + BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-protx-collaterals-empty"); + } + + // Spend ALL shield notes (4.3 masternode cost instead of 4.5 to pay fees): + { + WITH_LOCK(pwalletMain->cs_wallet, pwalletMain->UnlockAllNotes()); + SaplingOperation operation = CreateOperationAndBuildTx(pwalletMain, Params().GetConsensus().nMNCollateralAmt * 4.3, false); + pwalletMain->CommitTransaction(operation.getFinalTxRef(), reservekey, nullptr); + std::shared_ptr pblock = CreateBlockInternal(pwalletMain.get(), {}, nullptr, {}, false, true); + ProcessBlockAndUpdateUtxos(pblock, utxos); + nHeight += 1; + } + + // Verify that the SDMN is gone + BOOST_CHECK(!deterministicMNManager->GetListAtChainTip().HasMN(txShield.GetHash())); + + // Try to create a SDMN with a spent collateral: + { + auto tx = CreateProRegTx(nullopt, utxos, port, GenerateRandomAddress(), coinbaseKey, GetRandomKey(), GetRandomBLSKey().GetPublicKey(), 0, false, MasternodeType::MN_SHIELD, &collateralNote2[0], pwalletMain.get(), Params().GetConsensus().nMNCollateralAmt, false, false); + CValidationState state; + CBlockIndex* chainTip = chainActive.Tip(); + CCoinsViewCache* view = pcoinsTip.get(); + WITH_LOCK(cs_main, return CheckSpecialTx(tx, chainTip, view, state);); + BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-txns-shielded-requirements-not-met"); + } +} + // Dummy commitment where the DKG shares are replaced with the operator keys of each member. // members at index skeys.size(), ..., llmqType.size - 1 are invalid static llmq::CFinalCommitment CreateFinalCommitment(std::vector& pkeys, diff --git a/src/txmempool.cpp b/src/txmempool.cpp index dbda5c39fdf30..591526a946c76 100644 --- a/src/txmempool.cpp +++ b/src/txmempool.cpp @@ -374,8 +374,14 @@ void CTxMemPool::addUncheckedSpecialTx(const CTransaction& tx) bool ok = GetTxPayload(tx, pl); assert(ok); if (!pl.collateralOutpoint.hash.IsNull()) { - mapProTxRefs.emplace(txid, pl.collateralOutpoint.hash); - mapProTxCollaterals.emplace(pl.collateralOutpoint, txid); + // TODO: recheck this change (but I'm pretty sure that's the right way ) + mapProTxRefs.emplace(txid, txid); + mapProTxCollaterals.emplace(pl.collateralOutpoint, txid); + } + if (!pl.shieldCollateral.IsNull()) { + // TODO: recheck this (but I'm pretty sure that's the right way ) + mapProTxRefs.emplace(txid, txid); + mapProTxNullifiers.emplace(pl.shieldCollateral.input.nullifier, txid); } mapProTxAddresses.emplace(pl.addr, txid); mapProTxPubKeyIDs.emplace(pl.keyIDOwner, txid); @@ -500,8 +506,11 @@ void CTxMemPool::removeUncheckedSpecialTx(const CTransaction& tx) assert(ok); if (!pl.collateralOutpoint.IsNull()) { eraseProTxRef(txid, pl.collateralOutpoint.hash); + mapProTxCollaterals.erase(pl.collateralOutpoint); + } + if (!pl.shieldCollateral.IsNull()) { + mapProTxNullifiers.erase(pl.shieldCollateral.input.nullifier); } - mapProTxCollaterals.erase(pl.collateralOutpoint); mapProTxAddresses.erase(pl.addr); mapProTxPubKeyIDs.erase(pl.keyIDOwner); mapProTxBlsPubKeyHashes.erase(pl.pubKeyOperator.GetHash()); @@ -745,6 +754,16 @@ void CTxMemPool::removeProTxCollateralConflicts(const CTransaction &tx, const CO } } +void CTxMemPool::removeProTxNullifiterConflicts(const CTransaction& tx, const uint256& nullifier) +{ + if (mapProTxNullifiers.count(nullifier)) { + const uint256& conflictHash = mapProTxNullifiers.at(nullifier); + if (conflictHash != tx.GetHash() && mapTx.count(conflictHash)) { + removeRecursive(mapTx.find(conflictHash)->GetTx(), MemPoolRemovalReason::CONFLICT); + } + } +} + void CTxMemPool::removeProTxReferences(const uint256& proTxHash, MemPoolRemovalReason reason) { // Remove TXs that refer to a certain MN @@ -769,6 +788,7 @@ void CTxMemPool::removeProTxSpentCollateralConflicts(const CTransaction &tx) { auto mnList = deterministicMNManager->GetListAtChainTip(); for (const auto& in : tx.vin) { + if (in.prevout.IsNull()) continue; auto collateralIt = mapProTxCollaterals.find(in.prevout); if (collateralIt != mapProTxCollaterals.end()) { // These are not yet mined ProRegTxs @@ -782,9 +802,28 @@ void CTxMemPool::removeProTxSpentCollateralConflicts(const CTransaction &tx) } } +void CTxMemPool::removeProTxSpentNullifierConflicts(const CTransaction& tx) +{ + if (!tx.hasSaplingData()) return; + auto mnList = deterministicMNManager->GetListAtChainTip(); + for (const auto& in : (*tx.sapData).vShieldedSpend) { + auto nullifierIt = mapProTxNullifiers.find(in.nullifier); + if (nullifierIt != mapProTxNullifiers.end()) { + // These are not yet mined ProRegTxs + removeProTxReferences(nullifierIt->second, MemPoolRemovalReason::CONFLICT); + } + auto dmn = mnList.GetMNByNullifier(in.nullifier); + if (dmn) { + // These are updates refering to a mined ProRegTx + removeProTxReferences(dmn->proTxHash, MemPoolRemovalReason::CONFLICT); + } + } +} + void CTxMemPool::removeProTxConflicts(const CTransaction &tx) { removeProTxSpentCollateralConflicts(tx); + removeProTxSpentNullifierConflicts(tx); if (!tx.IsSpecialTx()) return; @@ -807,6 +846,9 @@ void CTxMemPool::removeProTxConflicts(const CTransaction &tx) if (!pl.collateralOutpoint.hash.IsNull()) { removeProTxCollateralConflicts(tx, pl.collateralOutpoint); } + if (!pl.shieldCollateral.IsNull()) { + removeProTxNullifiterConflicts(tx, pl.shieldCollateral.input.nullifier); + } break; } @@ -1170,6 +1212,16 @@ bool CTxMemPool::existsProviderTxConflict(const CTransaction &tx) const return true; } } + if (!pl.shieldCollateral.IsNull()) { + if (mapProTxNullifiers.count(pl.shieldCollateral.input.nullifier)) { + // there is another ProRegTx that refers to the same shield collateral + return true; + } + if (mapSaplingNullifiers.count(pl.shieldCollateral.input.nullifier)) { + // there is another shield tx that spends the collateral + return true; + } + } return false; } diff --git a/src/txmempool.h b/src/txmempool.h index acf8e873ae6e1..04f0e0b49cd49 100644 --- a/src/txmempool.h +++ b/src/txmempool.h @@ -499,6 +499,7 @@ class CTxMemPool std::map mapProTxPubKeyIDs; std::map mapProTxBlsPubKeyHashes; std::map mapProTxCollaterals; + std::map mapProTxNullifiers; void UpdateParent(txiter entry, txiter parent, bool add); void UpdateChild(txiter entry, txiter child, bool add); @@ -716,7 +717,9 @@ class CTxMemPool void removeProTxPubKeyConflicts(const CTransaction &tx, const CKeyID &keyId); void removeProTxPubKeyConflicts(const CTransaction& tx, const CBLSPublicKey& pubKey); void removeProTxCollateralConflicts(const CTransaction &tx, const COutPoint &collateralOutpoint); + void removeProTxNullifiterConflicts(const CTransaction& tx, const uint256& nullifier); void removeProTxSpentCollateralConflicts(const CTransaction &tx); + void removeProTxSpentNullifierConflicts(const CTransaction& tx); void removeProTxConflicts(const CTransaction &tx); }; diff --git a/src/wallet/test/pos_test_fixture.cpp b/src/wallet/test/pos_test_fixture.cpp index 3a8d4260235eb..381008c3c67ca 100644 --- a/src/wallet/test/pos_test_fixture.cpp +++ b/src/wallet/test/pos_test_fixture.cpp @@ -3,8 +3,13 @@ // file COPYING or https://www.opensource.org/licenses/mit-license.php. #include "wallet/test/pos_test_fixture.h" +#include "consensus/merkle.h" #include "wallet/wallet.h" +#include "blockassembler.h" +#include "blocksignature.h" +#include "sapling/address.h" + #include TestPoSChainSetup::TestPoSChainSetup() : TestChainSetup(0) @@ -38,3 +43,83 @@ TestPoSChainSetup::~TestPoSChainSetup() SyncWithValidationInterfaceQueue(); UnregisterValidationInterface(pwalletMain.get()); } + +// Util functions that are useful in all unit tests that use this POS chain +// Create a sapling operation that can build a shield tx +SaplingOperation CreateOperationAndBuildTx(std::unique_ptr& pwallet, + CAmount amount, + bool selectTransparentCoins) +{ + // Create the operation + libzcash::SaplingPaymentAddress pa = pwallet->GenerateNewSaplingZKey("s1"); + std::vector recipients; + recipients.emplace_back(pa, amount, "", false); + SaplingOperation operation(Params().GetConsensus(), pwallet.get()); + operation.setMinDepth(1); + auto operationResult = operation.setRecipients(recipients) + ->setSelectTransparentCoins(selectTransparentCoins) + ->setSelectShieldedCoins(!selectTransparentCoins) + ->build(); + BOOST_ASSERT_MSG(operationResult, operationResult.getError().c_str()); + + CValidationState state; + BOOST_ASSERT_MSG( + CheckTransaction(operation.getFinalTx(), state, true), + "Invalid Sapling transaction"); + return operation; +} + +bool IsSpentOnFork(const COutput& coin, std::initializer_list> forkchain) +{ + for (const auto& block : forkchain) { + const auto& usedOutput = block->vtx[1]->vin.at(0).prevout; + if (coin.tx->GetHash() == usedOutput.hash && coin.i == (int)usedOutput.n) { + // spent on fork + return true; + } + } + return false; +} + +std::shared_ptr CreateBlockInternal(CWallet* pwalletMain, const std::vector& txns, CBlockIndex* customPrevBlock, std::initializer_list> forkchain, bool fNoMempoolTx, bool testValidity) +{ + std::vector availableCoins; + BOOST_CHECK(pwalletMain->StakeableCoins(&availableCoins)); + + // Remove any utxo which is not deeper than 120 blocks (for the same reasoning + // used when selecting tx inputs in CreateAndCommitTx) + // Also, as the wallet is not prepared to follow several chains at the same time, + // need to manually remove from the stakeable utxo set every already used + // coinstake inputs on the previous blocks of the parallel chain so they + // are not used again. + for (auto it = availableCoins.begin(); it != availableCoins.end();) { + if (it->nDepth <= 120 || IsSpentOnFork(*it, forkchain)) { + it = availableCoins.erase(it); + } else { + it++; + } + } + + std::unique_ptr pblocktemplate = BlockAssembler( + Params(), false) + .CreateNewBlock(CScript(), + pwalletMain, + true, + &availableCoins, + fNoMempoolTx, + testValidity, + customPrevBlock, + false); + BOOST_ASSERT(pblocktemplate); + auto pblock = std::make_shared(pblocktemplate->block); + if (!txns.empty()) { + for (const auto& tx : txns) { + pblock->vtx.emplace_back(MakeTransactionRef(tx)); + } + pblock->hashMerkleRoot = BlockMerkleRoot(*pblock); + const int nHeight = (customPrevBlock != nullptr ? customPrevBlock->nHeight + 1 : WITH_LOCK(cs_main, return chainActive.Height()) + 1); + pblock->hashFinalSaplingRoot = CalculateSaplingTreeRoot(&*pblock, nHeight, Params()); + assert(SignBlock(*pblock, *pwalletMain)); + } + return pblock; +} diff --git a/src/wallet/test/pos_test_fixture.h b/src/wallet/test/pos_test_fixture.h index 433e1d39d8530..1b7cbd548d56f 100644 --- a/src/wallet/test/pos_test_fixture.h +++ b/src/wallet/test/pos_test_fixture.h @@ -7,6 +7,9 @@ #include "test/test_pivx.h" + +#include "sapling/sapling_operation.h" + class CWallet; /* @@ -21,4 +24,10 @@ struct TestPoSChainSetup: public TestChainSetup ~TestPoSChainSetup(); }; +SaplingOperation CreateOperationAndBuildTx(std::unique_ptr& pwallet, + CAmount amount, + bool selectTransparentCoins); +bool IsSpentOnFork(const COutput& coin, std::initializer_list> forkchain = {}); +std::shared_ptr CreateBlockInternal(CWallet* pwalletMain, const std::vector& txns = {}, CBlockIndex* customPrevBlock = nullptr, std::initializer_list> forkchain = {}, bool fNoMempoolTx = true, bool testValidity = false); + #endif // PIVX_POS_TEST_FIXTURE_H diff --git a/src/wallet/test/pos_validations_tests.cpp b/src/wallet/test/pos_validations_tests.cpp index e07d89d1c5882..706e596f8dd4a 100644 --- a/src/wallet/test/pos_validations_tests.cpp +++ b/src/wallet/test/pos_validations_tests.cpp @@ -126,60 +126,6 @@ COutPoint GetOutpointWithAmount(const CTransaction& tx, CAmount outpointValue) return {}; } -static bool IsSpentOnFork(const COutput& coin, std::initializer_list> forkchain = {}) -{ - for (const auto& block : forkchain) { - const auto& usedOutput = block->vtx[1]->vin.at(0).prevout; - if (coin.tx->GetHash() == usedOutput.hash && coin.i == (int)usedOutput.n) { - // spent on fork - return true; - } - } - return false; -} - -std::shared_ptr CreateBlockInternal(CWallet* pwalletMain, const std::vector& txns = {}, - CBlockIndex* customPrevBlock = nullptr, - std::initializer_list> forkchain = {}) -{ - std::vector availableCoins; - BOOST_CHECK(pwalletMain->StakeableCoins(&availableCoins)); - - // Remove any utxo which is not deeper than 120 blocks (for the same reasoning - // used when selecting tx inputs in CreateAndCommitTx) - // Also, as the wallet is not prepared to follow several chains at the same time, - // need to manually remove from the stakeable utxo set every already used - // coinstake inputs on the previous blocks of the parallel chain so they - // are not used again. - for (auto it = availableCoins.begin(); it != availableCoins.end() ;) { - if (it->nDepth <= 120 || IsSpentOnFork(*it, forkchain)) { - it = availableCoins.erase(it); - } else { - it++; - } - } - - std::unique_ptr pblocktemplate = BlockAssembler( - Params(), false).CreateNewBlock(CScript(), - pwalletMain, - true, - &availableCoins, - true, - false, - customPrevBlock, - false); - BOOST_ASSERT(pblocktemplate); - auto pblock = std::make_shared(pblocktemplate->block); - if (!txns.empty()) { - for (const auto& tx : txns) { - pblock->vtx.emplace_back(MakeTransactionRef(tx)); - } - pblock->hashMerkleRoot = BlockMerkleRoot(*pblock); - assert(SignBlock(*pblock, *pwalletMain)); - } - return pblock; -} - static COutput GetUnspentCoin(CWallet* pwallet, std::initializer_list> forkchain = {}) { std::vector availableCoins; diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 5774b168454ff..fbf8c4367f3db 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -6,6 +6,7 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include "optional.h" +#include "uint256.h" #if defined(HAVE_CONFIG_H) #include "config/pivx-config.h" #endif @@ -4204,14 +4205,29 @@ void CWallet::LockOutpointIfMine(const CTransactionRef& ptx, const COutPoint& c) } } +void CWallet::LockNullifierIfMine(uint256 nullifier) +{ + AssertLockHeld(cs_wallet); + if (m_sspk_man->mapSaplingNullifiersToNotes.count(nullifier)) { + LockNote(m_sspk_man->mapSaplingNullifiersToNotes.at(nullifier)); + } +} + // Called from AddToWalletIfInvolvingMe void CWallet::LockIfMyCollateral(const CTransactionRef& ptx) { AssertLockHeld(cs_wallet); - COutPoint o; - if (GetProRegCollateral(ptx, o)) { - LockOutpointIfMine(ptx, o); + if (!IsShieldProReg(ptx)) { + COutPoint o; + if (GetProRegCollateral(ptx, o)) { + LockOutpointIfMine(ptx, o); + } + } else { + uint256 nullifier; + if (GetProRegNullifier(ptx, nullifier)) { + LockNullifierIfMine(nullifier); + } } } diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index b85b3e287b2fd..23d37d2d8141d 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -904,6 +904,7 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface */ void LockOutpointIfMineWithMutex(const CTransactionRef& ptx, const COutPoint& c); + void LockNullifierIfMine(uint256 nullifier); /* * Requires cs_wallet lock. * Called from AddToWalletIfInvolvingMe. If ptx is a ProRegTx, and the diff --git a/test/functional/test_framework/budget_util.py b/test/functional/test_framework/budget_util.py new file mode 100644 index 0000000000000..9402ba47e2770 --- /dev/null +++ b/test/functional/test_framework/budget_util.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +# Copyright (c) 2023 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/messages.py b/test/functional/test_framework/messages.py index fb727d925243e..16bfe4c5a570a 100755 --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -350,11 +350,12 @@ def __repr__(self): class COutPoint: - __slots__ = ("hash", "n") + __slots__ = ("hash", "n", "transparent") - def __init__(self, hash=0, n=0): + def __init__(self, hash=0, n=0, transparent=True): self.hash = hash self.n = n + self.transparent = transparent def deserialize(self, f): self.hash = deser_uint256(f) @@ -380,7 +381,8 @@ def __repr__(self): return "COutPoint(hash=%064x n=%i)" % (self.hash, self.n) def to_json(self): - return {"txid": "%064x" % self.hash, "vout": self.n} + voutStr = "vout" if self.transparent else "vShieldedOutput" + return {"txid": "%064x" % self.hash, voutStr: self.n} NullOutPoint = COutPoint(0, 0xffffffff) @@ -1575,10 +1577,12 @@ def serialize(self): # PIVX Classes +# NB: for shielded masternode the field collateral is the ShieldOutPoint of the shield collateral +# notice the difference from the ProRegTx in which the collateral is the Null default value class Masternode(object): - __slots__ = ("idx", "owner", "operator_pk", "voting", "ipport", "payee", "operator_sk", "proTx", "collateral") + __slots__ = ("idx", "owner", "operator_pk", "voting", "ipport", "payee", "operator_sk", "proTx", "collateral", "nullifier", "transparent") - def __init__(self, idx, owner_addr, operator_pk, voting_addr, ipport, payout_addr, operator_sk): + def __init__(self, idx, owner_addr, operator_pk, voting_addr, ipport, payout_addr, operator_sk, transparent): self.idx = idx self.owner = owner_addr self.operator_pk = operator_pk @@ -1588,6 +1592,8 @@ def __init__(self, idx, owner_addr, operator_pk, voting_addr, ipport, payout_add self.operator_sk = operator_sk self.proTx = None self.collateral = None + self.nullifier = "%064x" % 0 if transparent else None + self.transparent = transparent def revoked(self): self.ipport = "[::]:0" @@ -1595,9 +1601,9 @@ def revoked(self): self.operator_sk = None def __repr__(self): - return "Masternode(idx=%d, owner=%s, operator=%s, voting=%s, ip=%s, payee=%s, opkey=%s, protx=%s, collateral=%s)" % ( + return "Masternode(idx=%d, owner=%s, operator=%s, voting=%s, ip=%s, payee=%s, opkey=%s, protx=%s, collateral=%s, transparent=%s)" % ( self.idx, str(self.owner), str(self.operator_pk), str(self.voting), str(self.ipport), - str(self.payee), str(self.operator_sk), str(self.proTx), str(self.collateral) + str(self.payee), str(self.operator_sk), str(self.proTx), str(self.collateral), str(self.transparent) ) def __str__(self): diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index c8d6452028c05..21307405f015c 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -1117,13 +1117,13 @@ def setupDMN(self, break assert_greater_than(collateralTxId_n, -1) assert_greater_than(json_tx["confirmations"], 0) - proTxId = mnOwner.protx_register(collateralTxId, collateralTxId_n, ipport, ownerAdd, + proTxId = mnOwner.protx_register(collateralTxId, collateralTxId_n, True, ipport, ownerAdd, bls_keypair["public"], votingAdd, collateralAdd) elif strType == "external": self.log.info("Setting up ProRegTx with collateral externally-signed...") # send the tx from the miner payoutAdd = mnOwner.getnewaddress("payout") - register_res = miner.protx_register_prepare(outpoint.hash, outpoint.n, ipport, ownerAdd, + register_res = miner.protx_register_prepare(outpoint.hash, outpoint.n, True, ipport, ownerAdd, bls_keypair["public"], votingAdd, payoutAdd) self.log.info("ProTx prepared") message_to_sign = register_res["signMessage"] @@ -1218,19 +1218,24 @@ def protx_register_fund(self, miner, controller, dmn, collateral_addr, op_rew=No Create a ProReg tx, which references an 100 PIV UTXO as collateral. The controller node owns the collateral and creates the ProReg tx. """ - def protx_register(self, miner, controller, dmn, collateral_addr): - # send to the owner the exact collateral tx amount - funding_txid = miner.sendtoaddress(collateral_addr, Decimal('100')) + def protx_register(self, miner, controller, dmn, collateral_addr, transparent, outpoint): + # send to the owner the exact collateral tx amount, unless we already have an outpoint that we want to use + if outpoint is None: + funding_txid = miner.sendtoaddress(collateral_addr, Decimal('100')) # send another output to be used for the fee of the proReg tx - miner.sendtoaddress(collateral_addr, Decimal('1')) + feeAddr = collateral_addr if transparent else controller.getnewaddress("feeAddr") + miner.sendtoaddress(feeAddr, Decimal('1')) # confirm and verify reception miner.generate(1) self.sync_blocks([miner, controller]) - json_tx = controller.getrawtransaction(funding_txid, True) - assert_greater_than(json_tx["confirmations"], 0) - # create and send the ProRegTx - dmn.collateral = COutPoint(int(funding_txid, 16), get_collateral_vout(json_tx)) - dmn.proTx = controller.protx_register(funding_txid, dmn.collateral.n, dmn.ipport, dmn.owner, + if outpoint is None: + json_tx = controller.getrawtransaction(funding_txid, True) + assert_greater_than(json_tx["confirmations"], 0) + # create and send the ProRegTx, FOR SHIELD DMNS THIS IS NOT THE COLLATERAL CONTAINED IN THE PROREGTX (which is instead the null COutPoint (0,-1)) + dmn.collateral = COutPoint(int(funding_txid, 16), get_collateral_vout(json_tx)) if transparent else COutPoint(int(funding_txid, 16), 0, transparent) + else: + dmn.collateral = outpoint + dmn.proTx = controller.protx_register("%064x" % dmn.collateral.hash, dmn.collateral.n, transparent, dmn.ipport, dmn.owner, dmn.operator_pk, dmn.voting, dmn.payee) """ @@ -1249,7 +1254,7 @@ def protx_register_ext(self, miner, controller, dmn, outpoint, fSubmit): outpoint = COutPoint(int(funding_txid, 16), get_collateral_vout(json_tx)) dmn.collateral = outpoint # Prepare the message to be signed externally by the owner of the collateral (the controller) - reg_tx = miner.protx_register_prepare("%064x" % outpoint.hash, outpoint.n, dmn.ipport, dmn.owner, + reg_tx = miner.protx_register_prepare("%064x" % outpoint.hash, outpoint.n, True, dmn.ipport, dmn.owner, dmn.operator_pk, dmn.voting, dmn.payee) sig = controller.signmessage(reg_tx["collateralAddress"], reg_tx["signMessage"]) if fSubmit: @@ -1270,7 +1275,7 @@ def protx_register_ext(self, miner, controller, dmn, outpoint, fSubmit): If not provided, a new address-key pair is generated. :return: dmn: (Masternode) the deterministic masternode object """ - def register_new_dmn(self, idx, miner_idx, controller_idx, strType, + def register_new_dmn(self, idx, miner_idx, controller_idx, strType, transparent, payout_addr=None, outpoint=None, op_blskeys=None): # Prepare remote node assert idx != miner_idx @@ -1280,19 +1285,21 @@ def register_new_dmn(self, idx, miner_idx, controller_idx, strType, mn_node = self.nodes[idx] # Generate ip and addresses/keys - collateral_addr = controller_node.getnewaddress("mncollateral-%d" % idx) + collateral_addr = controller_node.getnewaddress("mncollateral-%d" % idx) if transparent else controller_node.getnewshieldaddress("shieldmncollateral-%d" % idx) if payout_addr is None: - payout_addr = collateral_addr - dmn = create_new_dmn(idx, controller_node, payout_addr, op_blskeys) + payout_addr = collateral_addr if transparent else controller_node.getnewaddress("mncollateral-%d" % idx) + dmn = create_new_dmn(idx, controller_node, payout_addr, op_blskeys, transparent) # Create ProRegTx self.log.info("Creating%s proRegTx for deterministic masternode idx=%d..." % ( " and funding" if strType == "fund" else "", idx)) if strType == "fund": + assert (transparent) self.protx_register_fund(miner_node, controller_node, dmn, collateral_addr) elif strType == "internal": - self.protx_register(miner_node, controller_node, dmn, collateral_addr) + self.protx_register(miner_node, controller_node, dmn, collateral_addr, transparent, outpoint) elif strType == "external": + assert (transparent) self.protx_register_ext(miner_node, controller_node, dmn, outpoint, True) else: raise Exception("Type %s not available" % strType) @@ -1307,7 +1314,7 @@ def register_new_dmn(self, idx, miner_idx, controller_idx, strType, assert dmn.proTx in mn_node.protx_list(False) # check coin locking - assert is_coin_locked_by(controller_node, dmn.collateral) + assert is_coin_locked_by(controller_node, dmn.collateral, dmn.transparent) # check json payload against local dmn object self.check_proreg_payload(dmn, json_tx) @@ -1337,23 +1344,38 @@ def check_mn_list_on_node(self, idx, mns): assert_equal(mn.voting, mn2["dmnstate"]["votingAddress"]) assert_equal(mn.ipport, mn2["dmnstate"]["service"]) assert_equal(mn.payee, mn2["dmnstate"]["payoutAddress"]) - assert_equal(collateral["txid"], mn2["collateralHash"]) - assert_equal(collateral["vout"], mn2["collateralIndex"]) + assert_equal(mn.nullifier, mn2["nullifier"]) + # Usual story, For shield Dmns the value we store in collateral (i.e. the sapling outpoint referring to the note) + # Is different from the default null collateral in the ProRegTx + if mn.transparent: + assert_equal(collateral["txid"], mn2["collateralHash"]) + assert_equal(collateral["vout"], mn2["collateralIndex"]) + else: + assert_equal("%064x" % 0, mn2["collateralHash"]) + assert_equal(-1, mn2["collateralIndex"]) def check_proreg_payload(self, dmn, json_tx): assert "payload" in json_tx # null hash if funding collateral collateral_hash = 0 if int(json_tx["txid"], 16) == dmn.collateral.hash \ else dmn.collateral.hash + collateral_n = dmn.collateral.n + # null Outpoint if dmn is shielded + if not dmn.transparent: + collateral_hash = 0 + collateral_n = -1 pl = json_tx["payload"] - assert_equal(pl["version"], 1) + assert_equal(pl["version"], 2) assert_equal(pl["collateralHash"], "%064x" % collateral_hash) - assert_equal(pl["collateralIndex"], dmn.collateral.n) + assert_equal(pl["collateralIndex"], collateral_n) assert_equal(pl["service"], dmn.ipport) assert_equal(pl["ownerAddress"], dmn.owner) assert_equal(pl["votingAddress"], dmn.voting) assert_equal(pl["operatorPubKey"], dmn.operator_pk) assert_equal(pl["payoutAddress"], dmn.payee) + # fix the nullifier + dmn.nullifier = pl["nullifier"] + # ------------------------------------------------------ @@ -1383,17 +1405,18 @@ def __init__(self, class PivxDMNTestFramework(PivxTestFramework): def set_base_test_params(self): - # 1 miner, 1 controller, 6 remote mns + # 1 miner, 1 controller, 6 remote mns 2 of which shielded self.num_nodes = 8 self.minerPos = 0 self.controllerPos = 1 self.setup_clean_chain = True - def add_new_dmn(self, strType, op_keys=None, from_out=None): + def add_new_dmn(self, strType, transparent=True, op_keys=None, from_out=None): self.mns.append(self.register_new_dmn(2 + len(self.mns), self.minerPos, self.controllerPos, strType, + transparent, outpoint=from_out, op_blskeys=op_keys)) @@ -1453,10 +1476,14 @@ def setup_test(self): # Create 6 DMNs and init the remote nodes self.log.info("Initializing masternodes...") for _ in range(2): - self.add_new_dmn("internal") + self.add_new_dmn("internal", False) self.add_new_dmn("external") self.add_new_dmn("fund") assert_equal(len(self.mns), 6) + # Sanity check that we have 2 shielded masternodes + assert_equal(len(self.nodes[self.controllerPos].listlockunspent()["shielded"]), 2) + assert_equal(self.mns[0].transparent, False) + assert_equal(self.mns[3].transparent, False) for mn in self.mns: self.nodes[mn.idx].initmasternode(mn.operator_sk) time.sleep(1) diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py index 2c5856d294620..638938b9e29a5 100644 --- a/test/functional/test_framework/util.py +++ b/test/functional/test_framework/util.py @@ -587,8 +587,9 @@ def get_coinstake_address(node, expected_utxos=None): return addrs[0] # Deterministic masternodes -def is_coin_locked_by(node, outpoint): - return outpoint.to_json() in node.listlockunspent()["transparent"] +def is_coin_locked_by(node, outpoint, transparent=True): + returnStr = "transparent" if transparent else "shielded" + return outpoint.to_json() in node.listlockunspent()[returnStr] def get_collateral_vout(json_tx): funding_txidn = -1 @@ -601,7 +602,7 @@ def get_collateral_vout(json_tx): # owner and voting keys are created from controller node. # operator keys are created, if operator_keys is None. -def create_new_dmn(idx, controller, payout_addr, operator_keys): +def create_new_dmn(idx, controller, payout_addr, operator_keys, transparent): port = p2p_port(idx) if idx <= MAX_NODES else p2p_port(MAX_NODES) + (idx - MAX_NODES) ipport = "127.0.0.1:" + str(port) owner_addr = controller.getnewaddress("mnowner-%d" % idx) @@ -613,7 +614,7 @@ def create_new_dmn(idx, controller, payout_addr, operator_keys): else: operator_pk = operator_keys[0] operator_sk = operator_keys[1] - return messages.Masternode(idx, owner_addr, operator_pk, voting_addr, ipport, payout_addr, operator_sk) + return messages.Masternode(idx, owner_addr, operator_pk, voting_addr, ipport, payout_addr, operator_sk, transparent) def spend_mn_collateral(spender, dmn): inputs = [dmn.collateral.to_json()] diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index bcec57836aecb..dfee2e76da1a1 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -171,7 +171,9 @@ 'tiertwo_masternode_activation.py', # ~ 352 sec 'tiertwo_masternode_ping.py', # ~ 293 sec 'tiertwo_governance_invalid_budget.py', # ~ 266 sec + 'tiertwo_shield_deterministicmns.py', # ~ 160 sec 'tiertwo_reorg_mempool.py', # ~ 97 sec + 'tiertwo_shield_governance.py' # ~ 80 sec ] SAPLING_SCRIPTS = [ @@ -246,7 +248,15 @@ 'wallet_importmulti.py', 'wallet_import_rescan.py', 'wallet_multiwallet.py', - 'sapling_wallet_encryption.py' + 'sapling_wallet_encryption.py', + 'tiertwo_shield_deterministicmns.py', + 'tiertwo_shield_governance.py', + 'tiertwo_dkg_errors.py', + 'tiertwo_chainlocks.py', + 'tiertwo_dkg_errors.py', + 'tiertwo_dkg_pose.py', + 'tiertwo_signing_session.py', + 'p2p_quorum_connect.py' ] # Place the lists with the longest tests (on average) first diff --git a/test/functional/tiertwo_deterministicmns.py b/test/functional/tiertwo_deterministicmns.py index 8e55980922490..965af85ac748b 100755 --- a/test/functional/tiertwo_deterministicmns.py +++ b/test/functional/tiertwo_deterministicmns.py @@ -34,11 +34,12 @@ def set_test_params(self): self.extra_args = [["-nuparams=v5_shield:1", "-nuparams=v6_evo:130"]] * self.num_nodes self.extra_args[0].append("-sporkkey=932HEevBSujW2ud7RfB1YF91AFygbBRQj3de3LyaCRqNzKKgWXi") - def add_new_dmn(self, mns, strType, op_keys=None, from_out=None): + def add_new_dmn(self, mns, strType, transparent=True, op_keys=None, from_out=None): mns.append(self.register_new_dmn(2 + len(mns), self.minerPos, self.controllerPos, strType, + transparent, outpoint=from_out, op_blskeys=op_keys)) @@ -119,7 +120,7 @@ def run_test(self): assert_raises_rpc_error(-1, "Evo upgrade is not active yet", self.add_new_dmn, mns, "internal") assert_raises_rpc_error(-1, "Evo upgrade is not active yet", self.add_new_dmn, mns, "fund") # Can create the raw proReg - dmn = create_new_dmn(2, controller, dummy_add, None) + dmn = create_new_dmn(2, controller, dummy_add, None, True) tx, sig = self.protx_register_ext(miner, controller, dmn, None, False) # but cannot send it assert_raises_rpc_error(-1, "Evo upgrade is not active yet", miner.protx_register_submit, tx, sig) @@ -164,9 +165,9 @@ def run_test(self): time.sleep(1) # Now send the ProReg txes and check list - self.add_new_dmn(mns, "internal", op_keys[0]) - self.add_new_dmn(mns, "external", op_keys[1]) - self.add_new_dmn(mns, "fund", op_keys[2]) + self.add_new_dmn(mns, "internal", True, op_keys[0]) + self.add_new_dmn(mns, "external", True, op_keys[1]) + self.add_new_dmn(mns, "fund", True, op_keys[2]) miner.generate(2) self.sync_blocks() time.sleep(1) @@ -211,7 +212,7 @@ def run_test(self): dmn_keys = [dmn.operator_pk, dmn.operator_sk] dmn2_keys = [dmn2.operator_pk, dmn2.operator_sk] self.log.info("Reactivating node %d reusing the collateral of node %d..." % (dmn.idx, dmn2.idx)) - mns.append(self.register_new_dmn(dmn.idx, self.minerPos, self.controllerPos, "external", + mns.append(self.register_new_dmn(dmn.idx, self.minerPos, self.controllerPos, "external", True, outpoint=dmn2.collateral, op_blskeys=dmn_keys)) miner.generate(1) self.sync_blocks() @@ -224,18 +225,18 @@ def run_test(self): self.log.info("Trying duplicate IP...") rand_idx = mns[randrange(len(mns))].idx assert_raises_rpc_error(-1, "bad-protx-dup-IP-address", - self.register_new_dmn, rand_idx, self.minerPos, self.controllerPos, "fund", + self.register_new_dmn, rand_idx, self.minerPos, self.controllerPos, "fund", True, op_blskeys=dmn2_keys) # Now try with duplicate operator key self.log.info("Trying duplicate operator key...") - dmn2b = create_new_dmn(dmn2.idx, controller, dummy_add, dmn_keys) + dmn2b = create_new_dmn(dmn2.idx, controller, dummy_add, dmn_keys, True) assert_raises_rpc_error(-1, "bad-protx-dup-operator-key", self.protx_register_fund, miner, controller, dmn2b, dummy_add) # Now try with duplicate owner key self.log.info("Trying duplicate owner key...") - dmn2c = create_new_dmn(dmn2.idx, controller, dummy_add, dmn2_keys) + dmn2c = create_new_dmn(dmn2.idx, controller, dummy_add, dmn2_keys, True) dmn2c.owner = mns[randrange(len(mns))].owner assert_raises_rpc_error(-1, "bad-protx-dup-owner-key", self.protx_register_fund, miner, controller, dmn2c, dummy_add) @@ -243,7 +244,7 @@ def run_test(self): # Finally, register it properly. This time setting 10% of the reward for the operator op_rew = {"reward": 10.00, "address": self.nodes[dmn2.idx].getnewaddress()} self.log.info("Reactivating the node with a new registration (with operator reward)...") - dmn2c = create_new_dmn(dmn2.idx, controller, dummy_add, dmn2_keys) + dmn2c = create_new_dmn(dmn2.idx, controller, dummy_add, dmn2_keys, True) self.protx_register_fund(miner, controller, dmn2c, dummy_add, op_rew) mns.append(dmn2c) time.sleep(1) @@ -362,7 +363,7 @@ def run_test(self): miner.protx_update_registrar(mns[0].proTx, mns[0].operator_pk, "", "", ownerKey) miner.generate(1) self.sync_blocks() - self.check_mn_enabled_count(5, 6) # still not valid until new operator sends proUpServ + self.check_mn_enabled_count(5, 6) # still not valid until new operator sends proUpServ self.check_mn_list(mns) self.log.info("Update voting address...") mns[1].voting = controller.getnewaddress() diff --git a/test/functional/tiertwo_governance_sync_basic.py b/test/functional/tiertwo_governance_sync_basic.py index 3af4bbb203cc9..c027f956417b4 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=None): if with_ping_mns is None: with_ping_mns = [] @@ -77,92 +53,12 @@ def broadcastbudgetfinalization(self, node, with_ping_mns=None): 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]) # check fee tx existence @@ -170,11 +66,11 @@ def submit_proposals(self, 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 @@ -183,14 +79,13 @@ def run_test(self): self.setup_3_masternodes_network() txHashSet = set([self.mnOneCollateral.hash, self.mnTwoCollateral.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(self.remoteDMN1, self.proRegTx1) self.log.info("DMN1 active") # activate sporks @@ -234,7 +129,7 @@ def run_test(self): # 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) + 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. @@ -252,13 +147,13 @@ def run_test(self): # 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) + 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) + 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 @@ -268,7 +163,7 @@ def run_test(self): # check that the vote was accepted everywhere self.stake(1, [self.remoteOne, self.remoteTwo]) - self.check_vote_existence(firstProposal.name, self.proRegTx1, "YES", True) + check_vote_existence(self.nodes, firstProposal.name, self.proRegTx1, "YES", True) self.log.info("all good, DMN1 vote accepted everywhere!") # Now check the budget @@ -278,12 +173,12 @@ def run_test(self): Allotted = firstProposal.amountPerCycle RemainingPaymentCount = firstProposal.cycles expected_budget = [ - self.get_proposal_obj(firstProposal.name, firstProposal.link, firstProposal.proposalHash, firstProposal.feeTxId, blockStart, + 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) @@ -301,7 +196,7 @@ 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. @@ -311,7 +206,7 @@ def run_test(self): 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") + check_budget_finalization_sync(self.nodes, 1, "OK") self.log.info("Remote One voted successfully.") # before broadcast the second finalization vote, let's drop the budget data of remoteOne. @@ -332,7 +227,7 @@ def run_test(self): self.stake(2, [self.remoteOne, self.remoteTwo]) self.log.info("checking finalization votes..") - self.check_budget_finalization_sync(3, "OK") + check_budget_finalization_sync(self.nodes, 3, "OK") self.log.info("orphan vote based finalization re-sync succeeded") self.stake(6, [self.remoteOne, self.remoteTwo]) @@ -343,7 +238,7 @@ 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]) @@ -356,7 +251,7 @@ def run_test(self): 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) @@ -391,14 +286,14 @@ def run_test(self): 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 + 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, 3, "OK") self.log.info("Remote incremental sync succeeded") # now let's verify that votes expire properly. @@ -408,8 +303,8 @@ def run_test(self): 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 + check_vote_existence(self.nodes, firstProposal.name, self.mnOneCollateral.hash, "YES", False) + check_budget_finalization_sync(self.nodes, 2, "OK") # budget finalization vote removal self.log.info("MN1 vote expired after collateral spend, all good") self.log.info("expiring DMN1..") @@ -418,8 +313,8 @@ def run_test(self): 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 + check_vote_existence(self.nodes, firstProposal.name, self.proRegTx1, "YES", False) + check_budget_finalization_sync(self.nodes, 1, "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 @@ -436,5 +331,6 @@ def run_test(self): assert_equal(len(self.miner.mnfinalbudget("show")), 0) self.log.info("All good.") + if __name__ == '__main__': MasternodeGovernanceBasicTest().main() diff --git a/test/functional/tiertwo_reorg_mempool.py b/test/functional/tiertwo_reorg_mempool.py index 12bfddcc09e1c..cf2cd0b0c79d9 100755 --- a/test/functional/tiertwo_reorg_mempool.py +++ b/test/functional/tiertwo_reorg_mempool.py @@ -82,8 +82,8 @@ def run_test(self): # Register two masternodes before the split collateral_addr = nodeA.getnewaddress() # for both collateral and payouts - pre_split_mn1 = create_new_dmn(100, nodeA, nodeA.getnewaddress(), None) - pre_split_mn2 = create_new_dmn(200, nodeA, nodeA.getnewaddress(), None) + pre_split_mn1 = create_new_dmn(100, nodeA, nodeA.getnewaddress(), None, True) + pre_split_mn2 = create_new_dmn(200, nodeA, nodeA.getnewaddress(), None, True) self.register_masternode(nodeA, pre_split_mn1, collateral_addr) self.register_masternode(nodeA, pre_split_mn2, collateral_addr) nodeA.generate(1) @@ -104,7 +104,7 @@ def run_test(self): # Register 5 masternodes, then mine 5 blocks self.log.info("Registering masternodes on chain A...") for _ in range(5): - dmn = create_new_dmn(free_idx, nodeA, collateral_addr, None) + dmn = create_new_dmn(free_idx, nodeA, collateral_addr, None, True) free_idx += 1 self.register_masternode(nodeA, dmn, collateral_addr) mnsA.append(dmn) @@ -124,14 +124,14 @@ def run_test(self): nodeA.lockunspent(False, True, [{"txid": x["txid"], "vout": x["vout"]}]) # Now send a valid proReg tx to the mempool, without mining it - mempool_dmn1 = create_new_dmn(free_idx, nodeA, collateral_addr, None) + mempool_dmn1 = create_new_dmn(free_idx, nodeA, collateral_addr, None, True) free_idx += 1 self.register_masternode(nodeA, mempool_dmn1, collateral_addr) assert mempool_dmn1.proTx in nodeA.getrawmempool() # Try sending a proReg tx with same owner self.log.info("Testing in-mempool duplicate-owner rejection...") - dmn_A1 = create_new_dmn(free_idx, nodeA, collateral_addr, None) + dmn_A1 = create_new_dmn(free_idx, nodeA, collateral_addr, None, True) free_idx += 1 dmn_A1.owner = mempool_dmn1.owner assert_raises_rpc_error(-26, "protx-dup", @@ -140,7 +140,7 @@ def run_test(self): # Try sending a proReg tx with same operator self.log.info("Testing in-mempool duplicate-operator rejection...") - dmn_A2 = create_new_dmn(free_idx, nodeA, collateral_addr, None) + dmn_A2 = create_new_dmn(free_idx, nodeA, collateral_addr, None, True) free_idx += 1 dmn_A2.operator_pk = mempool_dmn1.operator_pk assert_raises_rpc_error(-26, "protx-dup", @@ -149,7 +149,7 @@ def run_test(self): # Try sending a proReg tx with same IP self.log.info("Testing proReg in-mempool duplicate-IP rejection...") - dmn_A3 = create_new_dmn(free_idx, nodeA, collateral_addr, None) + dmn_A3 = create_new_dmn(free_idx, nodeA, collateral_addr, None, True) free_idx += 1 dmn_A3.ipport = mempool_dmn1.ipport assert_raises_rpc_error(-26, "protx-dup", @@ -158,15 +158,15 @@ def run_test(self): # Now send other 2 valid proReg tx to the mempool, without mining them self.log.info("Sending more ProReg txes to the mempool...") - mempool_dmn2 = create_new_dmn(free_idx, nodeA, collateral_addr, None) + mempool_dmn2 = create_new_dmn(free_idx, nodeA, collateral_addr, None, True) free_idx += 1 - mempool_dmn3 = create_new_dmn(free_idx, nodeA, collateral_addr, None) + mempool_dmn3 = create_new_dmn(free_idx, nodeA, collateral_addr, None, True) free_idx += 1 self.register_masternode(nodeA, mempool_dmn2, collateral_addr) self.register_masternode(nodeA, mempool_dmn3, collateral_addr) # Send to the mempool a ProRegTx using the collateral mined after the split - mempool_dmn4 = create_new_dmn(free_idx, nodeA, collateral_addr, None) + mempool_dmn4 = create_new_dmn(free_idx, nodeA, collateral_addr, None, True) mempool_dmn4.collateral = initial_collateral self.protx_register_ext(nodeA, nodeA, mempool_dmn4, mempool_dmn4.collateral, True) @@ -256,7 +256,7 @@ def run_test(self): # Register 5 more masternodes. One per block. for _ in range(5): - dmn = create_new_dmn(free_idx, nodeB, collateral_addr, None) + dmn = create_new_dmn(free_idx, nodeB, collateral_addr, None, True) free_idx += 1 self.register_masternode(nodeB, dmn, collateral_addr) mnsB.append(dmn) @@ -264,14 +264,14 @@ def run_test(self): self.check_mn_list_on_node(1, mnsB) # Register one masternode reusing the IP of the proUpServ mempool tx on chainA - dmn1000 = create_new_dmn(free_idx, nodeB, collateral_addr, None) + dmn1000 = create_new_dmn(free_idx, nodeB, collateral_addr, None, True) free_idx += 1 dmn1000.ipport = "127.0.0.1:1000" mnsB.append(dmn1000) self.register_masternode(nodeB, dmn1000, collateral_addr) # Register one masternode reusing the operator-key of the proUpReg mempool tx on chainA - dmnop = create_new_dmn(free_idx, nodeB, collateral_addr, None) + dmnop = create_new_dmn(free_idx, nodeB, collateral_addr, None, True) free_idx += 1 dmnop.operator_pk = operator_to_reuse mnsB.append(dmnop) diff --git a/test/functional/tiertwo_shield_deterministicmns.py b/test/functional/tiertwo_shield_deterministicmns.py new file mode 100755 index 0000000000000..dc378a6de1f8d --- /dev/null +++ b/test/functional/tiertwo_shield_deterministicmns.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +# Copyright (c) 2023 The PIVX Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test shield deterministic masternodes""" + +from decimal import Decimal +from random import randrange +import time + +from tiertwo_deterministicmns import DIP3Test +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, + is_coin_locked_by, +) + + +class DIP3ShieldTest(DIP3Test): + + def run_test(self): + self.disable_mocktime() + + # Additional connections to miner and owner + for nodePos in [self.minerPos, self.controllerPos]: + self.connect_to_all(nodePos) + miner = self.nodes[self.minerPos] + controller = self.nodes[self.controllerPos] + + # Enforce mn payments and reject legacy mns at block 131 + self.activate_spork(0, "SPORK_8_MASTERNODE_PAYMENT_ENFORCEMENT") + assert_equal("success", self.set_spork(self.minerPos, "SPORK_21_LEGACY_MNS_MAX_HEIGHT", 130)) + time.sleep(1) + assert_equal([130] * self.num_nodes, [self.get_spork(x, "SPORK_21_LEGACY_MNS_MAX_HEIGHT") + for x in range(self.num_nodes)]) + mns = [] + + # Mine 100 blocks + self.log.info("Mining...") + miner.generate(110) + self.sync_blocks() + self.assert_equal_for_all(110, "getblockcount") + + # DIP3 activates at block 130. + miner.generate(130 - miner.getblockcount()) + self.sync_blocks() + self.assert_equal_for_all(130, "getblockcount") + + # -- DIP3 enforced and SPORK_21 active here -- + self.wait_until_mnsync_completed() + + # enabled/total masternodes: 0/0 + self.check_mn_enabled_count(0, 0) + + # Create 2 SDMNs: + self.log.info("Initializing masternodes...") + self.add_new_dmn(mns, "internal", False) + self.add_new_dmn(mns, "internal", False) + + for mn in mns: + self.nodes[mn.idx].initmasternode(mn.operator_sk) + time.sleep(1) + miner.generate(1) + self.sync_blocks() + + # enabled/total masternodes: 2/2 + self.check_mn_enabled_count(2, 2) + self.log.info("Masternodes started.") + self.check_mn_list(mns) + + # Restart the controller and check that the collaterals are still locked + self.log.info("Restarting controller...") + self.restart_controller() + time.sleep(1) + for mn in mns: + if not is_coin_locked_by(controller, mn.collateral, mn.transparent): + raise Exception( + "Collateral %s of mn with idx=%d is not locked" % (mn.collateral, mn.idx) + ) + self.log.info("Collaterals still locked.") + + # Test collateral spending of a SDMN + s_dmn = mns.pop(randrange(len(mns))) # pop one at random + self.log.info("Spending collateral of mn with idx=%d..." % s_dmn.idx) + + # Try to spend the shield collateral + shield_collateral = {"txid": "%064x" % s_dmn.collateral.hash, "vout": s_dmn.collateral.n} + controller.lockunspent(True, False, [shield_collateral]) + + # Sanity check on the controller notes: the collateral of the other sdmn is still locked + assert_equal(len(controller.listlockunspent()["shielded"]), 1) + assert_equal(controller.getsaplingnotescount(), 2) + + recipient2 = [{"address": controller.getnewshieldaddress(), "amount": Decimal('90')}] + controller.shieldsendmany("from_shield", recipient2) + self.sync_mempools([miner, controller]) + miner.generate(2) + self.sync_all() + + # enabled/total masternodes: 1/1 + self.check_mn_enabled_count(1, 1) + self.check_mn_list(mns) + + s_dmn_keys = [s_dmn.operator_pk, s_dmn.operator_sk] + + # Try to register s_dmn with the already spent shield collateral + self.log.info("Trying using a spent note...") + assert_raises_rpc_error(-1, "bad-txns-shielded-requirements-not-met", + self.register_new_dmn, s_dmn.idx, self.minerPos, self.controllerPos, "internal", False, outpoint=s_dmn.collateral, op_blskeys=s_dmn_keys) + + # Register s_dmn again, with the collateral of s_dmn2 + # s_dmn must be added again to the list, and s_dmn2 must be removed + s_dmn2 = mns.pop(0) + s_dmn2_keys = [s_dmn2.operator_pk, s_dmn2.operator_sk] + self.log.info("Reactivating node %d reusing the collateral of node %d..." % (s_dmn.idx, s_dmn2.idx)) + mns.append(self.register_new_dmn(s_dmn.idx, self.minerPos, self.controllerPos, "internal", False, + outpoint=s_dmn2.collateral, op_blskeys=s_dmn_keys)) + miner.generate(1) + self.sync_blocks() + + # enabled/total masternodes: 1/1 + self.check_mn_enabled_count(1, 1) + self.check_mn_list(mns) + + # Now try to register dmn2 again with an already-used IP + self.log.info("Trying duplicate IP...") + rand_idx = mns[randrange(len(mns))].idx + assert_raises_rpc_error(-1, "bad-protx-dup-IP-address", + self.register_new_dmn, rand_idx, self.minerPos, self.controllerPos, "internal", False, + op_blskeys=s_dmn2_keys) + + # Now try with duplicate operator key + self.log.info("Trying duplicate operator key...") + assert_raises_rpc_error(-1, "bad-protx-dup-operator-key", + self.register_new_dmn, s_dmn2.idx, self.minerPos, self.controllerPos, "internal", False, + op_blskeys=s_dmn_keys) + + # Finally, register it properly. + mns.append(self.register_new_dmn(s_dmn2.idx, + self.minerPos, + self.controllerPos, + "internal", + False)) + + self.nodes[s_dmn2.idx].initmasternode(s_dmn2.operator_sk) + time.sleep(1) + miner.generate(1) + self.sync_blocks() + self.check_mn_enabled_count(2, 2) + self.check_mn_list(mns) + + # Test payments. + # Mine 4 blocks and check that each masternode has been paid exactly twice. + # Save last paid masternode. Check that it's the last paid also after the 4 blocks. + self.log.info("Testing masternode payments...") + last_paid_mn = self.get_last_paid_mn() + starting_balances = {} + for mn in mns: + starting_balances[mn.payee] = self.get_addr_balance(controller, mn.payee) + miner.generate(4) + self.sync_blocks() + for mn in mns: + bal = self.get_addr_balance(controller, mn.payee) + expected = starting_balances[mn.payee] + Decimal('6.0') + if bal != expected: + raise Exception("Invalid balance (%s != %s) for node %d" % (bal, expected, mn.idx)) + self.log.info("All masternodes paid twice.") + assert_equal(last_paid_mn, self.get_last_paid_mn()) + self.log.info("Order preserved.") + + +if __name__ == '__main__': + DIP3ShieldTest().main() diff --git a/test/functional/tiertwo_shield_governance.py b/test/functional/tiertwo_shield_governance.py new file mode 100755 index 0000000000000..845206731dfcd --- /dev/null +++ b/test/functional/tiertwo_shield_governance.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +# Copyright (c) 2023 The PIVX Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://www.opensource.org/licenses/mit-license.php. +""" +This (temporary) test checks the new vote system for SDMNs and DMNs. +Once legacy are obsolete this test will be merged with tiertwo_governance_sync_basic, +where we can simply change the two legacy MNs in SDMNs. +""" + +import time + +from test_framework.test_framework import PivxTestFramework +from test_framework.budget_util import ( + check_budget_finalization_sync, + create_proposals_tx, + check_budgetprojection, + check_proposal_existence, + check_mns_status, + check_vote_existence, + get_proposal_obj, + Proposal, + propagate_proposals +) +from test_framework.util import ( + assert_equal, + satoshi_round, + set_node_times +) + + +class SDMNsGovernanceTest(PivxTestFramework): + + def wait_until_mnsync_completed(self): + SYNC_FINISHED = [999] * self.num_nodes + synced = [-1] * self.num_nodes + timeout = time.time() + 120 + while synced != SYNC_FINISHED and time.time() < timeout: + synced = [node.mnsync("status")["RequestedMasternodeAssets"] + for node in self.nodes] + if synced != SYNC_FINISHED: + time.sleep(5) + if synced != SYNC_FINISHED: + raise AssertionError("Unable to complete mnsync: %s" % str(synced)) + + def broadcastbudgetfinalization(self): + miner = self.nodes[0] + self.log.info("suggesting the budget finalization..") + assert miner.mnfinalbudgetsuggest() is not None + + self.log.info("confirming the budget finalization..") + time.sleep(1) + miner.generate(4) + self.sync_blocks() + + self.log.info("broadcasting the budget finalization..") + return miner.mnfinalbudgetsuggest() + + def submit_proposals(self, props): + miner = self.nodes[0] + props = create_proposals_tx(miner, props) + # generate 3 blocks to confirm the tx (and update the mnping) + miner.generate(3) + self.sync_blocks() + # check fee tx existence + for entry in props: + txinfo = miner.gettransaction(entry.feeTxId) + assert_equal(txinfo['amount'], -50.00) + # propagate proposals + props = propagate_proposals(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 set_test_params(self): + # 1 miner, 1 controller, 1 DMNs and 1 SDMN + self.num_nodes = 4 + self.minerPos = 0 + self.controllerPos = 1 + self.setup_clean_chain = True + self.extra_args = [["-nuparams=v5_shield:1", "-nuparams=v6_evo:130"]] * self.num_nodes + self.extra_args[0].append("-sporkkey=932HEevBSujW2ud7RfB1YF91AFygbBRQj3de3LyaCRqNzKKgWXi") + + def run_test(self): + # Additional connections to miner and owner + for nodePos in [self.minerPos, self.controllerPos]: + self.connect_to_all(nodePos) + miner = self.nodes[self.minerPos] + controller = self.nodes[self.controllerPos] + + # Enforce mn payments and reject legacy mns at block 131 + 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") + assert_equal("success", self.set_spork(self.minerPos, "SPORK_21_LEGACY_MNS_MAX_HEIGHT", 130)) + time.sleep(1) + assert_equal([130] * self.num_nodes, [self.get_spork(x, "SPORK_21_LEGACY_MNS_MAX_HEIGHT") + for x in range(self.num_nodes)]) + mns = [] + + # DIP3 activates at block 130 but we mine until block 145 (so we are over the first budget payment). + miner.generate(145 - miner.getblockcount()) + self.sync_blocks() + self.assert_equal_for_all(145, "getblockcount") + self.wait_until_mnsync_completed() + + # Create 1 SDMN and 1 DMN: + self.log.info("Initializing masternodes...") + mns.append(self.register_new_dmn(2, self.minerPos, self.controllerPos, "internal", False)) + mns.append(self.register_new_dmn(3, self.minerPos, self.controllerPos, "internal", True)) + + for mn in mns: + self.nodes[mn.idx].initmasternode(mn.operator_sk) + time.sleep(1) + miner.generate(1) + self.sync_blocks() + + for mn in mns: + check_mns_status(self.nodes[mn.idx], mn.proTx) + self.log.info("All masternodes activated") + + nextSuperBlockHeight = miner.getnextsuperblock() + + # Submit first proposal + self.log.info("preparing budget proposal..") + firstProposal = Proposal( + "sdmns-are-the-best", + "https://forum.pivx.org/t/fund-my-proposal", + 2, + miner.getnewaddress(), + 300 + ) + self.submit_proposals([firstProposal]) + + # Check proposals existence + for i in range(self.num_nodes): + assert_equal(len(self.nodes[i].getbudgetinfo()), 1) + + # now let's vote for the proposal with the first DMN + self.log.info("Voting with DMN1...") + voteResult = controller.mnbudgetvote("alias", firstProposal.proposalHash, "yes", mns[1].proTx) + assert_equal(voteResult["detail"][0]["result"], "success") + + # check that the vote was accepted everywhere + miner.generate(1) + self.sync_blocks() + check_vote_existence(self.nodes, firstProposal.name, mns[1].proTx, "YES", True) + self.log.info("all good, DMN1 vote accepted everywhere!") + + # now let's vote for the proposal with the first SDMN + self.log.info("Voting with SDMN1...") + voteResult = controller.mnbudgetvote("alias", firstProposal.proposalHash, "yes", mns[0].proTx) + assert_equal(voteResult["detail"][0]["result"], "success") + + # check that the vote was accepted everywhere + miner.generate(1) + self.sync_blocks() + check_vote_existence(self.nodes, firstProposal.name, mns[0].proTx, "YES", True) + self.log.info("all good, SDMN1 vote accepted everywhere!") + + # instead of waiting for 5 minutes for proposal to be established advance the nodes time of 10 minutes + set_node_times(self.nodes, int(time.time()) + 10*60) + + # check budgets + blockStart = nextSuperBlockHeight + blockEnd = blockStart + firstProposal.cycles * 145 + TotalPayment = firstProposal.amountPerCycle * firstProposal.cycles + Allotted = firstProposal.amountPerCycle + RemainingPaymentCount = firstProposal.cycles + expected_budget = [ + get_proposal_obj(firstProposal.name, firstProposal.link, firstProposal.proposalHash, firstProposal.feeTxId, blockStart, + blockEnd, firstProposal.cycles, RemainingPaymentCount, firstProposal.paymentAddr, 1, + 2, 0, 0, satoshi_round(TotalPayment), satoshi_round(firstProposal.amountPerCycle), + True, True, satoshi_round(Allotted), satoshi_round(Allotted)) + ] + check_budgetprojection(self.nodes, expected_budget, self.log) + + # Mine more block in order to finalize the budget. + while (miner.getblockcount() < 279): + miner.generate(1) + self.sync_blocks() + assert_equal(controller.getblockcount(), 279) + + self.log.info("starting budget finalization sync test..") + miner.generate(2) + self.sync_blocks() + + # assert that there is no budget finalization first. + assert_equal(len(controller.mnfinalbudget("show")), 0) + + # suggest the budget finalization and confirm the tx (+4 blocks). + budgetFinHash = self.broadcastbudgetfinalization() + assert budgetFinHash != "" + time.sleep(2) + + self.log.info("checking budget finalization sync..") + check_budget_finalization_sync(self.nodes, 0, "OK") + + self.log.info("budget finalization synced!, now voting for the budget finalization..") + for mn in mns: + voteResult = self.nodes[mn.idx].mnfinalbudget("vote", budgetFinHash) + assert_equal(voteResult["detail"][0]["result"], "success") + miner.generate(2) + self.sync_blocks() + check_budget_finalization_sync(self.nodes, 2, "OK") + self.log.info("Both masternodes voted successfully.") + + miner.generate(6) + self.sync_blocks() + addrInfo = miner.listreceivedbyaddress(0, False, False, firstProposal.paymentAddr) + assert_equal(addrInfo[0]["amount"], firstProposal.amountPerCycle) + + self.log.info("budget proposal paid!, all good") + + # Check that the proposal info returns updated payment count + expected_budget[0]["RemainingPaymentCount"] -= 1 + check_budgetprojection(self.nodes, expected_budget, self.log) + + +if __name__ == '__main__': + SDMNsGovernanceTest().main()