From ae504495e7df94dda170941c0d9b402664df5bac Mon Sep 17 00:00:00 2001 From: Peter Bushnell Date: Wed, 31 Aug 2022 11:06:25 +0100 Subject: [PATCH 1/2] Updatemasternode --- src/consensus/tx_verify.cpp | 30 +- src/consensus/tx_verify.h | 2 +- src/masternodes/anchors.cpp | 2 +- src/masternodes/masternodes.cpp | 163 ++++----- src/masternodes/masternodes.h | 51 ++- src/masternodes/mn_checks.cpp | 187 ++++++++--- src/masternodes/mn_checks.h | 42 +-- src/masternodes/rpc_customtx.cpp | 29 +- src/masternodes/rpc_masternodes.cpp | 127 ++++--- src/miner.cpp | 16 +- src/pos.cpp | 18 +- src/rpc/client.cpp | 1 + src/rpc/mining.cpp | 6 +- src/test/blockencodings_tests.cpp | 2 +- src/test/pos_tests.cpp | 47 ++- src/test/setup_common.cpp | 2 +- src/txdb.cpp | 5 - src/txmempool.cpp | 13 +- src/txmempool.h | 2 +- src/validation.cpp | 38 ++- src/validation.h | 2 + test/functional/feature_update_mn.py | 22 -- test/functional/rpc_getcustomtx.py | 60 ++-- test/functional/rpc_updatemasternode.py | 419 ++++++++++++++++++++++++ test/functional/test_runner.py | 1 + 25 files changed, 920 insertions(+), 367 deletions(-) create mode 100644 test/functional/rpc_updatemasternode.py diff --git a/src/consensus/tx_verify.cpp b/src/consensus/tx_verify.cpp index 21d3038052..3ed6dd187e 100644 --- a/src/consensus/tx_verify.cpp +++ b/src/consensus/tx_verify.cpp @@ -161,7 +161,7 @@ int64_t GetTransactionSigOpCost(const CTransaction& tx, const CCoinsViewCache& i return nSigOps; } -bool Consensus::CheckTxInputs(const CTransaction& tx, CValidationState& state, const CCoinsViewCache& inputs, const CCustomCSView * mnview, int nSpendHeight, CAmount& txfee, const CChainParams& chainparams) +bool Consensus::CheckTxInputs(const CTransaction& tx, CValidationState& state, const CCoinsViewCache& inputs, CCustomCSView& mnview, int nSpendHeight, CAmount& txfee, const CChainParams& chainparams) { // are the actual inputs available? if (!inputs.HaveInputs(tx)) { @@ -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().GrandCentralHeight && txType == CustomTxType::UpdateMasternode)) { + CCustomCSView discardCache(mnview); + auto res = ApplyCustomTx(discardCache, inputs, tx, chainparams.GetConsensus(), nSpendHeight, 0, &canSpend); + if (!res.ok && (res.code & CustomTxErrCodes::Fatal)) { + 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/consensus/tx_verify.h b/src/consensus/tx_verify.h index 42293d7d11..f14ff5c203 100644 --- a/src/consensus/tx_verify.h +++ b/src/consensus/tx_verify.h @@ -26,7 +26,7 @@ namespace Consensus { * @param[out] txfee Set to the transaction fee if successful. * Preconditions: tx.IsCoinBase() is false. */ -bool CheckTxInputs(const CTransaction& tx, CValidationState& state, const CCoinsViewCache& inputs, const CCustomCSView * mnview, int nSpendHeight, CAmount& txfee, const CChainParams& chainparams); +bool CheckTxInputs(const CTransaction& tx, CValidationState& state, const CCoinsViewCache& inputs, CCustomCSView& mnview, int nSpendHeight, CAmount& txfee, const CChainParams& chainparams); } // namespace Consensus /** Auxiliary functions for transaction validation (ideally should not be exposed) */ diff --git a/src/masternodes/anchors.cpp b/src/masternodes/anchors.cpp index a2b9f6287d..90950050bc 100644 --- a/src/masternodes/anchors.cpp +++ b/src/masternodes/anchors.cpp @@ -993,7 +993,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 e61fb88dfe..ca692d3a82 100644 --- a/src/masternodes/masternodes.cpp +++ b/src/masternodes/masternodes.cpp @@ -3,6 +3,7 @@ // file LICENSE or http://www.opensource.org/licenses/mit-license.php. #include + #include #include #include @@ -87,11 +88,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; @@ -99,6 +100,16 @@ CMasternode::State CMasternode::GetState(int height) const return State::UNKNOWN; } + if (!collateralTx.IsNull()) { + auto idHeight = mnview.GetNewCollateral(collateralTx); + assert(idHeight); + if (static_cast(height) < idHeight->blockHeight) { + return State::TRANSFERRING; + } else if (static_cast(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); @@ -118,9 +129,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; } @@ -138,6 +149,8 @@ std::string CMasternode::GetHumanReadableState(State state) return "PRE_RESIGNED"; case RESIGNED: return "RESIGNED"; + case TRANSFERRING: + return "TRANSFERRING"; default: return "UNKNOWN"; } @@ -165,7 +178,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 ); } @@ -294,14 +307,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()); @@ -310,96 +318,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) @@ -770,7 +767,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}; @@ -810,7 +807,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. @@ -887,9 +884,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); @@ -1169,7 +1174,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 2d8adb8077..1978128fa8 100644 --- a/src/masternodes/masternodes.h +++ b/src/masternodes/masternodes.h @@ -31,6 +31,7 @@ class CAccountHistoryStorage; class CBlockIndex; +class CMasternodesView; class CTransaction; class CVaultHistoryStorage; @@ -42,6 +43,15 @@ CAmount GetMnCreationFee(int height); CAmount GetTokenCreationFee(int height); CAmount GetMnCollateralAmount(int height); +enum class UpdateMasternodeType : uint8_t +{ + None = 0x00, + OwnerAddress = 0x01, + OperatorAddress = 0x02, + SetRewardAddress = 0x03, + RemRewardAddress = 0x04 +}; + constexpr uint8_t SUBNODE_COUNT{4}; class CMasternode @@ -52,6 +62,7 @@ class CMasternode ENABLED, PRE_RESIGNED, RESIGNED, + TRANSFERRING, UNKNOWN // unreachable }; @@ -90,13 +101,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); @@ -117,7 +128,7 @@ class CMasternode READWRITE(version); READWRITE(resignTx); - READWRITE(banTx); + READWRITE(collateralTx); // Only available after FortCanning if (version > PRE_FORT_CANNING) { @@ -176,12 +187,25 @@ 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; public: -// CMasternodesView() = default; std::optional GetMasternode(uint256 const & id) const; std::optional GetMasternodeIdByOperator(CKeyID const & id) const; @@ -198,10 +222,16 @@ 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 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 ResignMasternode(CMasternode& node, uint256 const & nodeId, uint256 const & txid, int height); + + // 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); + [[nodiscard]] 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); @@ -226,6 +256,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'; } }; @@ -373,7 +404,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 211d68e3c8..b10c1f2104 100644 --- a/src/masternodes/mn_checks.cpp +++ b/src/masternodes/mn_checks.cpp @@ -31,8 +31,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"; @@ -130,8 +128,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{}; @@ -285,27 +281,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 = isPostGrandCentralFork(); return !res ? res : serialize(obj); } @@ -952,46 +929,138 @@ 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"); + } - auto const node = mnview.GetMasternode(obj.nodeId); + if (obj.updates.size() > 3) { + return Res::Err("Too many updates provided"); + } + + 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{}, operatorType{}, rewardType{}; + for (const auto& item : obj.updates) { + if (item.first == static_cast(UpdateMasternodeType::OwnerAddress)) { + if (ownerType) { + return Res::Err("Multiple owner updates provided"); + } + ownerType = true; + bool collateralFound{}; + for (const auto& vin : tx.vin) { + if (vin.prevout.hash == collateralTx && vin.prevout.n == 1) { + collateralFound = true; + } + } + if (!collateralFound) { + return Res::Err("Missing previous collateral from transaction inputs"); + } + if (tx.vout.size() == 1) { + return Res::Err("Missing new collateral output"); + } + if (!HasAuth(tx.vout[1].scriptPubKey)) { + return Res::Err("Missing auth input for new masternode owner"); + } - 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"); - } + 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"); + } - return mnview.RemForcedRewardAddress(obj.nodeId, height); - } + if (tx.vout[1].nValue != GetMnCollateralAmount(height)) { + return Res::Err("Incorrect collateral amount"); + } - Res operator()(const CUpdateMasterNodeMessage& obj) const { - // Temporarily disabled for 2.2 - return Res::Err("updatemasternode is disabled for Fort Canning"); + 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{}; + 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"); + } + 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"); + } + } - auto res = HasCollateralAuth(obj.mnId); - return !res ? res : mnview.UpdateMasternode(obj.mnId, obj.operatorType, obj.operatorAuthAddress, height); + return Res::Ok(); } Res operator()(const CCreateTokenMessage& obj) const { @@ -3724,7 +3793,7 @@ void PopulateVaultHistoryData(CHistoryWriters* writers, CAccountsHistoryWriter& } } -Res ApplyCustomTx(CCustomCSView& mnview, const CCoinsViewCache& coins, const CTransaction& tx, const Consensus::Params& consensus, uint32_t height, uint64_t time, uint32_t txn, CHistoryWriters* writers) { +Res ApplyCustomTx(CCustomCSView& mnview, const CCoinsViewCache& coins, const CTransaction& tx, const Consensus::Params& consensus, uint32_t height, uint64_t time, uint256* canSpend, uint32_t txn, CHistoryWriters* writers) { auto res = Res::Ok(); if (tx.IsCoinBase() && height > 0) { // genesis contains custom coinbase txs return res; @@ -3748,6 +3817,18 @@ Res ApplyCustomTx(CCustomCSView& mnview, const CCoinsViewCache& coins, const CTr } res = CustomTxVisit(view, coins, tx, height, consensus, txMessage, time, txn); + if (res && 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 979a0b54ec..0736cf5ca3 100644 --- a/src/masternodes/mn_checks.h +++ b/src/masternodes/mn_checks.h @@ -41,8 +41,6 @@ enum class CustomTxType : uint8_t CreateMasternode = 'C', ResignMasternode = 'R', UpdateMasternode = 'm', - SetForcedRewardAddress = 'F', - RemForcedRewardAddress = 'f', // custom tokens: CreateToken = 'T', MintToken = 'M', @@ -111,8 +109,6 @@ inline CustomTxType CustomTxCodeToType(uint8_t ch) { switch(type) { case CustomTxType::CreateMasternode: case CustomTxType::ResignMasternode: - case CustomTxType::SetForcedRewardAddress: - case CustomTxType::RemForcedRewardAddress: case CustomTxType::UpdateMasternode: case CustomTxType::CreateToken: case CustomTxType::MintToken: @@ -219,45 +215,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); } }; @@ -334,8 +300,6 @@ using CCustomTxMessage = std::variant< CCustomTxMessageNone, CCreateMasterNodeMessage, CResignMasterNodeMessage, - CSetForcedRewardAddressMessage, - CRemForcedRewardAddressMessage, CUpdateMasterNodeMessage, CCreateTokenMessage, CUpdateTokenPreAMKMessage, @@ -387,7 +351,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 CustomTxVisit(CCustomCSView& mnview, const CCoinsViewCache& coins, const CTransaction& tx, uint32_t height, const Consensus::Params& consensus, const CCustomTxMessage& txMessage, uint64_t time, uint32_t txn = 0); ResVal ApplyAnchorRewardTx(CCustomCSView& mnview, const CTransaction& tx, int height, const uint256& prevStakeModifier, const std::vector& metadata, const Consensus::Params& consensusParams); ResVal ApplyAnchorRewardTxPlus(CCustomCSView& mnview, const CTransaction& tx, int height, const std::vector& metadata, const Consensus::Params& consensusParams); diff --git a/src/masternodes/rpc_customtx.cpp b/src/masternodes/rpc_customtx.cpp index 25563724f3..5e2573f641 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 c0198cc2e6..864e48669f 100644 --- a/src/masternodes/rpc_masternodes.cpp +++ b/src/masternodes/rpc_masternodes.cpp @@ -3,12 +3,12 @@ #include // Here (but not a class method) just by similarity with other '..ToJSON' -UniValue mnToJSON(uint256 const & nodeId, CMasternode const& node, bool verbose, const std::set>& mnIds, const CWallet* pwallet) +UniValue mnToJSON(CCustomCSView& view, uint256 const & nodeId, CMasternode const& node, bool verbose, const std::set>& mnIds, const CWallet* pwallet) { 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, view))); } 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, view))); 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, view)) { // Get block times with next block as height const auto subNodesBlockTime = pcustomcsview->GetBlockTimes(node.operatorAuthAddress, currentHeight + 1, node.creationHeight, timelock); @@ -247,7 +247,7 @@ UniValue resignmasternode(const JSONRPCRequest& request) std::string const nodeIdStr = request.params[0].getValStr(); uint256 const nodeId = uint256S(nodeIdStr); - CTxDestination ownerDest; + CTxDestination ownerDest, collateralDest; int targetHeight; { LOCK(cs_main); @@ -259,6 +259,13 @@ UniValue resignmasternode(const JSONRPCRequest& request) CTxDestination(PKHash(nodePtr->ownerAuthAddress)) : CTxDestination(WitnessV0KeyHash(nodePtr->ownerAuthAddress)); + if (!nodePtr->collateralTx.IsNull()) { + const auto& coin = ::ChainstateActive().CoinsTip().AccessCoin({nodePtr->collateralTx, 1}); + if (coin.IsSpent() || !ExtractDestination(coin.out.scriptPubKey, collateralDest)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Masternode collateral not available"); + } + } + targetHeight = ::ChainActive().Height() + 1; } @@ -267,6 +274,9 @@ UniValue resignmasternode(const JSONRPCRequest& request) CTransactionRef optAuthTx; std::set auths{GetScriptForDestination(ownerDest)}; + if (collateralDest.index() != 0) { + auths.insert(GetScriptForDestination(collateralDest)); + } rawTx.vin = GetAuthInputsSmart(pwallet, rawTx.nVersion, auths, false, optAuthTx, request.params[1]); // Return change to owner address @@ -294,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", @@ -306,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 reward address."}, + }, + }, {"inputs", RPCArg::Type::ARR, RPCArg::Optional::OMITTED_NAMED_ARG, "A json array of json objects", { {"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "", @@ -333,23 +345,10 @@ UniValue updatemasternode(const JSONRPCRequest& request) } pwallet->BlockUntilSyncedToCurrentChain(); - bool forkCanning; - { - LOCK(cs_main); - forkCanning = ::ChainActive().Tip()->nHeight >= Params().GetConsensus().FortCanningHeight; - } + RPCTypeCheck(request.params, { UniValue::VSTR, UniValue::VOBJ, UniValue::VARR }, true); - 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"); - } - - std::string const nodeIdStr = request.params[0].getValStr(); - uint256 const nodeId = uint256S(nodeIdStr); + const std::string nodeIdStr = request.params[0].getValStr(); + const uint256 nodeId = uint256S(nodeIdStr); CTxDestination ownerDest; int targetHeight; { @@ -363,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 @@ -384,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); @@ -473,7 +520,7 @@ UniValue listmasternodes(const JSONRPCRequest& request) including_start = true; return (true); } - ret.pushKVs(mnToJSON(nodeId, node, verbose, mnIds, pwallet)); + ret.pushKVs(mnToJSON(*pcustomcsview, nodeId, node, verbose, mnIds, pwallet)); limit--; return limit != 0; }, start); @@ -506,7 +553,7 @@ UniValue getmasternode(const JSONRPCRequest& request) const auto mnIds = pcustomcsview->GetOperatorsMulti(); auto node = pcustomcsview->GetMasternode(id); if (node) { - auto res = mnToJSON(id, *node, true, mnIds, pwallet); // or maybe just node, w/o id? + auto res = mnToJSON(*pcustomcsview, id, *node, true, mnIds, pwallet); // or maybe just node, w/o id? return GetRPCResultCache().Set(request, res); } throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Masternode not found"); @@ -810,7 +857,7 @@ 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"}}, diff --git a/src/miner.cpp b/src/miner.cpp index b88f293f30..a4fcdf92da 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -116,12 +116,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; } @@ -336,7 +337,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.GrandCentralHeight ? nodePtr->ownerAuthAddress : myIDs->first; + pblock->stakeModifier = pos::ComputeStakeModifier(pindexPrev->stakeModifier, key); } pblocktemplate->vTxSigOpsCost[0] = WITNESS_SCALE_FACTOR * GetLegacySigOpCount(*pblock->vtx[0]); @@ -727,6 +729,7 @@ namespace pos { int64_t blockHeight; std::vector subNodesBlockTime; uint16_t timelock; + std::optional nodePtr; { LOCK(cs_main); @@ -737,8 +740,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; @@ -772,7 +775,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().GrandCentralHeight ? 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 dddcaf8b18..66aeff352f 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().GrandCentralHeight) { + 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); @@ -196,7 +204,7 @@ std::optional SignPosBlock(std::shared_ptr pblock, const CK bool signingRes = key.SignCompact(pblock->GetHashToSign(), pblock->sig); if (!signingRes) { - return {std::string{} + "Block signing error"}; + return "Block signing error"; } return {}; diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 1c6590ee86..5bde5daaf5 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -177,6 +177,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "createmasternode", 2, "inputs" }, { "resignmasternode", 1, "inputs" }, { "updatemasternode", 2, "inputs" }, + { "updatemasternode", 1, "values" }, { "listmasternodes", 0, "pagination" }, { "listmasternodes", 1, "verbose" }, { "getmasternodeblocks", 0, "identifier"}, diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index e704e01580..2189f7dff2 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 0b448a6077..925bb8326b 100644 --- a/src/test/blockencodings_tests.cpp +++ b/src/test/blockencodings_tests.cpp @@ -54,7 +54,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 4433da9aa1..75ba04d822 100644 --- a/src/test/pos_tests.cpp +++ b/src/test/pos_tests.cpp @@ -24,24 +24,18 @@ std::shared_ptr Block( const uint256& prev_hash, const uint64_t& height, auto ptemplate = BlockAssembler(Params()).CreateNewBlock(pubKey); auto pblock = std::make_shared(ptemplate->block); pblock->hashPrevBlock = prev_hash; - pblock->mintedBlocks = mintedBlocks; pblock->deprecatedHeight = 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 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); - pblock->nTime = time + 10; - BOOST_CHECK(!pos::SignPosBlock(pblock, minterKey)); return pblock; @@ -79,7 +73,7 @@ BOOST_AUTO_TEST_CASE(calc_kernel) BOOST_AUTO_TEST_CASE(check_stake_modifier) { uint256 masternodeID = testMasternodeKeys.begin()->first; - std::map::const_iterator pos = testMasternodeKeys.find(masternodeID); + auto pos = testMasternodeKeys.find(masternodeID); BOOST_CHECK(pos != testMasternodeKeys.end()); CKey minterKey = pos->second.operatorKey; @@ -95,20 +89,41 @@ BOOST_AUTO_TEST_CASE(check_stake_modifier) std::shared_ptr correctBlock = FinalizeBlock( 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().GrandCentralHeight; + const auto newModifierBlock = FinalizeBlock( + Block(blockTip.GetBlockHash(), blockTip.nHeight, blockTip.mintedBlocks), + newMinterKey, + blockTip.stakeModifier, + masternode.ownerAuthAddress); + BOOST_CHECK(pos::CheckStakeModifier(&blockTip, static_cast(*newModifierBlock))); } BOOST_AUTO_TEST_CASE(check_header_signature) { uint256 masternodeID = testMasternodeKeys.begin()->first; - std::map::const_iterator pos = testMasternodeKeys.find(masternodeID); + auto pos = testMasternodeKeys.find(masternodeID); BOOST_CHECK(pos != testMasternodeKeys.end()); CKey minterKey = pos->second.operatorKey; @@ -123,9 +138,9 @@ BOOST_AUTO_TEST_CASE(check_header_signature) FinalizeBlock( block, - masternodeID, minterKey, - prev_hash); + prev_hash, + minterKey.GetPubKey().GetID()); BOOST_CHECK(pos::CheckHeaderSignature(*(CBlockHeader*)block.get())); @@ -137,7 +152,7 @@ BOOST_AUTO_TEST_CASE(check_header_signature) BOOST_AUTO_TEST_CASE(contextual_check_pos) { uint256 masternodeID = testMasternodeKeys.begin()->first; - std::map::const_iterator pos = testMasternodeKeys.find(masternodeID); + auto pos = testMasternodeKeys.find(masternodeID); BOOST_CHECK(pos != testMasternodeKeys.end()); CKey minterKey = pos->second.operatorKey; CheckContextState ctxState; @@ -158,7 +173,7 @@ BOOST_AUTO_TEST_CASE(contextual_check_pos) BOOST_AUTO_TEST_CASE(sign_pos_block) { uint256 masternodeID = testMasternodeKeys.begin()->first; - std::map::const_iterator pos = testMasternodeKeys.find(masternodeID); + auto pos = testMasternodeKeys.find(masternodeID); BOOST_CHECK(pos != testMasternodeKeys.end()); CKey minterKey = pos->second.operatorKey; diff --git a/src/test/setup_common.cpp b/src/test/setup_common.cpp index 21807a4fb3..a29ad05681 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 9aa12de804..a43287ace3 100644 --- a/src/txdb.cpp +++ b/src/txdb.cpp @@ -281,11 +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 { return error("%s: failed to read value", __func__); diff --git a/src/txmempool.cpp b/src/txmempool.cpp index f1714a4266..35d1b092ee 100644 --- a/src/txmempool.cpp +++ b/src/txmempool.cpp @@ -623,17 +623,17 @@ void CTxMemPool::clear() acview.reset(); } -static void CheckInputsAndUpdateCoins(const CTransaction& tx, CCoinsViewCache& mempoolDuplicate, const CCustomCSView * mnview, const int64_t spendheight, const CChainParams& chainparams) +static void CheckInputsAndUpdateCoins(const CTransaction& tx, CCoinsViewCache& mempoolDuplicate, CCustomCSView& mnviewDuplicate, const int64_t spendheight, const CChainParams& chainparams) { CValidationState state; CAmount txfee = 0; - bool fCheckResult = tx.IsCoinBase() || Consensus::CheckTxInputs(tx, state, mempoolDuplicate, mnview, spendheight, txfee, chainparams); + bool fCheckResult = tx.IsCoinBase() || Consensus::CheckTxInputs(tx, state, mempoolDuplicate, mnviewDuplicate, spendheight, txfee, chainparams); fCheckResult = fCheckResult && CheckBurnSpend(tx, mempoolDuplicate); assert(fCheckResult); UpdateCoins(tx, mempoolDuplicate, std::numeric_limits::max()); } -void CTxMemPool::xcheck(const CCoinsViewCache *pcoins, const CCustomCSView * mnview, const CChainParams& chainparams) const +void CTxMemPool::xcheck(const CCoinsViewCache *pcoins, CCustomCSView *mnview, const CChainParams& chainparams) const { LOCK(cs); if (nCheckFrequency == 0) @@ -647,6 +647,7 @@ void CTxMemPool::xcheck(const CCoinsViewCache *pcoins, const CCustomCSView * mnv uint64_t checkTotal = 0; uint64_t innerUsage = 0; + CCustomCSView mnviewDuplicate(*mnview); CCoinsViewCache mempoolDuplicate(const_cast(pcoins)); const int64_t spendheight = GetSpendHeight(mempoolDuplicate); @@ -721,7 +722,7 @@ void CTxMemPool::xcheck(const CCoinsViewCache *pcoins, const CCustomCSView * mnv if (fDependsWait) waitingOnDependants.push_back(&(*it)); else { - CheckInputsAndUpdateCoins(tx, mempoolDuplicate, mnview, spendheight, chainparams); + CheckInputsAndUpdateCoins(tx, mempoolDuplicate, mnviewDuplicate, spendheight, chainparams); } } unsigned int stepsSinceLastRemove = 0; @@ -733,7 +734,7 @@ void CTxMemPool::xcheck(const CCoinsViewCache *pcoins, const CCustomCSView * mnv stepsSinceLastRemove++; assert(stepsSinceLastRemove < waitingOnDependants.size()); } else { - CheckInputsAndUpdateCoins(entry->GetTx(), mempoolDuplicate, mnview, spendheight, chainparams); + CheckInputsAndUpdateCoins(entry->GetTx(), mempoolDuplicate, mnviewDuplicate, spendheight, chainparams); stepsSinceLastRemove = 0; } } @@ -1107,7 +1108,7 @@ void CTxMemPool::rebuildAccountsView(int height, const CCoinsViewCache& coinsCac for (auto it = txsByEntryTime.begin(); it != txsByEntryTime.end(); ++it) { CValidationState state; const auto& tx = it->GetTx(); - if (!Consensus::CheckTxInputs(tx, state, coinsCache, &viewDuplicate, height, txfee, Params())) { + if (!Consensus::CheckTxInputs(tx, state, coinsCache, viewDuplicate, height, txfee, Params())) { LogPrintf("%s: Remove conflicting TX: %s\n", __func__, tx.GetHash().GetHex()); staged.insert(mapTx.project<0>(it)); vtx.push_back(it->GetSharedTx()); diff --git a/src/txmempool.h b/src/txmempool.h index 53d988fd83..f4176d1027 100644 --- a/src/txmempool.h +++ b/src/txmempool.h @@ -567,7 +567,7 @@ class CTxMemPool * all inputs are in the mapNextTx array). If sanity-checking is turned off, * check does nothing. */ - void xcheck(const CCoinsViewCache *pcoins, const CCustomCSView *mnview, const CChainParams& chainparams) const; + void xcheck(const CCoinsViewCache *pcoins, CCustomCSView *mnview, const CChainParams& chainparams) const; void setSanityCheck(double dFrequency = 1.0) { LOCK(cs); nCheckFrequency = static_cast(dFrequency * 4294967295.0); } // addUnchecked must updated state for all ancestors of a given transaction, diff --git a/src/validation.cpp b/src/validation.cpp index 69fc74bf9d..27584203e8 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -629,7 +629,7 @@ static bool AcceptToMemoryPoolWorker(const CChainParams& chainparams, CTxMemPool pool.rebuildAccountsView(height, view); CAmount nFees = 0; - if (!Consensus::CheckTxInputs(tx, state, view, &mnview, height, nFees, chainparams)) { + if (!Consensus::CheckTxInputs(tx, state, view, mnview, height, nFees, chainparams)) { return error("%s: Consensus::CheckTxInputs: %s, %s", __func__, tx.GetHash().ToString(), FormatStateMessage(state)); } @@ -2552,7 +2552,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); @@ -2598,8 +2598,9 @@ bool CChainState::ConnectBlock(const CBlock& block, CValidationState& state, CBl __func__, 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)) { - return state.Invalid( + const CKeyID key = pindex->nHeight >= chainparams.GetConsensus().GrandCentralHeight ? nodePtr->ownerAuthAddress : nodePtr->operatorAuthAddress; + const auto stakeModifier = pos::ComputeStakeModifier(stakeModifierPrevBlock, key); + if (block.stakeModifier != stakeModifier) { return state.Invalid( ValidationInvalidReason::CONSENSUS, error("%s: block's stake Modifier should be %d, got %d!", __func__, block.stakeModifier.ToString(), pos::ComputeStakeModifier(stakeModifierPrevBlock, nodePtr->operatorAuthAddress).ToString()), @@ -2766,7 +2767,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 (!Consensus::CheckTxInputs(tx, state, view, accountsView, pindex->nHeight, txfee, chainparams)) { if (!IsBlockReason(state.GetReason())) { // CheckTxInputs may return MISSING_INPUTS or // PREMATURE_SPEND but we can't return that, as it's not @@ -2827,7 +2828,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, @@ -3049,6 +3050,9 @@ bool CChainState::ConnectBlock(const CBlock& block, CValidationState& state, CBl // DFI-to-DUSD swaps ProcessFuturesDUSD(pindex, cache, chainparams); + // Masternode updates + ProcessMasternodeUpdates(pindex, cache, view, chainparams); + // construct undo auto& flushable = cache.GetStorage(); auto undo = CUndo::Construct(mnview.GetStorage(), flushable.GetRaw()); @@ -3926,6 +3930,28 @@ void CChainState::ProcessGovEvents(const CBlockIndex* pindex, CCustomCSView& cac cache.EraseStoredVariables(static_cast(pindex->nHeight)); } +void CChainState::ProcessMasternodeUpdates(const CBlockIndex* pindex, CCustomCSView& cache, const CCoinsViewCache& view, const CChainParams& chainparams) { + if (pindex->nHeight < chainparams.GetConsensus().GrandCentralHeight) { + return; + } + + // Apply any pending masternode owner changes + cache.ForEachNewCollateral([&](const uint256& key, const MNNewOwnerHeightValue& value){ + if (value.blockHeight == static_cast(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; + }); +} + void CChainState::ProcessTokenToGovVar(const CBlockIndex* pindex, CCustomCSView& cache, const CChainParams& chainparams) { // Migrate at +1 height so that GetLastHeight() in Gov var diff --git a/src/validation.h b/src/validation.h index 6a975b6ae4..e621e30cf5 100644 --- a/src/validation.h +++ b/src/validation.h @@ -790,6 +790,8 @@ class CChainState { static void ProcessTokenSplits(const CBlock& block, const CBlockIndex* pindex, CCustomCSView& cache, const CreationTxs& creationTxs, const CChainParams& chainparams); static void ProcessFuturesDUSD(const CBlockIndex* pindex, CCustomCSView& cache, const CChainParams& chainparams); + + static void ProcessMasternodeUpdates(const CBlockIndex* pindex, CCustomCSView& cache, const CCoinsViewCache& view, const CChainParams& chainparams); }; /** Mark a block as precious and reorganize. diff --git a/test/functional/feature_update_mn.py b/test/functional/feature_update_mn.py index 4badb8411c..0c70fa6358 100755 --- a/test/functional/feature_update_mn.py +++ b/test/functional/feature_update_mn.py @@ -57,28 +57,6 @@ def run_test(self): assert_equal(self.nodes[0].listmasternodes({}, False)[idnode0], "ENABLED") assert_equal(self.nodes[1].listmasternodes()[idnode0]["operatorAuthAddress"], collateral0) - # 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_blocks() - - #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 111476f065..73e4e2b664 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', '-fortcanninghillheight=122'], # Wallet TXs - ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', '-dakotaheight=120', '-eunosheight=120', '-eunospayaheight=120', '-fortcanningheight=120', '-fortcanninghillheight=122', '-txindex=1'], # Transaction index - ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', '-dakotaheight=120', '-eunosheight=120', '-fortcanningheight=120', '-fortcanninghillheight=122']] # Will not find historical TXs + self.extra_args = [['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', '-dakotaheight=120', '-eunosheight=120', '-eunospayaheight=120', '-fortcanningheight=120', '-fortcanninghillheight=122', '-grandcentralheight=189'], # Wallet TXs + ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', '-dakotaheight=120', '-eunosheight=120', '-eunospayaheight=120', '-fortcanningheight=120', '-fortcanninghillheight=122', '-grandcentralheight=189', '-txindex=1'], # Transaction index + ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', '-dakotaheight=120', '-eunosheight=120', '-fortcanningheight=120', '-fortcanninghillheight=122', '-grandcentralheight=189']] # Will not find historical TXs def check_result(self, result): # Get block hash and height @@ -417,44 +417,6 @@ 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]) - - # 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) - # Test appoint oracle oracle_address = self.nodes[0].getnewaddress("", "legacy") appoint_oracle_tx = self.nodes[0].appointoracle(oracle_address, [{"currency": "GBP", "token": "TSLA"}], 1) @@ -829,5 +791,21 @@ def run_test(self): assert_equal(attributes['v0/params/dfip2201/premium'], '0.025') assert_equal(attributes['v0/params/dfip2201/minswap'], '0.001') + # Test updating a masternode + 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) + print(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) + if __name__ == '__main__': TokensRPCGetCustomTX().main () diff --git a/test/functional/rpc_updatemasternode.py b/test/functional/rpc_updatemasternode.py new file mode 100644 index 0000000000..0611ec9064 --- /dev/null +++ b/test/functional/rpc_updatemasternode.py @@ -0,0 +1,419 @@ +#!/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', '-grandcentralheight=110'], + ['-txindex=1', '-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-grandcentralheight=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 GrandCentral 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', '-grandcentralheight=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', '-grandcentralheight=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, "bad-txns-collateral-locked, tried to spend locked collateral for", 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, "bad-txns-collateral-locked, tried to spend locked collateral for", 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) + self.nodes[0].generate(1) + mn2 = self.nodes[0].createmasternode(mn2_owner) + self.nodes[0].generate(1) + mn3 = self.nodes[0].createmasternode(mn3_owner) + self.nodes[0].generate(1) + mn4 = self.nodes[0].createmasternode(mn4_owner) + self.nodes[0].generate(1) + mn5 = self.nodes[0].createmasternode(mn5_owner) + self.nodes[0].generate(1) + 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}) + self.nodes[0].generate(1) + assert_raises_rpc_error(-32600, "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 199a8ee166..a1bf60b5aa 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -243,6 +243,7 @@ 'wallet_encryption.py', 'feature_dersig.py', 'feature_cltv.py', + 'rpc_updatemasternode.py', 'rpc_uptime.py', 'feature_longterm_lockin.py', 'wallet_resendwallettransactions.py', From dfe240ceb8247d36202e21e9ae5034d18a06eeba Mon Sep 17 00:00:00 2001 From: Peter Bushnell Date: Thu, 15 Sep 2022 11:46:56 +0100 Subject: [PATCH 2/2] Fix intermittent test failure --- test/functional/feature_accounts_n_utxos.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/functional/feature_accounts_n_utxos.py b/test/functional/feature_accounts_n_utxos.py index 729c5d8292..2877d56938 100755 --- a/test/functional/feature_accounts_n_utxos.py +++ b/test/functional/feature_accounts_n_utxos.py @@ -92,6 +92,7 @@ def run_test(self): self.nodes[0].accounttoaccount(accountGold, {toGold: "100@" + symbolGOLD}, []) self.nodes[0].generate(1) + self.sync_blocks(self.nodes[0:1]) assert_equal(self.nodes[0].getaccount(accountGold, {}, True)[idGold], initialGold - 100) assert_equal(self.nodes[0].getaccount(toGold, {}, True)[idGold], 100) @@ -103,6 +104,7 @@ def run_test(self): self.nodes[1].accounttoaccount(accountSilver, {toSilver: "100@" + symbolSILVER}, []) self.nodes[1].generate(1) + self.sync_blocks(self.nodes[0:1]) assert_equal(self.nodes[1].getaccount(accountSilver, {}, True)[idSilver], initialSilver - 100) assert_equal(self.nodes[0].getaccount(toSilver, {}, True)[idSilver], 100)