From e2eb658de8ea1cb964d3e4110d198ef02e97a057 Mon Sep 17 00:00:00 2001 From: Peter Bushnell Date: Tue, 4 Jan 2022 10:23:11 +0000 Subject: [PATCH] Restore updatemasternode --- src/consensus/tx_verify.cpp | 28 +- src/masternodes/anchors.cpp | 2 +- src/masternodes/masternodes.cpp | 176 +++++----- src/masternodes/masternodes.h | 43 ++- src/masternodes/mn_checks.cpp | 206 ++++++++---- src/masternodes/mn_checks.h | 51 +-- src/masternodes/rpc_customtx.cpp | 29 +- src/masternodes/rpc_masternodes.cpp | 325 +++++-------------- src/miner.cpp | 16 +- src/pos.cpp | 16 +- src/rpc/client.cpp | 3 +- src/rpc/mining.cpp | 6 +- src/test/blockencodings_tests.cpp | 2 +- src/test/pos_tests.cpp | 32 +- src/test/setup_common.cpp | 2 +- src/txdb.cpp | 4 - src/validation.cpp | 33 +- test/functional/feature_update_mn.py | 28 +- test/functional/rpc_getcustomtx.py | 51 +-- test/functional/rpc_updatemasternode.py | 412 ++++++++++++++++++++++++ test/functional/test_runner.py | 2 +- 21 files changed, 908 insertions(+), 559 deletions(-) create mode 100644 test/functional/rpc_updatemasternode.py diff --git a/src/consensus/tx_verify.cpp b/src/consensus/tx_verify.cpp index 21d30380527..cca50c2e498 100644 --- a/src/consensus/tx_verify.cpp +++ b/src/consensus/tx_verify.cpp @@ -169,6 +169,19 @@ bool Consensus::CheckTxInputs(const CTransaction& tx, CValidationState& state, c strprintf("%s: inputs missing/spent", __func__)); } + // check for tokens values + uint256 canSpend; + std::vector dummy; + const auto txType = GuessCustomTxType(tx, dummy); + + if (NotAllowedToFail(txType, nSpendHeight) || (nSpendHeight >= chainparams.GetConsensus().GreatWorldHeight && txType == CustomTxType::UpdateMasternode)) { + CCustomCSView discardCache(const_cast(*mnview)); + auto res = ApplyCustomTx(discardCache, inputs, tx, chainparams.GetConsensus(), nSpendHeight, 0, &canSpend); + if (!res.ok && (res.code & CustomTxErrCodes::Fatal) && txType != CustomTxType::UpdateMasternode) { + return state.Invalid(ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, "bad-txns-customtx", res.msg); + } + } + TAmounts nValuesIn; for (unsigned int i = 0; i < tx.vin.size(); ++i) { const COutPoint &prevout = tx.vin[i].prevout; @@ -187,8 +200,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? } @@ -215,18 +227,6 @@ bool Consensus::CheckTxInputs(const CTransaction& tx, CValidationState& state, c return state.Invalid(ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, "bad-txns-tokens-in-old-version-tx"); } - // check for tokens values - std::vector dummy; - const auto txType = GuessCustomTxType(tx, dummy); - - if (NotAllowedToFail(txType, nSpendHeight)) { - CCustomCSView discardCache(const_cast(*mnview)); - auto res = ApplyCustomTx(discardCache, inputs, tx, chainparams.GetConsensus(), nSpendHeight); - if (!res.ok && (res.code & CustomTxErrCodes::Fatal)) { - return state.Invalid(ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, "bad-txns-customtx", res.msg); - } - } - for (auto const & kv : non_minted_values_out) { DCT_ID const & tokenId = kv.first; diff --git a/src/masternodes/anchors.cpp b/src/masternodes/anchors.cpp index 2488d7f62dd..1c9b7fe6031 100644 --- a/src/masternodes/anchors.cpp +++ b/src/masternodes/anchors.cpp @@ -1011,7 +1011,7 @@ bool CAnchorAwaitingConfirms::Validate(CAnchorConfirmMessage const &confirmMessa } auto it = pcustomcsview->GetMasternodeIdByOperator(signer); - if (!it || !pcustomcsview->GetMasternode(*it)->IsActive(height)) { + if (!it || !pcustomcsview->GetMasternode(*it)->IsActive(height, *pcustomcsview)) { LogPrint(BCLog::ANCHORING, "%s: Warning! Masternode with operator key %s does not exist or not active!\n", __func__, signer.ToString()); return false; } diff --git a/src/masternodes/masternodes.cpp b/src/masternodes/masternodes.cpp index a9d1aea7ba3..e4bbac50c53 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 CMasternodesView& 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); @@ -113,9 +123,9 @@ CMasternode::State CMasternode::GetState(int height) const return State::UNKNOWN; } -bool CMasternode::IsActive(int height) const +bool CMasternode::IsActive(int height, const CMasternodesView& mnview) const { - State state = GetState(height); + State state = GetState(height, mnview); 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) { - // 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, *this); if (height >= Params().GetConsensus().EunosPayaHeight) { if (state != CMasternode::ENABLED) { return Res::Err("node %s state is not 'ENABLED'", nodeId.ToString()); @@ -305,96 +312,85 @@ 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(); } -Res CMasternodesView::SetForcedRewardAddress(uint256 const & nodeId, const char rewardAddressType, CKeyID const & rewardAddress, int height) +void CMasternodesView::SetForcedRewardAddress(uint256 const & nodeId, CMasternode& node, const char rewardAddressType, CKeyID const & rewardAddress, int height) { - // Temporarily disabled for 2.2 - return Res::Err("reward address change is disabled for Fort Canning"); - - auto node = GetMasternode(nodeId); - if (!node) { - return Res::Err("masternode %s does not exists", nodeId.ToString()); - } - auto state = node->GetState(height); - if ((state != CMasternode::PRE_ENABLED && state != CMasternode::ENABLED)) { - return Res::Err("masternode %s state is not 'PRE_ENABLED' or 'ENABLED'", nodeId.ToString()); - } - - // If old masternode update foor new serialisatioono - if (node->version < CMasternode::VERSION0) { - node->version = CMasternode::VERSION0; + // If old masternode update for new serialisation + if (node.version < CMasternode::VERSION0) { + node.version = CMasternode::VERSION0; } // Set new reward address - node->rewardAddressType = rewardAddressType; - node->rewardAddress = rewardAddress; - WriteBy(nodeId, *node); - - return Res::Ok(); + node.rewardAddressType = rewardAddressType; + node.rewardAddress = rewardAddress; + WriteBy(nodeId, node); } -Res CMasternodesView::RemForcedRewardAddress(uint256 const & nodeId, int height) +void CMasternodesView::RemForcedRewardAddress(uint256 const & nodeId, CMasternode& node, int height) { - // Temporarily disabled for 2.2 - return Res::Err("reward address change is disabled for Fort Canning"); + node.rewardAddressType = 0; + node.rewardAddress.SetNull(); + WriteBy(nodeId, node); +} - auto node = GetMasternode(nodeId); - if (!node) { - return Res::Err("masternode %s does not exists", nodeId.ToString()); - } - auto state = node->GetState(height); - if ((state != CMasternode::PRE_ENABLED && state != CMasternode::ENABLED)) { - return Res::Err("masternode %s state is not 'PRE_ENABLED' or 'ENABLED'", nodeId.ToString()); - } +void CMasternodesView::UpdateMasternodeOperator(uint256 const & nodeId, CMasternode& node, const char operatorType, const CKeyID& operatorAuthAddress, int height) +{ + // Remove old record + EraseBy(node.operatorAuthAddress); - node->rewardAddressType = 0; - node->rewardAddress.SetNull(); - WriteBy(nodeId, *node); + node.operatorType = operatorType; + node.operatorAuthAddress = operatorAuthAddress; - return Res::Ok(); + // Overwrite and create new record + WriteBy(nodeId, node); + WriteBy(node.operatorAuthAddress, nodeId); } -Res CMasternodesView::UpdateMasternode(uint256 const & nodeId, char operatorType, const CKeyID& operatorAuthAddress, int height) { - // Temporarily disabled for 2.2 - return Res::Err("updatemasternode is disabled for Fort Canning"); +void CMasternodesView::UpdateMasternodeOwner(uint256 const & nodeId, CMasternode& node, const char ownerType, const CKeyID& ownerAuthAddress) +{ + // Remove old record + EraseBy(node.ownerAuthAddress); - // auth already checked! - auto node = GetMasternode(nodeId); - if (!node) { - return Res::Err("node %s does not exists", nodeId.ToString()); - } + node.ownerType = ownerType; + node.ownerAuthAddress = ownerAuthAddress; - const auto state = node->GetState(height); - if (state != CMasternode::ENABLED) { - return Res::Err("node %s state is not 'ENABLED'", nodeId.ToString()); - } + // Overwrite and create new record + WriteBy(nodeId, node); + WriteBy(node.ownerAuthAddress, nodeId); +} - if (operatorType == node->operatorType && operatorAuthAddress == node->operatorAuthAddress) { - return Res::Err("The new operator is same as existing operator"); - } +void CMasternodesView::UpdateMasternodeCollateral(uint256 const & nodeId, CMasternode& node, const uint256& newCollateralTx, const int height) +{ + // Remove old record. + EraseBy(node.collateralTx); - // Remove old record - EraseBy(node->operatorAuthAddress); + // Store new collateral. Used by HasCollateralAuth. + node.collateralTx = newCollateralTx; + WriteBy(nodeId, node); - node->operatorType = operatorType; - node->operatorAuthAddress = operatorAuthAddress; + // Prioritise fast lookup in CanSpend() and GetState() + WriteBy(newCollateralTx, MNNewOwnerHeightValue{static_cast(height + GetMnResignDelay(height)), nodeId}); +} - // Overwrite and create new record - WriteBy(nodeId, *node); - WriteBy(node->operatorAuthAddress, nodeId); +std::optional CMasternodesView::GetNewCollateral(const uint256& txid) const +{ + return ReadBy(txid); +} - return Res::Ok(); +void CMasternodesView::ForEachNewCollateral(std::function)> callback) +{ + ForEach(callback); } void CMasternodesView::SetMasternodeLastBlockTime(const CKeyID & minter, const uint32_t &blockHeight, const int64_t& time) @@ -499,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 @@ -745,7 +737,7 @@ CTeamView::CTeam CCustomCSView::CalcNextTeam(int height, const uint256 & stakeMo std::map> priorityMN; ForEachMasternode([&] (uint256 const & id, CMasternode node) { - if(!node.IsActive(height)) + if(!node.IsActive(height, *this)) return true; CDataStream ss{SER_GETHASH, PROTOCOL_VERSION}; @@ -785,7 +777,7 @@ void CCustomCSView::CalcAnchoringTeams(const uint256 & stakeModifier, const CBlo std::map> authMN; std::map> confirmMN; ForEachMasternode([&] (uint256 const & id, CMasternode node) { - if(!node.IsActive(pindexNew->nHeight)) + if(!node.IsActive(pindexNew->nHeight, *this)) return true; // Not in our list of MNs from last week, skip. @@ -862,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); @@ -1071,7 +1071,7 @@ std::map AmISignerNow(int height, CAnchorData::CTeam const & team) continue; } - if (node->IsActive(height) && team.find(mnId.first) != team.end()) { + if (node->IsActive(height, *pcustomcsview) && team.find(mnId.first) != team.end()) { CKey masternodeKey; std::vector> wallets = GetWallets(); for (auto const & wallet : wallets) { diff --git a/src/masternodes/masternodes.h b/src/masternodes/masternodes.h index 35766236c8e..3984095886d 100644 --- a/src/masternodes/masternodes.h +++ b/src/masternodes/masternodes.h @@ -30,6 +30,7 @@ #include class CBlockIndex; +class CMasternodesView; class CTransaction; // Works instead of constants cause 'regtest' differs (don't want to overcharge chainparams) @@ -50,6 +51,7 @@ class CMasternode ENABLED, PRE_RESIGNED, RESIGNED, + TRANSFERRING, UNKNOWN // unreachable }; @@ -88,13 +90,13 @@ 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; - bool IsActive(int height) const; + State GetState(int height, const CMasternodesView& mnview) const; + bool IsActive(int height, const CMasternodesView& mnview) const; static std::string GetHumanReadableState(State state); static std::string GetTimelockToString(TimeLock timelock); @@ -115,7 +117,7 @@ class CMasternode READWRITE(version); READWRITE(resignTx); - READWRITE(banTx); + READWRITE(collateralTx); // Only available after FortCanning if (version > PRE_FORT_CANNING) { @@ -174,6 +176,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 +212,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); Res UnCreateMasternode(uint256 const & nodeId); - Res UnResignMasternode(uint256 const & nodeId, uint256 const & resignTx); - Res SetForcedRewardAddress(uint256 const & nodeId, const char rewardAddressType, CKeyID const & rewardAddress, int height); - Res RemForcedRewardAddress(uint256 const & nodeId, int height); - Res UpdateMasternode(uint256 const & nodeId, char operatorType, const CKeyID& operatorAuthAddress, int height); + 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 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 +246,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 +378,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 fd02c9da812..d22c07190ef 100644 --- a/src/masternodes/mn_checks.cpp +++ b/src/masternodes/mn_checks.cpp @@ -30,8 +30,6 @@ std::string ToString(CustomTxType type) { { case CustomTxType::CreateMasternode: return "CreateMasternode"; case CustomTxType::ResignMasternode: return "ResignMasternode"; - case CustomTxType::SetForcedRewardAddress: return "SetForcedRewardAddress"; - case CustomTxType::RemForcedRewardAddress: return "RemForcedRewardAddress"; case CustomTxType::UpdateMasternode: return "UpdateMasternode"; case CustomTxType::CreateToken: return "CreateToken"; case CustomTxType::UpdateToken: return "UpdateToken"; @@ -110,8 +108,6 @@ CCustomTxMessage customTypeToMessage(CustomTxType txType) { { case CustomTxType::CreateMasternode: return CCreateMasterNodeMessage{}; case CustomTxType::ResignMasternode: return CResignMasterNodeMessage{}; - case CustomTxType::SetForcedRewardAddress: return CSetForcedRewardAddressMessage{}; - case CustomTxType::RemForcedRewardAddress: return CRemForcedRewardAddressMessage{}; case CustomTxType::UpdateMasternode: return CUpdateMasterNodeMessage{}; case CustomTxType::CreateToken: return CCreateTokenMessage{}; case CustomTxType::UpdateToken: return CUpdateTokenPreAMKMessage{}; @@ -211,6 +207,13 @@ class CCustomMetadataParseVisitor return Res::Ok(); } + Res isPostGreatWorld() const { + if(static_cast(height) < consensus.GreatWorldHeight) { + return Res::Err("called before GreatWorld height"); + } + return Res::Ok(); + } + template Res serialize(T& obj) const { CDataStream ss(metadata, SER_NETWORK, PROTOCOL_VERSION); @@ -238,27 +241,8 @@ class CCustomMetadataParseVisitor return serialize(obj); } - Res operator()(CSetForcedRewardAddressMessage& obj) const { - // Temporarily disabled for 2.2 - return Res::Err("reward address change is disabled for Fort Canning"); - - auto res = isPostFortCanningFork(); - return !res ? res : serialize(obj); - } - - Res operator()(CRemForcedRewardAddressMessage& obj) const { - // Temporarily disabled for 2.2 - return Res::Err("reward address change is disabled for Fort Canning"); - - auto res = isPostFortCanningFork(); - return !res ? res : serialize(obj); - } - Res operator()(CUpdateMasterNodeMessage& obj) const { - // Temporarily disabled for 2.2 - return Res::Err("updatemasternode is disabled for Fort Canning"); - - auto res = isPostFortCanningFork(); + auto res = isPostGreatWorld(); return !res ? res : serialize(obj); } @@ -918,46 +902,142 @@ 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); } - Res operator()(const CSetForcedRewardAddressMessage& obj) const { - // Temporarily disabled for 2.2 - return Res::Err("reward address change is disabled for Fort Canning"); + Res operator()(const CUpdateMasterNodeMessage& obj) const { + if (obj.updates.empty()) { + return Res::Err("No update arguments provided"); + } + + if (obj.updates.size() > 3) { + return Res::Err("Too many updates provided"); + } - auto const node = mnview.GetMasternode(obj.nodeId); + auto node = mnview.GetMasternode(obj.mnId); if (!node) { - return Res::Err("masternode %s does not exist", obj.nodeId.ToString()); + return Res::Err("masternode %s does not exists", obj.mnId.ToString()); } - if (!HasCollateralAuth(obj.nodeId)) { - return Res::Err("%s: %s", obj.nodeId.ToString(), "tx must have at least one input from masternode owner"); + + const auto collateralTx = node->collateralTx.IsNull() ? obj.mnId : node->collateralTx; + const auto res = HasCollateralAuth(collateralTx); + if (!res) { + return res; } - return mnview.SetForcedRewardAddress(obj.nodeId, obj.rewardAddressType, obj.rewardAddress, height); - } + auto state = node->GetState(height, mnview); + if (state != CMasternode::ENABLED) { + return Res::Err("Masternode %s is not in 'ENABLED' state", obj.mnId.ToString()); + } - Res operator()(const CRemForcedRewardAddressMessage& obj) const { - // Temporarily disabled for 2.2 - return Res::Err("reward address change is disabled for Fort Canning"); + bool ownerType{false}, operatorType{false}, rewardType{false}; + for (const auto& item : obj.updates) { + if (item.first == static_cast(UpdateMasternodeType::OwnerAddress)) { + if (ownerType) { + return Res::Err("Multiple owner updates provided"); + } + ownerType = true; - auto const node = mnview.GetMasternode(obj.nodeId); - if (!node) { - return Res::Err("masternode %s does not exist", obj.nodeId.ToString()); - } - if (!HasCollateralAuth(obj.nodeId)) { - return Res::Err("%s: %s", obj.nodeId.ToString(), "tx must have at least one input from masternode owner"); - } + 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"); + } - return mnview.RemForcedRewardAddress(obj.nodeId, height); - } + if (tx.vout.size() == 1) { + return Res::Err("Missing new collateral output"); + } - Res operator()(const CUpdateMasterNodeMessage& obj) const { - // Temporarily disabled for 2.2 - return Res::Err("updatemasternode is disabled for Fort Canning"); + 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 (operatorType) { + return Res::Err("Multiple operator updates provided"); + } + operatorType = true; + + if (item.second.first != 1 && item.second.first != 4) { + return Res::Err("Operator address must be P2PKH or P2WPKH type"); + } + + const auto keyID = CKeyID(uint160(item.second.second)); + if (mnview.GetMasternodeIdByOwner(keyID) || mnview.GetMasternodeIdByOperator(keyID)) { + return Res::Err("Masternode with that operator address already exists"); + } - auto res = HasCollateralAuth(obj.mnId); - return !res ? res : mnview.UpdateMasternode(obj.mnId, obj.operatorType, obj.operatorAuthAddress, height); + mnview.UpdateMasternodeOperator(obj.mnId, *node, item.second.first, keyID, height); + } else if (item.first == static_cast(UpdateMasternodeType::SetRewardAddress)) { + if (rewardType) { + return Res::Err("Multiple reward address updates provided"); + } + rewardType = true; + + if (item.second.first != 1 && item.second.first != 4) { + return Res::Err("Reward address must be P2PKH or P2WPKH type"); + } + + const auto keyID = CKeyID(uint160(item.second.second)); + mnview.SetForcedRewardAddress(obj.mnId, *node, item.second.first, keyID, height); + } else if (item.first == static_cast(UpdateMasternodeType::RemRewardAddress)) { + if (rewardType) { + return Res::Err("Multiple reward address updates provided"); + } + rewardType = true; + + mnview.RemForcedRewardAddress(obj.mnId, *node, height); + } else { + return Res::Err("Unknown update type provided"); + } + } + + return Res::Ok(); } Res operator()(const CCreateTokenMessage& obj) const { @@ -2798,8 +2878,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 { @@ -3173,7 +3257,7 @@ bool IsDisabledTx(uint32_t height, CustomTxType type, const Consensus::Params& c } -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; @@ -3203,6 +3287,18 @@ Res ApplyCustomTx(CCustomCSView& mnview, const CCoinsViewCache& coins, const CTr } res = CustomTxVisit(view, coins, tx, height, consensus, txMessage, time); + if (canSpend && txType == CustomTxType::UpdateMasternode) { + auto obj = std::get(txMessage); + for (const auto item : obj.updates) { + if (item.first == static_cast(UpdateMasternodeType::OwnerAddress)) { + if (const auto node = mnview.GetMasternode(obj.mnId)) { + *canSpend = node->collateralTx.IsNull() ? obj.mnId : node->collateralTx; + } + break; + } + } + } + // 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 71453e465e9..e5d6044c1bb 100644 --- a/src/masternodes/mn_checks.h +++ b/src/masternodes/mn_checks.h @@ -43,8 +43,6 @@ enum class CustomTxType : uint8_t CreateMasternode = 'C', ResignMasternode = 'R', UpdateMasternode = 'm', - SetForcedRewardAddress = 'F', - RemForcedRewardAddress = 'f', // custom tokens: CreateToken = 'T', MintToken = 'M', @@ -101,13 +99,20 @@ enum class CustomTxType : uint8_t AuctionBid = 'I' }; +enum class UpdateMasternodeType : uint8_t +{ + None = 0x00, + OwnerAddress = 0x01, + OperatorAddress = 0x02, + SetRewardAddress = 0x03, + RemRewardAddress = 0x04 +}; + inline CustomTxType CustomTxCodeToType(uint8_t ch) { auto type = static_cast(ch); switch(type) { case CustomTxType::CreateMasternode: case CustomTxType::ResignMasternode: - case CustomTxType::SetForcedRewardAddress: - case CustomTxType::RemForcedRewardAddress: case CustomTxType::UpdateMasternode: case CustomTxType::CreateToken: case CustomTxType::MintToken: @@ -207,45 +212,15 @@ struct CResignMasterNodeMessage : public uint256 { } }; -struct CSetForcedRewardAddressMessage { - uint256 nodeId; - char rewardAddressType; - CKeyID rewardAddress; - - ADD_SERIALIZE_METHODS; - - template - inline void SerializationOp(Stream& s, Operation ser_action) - { - READWRITE(nodeId); - READWRITE(rewardAddressType); - READWRITE(rewardAddress); - } -}; - -struct CRemForcedRewardAddressMessage { - uint256 nodeId; - - ADD_SERIALIZE_METHODS; - - template - inline void SerializationOp(Stream& s, Operation ser_action) - { - READWRITE(nodeId); - } -}; - struct CUpdateMasterNodeMessage { uint256 mnId; - char operatorType; - CKeyID operatorAuthAddress; + std::vector>>> updates; ADD_SERIALIZE_METHODS; template inline void SerializationOp(Stream& s, Operation ser_action) { READWRITE(mnId); - READWRITE(operatorType); - READWRITE(operatorAuthAddress); + READWRITE(updates); } }; @@ -322,8 +297,6 @@ using CCustomTxMessage = std::variant< CCustomTxMessageNone, CCreateMasterNodeMessage, CResignMasterNodeMessage, - CSetForcedRewardAddressMessage, - CRemForcedRewardAddressMessage, CUpdateMasterNodeMessage, CCreateTokenMessage, CUpdateTokenPreAMKMessage, @@ -372,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_customtx.cpp b/src/masternodes/rpc_customtx.cpp index d5a417b8056..16fd9d04d67 100644 --- a/src/masternodes/rpc_customtx.cpp +++ b/src/masternodes/rpc_customtx.cpp @@ -74,24 +74,21 @@ class CCustomTxRpcVisitor rpcInfo.pushKV("id", obj.GetHex()); } - void operator()(const CSetForcedRewardAddressMessage& obj) const { - rpcInfo.pushKV("mc_id", obj.nodeId.GetHex()); - rpcInfo.pushKV("rewardAddress", EncodeDestination( - obj.rewardAddressType == 1 ? - CTxDestination(PKHash(obj.rewardAddress)) : - CTxDestination(WitnessV0KeyHash(obj.rewardAddress))) - ); - } - - void operator()(const CRemForcedRewardAddressMessage& obj) const { - rpcInfo.pushKV("mc_id", obj.nodeId.GetHex()); - } - void operator()(const CUpdateMasterNodeMessage& obj) const { rpcInfo.pushKV("id", obj.mnId.GetHex()); - rpcInfo.pushKV("masternodeoperator", EncodeDestination(obj.operatorType == PKHashType ? - CTxDestination(PKHash(obj.operatorAuthAddress)) : - CTxDestination(WitnessV0KeyHash(obj.operatorAuthAddress)))); + for (const auto& item : obj.updates) { + if (item.first == static_cast(UpdateMasternodeType::OperatorAddress)) { + rpcInfo.pushKV("operatorAddress", EncodeDestination(item.second.first == PKHashType ? + CTxDestination(PKHash(item.second.second)) : + CTxDestination(WitnessV0KeyHash(item.second.second)))); + } else if (item.first == static_cast(UpdateMasternodeType::SetRewardAddress)) { + rpcInfo.pushKV("rewardAddress", EncodeDestination(item.second.first == PKHashType ? + CTxDestination(PKHash(item.second.second)) : + CTxDestination(WitnessV0KeyHash(item.second.second)))); + } else if (item.first == static_cast(UpdateMasternodeType::RemRewardAddress)) { + rpcInfo.pushKV("rewardAddress", ""); + } + } } void operator()(const CCreateTokenMessage& obj) const { diff --git a/src/masternodes/rpc_masternodes.cpp b/src/masternodes/rpc_masternodes.cpp index 0c7c671805b..efd61376722 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)); @@ -48,7 +48,7 @@ UniValue mnToJSON(uint256 const & nodeId, CMasternode const& node, bool verbose, uint16_t timelock = pcustomcsview->GetTimelock(nodeId, node, currentHeight); // Only get targetMultiplier for active masternodes - if (node.IsActive(currentHeight)) { + if (node.IsActive(currentHeight, *pcustomcsview)) { // Get block times with next block as height const auto subNodesBlockTime = pcustomcsview->GetBlockTimes(node.operatorAuthAddress, currentHeight + 1, node.creationHeight, timelock); @@ -205,210 +205,6 @@ UniValue createmasternode(const JSONRPCRequest& request) return signsend(rawTx, pwallet, optAuthTx)->GetHash().GetHex(); } -UniValue setforcedrewardaddress(const JSONRPCRequest& request) -{ - // Temporarily disabled for 2.2 - throw JSONRPCError(RPC_INVALID_REQUEST, - "reward address change is disabled for Fort Canning"); - - auto pwallet = GetWallet(request); - - RPCHelpMan{"setforcedrewardaddress", - "\nCreates (and submits to local node and network) a set forced reward address transaction with given masternode id and reward address\n" - "The last optional argument (may be empty array) is an array of specific UTXOs to spend." + - HelpRequiringPassphrase(pwallet) + "\n", - { - {"mn_id", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The Masternode's ID"}, - {"rewardAddress", RPCArg::Type::STR, RPCArg::Optional::NO, "Masternode`s new reward address (any P2PKH or P2WKH address)"}, - {"inputs", RPCArg::Type::ARR, RPCArg::Optional::OMITTED_NAMED_ARG, "A json array of json objects", - { - {"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "", - { - {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"}, - {"vout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The output number"}, - }, - }, - }, - }, - }, - RPCResult{ - "\"hash\" (string) The hex-encoded hash of broadcasted transaction\n" - }, - RPCExamples{ - HelpExampleCli("setforcedrewardaddress", "mn_id rewardAddress '[{\"txid\":\"id\",\"vout\":0}]'") - + HelpExampleRpc("setforcedrewardaddress", "mn_id rewardAddress '[{\"txid\":\"id\",\"vout\":0}]'") - }, - }.Check(request); - - if (pwallet->chain().isInitialBlockDownload()) { - throw JSONRPCError(RPC_CLIENT_IN_INITIAL_DOWNLOAD, "Cannot update Masternode while still in Initial Block Download"); - } - - pwallet->BlockUntilSyncedToCurrentChain(); - - RPCTypeCheck(request.params, { UniValue::VSTR, UniValue::VSTR, UniValue::VARR }, true); - - // decode and verify - std::string const nodeIdStr = request.params[0].getValStr(); - uint256 const nodeId = uint256S(nodeIdStr); - CTxDestination ownerDest; - int targetHeight; - { - LOCK(cs_main); - auto nodePtr = pcustomcsview->GetMasternode(nodeId); - if (!nodePtr) { - throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("The masternode %s does not exist", nodeIdStr)); - } - ownerDest = nodePtr->ownerType == PKHashType ? - CTxDestination(PKHash(nodePtr->ownerAuthAddress)) : - CTxDestination(WitnessV0KeyHash(nodePtr->ownerAuthAddress)); - - targetHeight = ::ChainActive().Height() + 1; - } - - if (!::IsMine(*pwallet, ownerDest)) { - throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Masternode ownerAddress (%s) is not owned by the wallet", EncodeDestination(ownerDest))); - } - - std::string rewardAddress = request.params[1].getValStr(); - CTxDestination rewardDest = DecodeDestination(rewardAddress); - if (rewardDest.index() != PKHashType && rewardDest.index() != WitV0KeyHashType) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "rewardAddress (" + rewardAddress + ") does not refer to a P2PKH or P2WPKH address"); - } - - CKeyID const rewardAuthKey = rewardDest.index() == PKHashType ? - CKeyID(std::get(rewardDest)) : - CKeyID(std::get(rewardDest) - ); - - const auto txVersion = GetTransactionVersion(targetHeight); - CMutableTransaction rawTx(txVersion); - - CTransactionRef optAuthTx; - std::set auths{GetScriptForDestination(ownerDest)}; - rawTx.vin = GetAuthInputsSmart(pwallet, rawTx.nVersion, auths, false, optAuthTx, request.params[2]); - - // Return change to owner address - CCoinControl coinControl; - if (IsValidDestination(ownerDest)) { - coinControl.destChange = ownerDest; - } - - CSetForcedRewardAddressMessage msg{nodeId, static_cast(rewardDest.index()), rewardAuthKey}; - - CDataStream metadata(DfTxMarker, SER_NETWORK, PROTOCOL_VERSION); - metadata << static_cast(CustomTxType::SetForcedRewardAddress) - << msg; - - CScript scriptMeta; - scriptMeta << OP_RETURN << ToByteVector(metadata); - - rawTx.vout.push_back(CTxOut(0, scriptMeta)); - - fund(rawTx, pwallet, optAuthTx, &coinControl); - - // check execution - execTestTx(CTransaction(rawTx), targetHeight, optAuthTx); - - return signsend(rawTx, pwallet, optAuthTx)->GetHash().GetHex(); -} - -UniValue remforcedrewardaddress(const JSONRPCRequest& request) -{ - // Temporarily disabled for 2.2 - throw JSONRPCError(RPC_INVALID_REQUEST, - "reward address change is disabled for Fort Canning"); - - auto pwallet = GetWallet(request); - - RPCHelpMan{"remforcedrewardaddress", - "\nCreates (and submits to local node and network) a remove forced reward address transaction with given masternode id\n" - "The last optional argument (may be empty array) is an array of specific UTXOs to spend." + - HelpRequiringPassphrase(pwallet) + "\n", - { - {"mn_id", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The Masternode's ID"}, - {"inputs", RPCArg::Type::ARR, RPCArg::Optional::OMITTED_NAMED_ARG, "A json array of json objects", - { - {"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "", - { - {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"}, - {"vout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The output number"}, - }, - }, - }, - }, - }, - RPCResult{ - "\"hash\" (string) The hex-encoded hash of broadcasted transaction\n" - }, - RPCExamples{ - HelpExampleCli("remforcedrewardaddress", "mn_id '[{\"txid\":\"id\",\"vout\":0}]'") - + HelpExampleRpc("remforcedrewardaddress", "mn_id '[{\"txid\":\"id\",\"vout\":0}]'") - }, - }.Check(request); - - if (pwallet->chain().isInitialBlockDownload()) { - throw JSONRPCError(RPC_CLIENT_IN_INITIAL_DOWNLOAD, "Cannot update Masternode while still in Initial Block Download"); - } - - pwallet->BlockUntilSyncedToCurrentChain(); - - RPCTypeCheck(request.params, { UniValue::VSTR, UniValue::VARR }, true); - - // decode and verify - std::string const nodeIdStr = request.params[0].getValStr(); - uint256 const nodeId = uint256S(nodeIdStr); - CTxDestination ownerDest; - int targetHeight; - { - LOCK(cs_main); - auto nodePtr = pcustomcsview->GetMasternode(nodeId); - if (!nodePtr) { - throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("The masternode %s does not exist", nodeIdStr)); - } - ownerDest = nodePtr->ownerType == PKHashType ? - CTxDestination(PKHash(nodePtr->ownerAuthAddress)) : - CTxDestination(WitnessV0KeyHash(nodePtr->ownerAuthAddress)); - - targetHeight = ::ChainActive().Height() + 1; - } - - if (!::IsMine(*pwallet, ownerDest)) { - throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Masternode ownerAddress (%s) is not owned by the wallet", EncodeDestination(ownerDest))); - } - - const auto txVersion = GetTransactionVersion(targetHeight); - CMutableTransaction rawTx(txVersion); - - CTransactionRef optAuthTx; - std::set auths{GetScriptForDestination(ownerDest)}; - rawTx.vin = GetAuthInputsSmart(pwallet, rawTx.nVersion, auths, false, optAuthTx, request.params[1]); - - // Return change to owner address - CCoinControl coinControl; - if (IsValidDestination(ownerDest)) { - coinControl.destChange = ownerDest; - } - - CRemForcedRewardAddressMessage msg{nodeId}; - - CDataStream metadata(DfTxMarker, SER_NETWORK, PROTOCOL_VERSION); - metadata << static_cast(CustomTxType::RemForcedRewardAddress) - << msg; - - CScript scriptMeta; - scriptMeta << OP_RETURN << ToByteVector(metadata); - - rawTx.vout.push_back(CTxOut(0, scriptMeta)); - - fund(rawTx, pwallet, optAuthTx, &coinControl); - - // check execution - execTestTx(CTransaction(rawTx), targetHeight, optAuthTx); - - return signsend(rawTx, pwallet, optAuthTx)->GetHash().GetHex(); -} - UniValue resignmasternode(const JSONRPCRequest& request) { auto pwallet = GetWallet(request); @@ -451,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); @@ -463,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; } @@ -471,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 @@ -498,10 +304,6 @@ UniValue resignmasternode(const JSONRPCRequest& request) UniValue updatemasternode(const JSONRPCRequest& request) { - // Temporarily disabled for 2.2 - throw JSONRPCError(RPC_INVALID_REQUEST, - "updatemasternode is disabled for Fort Canning"); - auto pwallet = GetWallet(request); RPCHelpMan{"updatemasternode", @@ -510,7 +312,13 @@ UniValue updatemasternode(const JSONRPCRequest& request) HelpRequiringPassphrase(pwallet) + "\n", { {"mn_id", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The Masternode's ID"}, - {"operatorAddress", RPCArg::Type::STR, RPCArg::Optional::NO, "The new masternode operator auth address (P2PKH only, unique)"}, + {"values", RPCArg::Type::OBJ, RPCArg::Optional::NO, "", + { + {"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."}, + }, + }, {"inputs", RPCArg::Type::ARR, RPCArg::Optional::OMITTED_NAMED_ARG, "A json array of json objects", { {"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "", @@ -537,23 +345,10 @@ UniValue updatemasternode(const JSONRPCRequest& request) } pwallet->BlockUntilSyncedToCurrentChain(); - bool forkCanning; - { - LOCK(cs_main); - forkCanning = ::ChainActive().Tip()->nHeight >= Params().GetConsensus().FortCanningHeight; - } - - if (!forkCanning) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "updatemasternode cannot be called before Fortcanning hard fork"); - } - - RPCTypeCheck(request.params, { UniValue::VSTR, UniValue::VSTR, UniValue::VARR }, true); - if (request.params[0].isNull() || request.params[1].isNull()) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameters, at least argument 2 must be non-null"); - } + RPCTypeCheck(request.params, { UniValue::VSTR, UniValue::VOBJ, UniValue::VARR }, true); std::string const nodeIdStr = request.params[0].getValStr(); - uint256 const nodeId = uint256S(nodeIdStr); + const uint256 nodeId = uint256S(nodeIdStr); CTxDestination ownerDest; int targetHeight; { @@ -567,19 +362,40 @@ UniValue updatemasternode(const JSONRPCRequest& request) targetHeight = ::ChainActive().Height() + 1; } - std::string operatorAddress = request.params[1].getValStr(); - CTxDestination operatorDest = DecodeDestination(operatorAddress); + CTxDestination newOwnerDest, operatorDest, rewardDest; - // check type here cause need operatorAuthKey. all other validation (for owner for ex.) in further apply/create - if (operatorDest.index() != PKHashType && operatorDest.index() != WitV0KeyHashType) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "operatorAddress (" + operatorAddress + ") does not refer to a P2PKH or P2WPKH address"); + 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) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "operatorAddress (" + metaObj["operatorAddress"].getValStr() + ") does not refer to a P2PKH or P2WPKH address"); + } + } + + std::string rewardAddress; + if (!metaObj["rewardAddress"].isNull()) { + rewardAddress = metaObj["rewardAddress"].getValStr(); + if (!rewardAddress.empty()) { + rewardDest = DecodeDestination(rewardAddress); + if (rewardDest.index() != PKHashType && rewardDest.index() != WitV0KeyHashType) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "rewardAddress (" + rewardAddress + ") does not refer to a P2PKH or P2WPKH address"); + } + } } const auto txVersion = GetTransactionVersion(targetHeight); 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 @@ -588,17 +404,44 @@ UniValue updatemasternode(const JSONRPCRequest& request) coinControl.destChange = ownerDest; } - CKeyID const operatorAuthKey = operatorDest.index() == PKHashType ? CKeyID(std::get(operatorDest)) : CKeyID(std::get(operatorDest)); + 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()))); + } + + if (!metaObj["rewardAddress"].isNull()) { + if (rewardAddress.empty()) { + msg.updates.emplace_back(static_cast(UpdateMasternodeType::RemRewardAddress), std::pair>()); + } else { + const CKeyID keyID = rewardDest.index() == PKHashType ? CKeyID(std::get(rewardDest)) : CKeyID(std::get(rewardDest)); + msg.updates.emplace_back(static_cast(UpdateMasternodeType::SetRewardAddress), std::make_pair(static_cast(rewardDest.index()), std::vector(keyID.begin(), keyID.end()))); + } + } CDataStream metadata(DfTxMarker, SER_NETWORK, PROTOCOL_VERSION); metadata << static_cast(CustomTxType::UpdateMasternode) - << nodeId - << static_cast(operatorDest.index()) << operatorAuthKey; + << msg; 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); @@ -997,15 +840,13 @@ static const CRPCCommand commands[] = // --------------- ---------------------- --------------------- ---------- {"masternodes", "createmasternode", &createmasternode, {"ownerAddress", "operatorAddress", "inputs"}}, {"masternodes", "resignmasternode", &resignmasternode, {"mn_id", "inputs"}}, - //{"masternodes", "updatemasternode", &updatemasternode, {"mn_id", "operatorAddress", "inputs"}}, + {"masternodes", "updatemasternode", &updatemasternode, {"mn_id", "values", "inputs"}}, {"masternodes", "listmasternodes", &listmasternodes, {"pagination", "verbose"}}, {"masternodes", "getmasternode", &getmasternode, {"mn_id"}}, {"masternodes", "getmasternodeblocks", &getmasternodeblocks, {"identifier", "depth"}}, {"masternodes", "getanchorteams", &getanchorteams, {"blockHeight"}}, {"masternodes", "getactivemasternodecount", &getactivemasternodecount, {"blockCount"}}, {"masternodes", "listanchors", &listanchors, {}}, - //{"masternodes", "setforcedrewardaddress", &setforcedrewardaddress, {"mn_id", "rewardAddress", "inputs"}}, - //{"masternodes", "remforcedrewardaddress", &remforcedrewardaddress, {"mn_id", "inputs"}}, }; void RegisterMasternodesRPCCommands(CRPCTable& tableRPC) { diff --git a/src/miner.cpp b/src/miner.cpp index 1aef5c81607..5e1869f4764 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -114,12 +114,13 @@ std::unique_ptr BlockAssembler::CreateNewBlock(const CScript& sc nHeight = pindexPrev->nHeight + 1; // in fact, this may be redundant cause it was checked upthere in the miner std::optional> myIDs; + std::optional nodePtr; if (!blockTime) { myIDs = pcustomcsview->AmIOperator(); if (!myIDs) return nullptr; - auto nodePtr = pcustomcsview->GetMasternode(myIDs->second); - if (!nodePtr || !nodePtr->IsActive(nHeight)) + nodePtr = pcustomcsview->GetMasternode(myIDs->second); + if (!nodePtr || !nodePtr->IsActive(nHeight, *pcustomcsview)) return nullptr; } @@ -292,7 +293,8 @@ std::unique_ptr BlockAssembler::CreateNewBlock(const CScript& sc pblock->deprecatedHeight = pindexPrev->nHeight + 1; pblock->nBits = pos::GetNextWorkRequired(pindexPrev, pblock->nTime, consensus); if (myIDs) { - pblock->stakeModifier = pos::ComputeStakeModifier(pindexPrev->stakeModifier, myIDs->first); + const CKeyID key = nHeight >= consensus.GreatWorldHeight ? nodePtr->ownerAuthAddress : myIDs->first; + pblock->stakeModifier = pos::ComputeStakeModifier(pindexPrev->stakeModifier, key); } pblocktemplate->vTxSigOpsCost[0] = WITNESS_SCALE_FACTOR * GetLegacySigOpCount(*pblock->vtx[0]); @@ -708,6 +710,7 @@ namespace pos { int64_t blockHeight; std::vector subNodesBlockTime; uint16_t timelock; + std::optional nodePtr; { LOCK(cs_main); @@ -718,8 +721,8 @@ namespace pos { } tip = ::ChainActive().Tip(); masternodeID = *optMasternodeID; - auto nodePtr = pcustomcsview->GetMasternode(masternodeID); - if (!nodePtr || !nodePtr->IsActive(tip->nHeight + 1)) + nodePtr = pcustomcsview->GetMasternode(masternodeID); + if (!nodePtr || !nodePtr->IsActive(tip->nHeight + 1, *pcustomcsview)) { /// @todo may be new status for not activated (or already resigned) MN?? return Status::initWaiting; @@ -753,7 +756,8 @@ namespace pos { } auto nBits = pos::GetNextWorkRequired(tip, blockTime, chainparams.GetConsensus()); - auto stakeModifier = pos::ComputeStakeModifier(tip->stakeModifier, args.minterKey.GetPubKey().GetID()); + const CKeyID key = blockHeight >= chainparams.GetConsensus().GreatWorldHeight ? nodePtr->ownerAuthAddress : args.minterKey.GetPubKey().GetID(); + auto stakeModifier = pos::ComputeStakeModifier(tip->stakeModifier, key); // Set search time if null or last block has changed if (!nLastCoinStakeSearchTime || lastBlockSeen != tip->GetBlockHash()) { diff --git a/src/pos.cpp b/src/pos.cpp index 4b15bfcb94d..279fec72c5b 100644 --- a/src/pos.cpp +++ b/src/pos.cpp @@ -18,11 +18,19 @@ bool CheckStakeModifier(const CBlockIndex* pindexPrev, const CBlockHeader& block if (blockHeader.hashPrevBlock.IsNull()) return blockHeader.stakeModifier.IsNull(); - /// @todo is it possible to pass minter key here, or we really need to extract it srom sig??? CKeyID key; if (!blockHeader.ExtractMinterKey(key)) { - LogPrintf("CheckStakeModifier: Can't extract minter key\n"); - return false; + return error("%s: Can't extract minter key\n", __func__); + } + + if (pindexPrev->nHeight + 1 >= Params().GetConsensus().GreatWorldHeight) { + auto nodeId = pcustomcsview->GetMasternodeIdByOperator(key); + if (!nodeId) { + return error("%s: No master operator found with minter key\n", __func__); + } + auto nodePtr = pcustomcsview->GetMasternode(*nodeId); + assert(nodePtr); + key = nodePtr->ownerAuthAddress; } return blockHeader.stakeModifier == pos::ComputeStakeModifier(pindexPrev->stakeModifier, key); @@ -70,7 +78,7 @@ bool ContextualCheckProofOfStake(const CBlockHeader& blockHeader, const Consensu } masternodeID = *optMasternodeID; auto nodePtr = mnView->GetMasternode(masternodeID); - if (!nodePtr || !nodePtr->IsActive(height)) { + if (!nodePtr || !nodePtr->IsActive(height, *mnView)) { return false; } creationHeight = int64_t(nodePtr->creationHeight); diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 1b32d7d64c6..4615ddbe52f 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -176,8 +176,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "stop", 0, "wait" }, { "createmasternode", 2, "inputs" }, { "resignmasternode", 1, "inputs" }, - { "setforcedrewardaddress", 2, "inputs" }, - { "remforcedrewardaddress", 1, "inputs" }, + { "updatemasternode", 1, "values" }, { "updatemasternode", 2, "inputs" }, { "listmasternodes", 0, "pagination" }, { "listmasternodes", 1, "verbose" }, diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index e704e015802..2189f7dff28 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -279,12 +279,12 @@ 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 subObj.pushKV("state", CMasternode::GetHumanReadableState(state)); - auto generate = nodePtr->IsActive(height) && genCoins; + auto generate = nodePtr->IsActive(height, *pcustomcsview) && genCoins; subObj.pushKV("generate", generate); subObj.pushKV("mintedblocks", (uint64_t)nodePtr->mintedBlocks); @@ -300,7 +300,7 @@ static UniValue getmininginfo(const JSONRPCRequest& request) const auto timelock = pcustomcsview->GetTimelock(mnId.second, *nodePtr, height); // Get targetMultiplier if node is active - if (nodePtr->IsActive(height)) { + if (nodePtr->IsActive(height, *pcustomcsview)) { // Get block times const auto subNodesBlockTime = pcustomcsview->GetBlockTimes(nodePtr->operatorAuthAddress, height, nodePtr->creationHeight, timelock); diff --git a/src/test/blockencodings_tests.cpp b/src/test/blockencodings_tests.cpp index ff94e7fd17c..e0a5cc4fc5f 100644 --- a/src/test/blockencodings_tests.cpp +++ b/src/test/blockencodings_tests.cpp @@ -52,7 +52,7 @@ static CBlock BuildBlockTestCase() { tip = ::ChainActive().Tip(); auto nodePtr = pcustomcsview->GetMasternode(masternodeID); - if (!nodePtr || !nodePtr->IsActive(tip->nHeight)) + if (!nodePtr || !nodePtr->IsActive(tip->nHeight, *pcustomcsview)) return {}; mintedBlocks = nodePtr->mintedBlocks; diff --git a/src/test/pos_tests.cpp b/src/test/pos_tests.cpp index 4433da9aa18..6d7ee2a52c9 100644 --- a/src/test/pos_tests.cpp +++ b/src/test/pos_tests.cpp @@ -31,12 +31,12 @@ std::shared_ptr Block( const uint256& prev_hash, const uint64_t& height, return pblock; } -std::shared_ptr FinalizeBlock(std::shared_ptr pblock, const uint256& masternodeID, const CKey& minterKey, const uint256& prevStakeModifier) +std::shared_ptr FinalizeBlock(std::shared_ptr pblock, const uint256& masternodeID, const CKey& minterKey, const uint256& prevStakeModifier, const CKeyID& modifierKey) { LOCK(cs_main); // For LookupBlockIndex static uint64_t time = Params().GenesisBlock().nTime; - pblock->stakeModifier = pos::ComputeStakeModifier(prevStakeModifier, minterKey.GetPubKey().GetID()); + pblock->stakeModifier = pos::ComputeStakeModifier(prevStakeModifier, modifierKey); pblock->hashMerkleRoot = BlockMerkleRoot(*pblock); @@ -97,12 +97,35 @@ BOOST_AUTO_TEST_CASE(check_stake_modifier) Block(Params().GenesisBlock().GetHash(), height, mintedBlocks), masternodeID, minterKey, - prevStakeModifier); + prevStakeModifier, + minterKey.GetPubKey().GetID()); BOOST_CHECK(pos::CheckStakeModifier(::ChainActive().Tip(), *(CBlockHeader*)correctBlock.get())); correctBlock->SetNull(); correctBlock->hashPrevBlock = prev_hash; BOOST_CHECK(!pos::CheckStakeModifier(::ChainActive().Tip(), *(CBlockHeader*)correctBlock.get())); + + // Create masternode + const auto mnID = uint256S(std::string(64, 1)); + CKey newMinterKey; + newMinterKey.MakeNewKey(true); + CMasternode masternode; + masternode.operatorType = 1; + masternode.ownerType = 1; + masternode.ownerAuthAddress = CKeyID{uint160{std::vector(20, '0')}}; + masternode.operatorAuthAddress = newMinterKey.GetPubKey().GetID(); + BOOST_CHECK(pcustomcsview->CreateMasternode(mnID, masternode, 0)); + + // Check stake modifier calculated on owner address after fork + auto blockTip = *::ChainActive().Tip(); + blockTip.nHeight = Params().GetConsensus().GreatWorldHeight; + std::shared_ptr newModifierBlock = FinalizeBlock( + Block(blockTip.GetBlockHash(), blockTip.nHeight, blockTip.mintedBlocks), + mnID, + newMinterKey, + blockTip.stakeModifier, + masternode.ownerAuthAddress); + BOOST_CHECK(pos::CheckStakeModifier(&blockTip, *(CBlockHeader*)newModifierBlock.get())); } BOOST_AUTO_TEST_CASE(check_header_signature) @@ -125,7 +148,8 @@ BOOST_AUTO_TEST_CASE(check_header_signature) block, masternodeID, minterKey, - prev_hash); + prev_hash, + minterKey.GetPubKey().GetID()); BOOST_CHECK(pos::CheckHeaderSignature(*(CBlockHeader*)block.get())); diff --git a/src/test/setup_common.cpp b/src/test/setup_common.cpp index 21807a4fb3a..a29ad05681e 100644 --- a/src/test/setup_common.cpp +++ b/src/test/setup_common.cpp @@ -208,7 +208,7 @@ TestChain100Setup::CreateAndProcessBlock(const std::vector& tip = ::ChainActive().Tip(); auto nodePtr = pcustomcsview->GetMasternode(masternodeID); - if (!nodePtr || !nodePtr->IsActive(tip->nHeight)) + if (!nodePtr || !nodePtr->IsActive(tip->nHeight, *pcustomcsview)) throw std::runtime_error(std::string(__func__) + ": nodePtr does not exist"); mintedBlocks = nodePtr->mintedBlocks; diff --git a/src/txdb.cpp b/src/txdb.cpp index 9aa12de8047..dd4075b6c97 100644 --- a/src/txdb.cpp +++ b/src/txdb.cpp @@ -281,10 +281,6 @@ bool CBlockTreeDB::LoadBlockIndexGuts(const Consensus::Params& consensusParams, return error("%s: The block index #%d (%s) wasn't saved on disk correctly. Index content: %s", __func__, pindexNew->nHeight, pindexNew->GetBlockHash().ToString(), pindexNew->ToString()); } } -// if (pindexNew->nHeight > 0 && pindexNew->stakeModifier != pos::ComputeStakeModifier(pindexNew->pprev->stakeModifier, pindexNew->minter)) { // TODO: SS disable check stake modifier -// return error("%s: The block index #%d (%s) wasn't saved on disk correctly. Stake modifier is incorrect (%s != %s). Index content: %s", -// __func__, pindexNew->nHeight, pindexNew->GetBlockHash().ToString(), pindexNew->stakeModifier.ToString(), pos::ComputeStakeModifier(pindexNew->pprev->stakeModifier, pindexNew->minter).ToString(), pindexNew->ToString()); -// } pcursor->Next(); } else { diff --git a/src/validation.cpp b/src/validation.cpp index d07f129e264..60a3499c7b9 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -2298,7 +2298,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); @@ -2328,11 +2328,13 @@ bool CChainState::ConnectBlock(const CBlock& block, CValidationState& state, CBl nodeId->ToString(), nodePtr->mintedBlocks + 1, block.mintedBlocks), REJECT_INVALID, "bad-minted-blocks"); } uint256 stakeModifierPrevBlock = pindex->pprev == nullptr ? uint256() : pindex->pprev->stakeModifier; - if (block.stakeModifier != pos::ComputeStakeModifier(stakeModifierPrevBlock, nodePtr->operatorAuthAddress)) { + const CKeyID key = pindex->nHeight >= chainparams.GetConsensus().GreatWorldHeight ? nodePtr->ownerAuthAddress : nodePtr->operatorAuthAddress; + const auto stakeModifier = pos::ComputeStakeModifier(stakeModifierPrevBlock, key); + if (block.stakeModifier != stakeModifier) { return state.Invalid( ValidationInvalidReason::CONSENSUS, error("ConnectBlock(): block's stake Modifier should be %d, got %d!", - block.stakeModifier.ToString(), pos::ComputeStakeModifier(stakeModifierPrevBlock, nodePtr->operatorAuthAddress).ToString()), + block.stakeModifier.ToString(), stakeModifier.ToString()), REJECT_INVALID, "bad-minted-blocks"); } @@ -2494,6 +2496,7 @@ 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())) { @@ -2502,14 +2505,14 @@ bool CChainState::ConnectBlock(const CBlock& block, CValidationState& state, CBl // defined for a block, so we reset the reason flag to // CONSENSUS here. state.Invalid(ValidationInvalidReason::CONSENSUS, false, - state.GetRejectCode(), state.GetRejectReason(), state.GetDebugMessage()); + 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"); + REJECT_INVALID, "bad-txns-accumulated-fee-outofrange"); } // Check that transaction is BIP68 final @@ -2556,7 +2559,7 @@ 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); + const auto res = ApplyCustomTx(accountsView, view, tx, chainparams.GetConsensus(), pindex->nHeight, pindex->GetBlockTime(), nullptr, i, &writers); if (!res.ok && (res.code & CustomTxErrCodes::Fatal)) { if (pindex->nHeight >= chainparams.GetConsensus().EunosHeight) { return state.Invalid(ValidationInvalidReason::CONSENSUS, @@ -2819,6 +2822,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/feature_update_mn.py b/test/functional/feature_update_mn.py index 1a842626126..1d288ded0cc 100755 --- a/test/functional/feature_update_mn.py +++ b/test/functional/feature_update_mn.py @@ -19,9 +19,9 @@ class MasternodesRpcBasicTest (DefiTestFramework): def set_test_params(self): self.num_nodes = 3 self.setup_clean_chain = True - self.extra_args = [['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', '-dakotaheight=136', '-eunosheight=140', '-eunospayaheight=140', '-fortcanningheight=145'], - ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', '-dakotaheight=136', '-eunosheight=140', '-eunospayaheight=140', '-fortcanningheight=145'], - ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', '-dakotaheight=136', '-eunosheight=140', '-eunospayaheight=140', '-fortcanningheight=145']] + self.extra_args = [['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', '-dakotaheight=136', '-eunosheight=140', '-eunospayaheight=140', '-greatworldheight=145'], + ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', '-dakotaheight=136', '-eunosheight=140', '-eunospayaheight=140', '-greatworldheight=145'], + ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', '-dakotaheight=136', '-eunosheight=140', '-eunospayaheight=140', '-greatworldheight=145']] def run_test(self): assert_equal(len(self.nodes[0].listmasternodes()), 8) @@ -57,28 +57,6 @@ def run_test(self): self.sync_all() - # UPDATING - #======================== - #assert_raises_rpc_error(-8, "updatemasternode cannot be called before Fortcanning hard fork", self.nodes[0].updatemasternode, idnode0, collateral0) - - #self.nodes[0].generate(50) - - #assert_raises_rpc_error(-32600, "The new operator is same as existing operator", self.nodes[0].updatemasternode, idnode0, collateral0) - - # node 1 try to update node 0 which should be rejected. - #assert_raises_rpc_error(-5, "Incorrect authorization", self.nodes[1].updatemasternode, idnode0, collateral1) - - #self.nodes[0].updatemasternode(idnode0, collateral1) - #self.nodes[0].generate(1) - #self.sync_all() - - #assert_equal(self.nodes[1].listmasternodes()[idnode0]["operatorAuthAddress"], collateral1) - - # Test rollback - #blockhash = self.nodes[1].getblockhash(self.nodes[1].getblockcount()) - #self.nodes[1].invalidateblock(blockhash) - #self.nodes[1].reconsiderblock(blockhash) - # RESIGNING: #======================== diff --git a/test/functional/rpc_getcustomtx.py b/test/functional/rpc_getcustomtx.py index 3e09b343f6f..ee61d37e14d 100755 --- a/test/functional/rpc_getcustomtx.py +++ b/test/functional/rpc_getcustomtx.py @@ -17,9 +17,9 @@ class TokensRPCGetCustomTX(DefiTestFramework): def set_test_params(self): self.num_nodes = 3 self.setup_clean_chain = True - self.extra_args = [['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', '-dakotaheight=120', '-eunosheight=120', '-eunospayaheight=120', '-fortcanningheight=120'], # Wallet TXs - ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', '-dakotaheight=120', '-eunosheight=120', '-eunospayaheight=120', '-fortcanningheight=120', '-txindex=1'], # Transaction index - ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', '-dakotaheight=120', '-eunosheight=120', '-fortcanningheight=120']] # Will not find historical TXs + self.extra_args = [['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', '-dakotaheight=120', '-eunosheight=120', '-eunospayaheight=120', '-fortcanningheight=120', '-greatworldheight=120'], # Wallet TXs + ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', '-dakotaheight=120', '-eunosheight=120', '-eunospayaheight=120', '-fortcanningheight=120', '-greatworldheight=120', '-txindex=1'], # Transaction index + ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50']] # Will not find historical TXs def check_result(self, result): # Get block hash and height @@ -417,43 +417,20 @@ def run_test(self): assert_equal(result['type'], "DestroyLoanScheme") assert_equal(result['results']['id'], "LOANMAX") - # Test setting a forced address - #reward_address = self.nodes[0].getnewaddress('', 'legacy') - #forced_address_txid = self.nodes[0].setforcedrewardaddress(mn_txid, reward_address) - #self.nodes[0].generate(1) - #self.sync_blocks(self.nodes[0:2]) - - # Get custom TX - #result = self.nodes[1].getcustomtx(forced_address_txid) - #self.check_result(result) - #assert_equal(result['type'], "SetForcedRewardAddress") - #assert_equal(result['results']['mc_id'], mn_txid) - #assert_equal(result['results']['rewardAddress'], reward_address) - - # Test removing a forced address - #reward_address = self.nodes[0].getnewaddress('', 'legacy') - #forced_address_txid = self.nodes[0].remforcedrewardaddress(mn_txid) - #self.nodes[0].generate(1) - #self.sync_blocks(self.nodes[0:2]) - - # Get custom TX - #result = self.nodes[1].getcustomtx(forced_address_txid) - #self.check_result(result) - #assert_equal(result['type'], "RemForcedRewardAddress") - #assert_equal(result['results']['mc_id'], mn_txid) - # Test updating a masternode - #new_operator_address = self.nodes[0].getnewaddress('', 'legacy') - #update_mn_txid = self.nodes[0].updatemasternode(mn_txid, new_operator_address) - #self.nodes[0].generate(1) - #self.sync_blocks(self.nodes[0:2]) + new_operator_address = self.nodes[0].getnewaddress('', 'legacy') + reward_address = self.nodes[0].getnewaddress('', 'legacy') + update_mn_txid = self.nodes[0].updatemasternode(mn_txid, {'operatorAddress':new_operator_address,'rewardAddress':reward_address}) + self.nodes[0].generate(1) + self.sync_blocks(self.nodes[0:2]) # Get custom TX - #result = self.nodes[1].getcustomtx(update_mn_txid) - #self.check_result(result) - #assert_equal(result['type'], "UpdateMasternode") - #assert_equal(result['results']['id'], mn_txid) - #assert_equal(result['results']['masternodeoperator'], new_operator_address) + result = self.nodes[1].getcustomtx(update_mn_txid) + self.check_result(result) + assert_equal(result['type'], "UpdateMasternode") + assert_equal(result['results']['id'], mn_txid) + assert_equal(result['results']['operatorAddress'], new_operator_address) + assert_equal(result['results']['rewardAddress'], reward_address) # Test appoint oracle oracle_address = self.nodes[0].getnewaddress("", "legacy") diff --git a/test/functional/rpc_updatemasternode.py b/test/functional/rpc_updatemasternode.py new file mode 100644 index 00000000000..57e673cb3d2 --- /dev/null +++ b/test/functional/rpc_updatemasternode.py @@ -0,0 +1,412 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The DeFi Blockchain developers +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. + +from test_framework.test_framework import DefiTestFramework +from test_framework.authproxy import JSONRPCException +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, +) +from decimal import Decimal + +class TestForcedRewardAddress(DefiTestFramework): + def set_test_params(self): + self.num_nodes = 2 + self.setup_clean_chain = True + self.extra_args = [ + ['-txindex=1', '-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-greatworldheight=110'], + ['-txindex=1', '-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-greatworldheight=110'], + ] + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + @staticmethod + def list_unspent_tx(node, address): + result = [] + vals = node.listunspent() + for i in range(0, len(vals)): + if vals[i]['address'] == address: + result.append(vals[i]) + return result + + @staticmethod + def unspent_amount(node, address): + result = 0 + vals = node.listunspent() + for i in range(0, len(vals)): + if vals[i]['address'] == 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]]) + + 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 + 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") + assert_raises_rpc_error(-32600, "called before GreatWorld height".format(mn_id), self.nodes[0].updatemasternode, mn_id, {'operatorAddress':operator_address}) + + self.nodes[0].generate(4) + + # Test call before masternode active + operator_address = self.nodes[0].getnewaddress("", "legacy") + assert_raises_rpc_error(-32600, "Masternode {} is not in 'ENABLED' state".format(mn_id), self.nodes[0].updatemasternode, mn_id, {'operatorAddress':operator_address}) + + self.nodes[0].generate(5) + + assert_raises_rpc_error(-32600, "Masternode with that operator address already exists", self.nodes[0].updatemasternode, mn_id, {'operatorAddress':mn_owner}) + + # 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() + + assert_equal(self.nodes[1].listmasternodes()[mn_id]["operatorAuthAddress"], operator_address) + + # Set forced address + forced_reward_address = self.nodes[0].getnewaddress("", "legacy") + assert_raises_rpc_error(-8, + "The masternode {} does not exist".format("some_bad_mn_id"), + self.nodes[0].updatemasternode, "some_bad_mn_id", {'rewardAddress':forced_reward_address} + ) + assert_raises_rpc_error(-8, + "rewardAddress ({}) does not refer to a P2PKH or P2WPKH address".format("some_bad_address"), + self.nodes[0].updatemasternode, mn_id, {'rewardAddress':'some_bad_address'} + ) + + self.nodes[0].updatemasternode(mn_id, {'rewardAddress':forced_reward_address}) + self.nodes[0].generate(1) + + 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) + + 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) + + fra_amount = self.unspent_amount(self.nodes[0], forced_reward_address) + fra_unspent = self.list_unspent_tx(self.nodes[0], forced_reward_address) + assert_equal(len(fra_unspent), 0) + assert_equal(fra_amount, 0) + + self.stop_node(1) + self.restart_node(0, ['-gen', '-masternode_operator='+operator_address, '-txindex=1', '-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-greatworldheight=1']) + + # Mine blocks + self.nodes[0].generate(300) + + self.nodes[0].updatemasternode(mn_id, {'rewardAddress':''}) + self.nodes[0].generate(1) + + assert(len(self.list_unspent_tx(self.nodes[0], forced_reward_address)) > len(fra_unspent)) + assert(self.unspent_amount(self.nodes[0], forced_reward_address) > fra_amount) + + # CLI Reward address for test -rewardaddress + cli_reward_address = self.nodes[0].getnewaddress("", "legacy") + + self.restart_node(0, ['-gen', '-masternode_operator='+operator_address, '-rewardaddress='+cli_reward_address, '-txindex=1', '-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-greatworldheight=1']) + + cra_unspent = self.list_unspent_tx(self.nodes[0], cli_reward_address) + cra_amount = self.unspent_amount(self.nodes[0], cli_reward_address) + assert_equal(len(cra_unspent), 0) + assert_equal(cra_amount, 0) + + # Mine blocks + self.nodes[0].generate(400) + + assert(len(self.list_unspent_tx(self.nodes[0], cli_reward_address)) > len(fra_unspent)) + assert(self.unspent_amount(self.nodes[0], cli_reward_address) > fra_amount) + + # Test updating operator and reward address simultaniously + new_operator_address = self.nodes[0].getnewaddress("", "legacy") + new_reward_address = self.nodes[0].getnewaddress("", "legacy") + self.nodes[0].updatemasternode(mn_id, {'operatorAddress':new_operator_address,'rewardAddress':new_reward_address}) + self.nodes[0].generate(1) + + # Check results + result = self.nodes[0].getmasternode(mn_id)[mn_id] + assert_equal(result['rewardAddress'], new_reward_address) + assert_equal(result['ownerAuthAddress'], mn_owner) + assert_equal(result['operatorAuthAddress'], new_operator_address) + + # Test empty argument + try: + self.nodes[0].updatemasternode(mn_id, {}) + except JSONRPCException as e: + errorString = e.error['message'] + assert("No update arguments provided" in errorString) + + # Test unknown update type + 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: + self.nodes[0].sendrawtransaction(updated_tx) + except JSONRPCException as e: + 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:10.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() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 2b0a42e7af1..69de6a423e9 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -252,7 +252,7 @@ 'rpc_listvaulthistory.py', 'feature_logging.py', 'feature_loan_scheme.py', - #'feature_forced_reward_address.py', + 'rpc_updatemasternode.py', 'feature_loan_vault.py', 'feature_loan_deposittovault.py', 'feature_loan_interest.py',