From 0364a9011bd86c7ddb20cf5b9520f53122e0a6d9 Mon Sep 17 00:00:00 2001 From: Peter John Bushnell Date: Thu, 19 May 2022 08:37:55 +0100 Subject: [PATCH] Restore updatemasternode and combine setforcedrewardaddress / removeforcedaddress (#934) * Restore updatemasternode * test: create MN one at a time * Update MN update PR for latest v3 * Fix PoS test * Resolve issues after merge * Use local view variable * Resolve errors after merge * Remove outdated code and update ApplyCustomTx call * tests: resolve random failure --- src/consensus/tx_verify.cpp | 30 +- src/masternodes/anchors.cpp | 4 +- src/masternodes/consensus/masternodes.cpp | 159 ++++++-- src/masternodes/consensus/masternodes.h | 4 - src/masternodes/consensus/proposals.cpp | 2 +- src/masternodes/customtx.cpp | 4 - src/masternodes/customtx.h | 2 - src/masternodes/masternodes.cpp | 158 ++++---- src/masternodes/masternodes.h | 77 ++-- src/masternodes/mn_checks.cpp | 24 +- src/masternodes/mn_checks.h | 6 +- src/masternodes/rpc_customtx.cpp | 29 +- src/masternodes/rpc_masternodes.cpp | 319 +++++------------ src/masternodes/rpc_proposals.cpp | 2 +- src/miner.cpp | 16 +- src/pos.cpp | 18 +- src/rpc/client.cpp | 3 +- src/rpc/mining.cpp | 6 +- src/test/applytx_tests.cpp | 7 +- src/test/blockencodings_tests.cpp | 2 +- src/test/pos_tests.cpp | 42 ++- src/test/setup_common.cpp | 2 +- src/txdb.cpp | 4 - src/validation.cpp | 35 +- test/functional/feature_update_mn.py | 28 +- test/functional/rpc_getcustomtx.py | 59 +-- test/functional/rpc_updatemasternode.py | 418 ++++++++++++++++++++++ test/functional/test_runner.py | 2 +- 28 files changed, 916 insertions(+), 546 deletions(-) create mode 100644 test/functional/rpc_updatemasternode.py diff --git a/src/consensus/tx_verify.cpp b/src/consensus/tx_verify.cpp index 12104327c6d..a2ae6f911cc 100644 --- a/src/consensus/tx_verify.cpp +++ b/src/consensus/tx_verify.cpp @@ -170,6 +170,20 @@ bool Consensus::CheckTxInputs(const CTransaction& tx, CValidationState& state, c strprintf("%s: inputs missing/spent", __func__)); } + // check for tokens values + uint256 canSpend; + std::vector dummy; + const auto txType = GuessCustomTxType(tx, dummy); + + if (NotAllowedToFail(txType, nSpendHeight) || (nSpendHeight >= chainparams.GetConsensus().GreatWorldHeight && txType == CustomTxType::UpdateMasternode)) { + CCustomCSView discardCache(mnview); + CFutureSwapView futureSwapView(*pfutureSwapView); + auto res = ApplyCustomTx(discardCache, futureSwapView, inputs, tx, chainparams.GetConsensus(), nSpendHeight, 0, &canSpend); + if (!res.ok && (res.code & CustomTxErrCodes::Fatal) && txType != CustomTxType::UpdateMasternode) { + return state.Invalid(ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, "bad-txns-customtx", res.msg); + } + } + TAmounts nValuesIn; for (unsigned int i = 0; i < tx.vin.size(); ++i) { const COutPoint &prevout = tx.vin[i].prevout; @@ -188,8 +202,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? } @@ -216,19 +229,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(mnview); - CFutureSwapView futureSwapView(*pfutureSwapView); - auto res = ApplyCustomTx(discardCache, futureSwapView, inputs, tx, chainparams.GetConsensus(), nSpendHeight); - if (!res.ok && (res.code & CustomTxErrCodes::Fatal)) { - return state.Invalid(ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, "bad-txns-customtx", res.msg); - } - } - for (auto const & kv : non_minted_values_out) { DCT_ID const & tokenId = kv.first; diff --git a/src/masternodes/anchors.cpp b/src/masternodes/anchors.cpp index 4bf28482c3e..05c910c2c5b 100644 --- a/src/masternodes/anchors.cpp +++ b/src/masternodes/anchors.cpp @@ -970,7 +970,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; } @@ -1117,7 +1117,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; for (auto const & wallet : GetWallets()) { if (wallet->GetKey(mnId.first, masternodeKey)) { diff --git a/src/masternodes/consensus/masternodes.cpp b/src/masternodes/consensus/masternodes.cpp index 2d31a0bf667..15bce58eb03 100644 --- a/src/masternodes/consensus/masternodes.cpp +++ b/src/masternodes/consensus/masternodes.cpp @@ -2,9 +2,12 @@ // Distributed under the MIT software license, see the accompanying // file LICENSE or http://www.opensource.org/licenses/mit-license.php. +#include + +#include #include #include -#include +#include #include Res CMasternodesConsensus::operator()(const CCreateMasterNodeMessage& obj) const { @@ -62,42 +65,136 @@ Res CMasternodesConsensus::operator()(const CCreateMasterNodeMessage& obj) const } Res CMasternodesConsensus::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 CMasternodesConsensus::operator()(const CSetForcedRewardAddressMessage& obj) const { - // Temporarily disabled for 2.2 - return Res::Err("reward address change is disabled for Fort Canning"); - - auto 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"); - - return mnview.SetForcedRewardAddress(obj.nodeId, obj.rewardAddressType, obj.rewardAddress, height); -} +Res CMasternodesConsensus::operator()(const CUpdateMasterNodeMessage& obj) const { + if (obj.updates.empty()) { + return Res::Err("No update arguments provided"); + } -Res CMasternodesConsensus::operator()(const CRemForcedRewardAddressMessage& obj) const { - // Temporarily disabled for 2.2 - return Res::Err("reward address change is disabled for Fort Canning"); + if (obj.updates.size() > 3) { + return Res::Err("Too many updates provided"); + } - auto node = mnview.GetMasternode(obj.nodeId); - if (!node) - return Res::Err("masternode %s does not exist", obj.nodeId.ToString()); + auto node = mnview.GetMasternode(obj.mnId); + if (!node) { + 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.RemForcedRewardAddress(obj.nodeId, 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 CMasternodesConsensus::operator()(const CUpdateMasterNodeMessage& obj) const { - // Temporarily disabled for 2.2 - return Res::Err("updatemasternode is disabled for Fort Canning"); + bool ownerType{false}, operatorType{false}, rewardType{false}; + for (const auto& item : obj.updates) { + if (item.first == static_cast(UpdateMasternodeType::OwnerAddress)) { + if (ownerType) { + return Res::Err("Multiple owner updates provided"); + } + ownerType = true; + bool collateralFound{false}; + for (const auto vin : tx.vin) { + if (vin.prevout.hash == collateralTx && vin.prevout.n == 1) { + collateralFound = true; + } + } + if (!collateralFound) { + return Res::Err("Missing previous collateral from transaction inputs"); + } + if (tx.vout.size() == 1) { + return Res::Err("Missing new collateral output"); + } + if (!HasAuth(tx.vout[1].scriptPubKey)) { + return Res::Err("Missing auth input for new masternode owner"); + } + + CTxDestination dest; + if (!ExtractDestination(tx.vout[1].scriptPubKey, dest) || (dest.index() != PKHashType && dest.index() != WitV0KeyHashType)) { + return Res::Err("Owner address must be P2PKH or P2WPKH type"); + } + + if (tx.vout[1].nValue != GetMnCollateralAmount(height)) { + return Res::Err("Incorrect collateral amount"); + } + + const auto keyID = dest.index() == PKHashType ? CKeyID(std::get(dest)) : CKeyID(std::get(dest)); + if (mnview.GetMasternodeIdByOwner(keyID) || mnview.GetMasternodeIdByOperator(keyID)) { + return Res::Err("Masternode with that owner address already exists"); + } + + bool duplicate{false}; + mnview.ForEachNewCollateral([&](const uint256& key, CLazySerialize valueKey) { + const auto& value = valueKey.get(); + if (height > value.blockHeight) { + return true; + } + const auto& coin = coins.AccessCoin({key, 1}); + assert(!coin.IsSpent()); + CTxDestination pendingDest; + assert(ExtractDestination(coin.out.scriptPubKey, pendingDest)); + const CKeyID storedID = pendingDest.index() == PKHashType ? CKeyID(std::get(pendingDest)) : CKeyID(std::get(pendingDest)); + if (storedID == keyID) { + duplicate = true; + return false; + } + return true; + }); + if (duplicate) { + return Res::ErrCode(CustomTxErrCodes::Fatal, "Masternode exist with that owner address pending already"); + } + + mnview.UpdateMasternodeCollateral(obj.mnId, *node, tx.GetHash(), height); + } else if (item.first == static_cast(UpdateMasternodeType::OperatorAddress)) { + if (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(); } diff --git a/src/masternodes/consensus/masternodes.h b/src/masternodes/consensus/masternodes.h index 1665d04e28d..f3fba41d673 100644 --- a/src/masternodes/consensus/masternodes.h +++ b/src/masternodes/consensus/masternodes.h @@ -9,8 +9,6 @@ struct CCreateMasterNodeMessage; struct CResignMasterNodeMessage; -struct CSetForcedRewardAddressMessage; -struct CRemForcedRewardAddressMessage; struct CUpdateMasterNodeMessage; class CMasternodesConsensus : public CCustomTxVisitor { @@ -18,8 +16,6 @@ class CMasternodesConsensus : public CCustomTxVisitor { using CCustomTxVisitor::CCustomTxVisitor; Res operator()(const CCreateMasterNodeMessage& obj) const; Res operator()(const CResignMasterNodeMessage& obj) const; - Res operator()(const CSetForcedRewardAddressMessage& obj) const; - Res operator()(const CRemForcedRewardAddressMessage& obj) const; Res operator()(const CUpdateMasterNodeMessage& obj) const; }; diff --git a/src/masternodes/consensus/proposals.cpp b/src/masternodes/consensus/proposals.cpp index 3f7b3ff2042..cc06b06066a 100644 --- a/src/masternodes/consensus/proposals.cpp +++ b/src/masternodes/consensus/proposals.cpp @@ -64,7 +64,7 @@ Res CProposalsConsensus::operator()(const CPropVoteMessage& obj) const { if (!HasAuth(GetScriptForDestination(ownerDest))) return Res::Err("tx must have at least one input from the owner"); - if (!node->IsActive(height)) + if (!node->IsActive(height, mnview)) return Res::Err("masternode <%s> is not active", obj.masternodeId.GetHex()); if (node->mintedBlocks < 1) diff --git a/src/masternodes/customtx.cpp b/src/masternodes/customtx.cpp index 662683d509d..a2dc5da7029 100644 --- a/src/masternodes/customtx.cpp +++ b/src/masternodes/customtx.cpp @@ -13,8 +13,6 @@ 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: @@ -80,8 +78,6 @@ std::string ToString(CustomTxType type) { switch (type) { CustomTxTypeString(CreateMasternode); CustomTxTypeString(ResignMasternode); - CustomTxTypeString(SetForcedRewardAddress); - CustomTxTypeString(RemForcedRewardAddress); CustomTxTypeString(UpdateMasternode); CustomTxTypeString(CreateToken); CustomTxTypeString(UpdateToken); diff --git a/src/masternodes/customtx.h b/src/masternodes/customtx.h index 9b31a544857..d0725e72cce 100644 --- a/src/masternodes/customtx.h +++ b/src/masternodes/customtx.h @@ -26,8 +26,6 @@ enum struct CustomTxType : uint8_t CreateMasternode = 'C', ResignMasternode = 'R', UpdateMasternode = 'm', - SetForcedRewardAddress = 'F', - RemForcedRewardAddress = 'f', // tokens CreateToken = 'T', diff --git a/src/masternodes/masternodes.cpp b/src/masternodes/masternodes.cpp index e50a7fc0fb5..31e1cd0054a 100644 --- a/src/masternodes/masternodes.cpp +++ b/src/masternodes/masternodes.cpp @@ -93,11 +93,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; @@ -105,6 +105,16 @@ CMasternode::State CMasternode::GetState(int height) const return State::UNKNOWN; } + if (!collateralTx.IsNull()) { + auto idHeight = mnview.GetNewCollateral(collateralTx); + assert(idHeight); + if (height < idHeight->blockHeight) { + return State::TRANSFERRING; + } else if (height < idHeight->blockHeight + GetMnActivationDelay(idHeight->blockHeight)) { + return State::PRE_ENABLED; + } + } + if (resignHeight == -1 || height < resignHeight) { // enabled or pre-enabled // Special case for genesis block int activationDelay = height < EunosPayaHeight ? GetMnActivationDelay(height) : GetMnActivationDelay(creationHeight); @@ -124,9 +134,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; } @@ -144,6 +154,8 @@ std::string CMasternode::GetHumanReadableState(State state) return "PRE_RESIGNED"; case RESIGNED: return "RESIGNED"; + case TRANSFERRING: + return "TRANSFERRING"; default: return "UNKNOWN"; } @@ -171,7 +183,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 ); } @@ -300,14 +312,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()); @@ -316,96 +323,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) @@ -744,7 +740,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. @@ -799,9 +795,17 @@ bool CCustomCSView::CanSpend(const uint256 & txId, int height) const auto node = GetMasternode(txId); // check if it was mn collateral and mn was resigned or banned if (node) { - auto state = node->GetState(height); + auto state = node->GetState(height, *this); + return state == CMasternode::RESIGNED; + } + + if (auto mn = GetNewCollateral(txId)) { + auto node = GetMasternode(mn->masternodeID); + assert(node); + auto state = node->GetState(height, *this); return state == CMasternode::RESIGNED; } + // check if it was token collateral and token already destroyed /// @todo token check for total supply/limit when implemented auto pair = GetTokenByCreationTx(txId); diff --git a/src/masternodes/masternodes.h b/src/masternodes/masternodes.h index 8ecaab116ec..9891e2f2113 100644 --- a/src/masternodes/masternodes.h +++ b/src/masternodes/masternodes.h @@ -33,6 +33,7 @@ class ATTRIBUTES; class CBlockIndex; +class CMasternodesView; class CTransaction; // Works instead of constants cause 'regtest' differs (don't want to overcharge chainparams) @@ -44,6 +45,15 @@ CAmount GetTokenCreationFee(int height); CAmount GetMnCollateralAmount(int height); CAmount GetPropsCreationFee(int height, CPropType prop); +enum class UpdateMasternodeType : uint8_t +{ + None = 0x00, + OwnerAddress = 0x01, + OperatorAddress = 0x02, + SetRewardAddress = 0x03, + RemRewardAddress = 0x04 +}; + constexpr uint8_t SUBNODE_COUNT{4}; class CMasternode @@ -54,6 +64,7 @@ class CMasternode ENABLED, PRE_RESIGNED, RESIGNED, + TRANSFERRING, UNKNOWN // unreachable }; @@ -92,13 +103,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); @@ -119,7 +130,7 @@ class CMasternode READWRITE(version); READWRITE(resignTx); - READWRITE(banTx); + READWRITE(collateralTx); // Only available after FortCanning if (version > PRE_FORT_CANNING) { @@ -161,41 +172,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); } }; @@ -229,6 +214,20 @@ struct SubNodeBlockTimeKey } }; +struct MNNewOwnerHeightValue +{ + uint32_t blockHeight; + uint256 masternodeID; + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) { + READWRITE(blockHeight); + READWRITE(masternodeID); + } +}; + class CMasternodesView : public virtual CStorageView { public: @@ -248,11 +247,20 @@ class CMasternodesView : public virtual CStorageView std::set> GetOperatorsMulti() const; Res CreateMasternode(uint256 const & nodeId, CMasternode const & node, uint16_t timelock); - Res ResignMasternode(uint256 const & nodeId, uint256 const & txid, int height); + Res ResignMasternode(CMasternode& node, uint256 const & nodeId, uint256 const & txid, int height); Res 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); + // Masternode updates + void SetForcedRewardAddress(uint256 const & nodeId, CMasternode& node, const char rewardAddressType, CKeyID const & rewardAddress, int height); + void RemForcedRewardAddress(uint256 const & nodeId, CMasternode& node, int height); + void UpdateMasternodeOperator(uint256 const & nodeId, CMasternode& node, const char operatorType, const CKeyID& operatorAuthAddress, int height); + void UpdateMasternodeOwner(uint256 const & nodeId, CMasternode& node, const char ownerType, const CKeyID& ownerAuthAddress); + void UpdateMasternodeCollateral(uint256 const & nodeId, CMasternode& node, const uint256& newCollateralTx, const int height); + std::optional GetNewCollateral(const uint256& txid) const; + void ForEachNewCollateral(std::function)> callback); + // Get blocktimes for non-subnode and subnode with fork logic std::vector GetBlockTimes(const CKeyID& keyID, const uint32_t blockHeight, const int32_t creationHeight, const uint16_t timelock); @@ -274,6 +282,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'; } }; @@ -407,7 +416,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 f0db891b006..6b666611e68 100644 --- a/src/masternodes/mn_checks.cpp +++ b/src/masternodes/mn_checks.cpp @@ -36,8 +36,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{}; @@ -142,11 +140,6 @@ class CCustomMetadataParseVisitor template Res EnabledAfter() const { - if constexpr (IsOneOf()) - return Res::Err("tx is disabled for Fort Canning"); - else if constexpr (IsOneOf()) return IsHardforkEnabled(consensus.FortCanningRoadHeight); else - if constexpr (IsOneOf()) return IsHardforkEnabled(consensus.GreatWorldHeight); else @@ -426,7 +420,7 @@ void PopulateVaultHistoryData(CHistoryWriters* writers, const CCustomTxMessage& } } -Res ApplyCustomTx(CCustomCSView& mnview, CFutureSwapView& futureSwapView, 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, CFutureSwapView& futureSwapView, 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; @@ -453,6 +447,18 @@ Res ApplyCustomTx(CCustomCSView& mnview, CFutureSwapView& futureSwapView, const } res = CustomTxVisit(view, futureCopy, coins, tx, height, consensus, txMessage, time, txn); + if (canSpend && txType == CustomTxType::UpdateMasternode) { + auto obj = std::get(txMessage); + for (const auto item : obj.updates) { + if (item.first == static_cast(UpdateMasternodeType::OwnerAddress)) { + if (const auto node = mnview.GetMasternode(obj.mnId)) { + *canSpend = node->collateralTx.IsNull() ? obj.mnId : node->collateralTx; + } + break; + } + } + } + // Track burn fee if (txType == CustomTxType::CreateToken || txType == CustomTxType::CreateMasternode diff --git a/src/masternodes/mn_checks.h b/src/masternodes/mn_checks.h index 6b581c2235a..33f9cbee7ab 100644 --- a/src/masternodes/mn_checks.h +++ b/src/masternodes/mn_checks.h @@ -30,8 +30,6 @@ using CCustomTxMessage = std::variant< CCustomTxMessageNone, CCreateMasterNodeMessage, CResignMasterNodeMessage, - CSetForcedRewardAddressMessage, - CRemForcedRewardAddressMessage, CUpdateMasterNodeMessage, CCreateTokenMessage, CUpdateTokenPreAMKMessage, @@ -85,8 +83,8 @@ CCustomTxMessage customTypeToMessage(CustomTxType txType); bool IsMempooledCustomTxCreate(const CTxMemPool& pool, const uint256& txid) EXCLUSIVE_LOCKS_REQUIRED(cs_main); 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, CFutureSwapView& futureSwapView, const CCoinsViewCache& coins, const CTransaction& tx, const Consensus::Params& consensus, uint32_t height, uint64_t time = 0, uint32_t txn = 0, CHistoryWriters* writers = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_main); -Res CustomTxVisit(CCustomCSView& mnview, CFutureSwapView& futureSwapView, const CCoinsViewCache& coins, const CTransaction& tx, uint32_t height, const Consensus::Params& consensus, const CCustomTxMessage& txMessage, uint64_t time = 0, uint32_t txn = 0) EXCLUSIVE_LOCKS_REQUIRED(cs_main); +Res ApplyCustomTx(CCustomCSView& mnview, CFutureSwapView& futureSwapView, 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) EXCLUSIVE_LOCKS_REQUIRED(cs_main); +Res CustomTxVisit(CCustomCSView& mnview, CFutureSwapView &futureSwapView, const CCoinsViewCache& coins, const CTransaction& tx, uint32_t height, const Consensus::Params& consensus, const CCustomTxMessage& txMessage, uint64_t time = 0, uint32_t txn = 0) EXCLUSIVE_LOCKS_REQUIRED(cs_main); ResVal ApplyAnchorRewardTx(CCustomCSView& mnview, const CTransaction& tx, int height, const std::vector& metadata, const Consensus::Params& consensusParams) EXCLUSIVE_LOCKS_REQUIRED(cs_main); ResVal GetAggregatePrice(CCustomCSView& view, const std::string& token, const std::string& currency, uint64_t lastBlockTime); bool IsVaultPriceValid(CCustomCSView& mnview, const CVaultId& vaultId, uint32_t height); diff --git a/src/masternodes/rpc_customtx.cpp b/src/masternodes/rpc_customtx.cpp index eba7130c441..3997b4ea41b 100644 --- a/src/masternodes/rpc_customtx.cpp +++ b/src/masternodes/rpc_customtx.cpp @@ -83,24 +83,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 8d570ed2720..8034192cc01 100644 --- a/src/masternodes/rpc_masternodes.cpp +++ b/src/masternodes/rpc_masternodes.cpp @@ -8,7 +8,7 @@ UniValue mnToJSON(CImmutableCSView& view, uint256 const & nodeId, CMasternode co UniValue ret(UniValue::VOBJ); auto currentHeight = view.GetLastHeight(); 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(CImmutableCSView& view, uint256 const & nodeId, CMasternode co 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(CImmutableCSView& view, uint256 const & nodeId, CMasternode co uint16_t timelock = view.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 = view.GetBlockTimes(node.operatorAuthAddress, currentHeight + 1, node.creationHeight, timelock); @@ -193,212 +193,6 @@ UniValue createmasternode(const JSONRPCRequest& request) return signsend(rawTx, pwallet, optAuthTx)->GetHash().GetHex(); } -UniValue setforcedrewardaddress(const JSONRPCRequest& request) -{ - // Temporarily disabled for 2.2 - throw JSONRPCError(RPC_INVALID_REQUEST, - "reward address change is disabled for Fort Canning"); - - auto pwallet = GetWallet(request); - - RPCHelpMan{"setforcedrewardaddress", - "\nCreates (and submits to local node and network) a set forced reward address transaction with given masternode id and reward address\n" - "The last optional argument (may be empty array) is an array of specific UTXOs to spend." + - HelpRequiringPassphrase(pwallet) + "\n", - { - {"mn_id", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The Masternode's ID"}, - {"rewardAddress", RPCArg::Type::STR, RPCArg::Optional::NO, "Masternode`s new reward address (any P2PKH or P2WKH address)"}, - {"inputs", RPCArg::Type::ARR, RPCArg::Optional::OMITTED_NAMED_ARG, "A json array of json objects", - { - {"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "", - { - {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"}, - {"vout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The output number"}, - }, - }, - }, - }, - }, - RPCResult{ - "\"hash\" (string) The hex-encoded hash of broadcasted transaction\n" - }, - RPCExamples{ - HelpExampleCli("setforcedrewardaddress", "mn_id rewardAddress '[{\"txid\":\"id\",\"vout\":0}]'") - + HelpExampleRpc("setforcedrewardaddress", "mn_id rewardAddress '[{\"txid\":\"id\",\"vout\":0}]'") - }, - }.Check(request); - - if (pwallet->chain().isInitialBlockDownload()) { - throw JSONRPCError(RPC_CLIENT_IN_INITIAL_DOWNLOAD, "Cannot update Masternode while still in Initial Block Download"); - } - - pwallet->BlockUntilSyncedToCurrentChain(); - - RPCTypeCheck(request.params, { UniValue::VSTR, UniValue::VSTR, UniValue::VARR }, true); - - // decode and verify - std::string const nodeIdStr = request.params[0].getValStr(); - uint256 const nodeId = uint256S(nodeIdStr); - CTxDestination ownerDest; - int targetHeight; - { - CImmutableCSView view(*pcustomcsview); - - auto nodePtr = view.GetMasternode(nodeId); - if (!nodePtr) { - throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("The masternode %s does not exist", nodeIdStr)); - } - ownerDest = nodePtr->ownerType == PKHashType ? - CTxDestination(PKHash(nodePtr->ownerAuthAddress)) : - CTxDestination(WitnessV0KeyHash(nodePtr->ownerAuthAddress)); - - targetHeight = view.GetLastHeight() + 1; - } - - if (!::IsMine(*pwallet, ownerDest)) { - throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Masternode ownerAddress (%s) is not owned by the wallet", EncodeDestination(ownerDest))); - } - - std::string rewardAddress = request.params[1].getValStr(); - CTxDestination rewardDest = DecodeDestination(rewardAddress); - if (rewardDest.index() != PKHashType && rewardDest.index() != WitV0KeyHashType) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "rewardAddress (" + rewardAddress + ") does not refer to a P2PKH or P2WPKH address"); - } - - CKeyID const rewardAuthKey = rewardDest.index() == PKHashType ? - CKeyID(std::get(rewardDest)) : - CKeyID(std::get(rewardDest) - ); - - const auto txVersion = GetTransactionVersion(targetHeight); - CMutableTransaction rawTx(txVersion); - - CTransactionRef optAuthTx; - std::set auths{GetScriptForDestination(ownerDest)}; - rawTx.vin = GetAuthInputsSmart(pwallet, rawTx.nVersion, auths, false, optAuthTx, request.params[2]); - - // Return change to owner address - CCoinControl coinControl; - if (IsValidDestination(ownerDest)) { - coinControl.destChange = ownerDest; - } - - CSetForcedRewardAddressMessage msg{nodeId, static_cast(rewardDest.index()), rewardAuthKey}; - - CDataStream metadata(DfTxMarker, SER_NETWORK, PROTOCOL_VERSION); - metadata << static_cast(CustomTxType::SetForcedRewardAddress) - << msg; - - CScript scriptMeta; - scriptMeta << OP_RETURN << ToByteVector(metadata); - - rawTx.vout.push_back(CTxOut(0, scriptMeta)); - - fund(rawTx, pwallet, optAuthTx, &coinControl); - - // check execution - execTestTx(CTransaction(rawTx), targetHeight, optAuthTx); - - return signsend(rawTx, pwallet, optAuthTx)->GetHash().GetHex(); -} - -UniValue remforcedrewardaddress(const JSONRPCRequest& request) -{ - // Temporarily disabled for 2.2 - throw JSONRPCError(RPC_INVALID_REQUEST, - "reward address change is disabled for Fort Canning"); - - auto pwallet = GetWallet(request); - - RPCHelpMan{"remforcedrewardaddress", - "\nCreates (and submits to local node and network) a remove forced reward address transaction with given masternode id\n" - "The last optional argument (may be empty array) is an array of specific UTXOs to spend." + - HelpRequiringPassphrase(pwallet) + "\n", - { - {"mn_id", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The Masternode's ID"}, - {"inputs", RPCArg::Type::ARR, RPCArg::Optional::OMITTED_NAMED_ARG, "A json array of json objects", - { - {"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "", - { - {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"}, - {"vout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The output number"}, - }, - }, - }, - }, - }, - RPCResult{ - "\"hash\" (string) The hex-encoded hash of broadcasted transaction\n" - }, - RPCExamples{ - HelpExampleCli("remforcedrewardaddress", "mn_id '[{\"txid\":\"id\",\"vout\":0}]'") - + HelpExampleRpc("remforcedrewardaddress", "mn_id '[{\"txid\":\"id\",\"vout\":0}]'") - }, - }.Check(request); - - if (pwallet->chain().isInitialBlockDownload()) { - throw JSONRPCError(RPC_CLIENT_IN_INITIAL_DOWNLOAD, "Cannot update Masternode while still in Initial Block Download"); - } - - pwallet->BlockUntilSyncedToCurrentChain(); - - RPCTypeCheck(request.params, { UniValue::VSTR, UniValue::VARR }, true); - - // decode and verify - std::string const nodeIdStr = request.params[0].getValStr(); - uint256 const nodeId = uint256S(nodeIdStr); - CTxDestination ownerDest; - int targetHeight; - { - CImmutableCSView view(*pcustomcsview); - - auto nodePtr = view.GetMasternode(nodeId); - if (!nodePtr) { - throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("The masternode %s does not exist", nodeIdStr)); - } - ownerDest = nodePtr->ownerType == PKHashType ? - CTxDestination(PKHash(nodePtr->ownerAuthAddress)) : - CTxDestination(WitnessV0KeyHash(nodePtr->ownerAuthAddress)); - - targetHeight = view.GetLastHeight() + 1; - } - - if (!::IsMine(*pwallet, ownerDest)) { - throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Masternode ownerAddress (%s) is not owned by the wallet", EncodeDestination(ownerDest))); - } - - const auto txVersion = GetTransactionVersion(targetHeight); - CMutableTransaction rawTx(txVersion); - - CTransactionRef optAuthTx; - std::set auths{GetScriptForDestination(ownerDest)}; - rawTx.vin = GetAuthInputsSmart(pwallet, rawTx.nVersion, auths, false, optAuthTx, request.params[1]); - - // Return change to owner address - CCoinControl coinControl; - if (IsValidDestination(ownerDest)) { - coinControl.destChange = ownerDest; - } - - CRemForcedRewardAddressMessage msg{nodeId}; - - CDataStream metadata(DfTxMarker, SER_NETWORK, PROTOCOL_VERSION); - metadata << static_cast(CustomTxType::RemForcedRewardAddress) - << msg; - - CScript scriptMeta; - scriptMeta << OP_RETURN << ToByteVector(metadata); - - rawTx.vout.push_back(CTxOut(0, scriptMeta)); - - fund(rawTx, pwallet, optAuthTx, &coinControl); - - // check execution - execTestTx(CTransaction(rawTx), targetHeight, optAuthTx); - - return signsend(rawTx, pwallet, optAuthTx)->GetHash().GetHex(); -} - UniValue resignmasternode(const JSONRPCRequest& request) { auto pwallet = GetWallet(request); @@ -441,7 +235,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; { CImmutableCSView view(*pcustomcsview); @@ -454,7 +248,14 @@ UniValue resignmasternode(const JSONRPCRequest& request) CTxDestination(PKHash(nodePtr->ownerAuthAddress)) : CTxDestination(WitnessV0KeyHash(nodePtr->ownerAuthAddress)); - targetHeight = view.GetLastHeight() + 1; + 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; } const auto txVersion = GetTransactionVersion(targetHeight); @@ -462,6 +263,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 @@ -489,10 +293,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", @@ -501,7 +301,13 @@ UniValue updatemasternode(const JSONRPCRequest& request) HelpRequiringPassphrase(pwallet) + "\n", { {"mn_id", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The Masternode's ID"}, - {"operatorAddress", RPCArg::Type::STR, RPCArg::Optional::NO, "The new masternode operator auth address (P2PKH only, unique)"}, + {"values", RPCArg::Type::OBJ, RPCArg::Optional::NO, "", + { + {"ownerAddress", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "The new masternode owner address, requires masternode collateral fee (P2PKH or P2WPKH)"}, + {"operatorAddress", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "The new masternode operator address (P2PKH or P2WPKH)"}, + {"rewardAddress", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Masternode`s new reward address, empty \"\" to remove old reward address."}, + }, + }, {"inputs", RPCArg::Type::ARR, RPCArg::Optional::OMITTED_NAMED_ARG, "A json array of json objects", { {"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "", @@ -528,13 +334,10 @@ UniValue updatemasternode(const JSONRPCRequest& request) } pwallet->BlockUntilSyncedToCurrentChain(); - RPCTypeCheck(request.params, { UniValue::VSTR, UniValue::VSTR, UniValue::VARR }, true); - if (request.params[0].isNull() || request.params[1].isNull()) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameters, at least argument 2 must be non-null"); - } + RPCTypeCheck(request.params, { UniValue::VSTR, UniValue::VOBJ, UniValue::VARR }, true); std::string const nodeIdStr = request.params[0].getValStr(); - uint256 const nodeId = uint256S(nodeIdStr); + const uint256 nodeId = uint256S(nodeIdStr); CTxDestination ownerDest; int targetHeight; @@ -550,19 +353,40 @@ UniValue updatemasternode(const JSONRPCRequest& request) targetHeight = view.GetLastHeight() + 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 @@ -571,17 +395,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); @@ -980,15 +831,13 @@ static const CRPCCommand commands[] = // --------------- ---------------------- --------------------- ---------- {"masternodes", "createmasternode", &createmasternode, {"ownerAddress", "operatorAddress", "inputs"}}, {"masternodes", "resignmasternode", &resignmasternode, {"mn_id", "inputs"}}, - //{"masternodes", "updatemasternode", &updatemasternode, {"mn_id", "operatorAddress", "inputs"}}, + {"masternodes", "updatemasternode", &updatemasternode, {"mn_id", "values", "inputs"}}, {"masternodes", "listmasternodes", &listmasternodes, {"pagination", "verbose"}}, {"masternodes", "getmasternode", &getmasternode, {"mn_id"}}, {"masternodes", "getmasternodeblocks", &getmasternodeblocks, {"identifier", "depth"}}, {"masternodes", "getanchorteams", &getanchorteams, {"blockHeight"}}, {"masternodes", "getactivemasternodecount", &getactivemasternodecount, {"blockCount"}}, {"masternodes", "listanchors", &listanchors, {}}, - //{"masternodes", "setforcedrewardaddress", &setforcedrewardaddress, {"mn_id", "rewardAddress", "inputs"}}, - //{"masternodes", "remforcedrewardaddress", &remforcedrewardaddress, {"mn_id", "inputs"}}, }; void RegisterMasternodesRPCCommands(CRPCTable& tableRPC) { diff --git a/src/masternodes/rpc_proposals.cpp b/src/masternodes/rpc_proposals.cpp index 33d042373b9..15d2ecf339b 100644 --- a/src/masternodes/rpc_proposals.cpp +++ b/src/masternodes/rpc_proposals.cpp @@ -444,7 +444,7 @@ UniValue getproposal(const JSONRPCRequest& request) std::set activeMasternodes; view.ForEachMasternode([&](uint256 const & mnId, CMasternode node) { - if (node.IsActive(targetHeight) && node.mintedBlocks) { + if (node.IsActive(targetHeight, view) && node.mintedBlocks) { activeMasternodes.insert(mnId); } return true; diff --git a/src/miner.cpp b/src/miner.cpp index d0adae46364..f73ce90f8ec 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; } @@ -325,7 +326,8 @@ std::unique_ptr BlockAssembler::CreateNewBlock(const CScript& sc pblock->deprecatedHeight = pindexPrev->nHeight + 1; pblock->nBits = pos::GetNextWorkRequired(pindexPrev, pblock->nTime, consensus); if (myIDs) { - pblock->stakeModifier = pos::ComputeStakeModifier(pindexPrev->stakeModifier, myIDs->first); + const CKeyID key = nHeight >= consensus.GreatWorldHeight ? nodePtr->ownerAuthAddress : myIDs->first; + pblock->stakeModifier = pos::ComputeStakeModifier(pindexPrev->stakeModifier, key); } pblocktemplate->vTxSigOpsCost[0] = WITNESS_SCALE_FACTOR * GetLegacySigOpCount(*pblock->vtx[0]); @@ -741,6 +743,7 @@ namespace pos { int64_t blockHeight; std::vector subNodesBlockTime; uint16_t timelock; + std::optional nodePtr; { LOCK(cs_main); @@ -750,8 +753,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; } @@ -784,7 +787,8 @@ namespace pos { } auto nBits = pos::GetNextWorkRequired(tip, blockTime, chainparams.GetConsensus()); - auto stakeModifier = pos::ComputeStakeModifier(tip->stakeModifier, args.minterKey.GetPubKey().GetID()); + const CKeyID key = blockHeight >= chainparams.GetConsensus().GreatWorldHeight ? nodePtr->ownerAuthAddress : args.minterKey.GetPubKey().GetID(); + auto stakeModifier = pos::ComputeStakeModifier(tip->stakeModifier, key); // Set search time if null or last block has changed if (!nLastCoinStakeSearchTime || lastBlockSeen != tip->GetBlockHash()) { diff --git a/src/pos.cpp b/src/pos.cpp index 81cd0a856eb..a699862dbe7 100644 --- a/src/pos.cpp +++ b/src/pos.cpp @@ -18,11 +18,19 @@ bool CheckStakeModifier(const CBlockIndex* pindexPrev, const CBlockHeader& block if (blockHeader.hashPrevBlock.IsNull()) return blockHeader.stakeModifier.IsNull(); - /// @todo is it possible to pass minter key here, or we really need to extract it srom sig??? CKeyID key; if (!blockHeader.ExtractMinterKey(key)) { - LogPrintf("CheckStakeModifier: Can't extract minter key\n"); - return false; + return error("%s: Can't extract minter key\n", __func__); + } + + if (pindexPrev->nHeight + 1 >= Params().GetConsensus().GreatWorldHeight) { + auto nodeId = pcustomcsview->GetMasternodeIdByOperator(key); + if (!nodeId) { + return error("%s: No master operator found with minter key\n", __func__); + } + auto nodePtr = pcustomcsview->GetMasternode(*nodeId); + assert(nodePtr); + key = nodePtr->ownerAuthAddress; } return blockHeader.stakeModifier == pos::ComputeStakeModifier(pindexPrev->stakeModifier, key); @@ -70,7 +78,7 @@ bool ContextualCheckProofOfStake(const CBlockHeader& blockHeader, const Consensu } masternodeID = *optMasternodeID; auto nodePtr = mnView->GetMasternode(masternodeID); - if (!nodePtr || !nodePtr->IsActive(height)) { + if (!nodePtr || !nodePtr->IsActive(height, *mnView)) { return false; } creationHeight = int64_t(nodePtr->creationHeight); @@ -191,7 +199,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 a56af45ca13..abd2f75582f 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -176,8 +176,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "stop", 0, "wait" }, { "createmasternode", 2, "inputs" }, { "resignmasternode", 1, "inputs" }, - { "setforcedrewardaddress", 2, "inputs" }, - { "remforcedrewardaddress", 1, "inputs" }, + { "updatemasternode", 1, "values" }, { "updatemasternode", 2, "inputs" }, { "listmasternodes", 0, "pagination" }, { "listmasternodes", 1, "verbose" }, diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index 0d77f68ed11..0b981035fb0 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/applytx_tests.cpp b/src/test/applytx_tests.cpp index 52199c94c8c..6c2bda9bdb9 100644 --- a/src/test/applytx_tests.cpp +++ b/src/test/applytx_tests.cpp @@ -190,6 +190,7 @@ BOOST_AUTO_TEST_CASE(hardfork_guard) { consensus.EunosHeight, "called before Eunos height" }, { consensus.FortCanningHeight, "called before FortCanning height" }, { consensus.FortCanningHillHeight, "called before FortCanningHill height" }, + { consensus.GreatWorldHeight, "called before GreatWorld height" }, }; auto parseValidator = [&](int height, auto msg, std::string error = {}) -> bool { @@ -212,12 +213,6 @@ BOOST_AUTO_TEST_CASE(hardfork_guard) "CDataStream::read(): end of data: iostream error")); BOOST_REQUIRE(parseValidator(0, CResignMasterNodeMessage{}, "CDataStream::read(): end of data: iostream error")); - BOOST_REQUIRE(parseValidator(0, CSetForcedRewardAddressMessage{}, - "tx is disabled for Fort Canning")); - BOOST_REQUIRE(parseValidator(0, CRemForcedRewardAddressMessage{}, - "tx is disabled for Fort Canning")); - BOOST_REQUIRE(parseValidator(0, CUpdateMasterNodeMessage{}, - "tx is disabled for Fort Canning")); BOOST_REQUIRE(parseValidator(consensus.AMKHeight, CCreateTokenMessage{})); BOOST_REQUIRE(parseValidator(consensus.AMKHeight, CUpdateTokenPreAMKMessage{})); diff --git a/src/test/blockencodings_tests.cpp b/src/test/blockencodings_tests.cpp index 9c384c35633..365a7a3333e 100644 --- a/src/test/blockencodings_tests.cpp +++ b/src/test/blockencodings_tests.cpp @@ -53,7 +53,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 0498a688212..759a453082d 100644 --- a/src/test/pos_tests.cpp +++ b/src/test/pos_tests.cpp @@ -31,12 +31,12 @@ std::shared_ptr Block( const uint256& prev_hash, const uint64_t& height, return pblock; } -std::shared_ptr FinalizeBlock(std::shared_ptr pblock, const uint256& masternodeID, const CKey& minterKey, const uint256& prevStakeModifier) +std::shared_ptr FinalizeBlock(std::shared_ptr pblock, const 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); @@ -79,7 +79,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; @@ -96,20 +96,42 @@ 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)); + pcustomcsview->Flush(true); + + // Check stake modifier calculated on owner address after fork + auto blockTip = *::ChainActive().Tip(); + blockTip.nHeight = Params().GetConsensus().GreatWorldHeight; + std::shared_ptr newModifierBlock = FinalizeBlock( + Block(blockTip.GetBlockHash(), blockTip.nHeight, blockTip.mintedBlocks), + 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; @@ -124,9 +146,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())); @@ -138,7 +160,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; @@ -160,7 +182,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 9aee9717e34..7e0e6409749 100644 --- a/src/test/setup_common.cpp +++ b/src/test/setup_common.cpp @@ -212,7 +212,7 @@ TestChain100Setup::CreateAndProcessBlock(const std::vector& tip = ::ChainActive().Tip(); auto nodePtr = pcustomcsview->GetMasternode(masternodeID); - if (!nodePtr || !nodePtr->IsActive(tip->nHeight)) + if (!nodePtr || !nodePtr->IsActive(tip->nHeight, *pcustomcsview)) throw std::runtime_error(std::string(__func__) + ": nodePtr does not exist"); mintedBlocks = nodePtr->mintedBlocks; diff --git a/src/txdb.cpp b/src/txdb.cpp index 9aa12de8047..dd4075b6c97 100644 --- a/src/txdb.cpp +++ b/src/txdb.cpp @@ -281,10 +281,6 @@ bool CBlockTreeDB::LoadBlockIndexGuts(const Consensus::Params& consensusParams, return error("%s: The block index #%d (%s) wasn't saved on disk correctly. Index content: %s", __func__, pindexNew->nHeight, pindexNew->GetBlockHash().ToString(), pindexNew->ToString()); } } -// if (pindexNew->nHeight > 0 && pindexNew->stakeModifier != pos::ComputeStakeModifier(pindexNew->pprev->stakeModifier, pindexNew->minter)) { // TODO: SS disable check stake modifier -// return error("%s: The block index #%d (%s) wasn't saved on disk correctly. Stake modifier is incorrect (%s != %s). Index content: %s", -// __func__, pindexNew->nHeight, pindexNew->GetBlockHash().ToString(), pindexNew->stakeModifier.ToString(), pos::ComputeStakeModifier(pindexNew->pprev->stakeModifier, pindexNew->minter).ToString(), pindexNew->ToString()); -// } pcursor->Next(); } else { diff --git a/src/validation.cpp b/src/validation.cpp index 43b4d222431..4eb1b0bb85b 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -2330,7 +2330,7 @@ bool CChainState::ConnectBlock(const CBlock& block, CValidationState& state, CBl CCustomCSView mnviewCopy(mnview); CFutureSwapView futureCopy(futureSwapView); CHistoryWriters writers{paccountHistoryDB.get(), nullptr, nullptr}; - const auto res = ApplyCustomTx(mnviewCopy, futureCopy, view, tx, chainparams.GetConsensus(), pindex->nHeight, pindex->GetBlockTime(), i, &writers); + const auto res = ApplyCustomTx(mnviewCopy, futureCopy, view, tx, chainparams.GetConsensus(), pindex->nHeight, pindex->GetBlockTime(), nullptr, i, &writers); if (!res.ok) { return error("%s: Genesis block ApplyCustomTx failed. TX: %s Error: %s", __func__, tx.GetHash().ToString(), res.msg); @@ -2373,11 +2373,13 @@ 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)) { + const CKeyID key = pindex->nHeight >= chainparams.GetConsensus().GreatWorldHeight ? nodePtr->ownerAuthAddress : nodePtr->operatorAuthAddress; + const auto stakeModifier = pos::ComputeStakeModifier(stakeModifierPrevBlock, key); + if (block.stakeModifier != stakeModifier) { return state.Invalid( ValidationInvalidReason::CONSENSUS, error("%s: block's stake Modifier should be %d, got %d!", - __func__, block.stakeModifier.ToString(), pos::ComputeStakeModifier(stakeModifierPrevBlock, nodePtr->operatorAuthAddress).ToString()), + __func__, block.stakeModifier.ToString(), block.stakeModifier.ToString()), REJECT_INVALID, "bad-minted-blocks"); } @@ -2537,6 +2539,7 @@ bool CChainState::ConnectBlock(const CBlock& block, CValidationState& state, CBl if (!tx.IsCoinBase()) { + CAmount txfee = 0; if (!Consensus::CheckTxInputs(tx, state, view, accountsView, pindex->nHeight, txfee, chainparams)) { if (!IsBlockReason(state.GetReason())) { @@ -2545,14 +2548,14 @@ bool CChainState::ConnectBlock(const CBlock& block, CValidationState& state, CBl // defined for a block, so we reset the reason flag to // CONSENSUS here. state.Invalid(ValidationInvalidReason::CONSENSUS, false, - state.GetRejectCode(), state.GetRejectReason(), state.GetDebugMessage()); + state.GetRejectCode(), state.GetRejectReason(), state.GetDebugMessage()); } return error("%s: Consensus::CheckTxInputs: %s, %s", __func__, tx.GetHash().ToString(), FormatStateMessage(state)); } nFees += txfee; if (!MoneyRange(nFees)) { return state.Invalid(ValidationInvalidReason::CONSENSUS, error("%s: accumulated fee in the block out of range.", __func__), - REJECT_INVALID, "bad-txns-accumulated-fee-outofrange"); + REJECT_INVALID, "bad-txns-accumulated-fee-outofrange"); } // Check that transaction is BIP68 final @@ -2601,7 +2604,7 @@ bool CChainState::ConnectBlock(const CBlock& block, CValidationState& state, CBl CCustomCSView mnviewCopy(accountsView); CFutureSwapView futureCopy(futureSwapView); CHistoryWriters writers{paccountHistoryDB.get(), pburnHistoryDB.get(), pvaultHistoryDB.get()}; - const auto res = ApplyCustomTx(mnviewCopy, futureCopy, view, tx, chainparams.GetConsensus(), pindex->nHeight, pindex->GetBlockTime(), i, &writers); + const auto res = ApplyCustomTx(mnviewCopy, futureCopy, 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, @@ -2768,6 +2771,24 @@ bool CChainState::ConnectBlock(const CBlock& block, CValidationState& state, CBl // Loan splits ProcessTokenSplits(block, pindex, cache, futureSwapCache, chainparams); + if (pindex->nHeight >= chainparams.GetConsensus().GreatWorldHeight) { + // Apply any pending masternode owner changes + cache.ForEachNewCollateral([&](const uint256& key, const MNNewOwnerHeightValue& value){ + if (value.blockHeight == pindex->nHeight) { + auto node = cache.GetMasternode(value.masternodeID); + assert(node); + assert(key == node->collateralTx); + const auto& coin = view.AccessCoin({node->collateralTx, 1}); + assert(!coin.IsSpent()); + CTxDestination dest; + assert(ExtractDestination(coin.out.scriptPubKey, dest)); + const CKeyID keyId = dest.index() == PKHashType ? CKeyID(std::get(dest)) : CKeyID(std::get(dest)); + cache.UpdateMasternodeOwner(value.masternodeID, *node, dest.index(), keyId); + } + return true; + }); + } + // proposal activations ProcessProposalEvents(pindex, cache, chainparams); @@ -3606,7 +3627,7 @@ void CChainState::ProcessProposalEvents(const CBlockIndex* pindex, CCustomCSView if (activeMasternodes.empty()) { cache.ForEachMasternode([&](uint256 const & mnId, CMasternode node) { - if (node.IsActive(pindex->nHeight) && node.mintedBlocks) { + if (node.IsActive(pindex->nHeight, cache) && node.mintedBlocks) { activeMasternodes.insert(mnId); } return true; diff --git a/test/functional/feature_update_mn.py b/test/functional/feature_update_mn.py index 4badb8411c5..231a2a96363 100755 --- a/test/functional/feature_update_mn.py +++ b/test/functional/feature_update_mn.py @@ -19,9 +19,9 @@ class MasternodesRpcBasicTest (DefiTestFramework): def set_test_params(self): self.num_nodes = 3 self.setup_clean_chain = True - self.extra_args = [['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', '-dakotaheight=136', '-eunosheight=140', '-eunospayaheight=140', '-fortcanningheight=145'], - ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', '-dakotaheight=136', '-eunosheight=140', '-eunospayaheight=140', '-fortcanningheight=145'], - ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', '-dakotaheight=136', '-eunosheight=140', '-eunospayaheight=140', '-fortcanningheight=145']] + self.extra_args = [['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', '-dakotaheight=136', '-eunosheight=140', '-eunospayaheight=140', '-greatworldheight=145'], + ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', '-dakotaheight=136', '-eunosheight=140', '-eunospayaheight=140', '-greatworldheight=145'], + ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', '-dakotaheight=136', '-eunosheight=140', '-eunospayaheight=140', '-greatworldheight=145']] def run_test(self): assert_equal(len(self.nodes[0].listmasternodes()), 8) @@ -57,27 +57,7 @@ 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) + self.sync_all() # RESIGNING: #======================== diff --git a/test/functional/rpc_getcustomtx.py b/test/functional/rpc_getcustomtx.py index 111476f065e..109fc1041d9 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', '-greatworldheight=189'], # Wallet TXs + ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', '-dakotaheight=120', '-eunosheight=120', '-eunospayaheight=120', '-fortcanningheight=120', '-fortcanninghillheight=122', '-greatworldheight=189', '-txindex=1'], # Transaction index + ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', '-dakotaheight=120', '-eunosheight=120', '-fortcanningheight=120', '-fortcanninghillheight=122', '-greatworldheight=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,20 @@ 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) + 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 00000000000..a5f5471293f --- /dev/null +++ b/test/functional/rpc_updatemasternode.py @@ -0,0 +1,418 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The DeFi Blockchain developers +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. + +from test_framework.test_framework import DefiTestFramework +from test_framework.authproxy import JSONRPCException +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, +) +from decimal import Decimal + +class TestForcedRewardAddress(DefiTestFramework): + def set_test_params(self): + self.num_nodes = 2 + self.setup_clean_chain = True + self.extra_args = [ + ['-txindex=1', '-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-greatworldheight=110'], + ['-txindex=1', '-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-greatworldheight=110'], + ] + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + @staticmethod + def list_unspent_tx(node, address): + result = [] + vals = node.listunspent() + for i in range(0, len(vals)): + if vals[i]['address'] == address: + result.append(vals[i]) + return result + + @staticmethod + def unspent_amount(node, address): + result = 0 + vals = node.listunspent() + for i in range(0, len(vals)): + if vals[i]['address'] == address: + result += vals[i]['amount'] + return result + + def fund_tx(self, address, amount): + missing_auth_tx = self.nodes[0].sendtoaddress(address, amount) + count, missing_input_vout = 0, 0 + for vout in self.nodes[0].getrawtransaction(missing_auth_tx, 1)['vout']: + if vout['scriptPubKey']['addresses'][0] == address: + missing_input_vout = count + break + count += 1 + self.nodes[0].generate(1) + return missing_auth_tx, missing_input_vout + + def transfer_owner(self, mn_id): + # Get current collateral + result = self.nodes[0].getmasternode(mn_id)[mn_id] + if (result['collateralTx'] == "0000000000000000000000000000000000000000000000000000000000000000"): + collateral = mn_id + else: + collateral = result['collateralTx'] + owner = result['ownerAuthAddress'] + + # Create new owner address + new_owner = self.nodes[0].getnewaddress("", "legacy") + + # Test update of owner address + mn_transfer_tx = self.nodes[0].updatemasternode(mn_id, {'ownerAddress':new_owner}) + mn_transfer_rawtx = self.nodes[0].getrawtransaction(mn_transfer_tx, 1) + self.nodes[0].generate(1) + + # Make sure new collateral is present + assert_equal(mn_transfer_rawtx['vout'][1]['value'], Decimal('10.00000000')) + assert_equal(mn_transfer_rawtx['vout'][1]['scriptPubKey']['hex'], self.nodes[0].getaddressinfo(new_owner)['scriptPubKey']) + + # Test spending of new collateral + rawtx = self.nodes[0].createrawtransaction([{"txid":mn_transfer_tx, "vout":1}], [{new_owner:9.9999}]) + signed_rawtx = self.nodes[0].signrawtransactionwithwallet(rawtx) + assert_raises_rpc_error(-26, "tried to spend locked collateral for {}".format(mn_transfer_tx), self.nodes[0].sendrawtransaction, signed_rawtx['hex']) + + # Make sure old collateral is set as input + found = False + for vin in mn_transfer_rawtx['vin']: + if vin['txid'] == collateral and vin['vout'] == 1: + found = True + assert(found) + + # Check new state is TRANSFERRING and owner is still the same + result = self.nodes[0].getmasternode(mn_id)[mn_id] + assert_equal(result['state'], 'TRANSFERRING') + assert_equal(result['collateralTx'], mn_transfer_tx) + assert_equal(result['ownerAuthAddress'], owner) + + # Test update while masternode is in TRANSFERRING state + assert_raises_rpc_error(-32600, "Masternode {} is not in 'ENABLED' state".format(mn_id), self.nodes[0].updatemasternode, mn_id, {'ownerAddress':new_owner}) + + # Test PRE_ENABLED state and owner change + self.nodes[0].generate(10) + result = self.nodes[0].getmasternode(mn_id)[mn_id] + assert_equal(result['state'], 'PRE_ENABLED') + assert_equal(result['collateralTx'], mn_transfer_tx) + assert_equal(result['ownerAuthAddress'], new_owner) + + # Try another transfer during pre-enabled + assert_raises_rpc_error(-32600, "Masternode {} is not in 'ENABLED' state".format(mn_id), self.nodes[0].updatemasternode, mn_id, {'ownerAddress':new_owner}) + + # Test ENABLED state and owner change + self.nodes[0].generate(10) + result = self.nodes[0].getmasternode(mn_id)[mn_id] + assert_equal(result['state'], 'ENABLED') + assert_equal(result['collateralTx'], mn_transfer_tx) + assert_equal(result['ownerAuthAddress'], new_owner) + + def run_test(self): + self.nodes[0].generate(105) + self.sync_all([self.nodes[0], self.nodes[1]]) + + num_mns = len(self.nodes[0].listmasternodes()) + mn_owner = self.nodes[0].getnewaddress("", "legacy") + mn_owner2 = self.nodes[0].getnewaddress("", "legacy") + + mn_id = self.nodes[0].createmasternode(mn_owner) + mn_id2 = self.nodes[0].createmasternode(mn_owner2) + self.nodes[0].generate(1) + + assert_equal(len(self.nodes[0].listmasternodes()), num_mns + 2) + result = self.nodes[0].getmasternode(mn_id)[mn_id] + assert_equal(result['collateralTx'], "0000000000000000000000000000000000000000000000000000000000000000") + assert_equal(result['rewardAddress'], '') + assert_equal(result['ownerAuthAddress'], mn_owner) + assert_equal(result['operatorAuthAddress'], mn_owner) + + # Test call before for height + operator_address = self.nodes[0].getnewaddress("", "legacy") + assert_raises_rpc_error(-32600, "called before GreatWorld height".format(mn_id), self.nodes[0].updatemasternode, mn_id, {'operatorAddress':operator_address}) + + self.nodes[0].generate(4) + + # Test call before masternode active + operator_address = self.nodes[0].getnewaddress("", "legacy") + assert_raises_rpc_error(-32600, "Masternode {} is not in 'ENABLED' state".format(mn_id), self.nodes[0].updatemasternode, mn_id, {'operatorAddress':operator_address}) + + self.nodes[0].generate(5) + + assert_raises_rpc_error(-32600, "Masternode with that operator address already exists", self.nodes[0].updatemasternode, mn_id, {'operatorAddress':mn_owner}) + + # node 1 try to update node 0 which should be rejected. + assert_raises_rpc_error(-5, "Incorrect authorization for {}".format(mn_owner), self.nodes[1].updatemasternode, mn_id, {'operatorAddress':operator_address}) + + # Update operator address + self.nodes[0].updatemasternode(mn_id, {'operatorAddress':operator_address}) + + # Test updating another node to the same address + assert_raises_rpc_error(-26, "Masternode with that operator address already exists", self.nodes[0].updatemasternode, mn_id2, {'operatorAddress':operator_address}) + self.nodes[0].resignmasternode(mn_id2) + self.nodes[0].generate(1) + self.sync_all() + + assert_equal(self.nodes[1].listmasternodes()[mn_id]["operatorAuthAddress"], operator_address) + + # Set forced address + forced_reward_address = self.nodes[0].getnewaddress("", "legacy") + assert_raises_rpc_error(-8, + "The masternode {} does not exist".format("some_bad_mn_id"), + self.nodes[0].updatemasternode, "some_bad_mn_id", {'rewardAddress':forced_reward_address} + ) + assert_raises_rpc_error(-8, + "rewardAddress ({}) does not refer to a P2PKH or P2WPKH address".format("some_bad_address"), + self.nodes[0].updatemasternode, mn_id, {'rewardAddress':'some_bad_address'} + ) + + self.nodes[0].updatemasternode(mn_id, {'rewardAddress':forced_reward_address}) + self.nodes[0].generate(1) + + result = self.nodes[0].getmasternode(mn_id)[mn_id] + assert_equal(result['rewardAddress'], forced_reward_address) + assert_equal(result['ownerAuthAddress'], mn_owner) + assert_equal(result['operatorAuthAddress'], operator_address) + + self.nodes[0].updatemasternode(mn_id, {'rewardAddress':''}) + self.nodes[0].generate(1) + + result = self.nodes[0].getmasternode(mn_id)[mn_id] + assert_equal(result['rewardAddress'], '') + assert_equal(result['ownerAuthAddress'], mn_owner) + assert_equal(result['operatorAuthAddress'], operator_address) + + self.nodes[0].updatemasternode(mn_id, {'rewardAddress':forced_reward_address}) + self.nodes[0].generate(1) + + fra_amount = self.unspent_amount(self.nodes[0], forced_reward_address) + fra_unspent = self.list_unspent_tx(self.nodes[0], forced_reward_address) + assert_equal(len(fra_unspent), 0) + assert_equal(fra_amount, 0) + + self.stop_node(1) + self.restart_node(0, ['-gen', '-masternode_operator='+operator_address, '-txindex=1', '-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-greatworldheight=1']) + + # Mine blocks + self.nodes[0].generate(300) + + self.nodes[0].updatemasternode(mn_id, {'rewardAddress':''}) + self.nodes[0].generate(1) + + assert(len(self.list_unspent_tx(self.nodes[0], forced_reward_address)) > len(fra_unspent)) + assert(self.unspent_amount(self.nodes[0], forced_reward_address) > fra_amount) + + # CLI Reward address for test -rewardaddress + cli_reward_address = self.nodes[0].getnewaddress("", "legacy") + + self.restart_node(0, ['-gen', '-masternode_operator='+operator_address, '-rewardaddress='+cli_reward_address, '-txindex=1', '-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-greatworldheight=1']) + + cra_unspent = self.list_unspent_tx(self.nodes[0], cli_reward_address) + cra_amount = self.unspent_amount(self.nodes[0], cli_reward_address) + assert_equal(len(cra_unspent), 0) + assert_equal(cra_amount, 0) + + # Mine blocks + self.nodes[0].generate(400) + + assert(len(self.list_unspent_tx(self.nodes[0], cli_reward_address)) > len(fra_unspent)) + assert(self.unspent_amount(self.nodes[0], cli_reward_address) > fra_amount) + + # Test updating operator and reward address simultaniously + new_operator_address = self.nodes[0].getnewaddress("", "legacy") + new_reward_address = self.nodes[0].getnewaddress("", "legacy") + self.nodes[0].updatemasternode(mn_id, {'operatorAddress':new_operator_address,'rewardAddress':new_reward_address}) + self.nodes[0].generate(1) + + # Check results + result = self.nodes[0].getmasternode(mn_id)[mn_id] + assert_equal(result['rewardAddress'], new_reward_address) + assert_equal(result['ownerAuthAddress'], mn_owner) + assert_equal(result['operatorAuthAddress'], new_operator_address) + + # Test empty argument + try: + self.nodes[0].updatemasternode(mn_id, {}) + except JSONRPCException as e: + errorString = e.error['message'] + assert("No update arguments provided" in errorString) + + # Test unknown update type + while True: + address = self.nodes[0].getnewaddress("", "legacy") + unknown_tx = self.nodes[0].updatemasternode(mn_id, {'rewardAddress':address}) + unknown_rawtx = self.nodes[0].getrawtransaction(unknown_tx) + self.nodes[0].clearmempool() + if unknown_rawtx.count('01030114') == 1: + break + + updated_tx = unknown_rawtx.replace('01030114', '01ff0114') + self.nodes[0].signrawtransactionwithwallet(updated_tx) + + try: + self.nodes[0].sendrawtransaction(updated_tx) + except JSONRPCException as e: + errorString = e.error['message'] + assert("Unknown update type provided" in errorString) + + # Test incorrect owner address + assert_raises_rpc_error(-8, + "ownerAddress ({}) does not refer to a P2PKH or P2WPKH address".format("some_bad_address"), + self.nodes[0].updatemasternode, mn_id, {'ownerAddress':'some_bad_address'} + ) + + # Test update of owner address with existing address + assert_raises_rpc_error(-32600, "Masternode with that owner address already exists", self.nodes[0].updatemasternode, mn_id, {'ownerAddress':mn_owner}) + + # Set up input / output tests + not_collateral = self.nodes[0].getnewaddress("", "legacy") + owner_address = self.nodes[0].getnewaddress("", "legacy") + [not_collateral_tx, not_collateral_vout] = self.fund_tx(not_collateral, 10) + [missing_auth_tx, missing_input_vout] = self.fund_tx(mn_owner, 0.1) + [owner_auth_tx, owner_auth_vout] = self.fund_tx(owner_address, 0.1) + + # Get TX to use OP_RETURN output + missing_tx = self.nodes[0].updatemasternode(mn_id, {'ownerAddress':owner_address}) + missing_rawtx = self.nodes[0].getrawtransaction(missing_tx, 1) + self.nodes[0].clearmempool() + + # Test owner update without collateral input + rawtx = self.nodes[0].createrawtransaction([{"txid":missing_auth_tx, "vout":missing_input_vout},{"txid":not_collateral_tx, "vout":not_collateral_vout}], [{"data":missing_rawtx['vout'][0]['scriptPubKey']['hex'][4:]},{owner_address:10}]) + signed_rawtx = self.nodes[0].signrawtransactionwithwallet(rawtx) + assert_raises_rpc_error(-26, "Missing previous collateral from transaction inputs", self.nodes[0].sendrawtransaction, signed_rawtx['hex']) + + # Test incorrect new collateral amount + rawtx = self.nodes[0].createrawtransaction([{"txid":mn_id, "vout":1},{"txid":missing_auth_tx, "vout":missing_input_vout},{"txid":owner_auth_tx, "vout":owner_auth_vout}], [{"data":missing_rawtx['vout'][0]['scriptPubKey']['hex'][4:]},{owner_address:10.1}]) + signed_rawtx = self.nodes[0].signrawtransactionwithwallet(rawtx) + assert_raises_rpc_error(-26, "Incorrect collateral amount", self.nodes[0].sendrawtransaction, signed_rawtx['hex']) + + # Test missing new owner auth + rawtx = self.nodes[0].createrawtransaction([{"txid":mn_id, "vout":1},{"txid":missing_auth_tx, "vout":missing_input_vout}], [{"data":missing_rawtx['vout'][0]['scriptPubKey']['hex'][4:]},{owner_address:10}]) + signed_rawtx = self.nodes[0].signrawtransactionwithwallet(rawtx) + assert_raises_rpc_error(-26, "Missing auth input for new masternode owner", self.nodes[0].sendrawtransaction, signed_rawtx['hex']) + + # Test transfer of owner + self.transfer_owner(mn_id) + + # Test second transfer of MN owner + self.transfer_owner(mn_id) + + # Test resigning MN with transferred collateral + self.nodes[0].resignmasternode(mn_id) + self.nodes[0].generate(1) + result = self.nodes[0].getmasternode(mn_id)[mn_id] + assert_equal(result['state'], 'PRE_RESIGNED') + + # Roll back resignation + self.nodes[0].invalidateblock(self.nodes[0].getblockhash(self.nodes[0].getblockcount())) + result = self.nodes[0].getmasternode(mn_id)[mn_id] + assert_equal(result['state'], 'ENABLED') + + # Check MN resigned + self.nodes[0].generate(11) + result = self.nodes[0].getmasternode(mn_id)[mn_id] + assert_equal(result['state'], 'RESIGNED') + + # Test spending of transferred collateral after resignation + rawtx = self.nodes[0].createrawtransaction([{"txid":result['collateralTx'], "vout":1}], [{owner_address:9.9999}]) + signed_rawtx = self.nodes[0].signrawtransactionwithwallet(rawtx) + self.nodes[0].sendrawtransaction(signed_rawtx['hex']) + + # Set up for multiple MN owner transfer + mn1_owner = self.nodes[0].getnewaddress("", "legacy") + mn2_owner = self.nodes[0].getnewaddress("", "legacy") + mn3_owner = self.nodes[0].getnewaddress("", "legacy") + mn4_owner = self.nodes[0].getnewaddress("", "legacy") + mn5_owner = self.nodes[0].getnewaddress("", "legacy") + mn6_owner = self.nodes[0].getnewaddress("", "legacy") + + mn1 = self.nodes[0].createmasternode(mn1_owner) + 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 768ea180380..a038097d5d9 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -264,7 +264,7 @@ 'rpc_listvaulthistory.py', 'feature_logging.py', 'feature_loan_scheme.py', - #'feature_forced_reward_address.py', + 'rpc_updatemasternode.py', 'feature_loan_vault.py', 'feature_loan_deposittovault.py', 'feature_loan_interest.py',