From 67e835984e964e7576516e464b985e66959d74eb Mon Sep 17 00:00:00 2001 From: Peter Bushnell Date: Tue, 7 Dec 2021 11:40:24 +0000 Subject: [PATCH] Add updating of owner to updatemasternode --- src/consensus/tx_verify.cpp | 5 +- src/consensus/tx_verify.h | 2 +- src/masternodes/masternodes.cpp | 99 ++++++--- src/masternodes/masternodes.h | 36 +++- src/masternodes/mn_checks.cpp | 103 +++++++-- src/masternodes/mn_checks.h | 9 +- src/masternodes/rpc_masternodes.cpp | 49 ++++- src/rpc/mining.cpp | 2 +- src/txmempool.cpp | 18 +- src/validation.cpp | 71 ++++--- test/functional/rpc_updatemasternode.py | 268 ++++++++++++++++++++++-- 11 files changed, 549 insertions(+), 113 deletions(-) diff --git a/src/consensus/tx_verify.cpp b/src/consensus/tx_verify.cpp index 21d30380527..a6f1276d5d3 100644 --- a/src/consensus/tx_verify.cpp +++ b/src/consensus/tx_verify.cpp @@ -161,7 +161,7 @@ int64_t GetTransactionSigOpCost(const CTransaction& tx, const CCoinsViewCache& i return nSigOps; } -bool Consensus::CheckTxInputs(const CTransaction& tx, CValidationState& state, const CCoinsViewCache& inputs, const CCustomCSView * mnview, int nSpendHeight, CAmount& txfee, const CChainParams& chainparams) +bool Consensus::CheckTxInputs(const CTransaction& tx, CValidationState& state, const CCoinsViewCache& inputs, const CCustomCSView * mnview, int nSpendHeight, CAmount& txfee, const CChainParams& chainparams, uint256 canSpend) { // are the actual inputs available? if (!inputs.HaveInputs(tx)) { @@ -187,8 +187,7 @@ bool Consensus::CheckTxInputs(const CTransaction& tx, CValidationState& state, c return state.Invalid(ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, "bad-txns-inputvalues-outofrange"); } /// @todo tokens: later match the range with TotalSupply - - if (prevout.n == 1 && !mnview->CanSpend(prevout.hash, nSpendHeight)) { + if (canSpend != prevout.hash && prevout.n == 1 && !mnview->CanSpend(prevout.hash, nSpendHeight)) { return state.Invalid(ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, "bad-txns-collateral-locked", strprintf("tried to spend locked collateral for %s", prevout.hash.ToString())); /// @todo may be somehow place the height of unlocking? } diff --git a/src/consensus/tx_verify.h b/src/consensus/tx_verify.h index 42293d7d114..8eb11d1b9a5 100644 --- a/src/consensus/tx_verify.h +++ b/src/consensus/tx_verify.h @@ -26,7 +26,7 @@ namespace Consensus { * @param[out] txfee Set to the transaction fee if successful. * Preconditions: tx.IsCoinBase() is false. */ -bool CheckTxInputs(const CTransaction& tx, CValidationState& state, const CCoinsViewCache& inputs, const CCustomCSView * mnview, int nSpendHeight, CAmount& txfee, const CChainParams& chainparams); +bool CheckTxInputs(const CTransaction& tx, CValidationState& state, const CCoinsViewCache& inputs, const CCustomCSView * mnview, int nSpendHeight, CAmount& txfee, const CChainParams& chainparams, uint256 canSpend); } // namespace Consensus /** Auxiliary functions for transaction validation (ideally should not be exposed) */ diff --git a/src/masternodes/masternodes.cpp b/src/masternodes/masternodes.cpp index 61babe70aa3..5614e7494ec 100644 --- a/src/masternodes/masternodes.cpp +++ b/src/masternodes/masternodes.cpp @@ -82,11 +82,11 @@ CMasternode::CMasternode() , resignHeight(-1) , version(-1) , resignTx() - , banTx() + , collateralTx() { } -CMasternode::State CMasternode::GetState(int height) const +CMasternode::State CMasternode::GetState(int height, const CCustomCSView& mnview) const { int EunosPayaHeight = Params().GetConsensus().EunosPayaHeight; @@ -94,6 +94,16 @@ CMasternode::State CMasternode::GetState(int height) const return State::UNKNOWN; } + if (!collateralTx.IsNull()) { + auto idHeight = mnview.GetNewCollateral(collateralTx); + assert(idHeight); + if (height < idHeight->blockHeight) { + return State::TRANSFERRING; + } else if (height < idHeight->blockHeight + GetMnActivationDelay(idHeight->blockHeight)) { + return State::PRE_ENABLED; + } + } + if (resignHeight == -1 || height < resignHeight) { // enabled or pre-enabled // Special case for genesis block int activationDelay = height < EunosPayaHeight ? GetMnActivationDelay(height) : GetMnActivationDelay(creationHeight); @@ -115,7 +125,7 @@ CMasternode::State CMasternode::GetState(int height) const bool CMasternode::IsActive(int height) const { - State state = GetState(height); + State state = GetState(height, *pcustomcsview); if (height >= Params().GetConsensus().EunosPayaHeight) { return state == ENABLED; } @@ -133,6 +143,8 @@ std::string CMasternode::GetHumanReadableState(State state) return "PRE_RESIGNED"; case RESIGNED: return "RESIGNED"; + case TRANSFERRING: + return "TRANSFERRING"; default: return "UNKNOWN"; } @@ -160,7 +172,7 @@ bool operator==(CMasternode const & a, CMasternode const & b) a.resignHeight == b.resignHeight && a.version == b.version && a.resignTx == b.resignTx && - a.banTx == b.banTx + a.collateralTx == b.collateralTx ); } @@ -289,14 +301,9 @@ Res CMasternodesView::CreateMasternode(const uint256 & nodeId, const CMasternode return Res::Ok(); } -Res CMasternodesView::ResignMasternode(const uint256 & nodeId, const uint256 & txid, int height) +Res CMasternodesView::ResignMasternode(CMasternode& node, const uint256 & nodeId, const uint256 & txid, int height, CCustomCSView& mnview) { - // auth already checked! - auto node = GetMasternode(nodeId); - if (!node) { - return Res::Err("node %s does not exists", nodeId.ToString()); - } - auto state = node->GetState(height); + auto state = node.GetState(height, mnview); if (height >= Params().GetConsensus().EunosPayaHeight) { if (state != CMasternode::ENABLED) { return Res::Err("node %s state is not 'ENABLED'", nodeId.ToString()); @@ -305,14 +312,14 @@ Res CMasternodesView::ResignMasternode(const uint256 & nodeId, const uint256 & t return Res::Err("node %s state is not 'PRE_ENABLED' or 'ENABLED'", nodeId.ToString()); } - const auto timelock = GetTimelock(nodeId, *node, height); + const auto timelock = GetTimelock(nodeId, node, height); if (timelock) { return Res::Err("Trying to resign masternode before timelock expiration."); } - node->resignTx = txid; - node->resignHeight = height; - WriteBy(nodeId, *node); + node.resignTx = txid; + node.resignHeight = height; + WriteBy(nodeId, node); return Res::Ok(); } @@ -337,7 +344,7 @@ void CMasternodesView::RemForcedRewardAddress(uint256 const & nodeId, CMasternod WriteBy(nodeId, node); } -void CMasternodesView::UpdateMasternode(uint256 const & nodeId, CMasternode& node, char operatorType, const CKeyID& operatorAuthAddress, int height) +void CMasternodesView::UpdateMasternodeOperator(uint256 const & nodeId, CMasternode& node, const char operatorType, const CKeyID& operatorAuthAddress, int height) { // Remove old record EraseBy(node.operatorAuthAddress); @@ -350,6 +357,42 @@ void CMasternodesView::UpdateMasternode(uint256 const & nodeId, CMasternode& nod WriteBy(node.operatorAuthAddress, nodeId); } +void CMasternodesView::UpdateMasternodeOwner(uint256 const & nodeId, CMasternode& node, const char ownerType, const CKeyID& ownerAuthAddress) +{ + // Remove old record + EraseBy(node.ownerAuthAddress); + + node.ownerType = ownerType; + node.ownerAuthAddress = ownerAuthAddress; + + // Overwrite and create new record + WriteBy(nodeId, node); + WriteBy(node.ownerAuthAddress, nodeId); +} + +void CMasternodesView::UpdateMasternodeCollateral(uint256 const & nodeId, CMasternode& node, const uint256& newCollateralTx, const int height) +{ + // Remove old record, allows spending of previous collateral in this TX. + EraseBy(node.collateralTx); + + // Store new collateral. Used by HasCollateralAuth. + node.collateralTx = newCollateralTx; + WriteBy(nodeId, node); + + // Prioritise fast lookup in CanSpend() and GetState() + WriteBy(newCollateralTx, MNNewOwnerHeightValue{static_cast(height + GetMnResignDelay(height)), nodeId}); +} + +std::optional CMasternodesView::GetNewCollateral(const uint256& txid) const +{ + return ReadBy(txid); +} + +void CMasternodesView::ForEachNewCollateral(std::function)> callback) +{ + ForEach(callback); +} + void CMasternodesView::SetMasternodeLastBlockTime(const CKeyID & minter, const uint32_t &blockHeight, const int64_t& time) { auto nodeId = GetMasternodeIdByOperator(minter); @@ -452,16 +495,12 @@ Res CMasternodesView::UnCreateMasternode(const uint256 & nodeId) return Res::Err("No such masternode %s", nodeId.GetHex()); } -Res CMasternodesView::UnResignMasternode(const uint256 & nodeId, const uint256 & resignTx) +Res CMasternodesView::UnResignMasternode(CMasternode& node, const uint256 & nodeId) { - auto node = GetMasternode(nodeId); - if (node && node->resignTx == resignTx) { - node->resignHeight = -1; - node->resignTx = {}; - WriteBy(nodeId, *node); - return Res::Ok(); - } - return Res::Err("No such masternode %s, resignTx: %s", nodeId.GetHex(), resignTx.GetHex()); + node.resignHeight = -1; + node.resignTx = {}; + WriteBy(nodeId, node); + return Res::Ok(); } uint16_t CMasternodesView::GetTimelock(const uint256& nodeId, const CMasternode& node, const uint64_t height) const @@ -815,9 +854,17 @@ bool CCustomCSView::CanSpend(const uint256 & txId, int height) const auto node = GetMasternode(txId); // check if it was mn collateral and mn was resigned or banned if (node) { - auto state = node->GetState(height); + auto state = node->GetState(height, *this); return state == CMasternode::RESIGNED; } + + if (auto mn = GetNewCollateral(txId)) { + auto node = GetMasternode(mn->masternodeID); + assert(node); + auto state = node->GetState(height, *this); + return state == CMasternode::RESIGNED; + } + // check if it was token collateral and token already destroyed /// @todo token check for total supply/limit when implemented auto pair = GetTokenByCreationTx(txId); diff --git a/src/masternodes/masternodes.h b/src/masternodes/masternodes.h index 1d43e0064a6..db8cc31af2f 100644 --- a/src/masternodes/masternodes.h +++ b/src/masternodes/masternodes.h @@ -50,6 +50,7 @@ class CMasternode ENABLED, PRE_RESIGNED, RESIGNED, + TRANSFERRING, UNKNOWN // unreachable }; @@ -88,12 +89,12 @@ class CMasternode //! This fields are for transaction rollback (by disconnecting block) uint256 resignTx; - uint256 banTx; + uint256 collateralTx; //! empty constructor CMasternode(); - State GetState(int height) const; + State GetState(int height, const CCustomCSView& mnview) const; bool IsActive(int height) const; static std::string GetHumanReadableState(State state); @@ -115,7 +116,7 @@ class CMasternode READWRITE(version); READWRITE(resignTx); - READWRITE(banTx); + READWRITE(collateralTx); // Only available after FortCanning if (version > PRE_FORT_CANNING) { @@ -174,6 +175,20 @@ struct SubNodeBlockTimeKey } }; +struct MNNewOwnerHeightValue +{ + uint32_t blockHeight; + uint256 masternodeID; + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) { + READWRITE(blockHeight); + READWRITE(masternodeID); + } +}; + class CMasternodesView : public virtual CStorageView { std::map> minterTimeCache; @@ -196,12 +211,18 @@ class CMasternodesView : public virtual CStorageView std::set> GetOperatorsMulti() const; Res CreateMasternode(uint256 const & nodeId, CMasternode const & node, uint16_t timelock); - Res ResignMasternode(uint256 const & nodeId, uint256 const & txid, int height); + Res ResignMasternode(CMasternode& node, uint256 const & nodeId, uint256 const & txid, int height, CCustomCSView& mnview); Res UnCreateMasternode(uint256 const & nodeId); - Res UnResignMasternode(uint256 const & nodeId, uint256 const & resignTx); + Res UnResignMasternode(CMasternode& node, uint256 const & nodeId); + + // Masternode updates void SetForcedRewardAddress(uint256 const & nodeId, CMasternode& node, const char rewardAddressType, CKeyID const & rewardAddress, int height); void RemForcedRewardAddress(uint256 const & nodeId, CMasternode& node, int height); - void UpdateMasternode(uint256 const & nodeId, CMasternode& node, char operatorType, const CKeyID& operatorAuthAddress, int height); + void UpdateMasternodeOperator(uint256 const & nodeId, CMasternode& node, const char operatorType, const CKeyID& operatorAuthAddress, int height); + void UpdateMasternodeOwner(uint256 const & nodeId, CMasternode& node, const char ownerType, const CKeyID& ownerAuthAddress); + void UpdateMasternodeCollateral(uint256 const & nodeId, CMasternode& node, const uint256& newCollateralTx, const int height); + std::optional GetNewCollateral(const uint256& txid) const; + void ForEachNewCollateral(std::function)> callback); // Get blocktimes for non-subnode and subnode with fork logic std::vector GetBlockTimes(const CKeyID& keyID, const uint32_t blockHeight, const int32_t creationHeight, const uint16_t timelock); @@ -224,6 +245,7 @@ class CMasternodesView : public virtual CStorageView struct ID { static constexpr uint8_t prefix() { return 'M'; } }; struct Operator { static constexpr uint8_t prefix() { return 'o'; } }; struct Owner { static constexpr uint8_t prefix() { return 'w'; } }; + struct NewCollateral { static constexpr uint8_t prefix() { return 's'; } }; // For storing last staked block time struct Staker { static constexpr uint8_t prefix() { return 'X'; } }; @@ -355,7 +377,7 @@ class CCustomCSView void CheckPrefixes() { CheckPrefix< - CMasternodesView :: ID, Operator, Owner, Staker, SubNode, Timelock, + CMasternodesView :: ID, NewCollateral, Operator, Owner, Staker, SubNode, Timelock, CLastHeightView :: Height, CTeamView :: AuthTeam, ConfirmTeam, CurrentTeam, CFoundationsDebtView :: Debt, diff --git a/src/masternodes/mn_checks.cpp b/src/masternodes/mn_checks.cpp index 6d7c4723799..3ce2a6762f5 100644 --- a/src/masternodes/mn_checks.cpp +++ b/src/masternodes/mn_checks.cpp @@ -902,32 +902,92 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor } Res operator()(const CResignMasterNodeMessage& obj) const { - auto res = HasCollateralAuth(obj); - return !res ? res : mnview.ResignMasternode(obj, tx.GetHash(), height); + auto node = mnview.GetMasternode(obj); + if (!node) { + return Res::Err("node %s does not exists", obj.ToString()); + } + auto res = HasCollateralAuth(node->collateralTx.IsNull() ? static_cast(obj) : node->collateralTx); + return !res ? res : mnview.ResignMasternode(*node, obj, tx.GetHash(), height, mnview); } Res operator()(const CUpdateMasterNodeMessage& obj) const { + if (obj.updates.empty()) { + return Res::Err("No update arguments provided"); + } + auto node = mnview.GetMasternode(obj.mnId); if (!node) { return Res::Err("masternode %s does not exists", obj.mnId.ToString()); } - const auto res = HasCollateralAuth(obj.mnId); + const auto collateralTx = node->collateralTx.IsNull() ? obj.mnId : node->collateralTx; + const auto res = HasCollateralAuth(collateralTx); if (!res) { return res; } - auto state = node->GetState(height); + auto state = node->GetState(height, mnview); if (state != CMasternode::ENABLED) { return Res::Err("Masternode %s is not in 'ENABLED' state", obj.mnId.ToString()); } - if (obj.updates.empty()) { - return Res::Err("No update arguments provided"); - } - for (const auto& item : obj.updates) { - if (item.first == static_cast(UpdateMasternodeType::OperatorAddress)) { + if (item.first == static_cast(UpdateMasternodeType::OwnerAddress)) { + bool collateralFound{false}; + for (const auto vin : tx.vin) { + if (vin.prevout.hash == collateralTx && vin.prevout.n == 1) { + collateralFound = true; + } + } + if (!collateralFound) { + return Res::Err("Missing previous collateral from transaction inputs"); + } + + if (tx.vout.size() == 1) { + return Res::Err("Missing new collateral output"); + } + + if (!HasAuth(tx.vout[1].scriptPubKey)) { + return Res::Err("Missing auth input for new masternode owner"); + } + + CTxDestination dest; + if (!ExtractDestination(tx.vout[1].scriptPubKey, dest) || dest.index() != PKHashType && dest.index() != WitV0KeyHashType) { + return Res::Err("Owner address must be P2PKH or P2WPKH type"); + } + + if (tx.vout[1].nValue != GetMnCollateralAmount(height)) { + return Res::Err("Incorrect collateral amount"); + } + + const auto keyID = dest.index() == PKHashType ? CKeyID(std::get(dest)) : CKeyID(std::get(dest)); + if (mnview.GetMasternodeIdByOwner(keyID) || mnview.GetMasternodeIdByOperator(keyID)) { + return Res::Err("Masternode with that owner address already exists"); + } + + bool duplicate{false}; + mnview.ForEachNewCollateral([&](const uint256& key, CLazySerialize valueKey) { + const auto& value = valueKey.get(); + if (height > value.blockHeight) { + return true; + } + const auto& coin = coins.AccessCoin({key, 1}); + assert(!coin.IsSpent()); + CTxDestination pendingDest; + assert(ExtractDestination(coin.out.scriptPubKey, pendingDest)); + const CKeyID storedID = pendingDest.index() == PKHashType ? CKeyID(std::get(pendingDest)) : CKeyID(std::get(pendingDest)); + if (storedID == keyID) { + duplicate = true; + return false; + } + return true; + }); + if (duplicate) { + return Res::ErrCode(CustomTxErrCodes::Fatal, "Masternode exist with that owner address pending already"); + } + + mnview.UpdateMasternodeCollateral(obj.mnId, *node, tx.GetHash(), height); + } else if (item.first == static_cast(UpdateMasternodeType::OperatorAddress)) { if (item.second.first != 1 && item.second.first != 4) { return Res::Err("Operator address must be P2PKH or P2WPKH type"); } @@ -935,7 +995,7 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor if (mnview.GetMasternodeIdByOwner(keyID) || mnview.GetMasternodeIdByOperator(keyID)) { return Res::Err("Masternode with that operator address already exists"); } - mnview.UpdateMasternode(obj.mnId, *node, item.second.first, keyID, height); + mnview.UpdateMasternodeOperator(obj.mnId, *node, item.second.first, keyID, height); } else if (item.first == static_cast(UpdateMasternodeType::SetRewardAddress)) { if (item.second.first != 1 && item.second.first != 4) { return Res::Err("Reward address must be P2PKH or P2WPKH type"); @@ -2771,8 +2831,12 @@ class CCustomTxRevertVisitor : public CCustomTxVisitor } Res operator()(const CResignMasterNodeMessage& obj) const { - auto res = HasCollateralAuth(obj); - return !res ? res : mnview.UnResignMasternode(obj, tx.GetHash()); + auto node = mnview.GetMasternode(obj); + if (!node || node->resignTx != tx.GetHash()) { + return Res::Err("No such masternode %s, resignTx: %s", obj.GetHex(), tx.GetHash().GetHex()); + } + auto res = HasCollateralAuth(node->collateralTx.IsNull() ? static_cast(obj) : node->collateralTx); + return !res ? res : mnview.UnResignMasternode(*node, obj); } Res operator()(const CCreateTokenMessage& obj) const { @@ -3119,7 +3183,7 @@ void PopulateVaultHistoryData(CHistoryWriters* writers, CAccountsHistoryWriter& } } -Res ApplyCustomTx(CCustomCSView& mnview, const CCoinsViewCache& coins, const CTransaction& tx, const Consensus::Params& consensus, uint32_t height, uint64_t time, uint32_t txn, CHistoryWriters* writers) { +Res ApplyCustomTx(CCustomCSView& mnview, const CCoinsViewCache& coins, const CTransaction& tx, const Consensus::Params& consensus, uint32_t height, uint64_t time, uint256* canSpend, uint32_t txn, CHistoryWriters* writers) { auto res = Res::Ok(); if (tx.IsCoinBase() && height > 0) { // genesis contains custom coinbase txs return res; @@ -3141,6 +3205,19 @@ Res ApplyCustomTx(CCustomCSView& mnview, const CCoinsViewCache& coins, const CTr } res = CustomTxVisit(view, coins, tx, height, consensus, txMessage, time); + if (canSpend && res && txType == CustomTxType::UpdateMasternode) { + auto obj = std::get(txMessage); + for (const auto item : obj.updates) { + if (item.first == static_cast(UpdateMasternodeType::OwnerAddress)) { + for (const auto input : tx.vin) { + if (input.prevout.hash == obj.mnId && input.prevout.n == 1) { + *canSpend = obj.mnId; + } + } + } + } + } + // Track burn fee if (txType == CustomTxType::CreateToken || txType == CustomTxType::CreateMasternode) { if (writers) { diff --git a/src/masternodes/mn_checks.h b/src/masternodes/mn_checks.h index 11b7bcefc06..67a92a13faf 100644 --- a/src/masternodes/mn_checks.h +++ b/src/masternodes/mn_checks.h @@ -102,9 +102,10 @@ enum class CustomTxType : uint8_t enum class UpdateMasternodeType : uint8_t { None = 0x00, - OperatorAddress = 0x01, - SetRewardAddress = 0x02, - RemRewardAddress = 0x03 + OwnerAddress = 0x01, + OperatorAddress = 0x02, + SetRewardAddress = 0x03, + RemRewardAddress = 0x04 }; inline CustomTxType CustomTxCodeToType(uint8_t ch) { @@ -344,7 +345,7 @@ CCustomTxMessage customTypeToMessage(CustomTxType txType); bool IsMempooledCustomTxCreate(const CTxMemPool& pool, const uint256& txid); Res RpcInfo(const CTransaction& tx, uint32_t height, CustomTxType& type, UniValue& results); Res CustomMetadataParse(uint32_t height, const Consensus::Params& consensus, const std::vector& metadata, CCustomTxMessage& txMessage); -Res ApplyCustomTx(CCustomCSView& mnview, const CCoinsViewCache& coins, const CTransaction& tx, const Consensus::Params& consensus, uint32_t height, uint64_t time = 0, uint32_t txn = 0, CHistoryWriters* writers = nullptr); +Res ApplyCustomTx(CCustomCSView& mnview, const CCoinsViewCache& coins, const CTransaction& tx, const Consensus::Params& consensus, uint32_t height, uint64_t time = 0, uint256* canSpend = nullptr, uint32_t txn = 0, CHistoryWriters* writers = nullptr); Res RevertCustomTx(CCustomCSView& mnview, const CCoinsViewCache& coins, const CTransaction& tx, const Consensus::Params& consensus, uint32_t height, uint32_t txn, CHistoryErasers& erasers); Res CustomTxVisit(CCustomCSView& mnview, const CCoinsViewCache& coins, const CTransaction& tx, uint32_t height, const Consensus::Params& consensus, const CCustomTxMessage& txMessage, uint64_t time = 0); ResVal ApplyAnchorRewardTx(CCustomCSView& mnview, const CTransaction& tx, int height, const uint256& prevStakeModifier, const std::vector& metadata, const Consensus::Params& consensusParams); diff --git a/src/masternodes/rpc_masternodes.cpp b/src/masternodes/rpc_masternodes.cpp index e9d53dc71fb..ad697578498 100644 --- a/src/masternodes/rpc_masternodes.cpp +++ b/src/masternodes/rpc_masternodes.cpp @@ -8,7 +8,7 @@ UniValue mnToJSON(uint256 const & nodeId, CMasternode const& node, bool verbose, UniValue ret(UniValue::VOBJ); auto currentHeight = ChainActive().Height(); if (!verbose) { - ret.pushKV(nodeId.GetHex(), CMasternode::GetHumanReadableState(node.GetState(currentHeight))); + ret.pushKV(nodeId.GetHex(), CMasternode::GetHumanReadableState(node.GetState(currentHeight, *pcustomcsview))); } else { UniValue obj(UniValue::VOBJ); @@ -30,8 +30,8 @@ UniValue mnToJSON(uint256 const & nodeId, CMasternode const& node, bool verbose, obj.pushKV("creationHeight", node.creationHeight); obj.pushKV("resignHeight", node.resignHeight); obj.pushKV("resignTx", node.resignTx.GetHex()); - obj.pushKV("banTx", node.banTx.GetHex()); - obj.pushKV("state", CMasternode::GetHumanReadableState(node.GetState(currentHeight))); + obj.pushKV("collateralTx", node.collateralTx.GetHex()); + obj.pushKV("state", CMasternode::GetHumanReadableState(node.GetState(currentHeight, *pcustomcsview))); obj.pushKV("mintedBlocks", (uint64_t) node.mintedBlocks); isminetype ownerMine = IsMineCached(*pwallet, ownerDest); obj.pushKV("ownerIsMine", bool(ownerMine & ISMINE_SPENDABLE)); @@ -247,7 +247,7 @@ UniValue resignmasternode(const JSONRPCRequest& request) std::string const nodeIdStr = request.params[0].getValStr(); uint256 const nodeId = uint256S(nodeIdStr); - CTxDestination ownerDest; + CTxDestination ownerDest, collateralDest; int targetHeight; { LOCK(cs_main); @@ -259,6 +259,13 @@ UniValue resignmasternode(const JSONRPCRequest& request) CTxDestination(PKHash(nodePtr->ownerAuthAddress)) : CTxDestination(WitnessV0KeyHash(nodePtr->ownerAuthAddress)); + if (!nodePtr->collateralTx.IsNull()) { + const auto& coin = ::ChainstateActive().CoinsTip().AccessCoin({nodePtr->collateralTx, 1}); + if (coin.IsSpent() || !ExtractDestination(coin.out.scriptPubKey, collateralDest)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Masternode collateral not available"); + } + } + targetHeight = ::ChainActive().Height() + 1; } @@ -267,6 +274,9 @@ UniValue resignmasternode(const JSONRPCRequest& request) CTransactionRef optAuthTx; std::set auths{GetScriptForDestination(ownerDest)}; + if (collateralDest.index() != 0) { + auths.insert(GetScriptForDestination(collateralDest)); + } rawTx.vin = GetAuthInputsSmart(pwallet, rawTx.nVersion, auths, false, optAuthTx, request.params[1]); // Return change to owner address @@ -304,7 +314,8 @@ UniValue updatemasternode(const JSONRPCRequest& request) {"mn_id", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The Masternode's ID"}, {"values", RPCArg::Type::OBJ, RPCArg::Optional::NO, "", { - {"operatorAddress", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "The new masternode operator auth address (P2PKH only, unique)"}, + {"ownerAddress", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "The new masternode owner address, requires masternode collateral fee (P2PKH or P2WPKH)"}, + {"operatorAddress", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "The new masternode operator address (P2PKH or P2WPKH)"}, {"rewardAddress", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Masternode`s new reward address, empty \"\" to remove old reward address."}, }, }, @@ -351,9 +362,16 @@ UniValue updatemasternode(const JSONRPCRequest& request) targetHeight = ::ChainActive().Height() + 1; } - CTxDestination operatorDest, rewardDest; + CTxDestination newOwnerDest, operatorDest, rewardDest; UniValue metaObj = request.params[1].get_obj(); + if (!metaObj["ownerAddress"].isNull()) { + newOwnerDest = DecodeDestination(metaObj["ownerAddress"].getValStr()); + if (newOwnerDest.index() != PKHashType && newOwnerDest.index() != WitV0KeyHashType) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "ownerAddress (" + metaObj["ownerAddress"].getValStr() + ") does not refer to a P2PKH or P2WPKH address"); + } + } + if (!metaObj["operatorAddress"].isNull()) { operatorDest = DecodeDestination(metaObj["operatorAddress"].getValStr()); if (operatorDest.index() != PKHashType && operatorDest.index() != WitV0KeyHashType) { @@ -376,7 +394,8 @@ UniValue updatemasternode(const JSONRPCRequest& request) CMutableTransaction rawTx(txVersion); CTransactionRef optAuthTx; - std::set auths{GetScriptForDestination(ownerDest)}; + const CScript ownerScript = !metaObj["ownerAddress"].isNull() ? GetScriptForDestination(newOwnerDest) : GetScriptForDestination(ownerDest); + std::set auths{ownerScript}; rawTx.vin = GetAuthInputsSmart(pwallet, rawTx.nVersion, auths, false, optAuthTx, request.params[2]); // Return change to owner address @@ -387,6 +406,10 @@ UniValue updatemasternode(const JSONRPCRequest& request) CUpdateMasterNodeMessage msg{nodeId}; + if (!metaObj["ownerAddress"].isNull()) { + msg.updates.emplace_back(static_cast(UpdateMasternodeType::OwnerAddress), std::pair>()); + } + if (!metaObj["operatorAddress"].isNull()) { const CKeyID keyID = operatorDest.index() == PKHashType ? CKeyID(std::get(operatorDest)) : CKeyID(std::get(operatorDest)); msg.updates.emplace_back(static_cast(UpdateMasternodeType::OperatorAddress), std::make_pair(static_cast(operatorDest.index()), std::vector(keyID.begin(), keyID.end()))); @@ -408,7 +431,17 @@ UniValue updatemasternode(const JSONRPCRequest& request) CScript scriptMeta; scriptMeta << OP_RETURN << ToByteVector(metadata); - rawTx.vout.push_back(CTxOut(0, scriptMeta)); + rawTx.vout.emplace_back(0, scriptMeta); + + // Add new owner collateral + if (!metaObj["ownerAddress"].isNull()) { + if (const auto node = pcustomcsview->GetMasternode(nodeId)) { + rawTx.vin.emplace_back(node->collateralTx.IsNull() ? nodeId : node->collateralTx, 1); + rawTx.vout.emplace_back(GetMnCollateralAmount(targetHeight), ownerScript); + } else { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("masternode %s does not exists", nodeIdStr)); + } + } fund(rawTx, pwallet, optAuthTx, &coinControl); diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index e704e015802..8c9043686ad 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -279,7 +279,7 @@ static UniValue getmininginfo(const JSONRPCRequest& request) //should not come here if the database has correct data. throw JSONRPCError(RPC_DATABASE_ERROR, strprintf("The masternode %s does not exist", mnId.second.GetHex())); } - auto state = nodePtr->GetState(height); + auto state = nodePtr->GetState(height, *pcustomcsview); CTxDestination operatorDest = nodePtr->operatorType == 1 ? CTxDestination(PKHash(nodePtr->operatorAuthAddress)) : CTxDestination(WitnessV0KeyHash(nodePtr->operatorAuthAddress)); subObj.pushKV("operator", EncodeDestination(operatorDest));// NOTE(sp) : Should this also be encoded? not the HEX diff --git a/src/txmempool.cpp b/src/txmempool.cpp index 9b8151eae5b..dc66e4805df 100644 --- a/src/txmempool.cpp +++ b/src/txmempool.cpp @@ -603,16 +603,17 @@ void CTxMemPool::removeForBlock(const std::vector& vtx, unsigne for (auto it = txsByEntryTime.begin(); it != txsByEntryTime.end(); ++it) { CValidationState state; const auto& tx = it->GetTx(); - if (!Consensus::CheckTxInputs(tx, state, mempoolDuplicate, &viewDuplicate, nBlockHeight, txfee, Params())) { - LogPrintf("%s: Remove conflicting TX: %s\n", __func__, tx.GetHash().GetHex()); - staged.insert(mapTx.project<0>(it)); - continue; - } - auto res = ApplyCustomTx(viewDuplicate, mempoolDuplicate, tx, Params().GetConsensus(), nBlockHeight); + uint256 canSpend; + auto res = ApplyCustomTx(viewDuplicate, mempoolDuplicate, tx, Params().GetConsensus(), nBlockHeight, 0, &canSpend); if (!res.ok && (res.code & CustomTxErrCodes::Fatal)) { LogPrintf("%s: Remove conflicting custom TX: %s\n", __func__, tx.GetHash().GetHex()); staged.insert(mapTx.project<0>(it)); } + if (!Consensus::CheckTxInputs(tx, state, mempoolDuplicate, &viewDuplicate, nBlockHeight, txfee, Params(), canSpend)) { + LogPrintf("%s: Remove conflicting TX: %s\n", __func__, tx.GetHash().GetHex()); + staged.insert(mapTx.project<0>(it)); + continue; + } } for (auto& it : staged) { @@ -655,7 +656,10 @@ static void CheckInputsAndUpdateCoins(const CTransaction& tx, CCoinsViewCache& m { CValidationState state; CAmount txfee = 0; - bool fCheckResult = tx.IsCoinBase() || Consensus::CheckTxInputs(tx, state, mempoolDuplicate, mnview, spendheight, txfee, chainparams); + uint256 canSpend; + CCustomCSView viewCopy(*pcustomcsview.get()); + ApplyCustomTx(viewCopy, mempoolDuplicate, tx, chainparams.GetConsensus(), spendheight, 0, &canSpend); + bool fCheckResult = tx.IsCoinBase() || Consensus::CheckTxInputs(tx, state, mempoolDuplicate, mnview, spendheight, txfee, chainparams, canSpend); fCheckResult = fCheckResult && CheckBurnSpend(tx, mempoolDuplicate); assert(fCheckResult); UpdateCoins(tx, mempoolDuplicate, std::numeric_limits::max()); diff --git a/src/validation.cpp b/src/validation.cpp index e2e18efa02f..92064b89e5a 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -612,9 +612,14 @@ static bool AcceptToMemoryPoolWorker(const CChainParams& chainparams, CTxMemPool const auto height = GetSpendHeight(view); // it does not need to check mempool anymore it has view there + uint256 canSpend; + auto res = ApplyCustomTx(mnview, view, tx, chainparams.GetConsensus(), height, nAcceptTime, &canSpend); + if (!res.ok || (res.code & CustomTxErrCodes::Fatal)) { + return state.Invalid(ValidationInvalidReason::TX_MEMPOOL_POLICY, false, REJECT_INVALID, res.msg); + } CAmount nFees = 0; - if (!Consensus::CheckTxInputs(tx, state, view, &mnview, height, nFees, chainparams)) { + if (!Consensus::CheckTxInputs(tx, state, view, &mnview, height, nFees, chainparams, canSpend)) { return error("%s: Consensus::CheckTxInputs: %s, %s", __func__, tx.GetHash().ToString(), FormatStateMessage(state)); } @@ -627,11 +632,6 @@ static bool AcceptToMemoryPoolWorker(const CChainParams& chainparams, CTxMemPool return state.Invalid(ValidationInvalidReason::TX_MEMPOOL_POLICY, false, REJECT_INVALID, "bad-txns-inputs-below-tx-fee"); } - auto res = ApplyCustomTx(mnview, view, tx, chainparams.GetConsensus(), height, nAcceptTime); - if (!res.ok || (res.code & CustomTxErrCodes::Fatal)) { - return state.Invalid(ValidationInvalidReason::TX_MEMPOOL_POLICY, false, REJECT_INVALID, res.msg); - } - // we have all inputs cached now, so switch back to dummy, so we don't need to keep lock on mempool view.SetBackend(dummy); @@ -2297,7 +2297,7 @@ bool CChainState::ConnectBlock(const CBlock& block, CValidationState& state, CBl // init view|db with genesis here for (size_t i = 0; i < block.vtx.size(); ++i) { CHistoryWriters writers{paccountHistoryDB.get(), nullptr, nullptr}; - const auto res = ApplyCustomTx(mnview, view, *block.vtx[i], chainparams.GetConsensus(), pindex->nHeight, pindex->GetBlockTime(), i, &writers); + const auto res = ApplyCustomTx(mnview, view, *block.vtx[i], chainparams.GetConsensus(), pindex->nHeight, pindex->GetBlockTime(), nullptr, i, &writers); if (!res.ok) { return error("%s: Genesis block ApplyCustomTx failed. TX: %s Error: %s", __func__, block.vtx[i]->GetHash().ToString(), res.msg); @@ -2495,24 +2495,6 @@ bool CChainState::ConnectBlock(const CBlock& block, CValidationState& state, CBl if (!tx.IsCoinBase()) { - CAmount txfee = 0; - if (!Consensus::CheckTxInputs(tx, state, view, &accountsView, pindex->nHeight, txfee, chainparams)) { - if (!IsBlockReason(state.GetReason())) { - // CheckTxInputs may return MISSING_INPUTS or - // PREMATURE_SPEND but we can't return that, as it's not - // defined for a block, so we reset the reason flag to - // CONSENSUS here. - state.Invalid(ValidationInvalidReason::CONSENSUS, false, - state.GetRejectCode(), state.GetRejectReason(), state.GetDebugMessage()); - } - return error("%s: Consensus::CheckTxInputs: %s, %s", __func__, tx.GetHash().ToString(), FormatStateMessage(state)); - } - nFees += txfee; - if (!MoneyRange(nFees)) { - return state.Invalid(ValidationInvalidReason::CONSENSUS, error("%s: accumulated fee in the block out of range.", __func__), - REJECT_INVALID, "bad-txns-accumulated-fee-outofrange"); - } - // Check that transaction is BIP68 final // BIP68 lock checks (as opposed to nLockTime checks) must // be in ConnectBlock because they require the UTXO set @@ -2557,7 +2539,8 @@ bool CChainState::ConnectBlock(const CBlock& block, CValidationState& state, CBl } CHistoryWriters writers{paccountHistoryDB.get(), pburnHistoryDB.get(), pvaultHistoryDB.get()}; - const auto res = ApplyCustomTx(accountsView, view, tx, chainparams.GetConsensus(), pindex->nHeight, pindex->GetBlockTime(), i, &writers); + uint256 canSpend; + const auto res = ApplyCustomTx(accountsView, view, tx, chainparams.GetConsensus(), pindex->nHeight, pindex->GetBlockTime(), &canSpend, i, &writers); if (!res.ok && (res.code & CustomTxErrCodes::Fatal)) { if (pindex->nHeight >= chainparams.GetConsensus().EunosHeight) { return state.Invalid(ValidationInvalidReason::CONSENSUS, @@ -2578,6 +2561,24 @@ bool CChainState::ConnectBlock(const CBlock& block, CValidationState& state, CBl } } + CAmount txfee = 0; + if (!Consensus::CheckTxInputs(tx, state, view, &accountsView, pindex->nHeight, txfee, chainparams, canSpend)) { + if (!IsBlockReason(state.GetReason())) { + // CheckTxInputs may return MISSING_INPUTS or + // PREMATURE_SPEND but we can't return that, as it's not + // defined for a block, so we reset the reason flag to + // CONSENSUS here. + state.Invalid(ValidationInvalidReason::CONSENSUS, false, + state.GetRejectCode(), state.GetRejectReason(), state.GetDebugMessage()); + } + return error("%s: Consensus::CheckTxInputs: %s, %s", __func__, tx.GetHash().ToString(), FormatStateMessage(state)); + } + nFees += txfee; + if (!MoneyRange(nFees)) { + return state.Invalid(ValidationInvalidReason::CONSENSUS, error("%s: accumulated fee in the block out of range.", __func__), + REJECT_INVALID, "bad-txns-accumulated-fee-outofrange"); + } + control.Add(vChecks); } else { std::vector metadata; @@ -2820,6 +2821,24 @@ bool CChainState::ConnectBlock(const CBlock& block, CValidationState& state, CBl cache.EraseStoredVariables(static_cast(pindex->nHeight)); } + if (pindex->nHeight >= chainparams.GetConsensus().GreatWorldHeight) { + // Apply any pending masternode owner changes + cache.ForEachNewCollateral([&](const uint256& key, const MNNewOwnerHeightValue& value){ + if (value.blockHeight == pindex->nHeight) { + auto node = cache.GetMasternode(value.masternodeID); + assert(node); + assert(key == node->collateralTx); + const auto& coin = view.AccessCoin({node->collateralTx, 1}); + assert(!coin.IsSpent()); + CTxDestination dest; + assert(ExtractDestination(coin.out.scriptPubKey, dest)); + const CKeyID keyId = dest.index() == PKHashType ? CKeyID(std::get(dest)) : CKeyID(std::get(dest)); + cache.UpdateMasternodeOwner(value.masternodeID, *node, dest.index(), keyId); + } + return true; + }); + } + // construct undo auto& flushable = cache.GetStorage(); auto undo = CUndo::Construct(mnview.GetStorage(), flushable.GetRaw()); diff --git a/test/functional/rpc_updatemasternode.py b/test/functional/rpc_updatemasternode.py index a34922ac5de..5b7894009ed 100644 --- a/test/functional/rpc_updatemasternode.py +++ b/test/functional/rpc_updatemasternode.py @@ -9,6 +9,7 @@ assert_equal, assert_raises_rpc_error, ) +from decimal import Decimal class TestForcedRewardAddress(DefiTestFramework): def set_test_params(self): @@ -40,21 +41,94 @@ def unspent_amount(node, address): result += vals[i]['amount'] return result + def fund_tx(self, address, amount): + missing_auth_tx = self.nodes[0].sendtoaddress(address, amount) + count, missing_input_vout = 0, 0 + for vout in self.nodes[0].getrawtransaction(missing_auth_tx, 1)['vout']: + if vout['scriptPubKey']['addresses'][0] == address: + missing_input_vout = count + break + count += 1 + self.nodes[0].generate(1) + return missing_auth_tx, missing_input_vout + + def transfer_owner(self, mn_id): + # Get current collateral + result = self.nodes[0].getmasternode(mn_id)[mn_id] + if (result['collateralTx'] == "0000000000000000000000000000000000000000000000000000000000000000"): + collateral = mn_id + else: + collateral = result['collateralTx'] + owner = result['ownerAuthAddress'] + + # Create new owner address + new_owner = self.nodes[0].getnewaddress("", "legacy") + + # Test update of owner address + mn_transfer_tx = self.nodes[0].updatemasternode(mn_id, {'ownerAddress':new_owner}) + mn_transfer_rawtx = self.nodes[0].getrawtransaction(mn_transfer_tx, 1) + self.nodes[0].generate(1) + + # Make sure new collateral is present + assert_equal(mn_transfer_rawtx['vout'][1]['value'], Decimal('10.00000000')) + assert_equal(mn_transfer_rawtx['vout'][1]['scriptPubKey']['hex'], self.nodes[0].getaddressinfo(new_owner)['scriptPubKey']) + + # Test spending of new collateral + rawtx = self.nodes[0].createrawtransaction([{"txid":mn_transfer_tx, "vout":1}], [{new_owner:9.9999}]) + signed_rawtx = self.nodes[0].signrawtransactionwithwallet(rawtx) + assert_raises_rpc_error(-26, "tried to spend locked collateral for {}".format(mn_transfer_tx), self.nodes[0].sendrawtransaction, signed_rawtx['hex']) + + # Make sure old collateral is set as input + found = False + for vin in mn_transfer_rawtx['vin']: + if vin['txid'] == collateral and vin['vout'] == 1: + found = True + assert(found) + + # Check new state is TRANSFERRING and owner is still the same + result = self.nodes[0].getmasternode(mn_id)[mn_id] + assert_equal(result['state'], 'TRANSFERRING') + assert_equal(result['collateralTx'], mn_transfer_tx) + assert_equal(result['ownerAuthAddress'], owner) + + # Test update while masternode is in TRANSFERRING state + assert_raises_rpc_error(-32600, "Masternode {} is not in 'ENABLED' state".format(mn_id), self.nodes[0].updatemasternode, mn_id, {'ownerAddress':new_owner}) + + # Test PRE_ENABLED state and owner change + self.nodes[0].generate(10) + result = self.nodes[0].getmasternode(mn_id)[mn_id] + assert_equal(result['state'], 'PRE_ENABLED') + assert_equal(result['collateralTx'], mn_transfer_tx) + assert_equal(result['ownerAuthAddress'], new_owner) + + # Try another transfer during pre-enabled + assert_raises_rpc_error(-32600, "Masternode {} is not in 'ENABLED' state".format(mn_id), self.nodes[0].updatemasternode, mn_id, {'ownerAddress':new_owner}) + + # Test ENABLED state and owner change + self.nodes[0].generate(10) + result = self.nodes[0].getmasternode(mn_id)[mn_id] + assert_equal(result['state'], 'ENABLED') + assert_equal(result['collateralTx'], mn_transfer_tx) + assert_equal(result['ownerAuthAddress'], new_owner) + def run_test(self): self.nodes[0].generate(105) self.sync_all([self.nodes[0], self.nodes[1]]) - self.log.info("Create new masternode for test...") num_mns = len(self.nodes[0].listmasternodes()) mn_owner = self.nodes[0].getnewaddress("", "legacy") + mn_owner2 = self.nodes[0].getnewaddress("", "legacy") mn_id = self.nodes[0].createmasternode(mn_owner) + mn_id2 = self.nodes[0].createmasternode(mn_owner2) self.nodes[0].generate(1) - assert_equal(len(self.nodes[0].listmasternodes()), num_mns + 1) - assert_equal(self.nodes[0].getmasternode(mn_id)[mn_id]['rewardAddress'], '') - assert_equal(self.nodes[0].getmasternode(mn_id)[mn_id]['ownerAuthAddress'], mn_owner) - assert_equal(self.nodes[0].getmasternode(mn_id)[mn_id]['operatorAuthAddress'], mn_owner) + assert_equal(len(self.nodes[0].listmasternodes()), num_mns + 2) + result = self.nodes[0].getmasternode(mn_id)[mn_id] + assert_equal(result['collateralTx'], "0000000000000000000000000000000000000000000000000000000000000000") + assert_equal(result['rewardAddress'], '') + assert_equal(result['ownerAuthAddress'], mn_owner) + assert_equal(result['operatorAuthAddress'], mn_owner) # Test call before for height operator_address = self.nodes[0].getnewaddress("", "legacy") @@ -73,7 +147,12 @@ def run_test(self): # node 1 try to update node 0 which should be rejected. assert_raises_rpc_error(-5, "Incorrect authorization for {}".format(mn_owner), self.nodes[1].updatemasternode, mn_id, {'operatorAddress':operator_address}) + # Update operator address self.nodes[0].updatemasternode(mn_id, {'operatorAddress':operator_address}) + + # Test updating another node to the same address + assert_raises_rpc_error(-26, "Masternode with that operator address already exists", self.nodes[0].updatemasternode, mn_id2, {'operatorAddress':operator_address}) + self.nodes[0].resignmasternode(mn_id2) self.nodes[0].generate(1) self.sync_all() @@ -93,16 +172,18 @@ def run_test(self): self.nodes[0].updatemasternode(mn_id, {'rewardAddress':forced_reward_address}) self.nodes[0].generate(1) - assert_equal(self.nodes[0].getmasternode(mn_id)[mn_id]['rewardAddress'], forced_reward_address) - assert_equal(self.nodes[0].getmasternode(mn_id)[mn_id]['ownerAuthAddress'], mn_owner) - assert_equal(self.nodes[0].getmasternode(mn_id)[mn_id]['operatorAuthAddress'], operator_address) + result = self.nodes[0].getmasternode(mn_id)[mn_id] + assert_equal(result['rewardAddress'], forced_reward_address) + assert_equal(result['ownerAuthAddress'], mn_owner) + assert_equal(result['operatorAuthAddress'], operator_address) self.nodes[0].updatemasternode(mn_id, {'rewardAddress':''}) self.nodes[0].generate(1) - assert_equal(self.nodes[0].getmasternode(mn_id)[mn_id]['rewardAddress'], '') - assert_equal(self.nodes[0].getmasternode(mn_id)[mn_id]['ownerAuthAddress'], mn_owner) - assert_equal(self.nodes[0].getmasternode(mn_id)[mn_id]['operatorAuthAddress'], operator_address) + result = self.nodes[0].getmasternode(mn_id)[mn_id] + assert_equal(result['rewardAddress'], '') + assert_equal(result['ownerAuthAddress'], mn_owner) + assert_equal(result['operatorAuthAddress'], operator_address) self.nodes[0].updatemasternode(mn_id, {'rewardAddress':forced_reward_address}) self.nodes[0].generate(1) @@ -126,7 +207,6 @@ def run_test(self): # CLI Reward address for test -rewardaddress cli_reward_address = self.nodes[0].getnewaddress("", "legacy") - self.log.info(cli_reward_address) self.restart_node(0, ['-gen', '-masternode_operator='+operator_address, '-rewardaddress='+cli_reward_address, '-txindex=1', '-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-greatworldheight=1']) @@ -161,11 +241,15 @@ def run_test(self): assert("No update arguments provided" in errorString) # Test unknown update type - unknown_tx = self.nodes[0].updatemasternode(mn_id, {'rewardAddress':new_reward_address}) - unknown_rawtx = self.nodes[0].getrawtransaction(unknown_tx) - self.nodes[0].clearmempool() - - updated_tx = unknown_rawtx.replace('01020114', '01ff0114') + while True: + address = self.nodes[0].getnewaddress("", "legacy") + unknown_tx = self.nodes[0].updatemasternode(mn_id, {'rewardAddress':address}) + unknown_rawtx = self.nodes[0].getrawtransaction(unknown_tx) + self.nodes[0].clearmempool() + if unknown_rawtx.count('01030114') == 1: + break + + updated_tx = unknown_rawtx.replace('01030114', '01ff0114') self.nodes[0].signrawtransactionwithwallet(updated_tx) try: @@ -174,5 +258,155 @@ def run_test(self): errorString = e.error['message'] assert("Unknown update type provided" in errorString) + # Test incorrect owner address + assert_raises_rpc_error(-8, + "ownerAddress ({}) does not refer to a P2PKH or P2WPKH address".format("some_bad_address"), + self.nodes[0].updatemasternode, mn_id, {'ownerAddress':'some_bad_address'} + ) + + # Test update of owner address with existing address + assert_raises_rpc_error(-32600, "Masternode with that owner address already exists", self.nodes[0].updatemasternode, mn_id, {'ownerAddress':mn_owner}) + + # Set up input / output tests + not_collateral = self.nodes[0].getnewaddress("", "legacy") + owner_address = self.nodes[0].getnewaddress("", "legacy") + [not_collateral_tx, not_collateral_vout] = self.fund_tx(not_collateral, 10) + [missing_auth_tx, missing_input_vout] = self.fund_tx(mn_owner, 0.1) + [owner_auth_tx, owner_auth_vout] = self.fund_tx(owner_address, 0.1) + + # Get TX to use OP_RETURN output + missing_tx = self.nodes[0].updatemasternode(mn_id, {'ownerAddress':owner_address}) + missing_rawtx = self.nodes[0].getrawtransaction(missing_tx, 1) + self.nodes[0].clearmempool() + + # Test owner update without collateral input + rawtx = self.nodes[0].createrawtransaction([{"txid":missing_auth_tx, "vout":missing_input_vout},{"txid":not_collateral_tx, "vout":not_collateral_vout}], [{"data":missing_rawtx['vout'][0]['scriptPubKey']['hex'][4:]},{owner_address:10}]) + signed_rawtx = self.nodes[0].signrawtransactionwithwallet(rawtx) + assert_raises_rpc_error(-26, "Missing previous collateral from transaction inputs", self.nodes[0].sendrawtransaction, signed_rawtx['hex']) + + # Test incorrect new collateral amount + rawtx = self.nodes[0].createrawtransaction([{"txid":mn_id, "vout":1},{"txid":missing_auth_tx, "vout":missing_input_vout},{"txid":owner_auth_tx, "vout":owner_auth_vout}], [{"data":missing_rawtx['vout'][0]['scriptPubKey']['hex'][4:]},{owner_address:1}]) + signed_rawtx = self.nodes[0].signrawtransactionwithwallet(rawtx) + assert_raises_rpc_error(-26, "Incorrect collateral amount", self.nodes[0].sendrawtransaction, signed_rawtx['hex']) + + # Test missing new owner auth + rawtx = self.nodes[0].createrawtransaction([{"txid":mn_id, "vout":1},{"txid":missing_auth_tx, "vout":missing_input_vout}], [{"data":missing_rawtx['vout'][0]['scriptPubKey']['hex'][4:]},{owner_address:10}]) + signed_rawtx = self.nodes[0].signrawtransactionwithwallet(rawtx) + assert_raises_rpc_error(-26, "Missing auth input for new masternode owner", self.nodes[0].sendrawtransaction, signed_rawtx['hex']) + + # Test transfer of owner + self.transfer_owner(mn_id) + + # Test second transfer of MN owner + self.transfer_owner(mn_id) + + # Test resigning MN with transferred collateral + self.nodes[0].resignmasternode(mn_id) + self.nodes[0].generate(1) + result = self.nodes[0].getmasternode(mn_id)[mn_id] + assert_equal(result['state'], 'PRE_RESIGNED') + + # Roll back resignation + self.nodes[0].invalidateblock(self.nodes[0].getblockhash(self.nodes[0].getblockcount())) + result = self.nodes[0].getmasternode(mn_id)[mn_id] + assert_equal(result['state'], 'ENABLED') + + # Check MN resigned + self.nodes[0].generate(11) + result = self.nodes[0].getmasternode(mn_id)[mn_id] + assert_equal(result['state'], 'RESIGNED') + + # Test spending of transferred collateral after resignation + rawtx = self.nodes[0].createrawtransaction([{"txid":result['collateralTx'], "vout":1}], [{owner_address:9.9999}]) + signed_rawtx = self.nodes[0].signrawtransactionwithwallet(rawtx) + self.nodes[0].sendrawtransaction(signed_rawtx['hex']) + + # Set up for multiple MN owner transfer + mn1_owner = self.nodes[0].getnewaddress("", "legacy") + mn2_owner = self.nodes[0].getnewaddress("", "legacy") + mn3_owner = self.nodes[0].getnewaddress("", "legacy") + mn4_owner = self.nodes[0].getnewaddress("", "legacy") + mn5_owner = self.nodes[0].getnewaddress("", "legacy") + mn6_owner = self.nodes[0].getnewaddress("", "legacy") + + mn1 = self.nodes[0].createmasternode(mn1_owner) + mn2 = self.nodes[0].createmasternode(mn2_owner) + mn3 = self.nodes[0].createmasternode(mn3_owner) + mn4 = self.nodes[0].createmasternode(mn4_owner) + mn5 = self.nodes[0].createmasternode(mn5_owner) + mn6 = self.nodes[0].createmasternode(mn6_owner) + self.nodes[0].generate(11) + + result = self.nodes[0].listmasternodes() + assert_equal(result[mn1]['state'], 'ENABLED') + assert_equal(result[mn2]['state'], 'ENABLED') + assert_equal(result[mn3]['state'], 'ENABLED') + assert_equal(result[mn4]['state'], 'ENABLED') + assert_equal(result[mn5]['state'], 'ENABLED') + assert_equal(result[mn6]['state'], 'ENABLED') + + new_mn1_owner = self.nodes[0].getnewaddress("", "legacy") + new_mn2_owner = self.nodes[0].getnewaddress("", "legacy") + new_mn3_owner = self.nodes[0].getnewaddress("", "legacy") + new_mn4_owner = self.nodes[0].getnewaddress("", "legacy") + new_mn5_owner = self.nodes[0].getnewaddress("", "legacy") + new_mn6_owner = self.nodes[0].getnewaddress("", "legacy") + + # Try updating two nodes to the same address + self.nodes[0].updatemasternode(mn1, {'ownerAddress':new_mn1_owner}) + assert_raises_rpc_error(-26, "Masternode exist with that owner address pending already", self.nodes[0].updatemasternode, mn2, {'ownerAddress':new_mn1_owner}) + + # Test updating several MNs owners in the same block + self.nodes[0].updatemasternode(mn2, {'ownerAddress':new_mn2_owner}) + self.nodes[0].updatemasternode(mn3, {'ownerAddress':new_mn3_owner}) + self.nodes[0].updatemasternode(mn4, {'ownerAddress':new_mn4_owner}) + self.nodes[0].updatemasternode(mn5, {'ownerAddress':new_mn5_owner}) + self.nodes[0].updatemasternode(mn6, {'ownerAddress':new_mn6_owner}) + self.nodes[0].generate(1) + + result = self.nodes[0].listmasternodes() + assert_equal(result[mn1]['state'], 'TRANSFERRING') + assert_equal(result[mn1]['ownerAuthAddress'], mn1_owner) + assert_equal(result[mn2]['state'], 'TRANSFERRING') + assert_equal(result[mn2]['ownerAuthAddress'], mn2_owner) + assert_equal(result[mn3]['state'], 'TRANSFERRING') + assert_equal(result[mn3]['ownerAuthAddress'], mn3_owner) + assert_equal(result[mn4]['state'], 'TRANSFERRING') + assert_equal(result[mn4]['ownerAuthAddress'], mn4_owner) + assert_equal(result[mn5]['state'], 'TRANSFERRING') + assert_equal(result[mn5]['ownerAuthAddress'], mn5_owner) + assert_equal(result[mn6]['state'], 'TRANSFERRING') + assert_equal(result[mn6]['ownerAuthAddress'], mn6_owner) + + self.nodes[0].generate(10) + result = self.nodes[0].listmasternodes() + assert_equal(result[mn1]['state'], 'PRE_ENABLED') + assert_equal(result[mn1]['ownerAuthAddress'], new_mn1_owner) + assert_equal(result[mn2]['state'], 'PRE_ENABLED') + assert_equal(result[mn2]['ownerAuthAddress'], new_mn2_owner) + assert_equal(result[mn3]['state'], 'PRE_ENABLED') + assert_equal(result[mn3]['ownerAuthAddress'], new_mn3_owner) + assert_equal(result[mn4]['state'], 'PRE_ENABLED') + assert_equal(result[mn4]['ownerAuthAddress'], new_mn4_owner) + assert_equal(result[mn5]['state'], 'PRE_ENABLED') + assert_equal(result[mn5]['ownerAuthAddress'], new_mn5_owner) + assert_equal(result[mn6]['state'], 'PRE_ENABLED') + assert_equal(result[mn6]['ownerAuthAddress'], new_mn6_owner) + + self.nodes[0].generate(10) + result = self.nodes[0].listmasternodes() + assert_equal(result[mn1]['state'], 'ENABLED') + assert_equal(result[mn1]['ownerAuthAddress'], new_mn1_owner) + assert_equal(result[mn2]['state'], 'ENABLED') + assert_equal(result[mn2]['ownerAuthAddress'], new_mn2_owner) + assert_equal(result[mn3]['state'], 'ENABLED') + assert_equal(result[mn3]['ownerAuthAddress'], new_mn3_owner) + assert_equal(result[mn4]['state'], 'ENABLED') + assert_equal(result[mn4]['ownerAuthAddress'], new_mn4_owner) + assert_equal(result[mn5]['state'], 'ENABLED') + assert_equal(result[mn5]['ownerAuthAddress'], new_mn5_owner) + assert_equal(result[mn6]['state'], 'ENABLED') + assert_equal(result[mn6]['ownerAuthAddress'], new_mn6_owner) + if __name__ == '__main__': TestForcedRewardAddress().main()