diff --git a/ci/test_run_all.sh b/ci/test_run_all.sh index 618af9ff9f..b527771ba7 100755 --- a/ci/test_run_all.sh +++ b/ci/test_run_all.sh @@ -6,9 +6,14 @@ export LC_ALL=C.UTF-8 +# latest_stage.log will contain logs for a silenced stage if it +# fails. +trap "cat latest_stage.log" EXIT + set -o errexit; source ./ci/test/00_setup_env.sh set -o errexit; source ./ci/test/03_before_install.sh -set -o errexit; source ./ci/test/04_install.sh &> 04.log || (cat 04.log && exit 1) -set -o errexit; source ./ci/test/05_before_script.sh &> 05.log || (cat 05.log && exit 1) +set -o errexit; source ./ci/test/04_install.sh &> latest_stage.log +set -o errexit; source ./ci/test/05_before_script.sh &> latest_stage.log +echo -n > latest_stage.log set -o errexit; source ./ci/test/06_script_a.sh set -o errexit; source ./ci/test/06_script_b.sh diff --git a/configure.ac b/configure.ac index 57c58f678c..d1e2fcf553 100755 --- a/configure.ac +++ b/configure.ac @@ -3,9 +3,9 @@ AC_PREREQ([2.60]) define(_CLIENT_VERSION_MAJOR, 5) define(_CLIENT_VERSION_MINOR, 4) define(_CLIENT_VERSION_REVISION, 5) -define(_CLIENT_VERSION_BUILD, 4) +define(_CLIENT_VERSION_BUILD, 6) define(_CLIENT_VERSION_IS_RELEASE, false) -define(_COPYRIGHT_YEAR, 2023) +define(_COPYRIGHT_YEAR, 2024) define(_COPYRIGHT_HOLDERS,[The %s developers]) define(_COPYRIGHT_HOLDERS_SUBSTITUTION,[[Gridcoin]]) AC_INIT([Gridcoin],[_CLIENT_VERSION_MAJOR._CLIENT_VERSION_MINOR._CLIENT_VERSION_REVISION],[https://github.com/gridcoin/Gridcoin-Research/issues],[gridcoin],[https://gridcoin.us/]) @@ -664,7 +664,8 @@ if test "$use_hardening" != "no"; then case $host in *mingw*) - dnl stack-clash-protection doesn't currently work, and likely should just be skipped for Windows. + dnl stack-clash-protection doesn't compile with GCC 10 and earlier. + dnl In any case, it is a no-op for Windows. dnl See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=90458 for more details. ;; *) diff --git a/depends/packages/zlib.mk b/depends/packages/zlib.mk index 8a2432b8ff..913888ed39 100644 --- a/depends/packages/zlib.mk +++ b/depends/packages/zlib.mk @@ -1,8 +1,8 @@ package=zlib -$(package)_version=1.3 +$(package)_version=1.3.1 $(package)_download_path=https://www.zlib.net $(package)_file_name=$(package)-$($(package)_version).tar.gz -$(package)_sha256_hash=ff0ba4c292013dbc27530b3a81e1f9a813cd39de01ca5e0f8bf355702efa593e +$(package)_sha256_hash=9a93b2b7dfdac77ceba5a558a580e74667dd6fede4585b91eefb60f03b72df23 define $(package)_set_vars $(package)_config_opts= CC="$($(package)_cc)" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 012281546c..479d210bee 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -135,6 +135,7 @@ add_library(gridcoin_util STATIC gridcoin/scraper/scraper.cpp gridcoin/scraper/scraper_net.cpp gridcoin/scraper/scraper_registry.cpp + gridcoin/sidestake.cpp gridcoin/staking/difficulty.cpp gridcoin/staking/exceptions.cpp gridcoin/staking/kernel.cpp diff --git a/src/Makefile.am b/src/Makefile.am index 258897767f..c521bd11fe 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -129,6 +129,7 @@ GRIDCOIN_CORE_H = \ gridcoin/scraper/scraper.h \ gridcoin/scraper/scraper_net.h \ gridcoin/scraper/scraper_registry.h \ + gridcoin/sidestake.h \ gridcoin/staking/chain_trust.h \ gridcoin/staking/difficulty.h \ gridcoin/staking/exceptions.h \ @@ -258,6 +259,7 @@ GRIDCOIN_CORE_CPP = addrdb.cpp \ gridcoin/scraper/scraper.cpp \ gridcoin/scraper/scraper_net.cpp \ gridcoin/scraper/scraper_registry.cpp \ + gridcoin/sidestake.cpp \ gridcoin/staking/difficulty.cpp \ gridcoin/staking/exceptions.cpp \ gridcoin/staking/kernel.cpp \ diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index f6fd404e71..6e67219bec 100755 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -82,6 +82,7 @@ QT_FORMS_UI = \ qt/forms/consolidateunspentwizardsendpage.ui \ qt/forms/diagnosticsdialog.ui \ qt/forms/editaddressdialog.ui \ + qt/forms/editsidestakedialog.ui \ qt/forms/favoritespage.ui \ qt/forms/intro.ui \ qt/forms/mrcrequestpage.ui \ @@ -143,6 +144,7 @@ QT_MOC_CPP = \ qt/moc_csvmodelwriter.cpp \ qt/moc_diagnosticsdialog.cpp \ qt/moc_editaddressdialog.cpp \ + qt/moc_editsidestakedialog.cpp \ qt/moc_favoritespage.cpp \ qt/moc_guiutil.cpp \ qt/moc_intro.cpp \ @@ -161,6 +163,7 @@ QT_MOC_CPP = \ qt/moc_rpcconsole.cpp \ qt/moc_sendcoinsdialog.cpp \ qt/moc_sendcoinsentry.cpp \ + qt/moc_sidestaketablemodel.cpp \ qt/moc_signverifymessagedialog.cpp \ qt/moc_trafficgraphwidget.cpp \ qt/moc_transactiondesc.cpp \ @@ -249,6 +252,7 @@ GRIDCOINRESEARCH_QT_H = \ qt/decoration.h \ qt/diagnosticsdialog.h \ qt/editaddressdialog.h \ + qt/editsidestakedialog.h \ qt/favoritespage.h \ qt/guiconstants.h \ qt/guiutil.h \ @@ -285,6 +289,7 @@ GRIDCOINRESEARCH_QT_H = \ qt/rpcconsole.h \ qt/sendcoinsdialog.h \ qt/sendcoinsentry.h \ + qt/sidestaketablemodel.h \ qt/signverifymessagedialog.h \ qt/trafficgraphwidget.h \ qt/transactiondesc.h \ @@ -340,6 +345,7 @@ GRIDCOINRESEARCH_QT_CPP = \ qt/decoration.cpp \ qt/diagnosticsdialog.cpp \ qt/editaddressdialog.cpp \ + qt/editsidestakedialog.cpp \ qt/favoritespage.cpp \ qt/guiutil.cpp \ qt/intro.cpp \ @@ -372,6 +378,7 @@ GRIDCOINRESEARCH_QT_CPP = \ qt/rpcconsole.cpp \ qt/sendcoinsdialog.cpp \ qt/sendcoinsentry.cpp \ + qt/sidestaketablemodel.cpp \ qt/signverifymessagedialog.cpp \ qt/trafficgraphwidget.cpp \ qt/transactiondesc.cpp \ diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 6c4da11b4e..53d41ea1a5 100755 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -56,6 +56,7 @@ GRIDCOIN_TESTS =\ test/gridcoin/protocol_tests.cpp \ test/gridcoin/researcher_tests.cpp \ test/gridcoin/scraper_registry_tests.cpp \ + test/gridcoin/sidestake_tests.cpp \ test/gridcoin/superblock_tests.cpp \ test/key_tests.cpp \ test/merkle_tests.cpp \ diff --git a/src/chainparams.cpp b/src/chainparams.cpp index 257bf80803..fa9e268052 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -74,6 +74,8 @@ class CMainParams : public CChainParams { consensus.InitialMRCFeeFractionPostZeroInterval = Fraction(2, 5); // Zero day interval is 14 days on mainnet consensus.MRCZeroPaymentInterval = 14 * 24 * 60 * 60; + // The maximum ratio of rewards that can be allocated to all of the mandatory sidestakes. + consensus.MaxMandatorySideStakeTotalAlloc = Fraction(1, 4); // The "standard" contract replay lookback for those contract types // that do not have a registry db. consensus.StandardContractReplayLookback = 180 * 24 * 60 * 60; @@ -187,6 +189,8 @@ class CTestNetParams : public CChainParams { consensus.InitialMRCFeeFractionPostZeroInterval = Fraction(2, 5); // Zero day interval is 10 minutes on testnet. The very short interval facilitates testing. consensus.MRCZeroPaymentInterval = 10 * 60; + // The maximum ratio of rewards that can be allocated to all of the mandatory sidestakes. + consensus.MaxMandatorySideStakeTotalAlloc = Fraction(1, 4); // The "standard" contract replay lookback for those contract types // that do not have a registry db. consensus.StandardContractReplayLookback = 180 * 24 * 60 * 60; diff --git a/src/consensus/params.h b/src/consensus/params.h index 83325378c7..b4e5e1a82e 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -47,6 +47,10 @@ struct Params { * forfeiture of fees to the staker and/or foundation. Only consensus critical at BlockV12Height or above. */ int64_t MRCZeroPaymentInterval; + /** + * @brief The maximum allocation (as a Fraction) that can be used by all of the mandatory sidestakes + */ + Fraction MaxMandatorySideStakeTotalAlloc; int64_t StandardContractReplayLookback; diff --git a/src/gridcoin/beacon.cpp b/src/gridcoin/beacon.cpp index debfa35a08..d0e70943bb 100644 --- a/src/gridcoin/beacon.cpp +++ b/src/gridcoin/beacon.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2023 The Gridcoin developers +// Copyright (c) 2014-2024 The Gridcoin developers // Distributed under the MIT/X11 software license, see the accompanying // file COPYING or https://opensource.org/licenses/mit-license.php. diff --git a/src/gridcoin/beacon.h b/src/gridcoin/beacon.h index 17f3dd69f5..df941b8ad4 100644 --- a/src/gridcoin/beacon.h +++ b/src/gridcoin/beacon.h @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2023 The Gridcoin developers +// Copyright (c) 2014-2024 The Gridcoin developers // Distributed under the MIT/X11 software license, see the accompanying // file COPYING or https://opensource.org/licenses/mit-license.php. diff --git a/src/gridcoin/contract/contract.cpp b/src/gridcoin/contract/contract.cpp index bb6b2a4b36..8630b5f532 100644 --- a/src/gridcoin/contract/contract.cpp +++ b/src/gridcoin/contract/contract.cpp @@ -16,6 +16,7 @@ #include "gridcoin/project.h" #include "gridcoin/researcher.h" #include "gridcoin/scraper/scraper_registry.h" +#include "gridcoin/sidestake.h" #include "gridcoin/support/block_finder.h" #include "gridcoin/support/xml.h" #include "gridcoin/tx_message.h" @@ -276,6 +277,7 @@ class Dispatcher case ContractType::SCRAPER: return GetScraperRegistry(); case ContractType::VOTE: return GetPollRegistry(); case ContractType::MRC: return m_mrc_contract_handler; + case ContractType::SIDESTAKE: return GetSideStakeRegistry(); default: return m_unknown_handler; } } @@ -677,12 +679,13 @@ bool Contract::RequiresMasterKey() const // beacons by signing them with the original private key: return m_version == 1 && m_action == ContractAction::REMOVE; - case ContractType::POLL: return m_action == ContractAction::REMOVE; - case ContractType::PROJECT: return true; - case ContractType::PROTOCOL: return true; - case ContractType::SCRAPER: return true; - case ContractType::VOTE: return m_action == ContractAction::REMOVE; - default: return false; + case ContractType::POLL: return m_action == ContractAction::REMOVE; + case ContractType::PROJECT: return true; + case ContractType::PROTOCOL: return true; + case ContractType::SCRAPER: return true; + case ContractType::VOTE: return m_action == ContractAction::REMOVE; + case ContractType::SIDESTAKE: return true; + default: return false; } } @@ -693,10 +696,23 @@ CAmount Contract::RequiredBurnAmount() const bool Contract::WellFormed() const { - return m_version > 0 && m_version <= Contract::CURRENT_VERSION - && m_type != ContractType::UNKNOWN - && m_action != ContractAction::UNKNOWN - && m_body.WellFormed(m_action.Value()); + bool result = m_version > 0 && m_version <= Contract::CURRENT_VERSION + && m_type != ContractType::UNKNOWN + && m_action != ContractAction::UNKNOWN + && m_body.WellFormed(m_action.Value()); + + if (!result) { + LogPrint(BCLog::LogFlags::CONTRACT, "WARN: %s: Contract was not well formed. m_version = %u, m_type = %s, " + "m_action = %s, m_body.Wellformed(m_action.Value()) = %u", + __func__, + m_version, + m_type.ToString(), + m_action.ToString(), + m_body.WellFormed(m_action.Value()) + ); + } + + return result; } ContractPayload Contract::SharePayload() const @@ -761,6 +777,7 @@ Contract::Type Contract::Type::Parse(std::string input) if (input == "scraper") return ContractType::SCRAPER; if (input == "protocol") return ContractType::PROTOCOL; if (input == "message") return ContractType::MESSAGE; + if (input == "sidestake") return ContractType::SIDESTAKE; return ContractType::UNKNOWN; } @@ -777,6 +794,7 @@ std::string Contract::Type::ToString() const case ContractType::PROTOCOL: return "protocol"; case ContractType::SCRAPER: return "scraper"; case ContractType::VOTE: return "vote"; + case ContractType::SIDESTAKE: return "sidestake"; default: return ""; } } @@ -793,6 +811,7 @@ std::string Contract::Type::ToString(ContractType contract_type) case ContractType::PROTOCOL: return "protocol"; case ContractType::SCRAPER: return "scraper"; case ContractType::VOTE: return "vote"; + case ContractType::SIDESTAKE: return "sidestake"; default: return ""; } } @@ -809,6 +828,7 @@ std::string Contract::Type::ToTranslatedString(ContractType contract_type) case ContractType::PROTOCOL: return _("protocol"); case ContractType::SCRAPER: return _("scraper"); case ContractType::VOTE: return _("vote"); + case ContractType::SIDESTAKE: return _("sidestake"); default: return ""; } } @@ -905,6 +925,9 @@ ContractPayload Contract::Body::ConvertFromLegacy(const ContractType type, uint3 case ContractType::VOTE: return ContractPayload::Make( LegacyVote::Parse(legacy.m_key, legacy.m_value)); + case ContractType::SIDESTAKE: + // Sidestakes have no legacy representation as a contract. + assert(false && "Attempted to convert non-existent legacy sidestake contract."); case ContractType::OUT_OF_BOUND: assert(false); } @@ -961,6 +984,9 @@ void Contract::Body::ResetType(const ContractType type) case ContractType::VOTE: m_payload.Reset(new Vote()); break; + case ContractType::SIDESTAKE: + m_payload.Reset(new SideStakePayload()); + break; case ContractType::OUT_OF_BOUND: assert(false); } diff --git a/src/gridcoin/contract/message.cpp b/src/gridcoin/contract/message.cpp index 7309afeb68..a08ff23e4f 100644 --- a/src/gridcoin/contract/message.cpp +++ b/src/gridcoin/contract/message.cpp @@ -5,6 +5,7 @@ #include "amount.h" #include "gridcoin/contract/message.h" #include "gridcoin/contract/contract.h" +#include "gridcoin/sidestake.h" #include "script.h" #include "wallet/wallet.h" @@ -143,16 +144,29 @@ std::string SendContractTx(CWalletTx& wtx_new) if (balance < COIN || balance < burn_fee + nTransactionFee) { std::string strError = _("Balance too low to create a contract."); - LogPrintf("%s: %s", __func__, strError); + error("%s: %s", __func__, strError); return strError; } if (!CreateContractTx(wtx_new, reserve_key, burn_fee)) { std::string strError = _("Error: Transaction creation failed."); - LogPrintf("%s: %s", __func__, strError); + error("%s: %s", __func__, strError); return strError; } + for (const auto& pool_tx : mempool.mapTx) { + for (const auto& pool_tx_contract : pool_tx.second.GetContracts()) { + if (pool_tx_contract.m_type == GRC::ContractType::SIDESTAKE) { + std::string strError = _( + "Error: The mandatory sidestake transaction was rejected. " + "There is already a mandatory sidestake transaction in the mempool. " + "Wait until that transaction is bound in a block."); + error("%s: %s", __func__, strError); + return strError; + } + } + } + if (!pwalletMain->CommitTransaction(wtx_new, reserve_key)) { std::string strError = _( "Error: The transaction was rejected. This might happen if some of " @@ -160,7 +174,7 @@ std::string SendContractTx(CWalletTx& wtx_new) "a copy of wallet.dat and coins were spent in the copy but not " "marked as spent here."); - LogPrintf("%s: %s", __func__, strError); + error("%s: %s", __func__, strError); return strError; } diff --git a/src/gridcoin/contract/payload.h b/src/gridcoin/contract/payload.h index ed48ef0240..be608b5e67 100644 --- a/src/gridcoin/contract/payload.h +++ b/src/gridcoin/contract/payload.h @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2023 The Gridcoin developers +// Copyright (c) 2014-2024 The Gridcoin developers // Distributed under the MIT/X11 software license, see the accompanying // file COPYING or https://opensource.org/licenses/mit-license.php. @@ -65,6 +65,7 @@ enum class ContractType SCRAPER, //!< Scraper node authorization grants and revocations. VOTE, //!< A vote cast by a wallet for a poll. MRC, //!< A manual rewards claim (MRC) request to pay rewards + SIDESTAKE, //!< Mandatory sidestakes OUT_OF_BOUND, //!< Marker value for the end of the valid range. }; @@ -82,6 +83,7 @@ static constexpr GRC::ContractType CONTRACT_TYPES[] = { ContractType::SCRAPER, ContractType::VOTE, ContractType::MRC, + ContractType::SIDESTAKE, ContractType::OUT_OF_BOUND }; diff --git a/src/gridcoin/contract/registry.cpp b/src/gridcoin/contract/registry.cpp index cb7e85e5b5..65d90f8652 100644 --- a/src/gridcoin/contract/registry.cpp +++ b/src/gridcoin/contract/registry.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2023 The Gridcoin developers +// Copyright (c) 2014-2024 The Gridcoin developers // Distributed under the MIT/X11 software license, see the accompanying // file COPYING or https://opensource.org/licenses/mit-license.php. @@ -11,6 +11,7 @@ const std::vector RegistryBookmarks::CONTRACT_TYPES_WITH_REG_ ContractType::PROJECT, ContractType::PROTOCOL, ContractType::SCRAPER, + ContractType::SIDESTAKE }; const std::vector RegistryBookmarks::CONTRACT_TYPES_SUPPORTING_REVERT = { @@ -20,6 +21,7 @@ const std::vector RegistryBookmarks::CONTRACT_TYPES_SUPPORTIN ContractType::PROTOCOL, ContractType::SCRAPER, ContractType::VOTE, + ContractType::SIDESTAKE }; } // namespace GRC diff --git a/src/gridcoin/contract/registry.h b/src/gridcoin/contract/registry.h index b92b840a5f..3896864316 100644 --- a/src/gridcoin/contract/registry.h +++ b/src/gridcoin/contract/registry.h @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2023 The Gridcoin developers +// Copyright (c) 2014-2024 The Gridcoin developers // Distributed under the MIT/X11 software license, see the accompanying // file COPYING or https://opensource.org/licenses/mit-license.php. @@ -9,6 +9,7 @@ #include "gridcoin/beacon.h" #include "gridcoin/project.h" #include "gridcoin/protocol.h" +#include "gridcoin/sidestake.h" #include "gridcoin/scraper/scraper_registry.h" #include "gridcoin/voting/registry.h" @@ -50,6 +51,7 @@ class RegistryBookmarks case ContractType::PROJECT: return GetWhitelist(); case ContractType::PROTOCOL: return GetProtocolRegistry(); case ContractType::SCRAPER: return GetScraperRegistry(); + case ContractType::SIDESTAKE: return GetSideStakeRegistry(); case ContractType::UNKNOWN: [[fallthrough]]; case ContractType::CLAIM: @@ -78,6 +80,7 @@ class RegistryBookmarks case ContractType::PROTOCOL: return GetProtocolRegistry(); case ContractType::SCRAPER: return GetScraperRegistry(); case ContractType::VOTE: return GetPollRegistry(); + case ContractType::SIDESTAKE: return GetSideStakeRegistry(); [[fallthrough]]; case ContractType::UNKNOWN: [[fallthrough]]; @@ -154,8 +157,21 @@ class RegistryBookmarks int lowest_height = std::numeric_limits::max(); for (const auto& iter : m_db_heights) { + int db_height = iter.second; + + //! When below the operational range of the sidestake contracts and registry, initialization of the sidestake + //! registry will report zero for height. It is undesirable to return this in the GetLowestRegistryBlockHeight() + //! method, because it will cause the contract replay clamp to go to the Fern mandatory blockheight. Setting + //! the db_height recorded in the bookmarks at V13 height for the sidestake registry for the purpose of contract + //! replay solves the problem. + //! + //! This code can be removed after the V13 mandatory blockheight has been reached. + if (iter.first == GRC::ContractType::SIDESTAKE and db_height < Params().GetConsensus().BlockV13Height) { + db_height = Params().GetConsensus().BlockV13Height; + } + if (iter.second < lowest_height) { - lowest_height = iter.second; + lowest_height = db_height; } } diff --git a/src/gridcoin/contract/registry_db.h b/src/gridcoin/contract/registry_db.h index 30694edfe2..826aa69bdf 100644 --- a/src/gridcoin/contract/registry_db.h +++ b/src/gridcoin/contract/registry_db.h @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2023 The Gridcoin developers +// Copyright (c) 2014-2024 The Gridcoin developers // Distributed under the MIT/X11 software license, see the accompanying // file COPYING or https://opensource.org/licenses/mit-license.php. diff --git a/src/gridcoin/gridcoin.cpp b/src/gridcoin/gridcoin.cpp index fb713be0d2..5ef78ce340 100644 --- a/src/gridcoin/gridcoin.cpp +++ b/src/gridcoin/gridcoin.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2023 The Gridcoin developers +// Copyright (c) 2014-2024 The Gridcoin developers // Distributed under the MIT/X11 software license, see the accompanying // file COPYING or https://opensource.org/licenses/mit-license.php. diff --git a/src/gridcoin/project.cpp b/src/gridcoin/project.cpp index 45d6321d3a..3c92fd8792 100644 --- a/src/gridcoin/project.cpp +++ b/src/gridcoin/project.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2023 The Gridcoin developers +// Copyright (c) 2014-2024 The Gridcoin developers // Distributed under the MIT/X11 software license, see the accompanying // file COPYING or https://opensource.org/licenses/mit-license.php. diff --git a/src/gridcoin/project.h b/src/gridcoin/project.h index 54c509c60a..c397e71bb9 100644 --- a/src/gridcoin/project.h +++ b/src/gridcoin/project.h @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2023 The Gridcoin developers +// Copyright (c) 2014-2024 The Gridcoin developers // Distributed under the MIT/X11 software license, see the accompanying // file COPYING or https://opensource.org/licenses/mit-license.php. diff --git a/src/gridcoin/protocol.cpp b/src/gridcoin/protocol.cpp index 793dd207b3..4281f01a15 100644 --- a/src/gridcoin/protocol.cpp +++ b/src/gridcoin/protocol.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2023 The Gridcoin developers +// Copyright (c) 2014-2024 The Gridcoin developers // Distributed under the MIT/X11 software license, see the accompanying // file COPYING or https://opensource.org/licenses/mit-license.php. diff --git a/src/gridcoin/protocol.h b/src/gridcoin/protocol.h index d1b93150a6..3e409634e3 100644 --- a/src/gridcoin/protocol.h +++ b/src/gridcoin/protocol.h @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2023 The Gridcoin developers +// Copyright (c) 2014-2024 The Gridcoin developers // Distributed under the MIT/X11 software license, see the accompanying // file COPYING or https://opensource.org/licenses/mit-license.php. diff --git a/src/gridcoin/sidestake.cpp b/src/gridcoin/sidestake.cpp new file mode 100644 index 0000000000..938d906c47 --- /dev/null +++ b/src/gridcoin/sidestake.cpp @@ -0,0 +1,1011 @@ +// Copyright (c) 2014-2024 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or https://opensource.org/licenses/mit-license.php. + +#include "sidestake.h" +#include "node/ui_interface.h" +#include "univalue.h" + +//! +//! \brief Model callback bound to the \c RwSettingsUpdated core signal. +//! +void RwSettingsUpdated(GRC::SideStakeRegistry* registry) +{ + LogPrint(BCLog::LogFlags::MISC, "INFO: %s: received RwSettingsUpdated() core signal", __func__); + + registry->LoadLocalSideStakesFromConfig(); +} + + +using namespace GRC; +using LogFlags = BCLog::LogFlags; + +namespace { +SideStakeRegistry g_sidestake_entries; +} // anonymous namespace + +// ----------------------------------------------------------------------------- +// Global Functions +// ----------------------------------------------------------------------------- + +SideStakeRegistry& GRC::GetSideStakeRegistry() +{ + return g_sidestake_entries; +} + +// ----------------------------------------------------------------------------- +// Class: Allocation +// ----------------------------------------------------------------------------- +Allocation::Allocation() + : Fraction() +{} + +Allocation::Allocation(const double& allocation) + : Fraction(static_cast(std::round(allocation * static_cast(10000.0))), static_cast(10000), true) +{} + +Allocation::Allocation(const Fraction& f) + : Fraction(f) +{} + +CAmount Allocation::ToCAmount() const +{ + return GetNumerator() / GetDenominator(); +} + +double Allocation::ToPercent() const +{ + return ToDouble() * 100.0; +} + +// ----------------------------------------------------------------------------- +// Class: LocalSideStake +// ----------------------------------------------------------------------------- +LocalSideStake::LocalSideStake() + : m_destination() + , m_allocation() + , m_description() + , m_status(LocalSideStakeStatus::UNKNOWN) +{} + +LocalSideStake::LocalSideStake(CTxDestination destination, Allocation allocation, std::string description) + : m_destination(destination) + , m_allocation(allocation) + , m_description(description) + , m_status(LocalSideStakeStatus::UNKNOWN) +{} + +LocalSideStake::LocalSideStake(CTxDestination destination, + Allocation allocation, + std::string description, + LocalSideStakeStatus status) + : m_destination(destination) + , m_allocation(allocation) + , m_description(description) + , m_status(status) +{} + +bool LocalSideStake::WellFormed() const +{ + return CBitcoinAddress(m_destination).IsValid() && m_allocation >= 0 && m_allocation <= 1; +} + +std::string LocalSideStake::StatusToString() const +{ + return StatusToString(m_status.Value()); +} + +std::string LocalSideStake::StatusToString(const LocalSideStakeStatus& status, const bool& translated) const +{ + if (translated) { + switch(status) { + case LocalSideStakeStatus::UNKNOWN: return _("Unknown"); + case LocalSideStakeStatus::ACTIVE: return _("Active"); + case LocalSideStakeStatus::INACTIVE: return _("Inactive"); + case LocalSideStakeStatus::OUT_OF_BOUND: break; + } + + assert(false); // Suppress warning + } else { + // The untranslated versions are really meant to serve as the string equivalent of the enum values. + switch(status) { + case LocalSideStakeStatus::UNKNOWN: return "Unknown"; + case LocalSideStakeStatus::ACTIVE: return "Active"; + case LocalSideStakeStatus::INACTIVE: return "Inactive"; + case LocalSideStakeStatus::OUT_OF_BOUND: break; + } + + assert(false); // Suppress warning + } + + // This will never be reached. Put it in anyway to prevent control reaches end of non-void function warning + // from some compiler versions. + return std::string{}; +} + +bool LocalSideStake::operator==(LocalSideStake b) +{ + bool result = true; + + result &= (m_destination == b.m_destination); + result &= (m_allocation == b.m_allocation); + result &= (m_description == b.m_description); + result &= (m_status == b.m_status); + + return result; +} + +bool LocalSideStake::operator!=(LocalSideStake b) +{ + return !(*this == b); +} + +// ----------------------------------------------------------------------------- +// Class: MandatorySideStake +// ----------------------------------------------------------------------------- +MandatorySideStake::MandatorySideStake() + : m_destination() + , m_allocation() + , m_description() + , m_timestamp(0) + , m_hash() + , m_previous_hash() + , m_status(MandatorySideStakeStatus::UNKNOWN) +{} + +MandatorySideStake::MandatorySideStake(CTxDestination destination, Allocation allocation, std::string description) + : m_destination(destination) + , m_allocation(allocation) + , m_description(description) + , m_timestamp(0) + , m_hash() + , m_previous_hash() + , m_status(MandatorySideStakeStatus::UNKNOWN) +{} + +MandatorySideStake::MandatorySideStake(CTxDestination destination, + Allocation allocation, + std::string description, + int64_t timestamp, + uint256 hash, + MandatorySideStakeStatus status) + : m_destination(destination) + , m_allocation(allocation) + , m_description(description) + , m_timestamp(timestamp) + , m_hash(hash) + , m_previous_hash() + , m_status(status) +{} + +bool MandatorySideStake::WellFormed() const +{ + return CBitcoinAddress(m_destination).IsValid() && m_allocation >= 0 && m_allocation <= 1; +} + +CTxDestination MandatorySideStake::Key() const +{ + return m_destination; +} + +std::pair MandatorySideStake::KeyValueToString() const +{ + return std::make_pair(CBitcoinAddress(m_destination).ToString(), StatusToString()); +} + +std::string MandatorySideStake::StatusToString() const +{ + return StatusToString(m_status.Value()); +} + +std::string MandatorySideStake::StatusToString(const MandatorySideStakeStatus& status, const bool& translated) const +{ + if (translated) { + switch(status) { + case MandatorySideStakeStatus::UNKNOWN: return _("Unknown"); + case MandatorySideStakeStatus::DELETED: return _("Deleted"); + case MandatorySideStakeStatus::MANDATORY: return _("Mandatory"); + case MandatorySideStakeStatus::OUT_OF_BOUND: break; + } + + assert(false); // Suppress warning + } else { + // The untranslated versions are really meant to serve as the string equivalent of the enum values. + switch(status) { + case MandatorySideStakeStatus::UNKNOWN: return "Unknown"; + case MandatorySideStakeStatus::DELETED: return "Deleted"; + case MandatorySideStakeStatus::MANDATORY: return "Mandatory"; + case MandatorySideStakeStatus::OUT_OF_BOUND: break; + } + + assert(false); // Suppress warning + } + + // This will never be reached. Put it in anyway to prevent control reaches end of non-void function warning + // from some compiler versions. + return std::string{}; +} + +bool MandatorySideStake::operator==(MandatorySideStake b) +{ + bool result = true; + + result &= (m_destination == b.m_destination); + result &= (m_allocation == b.m_allocation); + result &= (m_description == b.m_description); + result &= (m_timestamp == b.m_timestamp); + result &= (m_hash == b.m_hash); + result &= (m_previous_hash == b.m_previous_hash); + result &= (m_status == b.m_status); + + return result; +} + +bool MandatorySideStake::operator!=(MandatorySideStake b) +{ + return !(*this == b); +} + +// ----------------------------------------------------------------------------- +// Class: SideStake +// ----------------------------------------------------------------------------- +SideStake::SideStake() + : m_local_sidestake_ptr(nullptr) + , m_mandatory_sidestake_ptr(nullptr) + , m_type(Type::UNKNOWN) +{} + +SideStake::SideStake(LocalSideStake_ptr sidestake_ptr) + : m_local_sidestake_ptr(sidestake_ptr) + , m_mandatory_sidestake_ptr(nullptr) + , m_type(Type::LOCAL) +{} + +SideStake::SideStake(MandatorySideStake_ptr sidestake_ptr) + : m_local_sidestake_ptr(nullptr) + , m_mandatory_sidestake_ptr(sidestake_ptr) + , m_type(Type::MANDATORY) +{} + +bool SideStake::IsMandatory() const +{ + return (m_type == Type::MANDATORY) ? true : false; +} + +CTxDestination SideStake::GetDestination() const +{ + if (m_type == Type::MANDATORY && m_mandatory_sidestake_ptr != nullptr) { + return m_mandatory_sidestake_ptr->m_destination; + } else if (m_type == Type::LOCAL && m_local_sidestake_ptr != nullptr) { + return m_local_sidestake_ptr->m_destination; + } + + return CNoDestination(); +} + +Allocation SideStake::GetAllocation() const +{ + if (m_type == Type::MANDATORY && m_mandatory_sidestake_ptr != nullptr) { + return m_mandatory_sidestake_ptr->m_allocation; + } else if (m_type == Type::LOCAL && m_local_sidestake_ptr != nullptr) { + return m_local_sidestake_ptr->m_allocation; + } + + return Allocation(Fraction()); +} + +std::string SideStake::GetDescription() const +{ + if (m_type == Type::MANDATORY && m_mandatory_sidestake_ptr != nullptr) { + return m_mandatory_sidestake_ptr->m_description; + } else if (m_type == Type::LOCAL && m_local_sidestake_ptr != nullptr) { + return m_local_sidestake_ptr->m_description; + } + + return std::string {}; +} + +SideStake::Status SideStake::GetStatus() const +{ + // For trivial initializer case + if (m_mandatory_sidestake_ptr == nullptr && m_local_sidestake_ptr == nullptr) { + return {}; + } + + if (m_type == Type::MANDATORY && m_mandatory_sidestake_ptr != nullptr) { + return m_mandatory_sidestake_ptr->m_status; + } else if (m_type == Type::LOCAL && m_local_sidestake_ptr != nullptr) { + return m_local_sidestake_ptr->m_status; + } + + return {}; +} + +std::string SideStake::StatusToString() const +{ + // For trivial initializer case + if (m_mandatory_sidestake_ptr == nullptr && m_local_sidestake_ptr == nullptr) { + return {}; + } + + if (m_type == Type::MANDATORY && m_mandatory_sidestake_ptr != nullptr) { + return m_mandatory_sidestake_ptr->StatusToString(); + } else if (m_type == Type::LOCAL && m_local_sidestake_ptr != nullptr){ + return m_local_sidestake_ptr->StatusToString(); + } + + return std::string {}; +} + +// ----------------------------------------------------------------------------- +// Class: SideStakePayload +// ----------------------------------------------------------------------------- + +constexpr uint32_t SideStakePayload::CURRENT_VERSION; // For clang + +SideStakePayload::SideStakePayload(uint32_t version) + : IContractPayload() + , m_version(version) +{ +} + +SideStakePayload::SideStakePayload(const uint32_t version, + CTxDestination destination, + Allocation allocation, + std::string description, + MandatorySideStake::MandatorySideStakeStatus status) + : IContractPayload() + , m_version(version) + , m_entry(MandatorySideStake(destination, allocation, description, 0, uint256{}, status)) +{ +} + +SideStakePayload::SideStakePayload(const uint32_t version, MandatorySideStake entry) + : IContractPayload() + , m_version(version) + , m_entry(std::move(entry)) +{ +} + +SideStakePayload::SideStakePayload(MandatorySideStake entry) + : SideStakePayload(CURRENT_VERSION, std::move(entry)) +{ +} + +// ----------------------------------------------------------------------------- +// Class: SideStakeRegistry +// ----------------------------------------------------------------------------- +const std::vector SideStakeRegistry::SideStakeEntries() const +{ + std::vector sidestakes; + + LOCK(cs_lock); + + for (const auto& entry : m_mandatory_sidestake_entries) { + sidestakes.push_back(std::make_shared(entry.second)); + } + + for (const auto& entry : m_local_sidestake_entries) { + sidestakes.push_back(std::make_shared(entry.second)); + } + + return sidestakes; +} + +const std::vector SideStakeRegistry::ActiveSideStakeEntries(const SideStake::FilterFlag& filter, + const bool& include_zero_alloc) const +{ + std::vector sidestakes; + Allocation allocation_sum; + + // Note that LoadLocalSideStakesFromConfig is called upon a receipt of the core signal RwSettingsUpdated, which + // occurs immediately after the settings r-w file is updated. + + // The loops below prevent sidestakes from being added that cause a total allocation above 1.0 (100%). + + LOCK(cs_lock); + + if (filter & SideStake::FilterFlag::MANDATORY) { + for (const auto& entry : m_mandatory_sidestake_entries) + { + if (entry.second->m_status == MandatorySideStake::MandatorySideStakeStatus::MANDATORY + && allocation_sum + entry.second->m_allocation <= Params().GetConsensus().MaxMandatorySideStakeTotalAlloc) { + if ((include_zero_alloc && entry.second->m_allocation == 0) || entry.second->m_allocation > 0) { + sidestakes.push_back(std::make_shared(entry.second)); + allocation_sum += entry.second->m_allocation; + } + } + } + } + + if (filter & SideStake::FilterFlag::LOCAL) { + // Followed by local active sidestakes if sidestaking is enabled. Note that mandatory sidestaking cannot be disabled. + bool fEnableSideStaking = gArgs.GetBoolArg("-enablesidestaking"); + + if (fEnableSideStaking) { + LogPrint(BCLog::LogFlags::MINER, "INFO: %s: fEnableSideStaking = %u", __func__, fEnableSideStaking); + + for (const auto& entry : m_local_sidestake_entries) + { + if (entry.second->m_status == LocalSideStake::LocalSideStakeStatus::ACTIVE + && allocation_sum + entry.second->m_allocation <= 1) { + if ((include_zero_alloc && entry.second->m_allocation == 0) || entry.second->m_allocation > 0) { + sidestakes.push_back(std::make_shared(entry.second)); + allocation_sum += entry.second->m_allocation; + } + } + } + } + } + + return sidestakes; +} + +std::vector SideStakeRegistry::Try(const CTxDestination& key, const SideStake::FilterFlag& filter) const +{ + LOCK(cs_lock); + + std::vector result; + + if (filter & SideStake::FilterFlag::MANDATORY) { + const auto mandatory_entry = m_mandatory_sidestake_entries.find(key); + + if (mandatory_entry != m_mandatory_sidestake_entries.end()) { + result.push_back(std::make_shared(mandatory_entry->second)); + } + } + + if (filter & SideStake::FilterFlag::LOCAL) { + const auto local_entry = m_local_sidestake_entries.find(key); + + if (local_entry != m_local_sidestake_entries.end()) { + result.push_back(std::make_shared(local_entry->second)); + } + } + + return result; +} + +std::vector SideStakeRegistry::TryActive(const CTxDestination& key, const SideStake::FilterFlag& filter) const +{ + LOCK(cs_lock); + + std::vector result; + + for (const auto& iter : Try(key, filter)) { + if (iter->IsMandatory()) { + if (std::get(iter->GetStatus()) == MandatorySideStake::MandatorySideStakeStatus::MANDATORY) { + result.push_back(iter); + } + } else { + if (std::get(iter->GetStatus()) == LocalSideStake::LocalSideStakeStatus::ACTIVE) { + result.push_back(iter); + } + } + } + + return result; +} + +void SideStakeRegistry::Reset() +{ + LOCK(cs_lock); + + m_mandatory_sidestake_entries.clear(); + m_sidestake_db.clear(); +} + +void SideStakeRegistry::AddDelete(const ContractContext& ctx) +{ + // Poor man's mock. This is to prevent the tests from polluting the LevelDB database + int height = -1; + + if (ctx.m_pindex) + { + height = ctx.m_pindex->nHeight; + } + + SideStakePayload payload = ctx->CopyPayloadAs(); + + // Fill this in from the transaction context because these are not done during payload + // initialization. + payload.m_entry.m_hash = ctx.m_tx.GetHash(); + payload.m_entry.m_timestamp = ctx.m_tx.nTime; + + // Ensure status is DELETED if the contract action was REMOVE, regardless of what was actually + // specified. + if (ctx->m_action == ContractAction::REMOVE) { + payload.m_entry.m_status = MandatorySideStake::MandatorySideStakeStatus::DELETED; + } + + LOCK(cs_lock); + + auto sidestake_entry_pair_iter = m_mandatory_sidestake_entries.find(payload.m_entry.m_destination); + + MandatorySideStake_ptr current_sidestake_entry_ptr = nullptr; + + // Is there an existing SideStake entry in the map? + bool current_sidestake_entry_present = (sidestake_entry_pair_iter != m_mandatory_sidestake_entries.end()); + + // If so, then get a smart pointer to it. + if (current_sidestake_entry_present) { + current_sidestake_entry_ptr = sidestake_entry_pair_iter->second; + + // Set the payload m_entry's prev entry ctx hash = to the existing entry's hash. + payload.m_entry.m_previous_hash = current_sidestake_entry_ptr->m_hash; + } else { // Original entry for this SideStake entry key + payload.m_entry.m_previous_hash = uint256 {}; + } + + LogPrint(LogFlags::CONTRACT, "INFO: %s: SideStake entry add/delete: contract m_version = %u, payload " + "m_version = %u, address = %s, allocation = %f, m_timestamp = %" PRId64 ", " + "m_hash = %s, m_previous_hash = %s, m_status = %s", + __func__, + ctx->m_version, + payload.m_version, + CBitcoinAddress(payload.m_entry.m_destination).ToString(), + payload.m_entry.m_allocation.ToPercent(), + payload.m_entry.m_timestamp, + payload.m_entry.m_hash.ToString(), + payload.m_entry.m_previous_hash.ToString(), + payload.m_entry.StatusToString() + ); + + MandatorySideStake& historical = payload.m_entry; + + if (!m_sidestake_db.insert(ctx.m_tx.GetHash(), height, historical)) + { + LogPrint(LogFlags::CONTRACT, "INFO: %s: In recording of the SideStake entry for key %s, value %f, hash %s, " + "the SideStake entry db record already exists. This can be expected on a restart " + "of the wallet to ensure multiple contracts in the same block get stored/replayed.", + __func__, + CBitcoinAddress(historical.m_destination).ToString(), + historical.m_allocation.ToPercent(), + historical.m_hash.GetHex()); + } + + // Finally, insert the new SideStake entry (payload) smart pointer into the m_sidestake_entries map. + m_mandatory_sidestake_entries[payload.m_entry.m_destination] = m_sidestake_db.find(ctx.m_tx.GetHash())->second; + + return; +} + +void SideStakeRegistry::Add(const ContractContext& ctx) +{ + AddDelete(ctx); +} + +void SideStakeRegistry::Delete(const ContractContext& ctx) +{ + AddDelete(ctx); +} + +void SideStakeRegistry::NonContractAdd(const LocalSideStake& sidestake, const bool& save_to_file) +{ + LOCK(cs_lock); + + // Using this form of insert because we want the latest record with the same key to override any previous one. + m_local_sidestake_entries[sidestake.m_destination] = std::make_shared(sidestake); + + if (save_to_file) { + SaveLocalSideStakesToConfig(); + } +} + +void SideStakeRegistry::NonContractDelete(const CTxDestination& destination, const bool& save_to_file) +{ + LOCK(cs_lock); + + auto sidestake_entry_pair_iter = m_local_sidestake_entries.find(destination); + + if (sidestake_entry_pair_iter != m_local_sidestake_entries.end()) { + m_local_sidestake_entries.erase(sidestake_entry_pair_iter); + } + + if (save_to_file) { + SaveLocalSideStakesToConfig(); + } +} + +void SideStakeRegistry::Revert(const ContractContext& ctx) +{ + const auto payload = ctx->SharePayloadAs(); + + // For SideStake entries, both adds and removes will have records to revert in the m_sidestake_entries map, + // and also, if not the first entry for that SideStake key, will have a historical record to + // resurrect. + LOCK(cs_lock); + + auto entry_to_revert = m_mandatory_sidestake_entries.find(payload->m_entry.m_destination); + + if (entry_to_revert == m_mandatory_sidestake_entries.end()) { + error("%s: The SideStake entry for key %s to revert was not found in the SideStake entry map.", + __func__, + CBitcoinAddress(entry_to_revert->second->m_destination).ToString()); + + // If there is no record in the current m_sidestake_entries map, then there is nothing to do here. This + // should not occur. + return; + } + + // If this is not a null hash, then there will be a prior entry to resurrect. + CTxDestination key = entry_to_revert->second->m_destination; + uint256 resurrect_hash = entry_to_revert->second->m_previous_hash; + + // Revert the ADD or REMOVE action. Unlike the beacons, this is symmetric. + if (ctx->m_action == ContractAction::ADD || ctx->m_action == ContractAction::REMOVE) { + // Erase the record from m_sidestake_entries. + if (m_mandatory_sidestake_entries.erase(payload->m_entry.m_destination) == 0) { + error("%s: The SideStake entry to erase during a SideStake entry revert for key %s was not found.", + __func__, + CBitcoinAddress(key).ToString()); + // If the record to revert is not found in the m_sidestake_entries map, no point in continuing. + return; + } + + // Also erase the record from the db. + if (!m_sidestake_db.erase(ctx.m_tx.GetHash())) { + error("%s: The db entry to erase during a SideStake entry revert for key %s was not found.", + __func__, + CBitcoinAddress(key).ToString()); + + // Unlike the above we will keep going even if this record is not found, because it is identical to the + // m_sidestake_entries record above. This should not happen, because during contract adds and removes, + // entries are made simultaneously to the m_sidestake_entries and m_sidestake_db. + } + + if (resurrect_hash.IsNull()) { + return; + } + + auto resurrect_entry = m_sidestake_db.find(resurrect_hash); + + if (resurrect_entry == m_sidestake_db.end()) { + error("%s: The prior entry to resurrect during a SideStake entry ADD revert for key %s was not found.", + __func__, + CBitcoinAddress(key).ToString()); + return; + } + + // Resurrect the entry prior to the reverted one. It is safe to use the bracket form here, because of the protection + // of the logic above. There cannot be any entry in m_sidestake_entries with that key value left if we made it here. + m_mandatory_sidestake_entries[resurrect_entry->second->m_destination] = resurrect_entry->second; + } +} + +bool SideStakeRegistry::Validate(const Contract& contract, const CTransaction& tx, int &DoS) const +{ + const auto payload = contract.SharePayloadAs(); + + if (contract.m_version < 3) { + DoS = 25; + error("%s: Sidestake entries only valid in contract v3 and above", __func__); + return false; + } + + if (!payload->WellFormed(contract.m_action.Value())) { + DoS = 25; + error("%s: Malformed SideStake entry contract", __func__); + return false; + } + + Allocation allocation = payload->m_entry.m_allocation; + + // Contracts that would result in a total active mandatory sidestake allocation greater than the maximum allowed by consensus + // protocol must be rejected. Note that this is not a perfect validation, because there could be more than one sidestake + // contract transaction in the memory pool, and this is using already committed sidestake contracts (i.e. in blocks already + // accepted) as a basis. + if (GetMandatoryAllocationsTotal() + allocation > Params().GetConsensus().MaxMandatorySideStakeTotalAlloc) { + DoS = 25; + return false; + } + + return true; +} + +bool SideStakeRegistry::BlockValidate(const ContractContext& ctx, int& DoS) const +{ + return (IsV13Enabled(ctx.m_pindex->nHeight) && Validate(ctx.m_contract, ctx.m_tx, DoS)); +} + +int SideStakeRegistry::Initialize() +{ + LOCK(cs_lock); + + int height = m_sidestake_db.Initialize(m_mandatory_sidestake_entries, m_pending_sidestake_entries); + + SubscribeToCoreSignals(); + + LogPrint(LogFlags::CONTRACT, "INFO: %s: m_sidestake_db size after load: %u", __func__, m_sidestake_db.size()); + LogPrint(LogFlags::CONTRACT, "INFO: %s: m_sidestake_entries size after load: %u", __func__, m_mandatory_sidestake_entries.size()); + + // Add the local sidestakes specified in the config file(s) to the local sidestakes map. + LoadLocalSideStakesFromConfig(); + + m_local_entry_already_saved_to_config = false; + + return height; +} + +void SideStakeRegistry::SetDBHeight(int& height) +{ + LOCK(cs_lock); + + m_sidestake_db.StoreDBHeight(height); +} + +int SideStakeRegistry::GetDBHeight() +{ + int height = 0; + + LOCK(cs_lock); + + m_sidestake_db.LoadDBHeight(height); + + return height; +} + +void SideStakeRegistry::ResetInMemoryOnly() +{ + LOCK(cs_lock); + + m_local_sidestake_entries.clear(); + m_mandatory_sidestake_entries.clear(); + m_sidestake_db.clear_in_memory_only(); +} + +uint64_t SideStakeRegistry::PassivateDB() +{ + LOCK(cs_lock); + + return m_sidestake_db.passivate_db(); +} + +void SideStakeRegistry::LoadLocalSideStakesFromConfig() +{ + // If the m_local_entry_already_saved_to_config is set, then SaveLocalSideStakeToConfig was just called, + // and we want to then ignore the update signal from the r-w file change that calls this function for + // that action (only) and then reset the flag to be responsive to any changes on the core r-w file side + // through changesettings, for example. + if (m_local_entry_already_saved_to_config) { + m_local_entry_already_saved_to_config = false; + + return; + } + + std::vector vLocalSideStakes; + std::vector> raw_vSideStakeAlloc; + Allocation sum_allocation; + + // Parse destinations and allocations. We don't need to worry about any that are rejected other than a warning + // message, because any unallocated rewards will go back into the coinstake output(s). + + // If -sidestakeaddresses and -sidestakeallocations is set in either the config file or the r-w settings file + // and the settings are not empty and they are the same size, this will take precedence over the multiple entry + // -sidestake format. Note that -descriptions is optional; however, if descriptions is used, the size must + // match the other two if present. + std::vector addresses; + std::vector allocations; + std::vector descriptions; + + ParseString(gArgs.GetArg("-sidestakeaddresses", ""), ',', addresses); + ParseString(gArgs.GetArg("-sidestakeallocations", ""), ',', allocations); + ParseString(gArgs.GetArg("-sidestakedescriptions", ""), ',', descriptions); + + bool new_format_valid = false; + + if (!addresses.empty()) { + if (addresses.size() != allocations.size() || (!descriptions.empty() && addresses.size() != descriptions.size())) { + LogPrintf("WARN: %s: Malformed new style sidestaking configuration entries. " + "Reverting to original format in read only gridcoinresearch.conf file.", + __func__); + } else { + new_format_valid = true; + + for (unsigned int i = 0; i < addresses.size(); ++i) + { + if (descriptions.empty()) { + raw_vSideStakeAlloc.push_back(std::make_tuple(addresses[i], allocations[i], "")); + } else { + raw_vSideStakeAlloc.push_back(std::make_tuple(addresses[i], allocations[i], descriptions[i])); + } + } + } + } + + if (new_format_valid == false && gArgs.GetArgs("-sidestake").size()) + { + for (auto const& sSubParam : gArgs.GetArgs("-sidestake")) + { + std::vector vSubParam; + + ParseString(sSubParam, ',', vSubParam); + if (vSubParam.size() < 2) + { + LogPrintf("WARN: %s: Incomplete SideStake Allocation specified. Skipping SideStake entry.", __func__); + continue; + } + + // Deal with optional description. + if (vSubParam.size() == 3) { + raw_vSideStakeAlloc.push_back(std::make_tuple(vSubParam[0], vSubParam[1], vSubParam[2])); + } else { + raw_vSideStakeAlloc.push_back(std::make_tuple(vSubParam[0], vSubParam[1], "")); + } + } + } + + // First, add the allocation already taken by mandatory sidestakes, because they must be allocated first. + sum_allocation += GetMandatoryAllocationsTotal(); + + LOCK(cs_lock); + + for (const auto& entry : raw_vSideStakeAlloc) + { + std::string sAddress = std::get<0>(entry); + std::string sAllocation = std::get<1>(entry); + std::string sDescription = std::get<2>(entry); + + CBitcoinAddress address(sAddress); + if (!address.IsValid()) + { + LogPrintf("WARN: %s: ignoring sidestake invalid address %s.", __func__, sAddress); + continue; + } + + double read_allocation = 0.0; + if (!ParseDouble(sAllocation, &read_allocation)) + { + LogPrintf("WARN: %s: Invalid allocation %s provided. Skipping allocation.", __func__, sAllocation); + continue; + } + + LogPrintf("INFO: %s: allocation = %f", __func__, read_allocation); + + //int64_t numerator = read_allocation * 100.0; + //Allocation allocation(Fraction(numerator, 10000, true)); + + Allocation allocation(read_allocation / 100.0); + + if (allocation < 0) + { + LogPrintf("WARN: %s: Negative allocation provided. Skipping allocation.", __func__); + continue; + } + + // The below will stop allocations if someone has made a mistake and the total adds up to more than 100%. + // Note this same check is also done in SplitCoinStakeOutput, but it needs to be done here for two reasons: + // 1. Early alertment in the debug log, rather than when the first kernel is found, and 2. When the UI is + // hooked up, the SideStakeAlloc vector will be filled in by other than reading the config file and will + // skip the above code. + sum_allocation += allocation; + if (sum_allocation > 1) + { + LogPrintf("WARN: %s: allocation percentage over 100 percent, ending sidestake allocations.", __func__); + break; + } + + LocalSideStake sidestake(address.Get(), + allocation, + sDescription, + LocalSideStake::LocalSideStakeStatus::ACTIVE); + + // This will add or update (replace) a non-contract entry in the registry for the local sidestake. + NonContractAdd(sidestake, false); + + // This is needed because we need to detect entries in the registry map that are no longer in the config file to mark + // them deleted. + vLocalSideStakes.push_back(sidestake); + + LogPrint(BCLog::LogFlags::MINER, "INFO: %s: SideStakeAlloc Address %s, Allocation %f", + __func__, sAddress, allocation.ToPercent()); + } + + for (auto& entry : m_local_sidestake_entries) + { + // Only look at active entries. The others are NA for this alignment. + if (entry.second->m_status == LocalSideStake::LocalSideStakeStatus::ACTIVE) { + auto iter = std::find(vLocalSideStakes.begin(), vLocalSideStakes.end(), *entry.second); + + if (iter == vLocalSideStakes.end()) { + // Entry in map is no longer found in config files, so mark map entry inactive. + + entry.second->m_status = LocalSideStake::LocalSideStakeStatus::INACTIVE; + } + } + } + + // If we get here and dSumAllocation is zero then the enablesidestaking flag was set, but no VALID distribution + // was provided in the config file, so warn in the debug log. + if (!sum_allocation) + LogPrintf("WARN: %s: enablesidestaking was set in config but nothing has been allocated for" + " distribution!", __func__); +} + +bool SideStakeRegistry::SaveLocalSideStakesToConfig() +{ + bool status = false; + + std::string addresses; + std::string allocations; + std::string descriptions; + + std::string separator; + + std::vector> settings; + + LOCK(cs_lock); + + unsigned int i = 0; + for (const auto& iter : m_local_sidestake_entries) { + if (i) { + separator = ","; + } + + addresses += separator + CBitcoinAddress(iter.second->m_destination).ToString(); + allocations += separator + ToString(iter.second->m_allocation.ToPercent()); + descriptions += separator + iter.second->m_description; + + ++i; + } + + settings.push_back(std::make_pair("sidestakeaddresses", addresses)); + settings.push_back(std::make_pair("sidestakeallocations", allocations)); + settings.push_back(std::make_pair("sidestakedescriptions", descriptions)); + + status = updateRwSettings(settings); + + m_local_entry_already_saved_to_config = true; + + return status; +} + +Allocation SideStakeRegistry::GetMandatoryAllocationsTotal() const +{ + std::vector sidestakes = ActiveSideStakeEntries(SideStake::FilterFlag::MANDATORY, false); + Allocation allocation_total; + + for (const auto& entry : sidestakes) { + allocation_total += entry->GetAllocation(); + } + + return allocation_total; +} + +void SideStakeRegistry::SubscribeToCoreSignals() +{ + uiInterface.RwSettingsUpdated_connect(std::bind(RwSettingsUpdated, this)); +} + +void SideStakeRegistry::UnsubscribeFromCoreSignals() +{ + // Disconnect signals from client (no-op currently) +} + +SideStakeRegistry::SideStakeDB &SideStakeRegistry::GetSideStakeDB() +{ + LOCK(cs_lock); + + return m_sidestake_db; +} + +// This is static and called by the scheduler. +void SideStakeRegistry::RunDBPassivation() +{ + TRY_LOCK(cs_main, locked_main); + + if (!locked_main) + { + return; + } + + SideStakeRegistry& SideStake_entries = GetSideStakeRegistry(); + + SideStake_entries.PassivateDB(); +} + +template<> const std::string SideStakeRegistry::SideStakeDB::KeyType() +{ + return std::string("SideStake"); +} + diff --git a/src/gridcoin/sidestake.h b/src/gridcoin/sidestake.h new file mode 100644 index 0000000000..8702701643 --- /dev/null +++ b/src/gridcoin/sidestake.h @@ -0,0 +1,847 @@ +// Copyright (c) 2014-2024 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or https://opensource.org/licenses/mit-license.php. + +#ifndef GRIDCOIN_SIDESTAKE_H +#define GRIDCOIN_SIDESTAKE_H + +#include "base58.h" +#include "gridcoin/contract/handler.h" +#include "gridcoin/contract/payload.h" +#include "gridcoin/contract/registry_db.h" +#include "gridcoin/support/enumbytes.h" +#include "serialize.h" +#include "logging.h" + +namespace GRC { + +//! +//! \brief The Allocation class extends the Fraction class to provide functionality useful for sidestake allocations. +//! +class Allocation : public Fraction +{ +public: + //! + //! \brief Default constructor. Creates a zero allocation fraction. + //! + Allocation(); + + //! + //! \brief Allocation constructor from a double input. This multiplies the double by 1000, rounds, casts to int64_t, + //! and then constructs Fraction(x, 1000, true), which essentially creates a fraction representative of the double + //! to the third decimal place. + //! + //! \param double allocation + //! + Allocation(const double& allocation); + + //! + //! \brief Initialize an allocation from a Fraction. This is primarily used for casting. Note that no attempt to + //! limit the denominator size or simplify the fraction is made. + //! + //! \param Fraction f + //! + Allocation(const Fraction& f); + + //! + //! \brief Allocations extend the Fraction class and can also represent the result of the allocation constructed fraction + //! and the result of the muliplication of that fraction times the reward, which is in CAmount (i.e. int64_t). + //! + //! \return CAmount of the Fraction representation of the actual allocation. + //! + CAmount ToCAmount() const; + + //! + //! \brief Returns a double equivalent of the allocation fraction multiplied times 100. + //! + //! \return double percent representation of the allocation fraction. + //! + double ToPercent() const; +}; + +//! +//! \brief The LocalSideStake class. This class formalizes the local sidestake, which is a directive to apportion +//! a percentage of the total stake value to a designated destination. This destination must be valid, but +//! may or may not be owned by the staker. This is the primary mechanism to do automatic "donations" to +//! defined network addresses. +//! +//! Local (voluntary) entries will be picked up from the config file(s) and will be managed dynamically based on the +//! initial load of the config file + the r-w file + any changes in any GUI implementation on top of this. +//! +class LocalSideStake +{ +public: + enum class LocalSideStakeStatus + { + UNKNOWN, + ACTIVE, //!< A user specified sidestake that is active + INACTIVE, //!< A user specified sidestake that is inactive + OUT_OF_BOUND + }; + + //! + //! \brief Wrapped Enumeration of sidestake entry status, mainly for serialization/deserialization. + //! + using Status = EnumByte; + + CTxDestination m_destination; //!< The destination of the sidestake. + + Allocation m_allocation; //!< The allocation is a Fraction in the form x / 1000 where x is between 0 and 1000 inclusive. + + std::string m_description; //!< The description of the sidestake (optional) + + Status m_status; //!< The status of the sidestake. It is of type int instead of enum for serialization. + + + //! + //! \brief Initialize an empty, invalid sidestake instance. + //! + LocalSideStake(); + + //! + //! \brief Initialize a sidestake instance with the provided destination and allocation. This is used to construct a user + //! specified sidestake. + //! + //! \param destination + //! \param allocation + //! \param description (optional) + //! + LocalSideStake(CTxDestination destination, Allocation allocation, std::string description); + + //! + //! \brief Initialize a sidestake instance with the provided parameters. + //! + //! \param destination + //! \param allocation + //! \param description (optional) + //! \param status + //! + LocalSideStake(CTxDestination destination, Allocation allocation, std::string description, LocalSideStakeStatus status); + + //! + //! \brief Determine whether a sidestake contains each of the required elements. + //! \return true if the sidestake is well-formed. + //! + bool WellFormed() const; + + //! + //! \brief Returns the string representation of the current sidestake status + //! + //! \return Translated string representation of sidestake status + //! + std::string StatusToString() const; + + //! + //! \brief Returns the translated or untranslated string of the input sidestake status + //! + //! \param status. SideStake status + //! \param translated. True for translated, false for not translated. Defaults to true. + //! + //! \return SideStake status string. + //! + std::string StatusToString(const LocalSideStakeStatus& status, const bool& translated = true) const; + + //! + //! \brief Comparison operator overload used in the unit test harness. + //! + //! \param b The right hand side sidestake to compare for equality. + //! + //! \return Equal or not. + //! + bool operator==(LocalSideStake b); + + //! + //! \brief Comparison operator overload used in the unit test harness. + //! + //! \param b The right hand side sidestake to compare for equality. + //! + //! \return Equal or not. + //! + bool operator!=(LocalSideStake b); + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) + { + READWRITE(m_destination); + READWRITE(m_allocation); + READWRITE(m_description); + READWRITE(m_status); + } +}; + +//! +//! \brief The type that defines a shared pointer to a local sidestake +//! +typedef std::shared_ptr LocalSideStake_ptr; + +//! +//! \brief The MandatorySideStake class. This class formalizes the mandatory sidestake, which is a directive to apportion +//! a percentage of the total stake value to a designated destination. This destination must be valid, but +//! may or may not be owned by the staker. This is the primary mechanism to do automatic "donations" to +//! defined network addresses. +//! +//! Mandatory entries will be picked up by contract handlers similar to other contract types (cf. protocol entries). +//! +class MandatorySideStake +{ +public: + enum class MandatorySideStakeStatus + { + UNKNOWN, + DELETED, //!< A mandatory sidestake that has been deleted by contract + MANDATORY, //!< An active mandatory sidetake by contract + OUT_OF_BOUND + }; + + //! + //! \brief Wrapped Enumeration of sidestake entry status, mainly for serialization/deserialization. + //! + using Status = EnumByte; + + CTxDestination m_destination; //!< The destination of the sidestake. + + Allocation m_allocation; //!< The allocation is a Fraction in the form x / 1000 where x is between 0 and 1000 inclusive. + + std::string m_description; //!< The description of the sidestake (optional) + + int64_t m_timestamp; //!< Time of the sidestake contract transaction. + + uint256 m_hash; //!< The hash of the transaction that contains a mandatory sidestake. + + uint256 m_previous_hash; //!< The m_hash of the previous mandatory sidestake allocation with the same destination. + + Status m_status; //!< The status of the sidestake. It is of type EnumByte instead of enum for serialization. + + //! + //! \brief Initialize an empty, invalid sidestake instance. + //! + MandatorySideStake(); + + //! + //! \brief Initialize a sidestake instance with the provided destination and allocation. This is used to construct a user + //! specified sidestake. + //! + //! \param destination + //! \param allocation + //! \param description (optional) + //! + MandatorySideStake(CTxDestination destination, Allocation allocation, std::string description); + + //! + //! \brief Initialize a sidestake instance with the provided parameters. + //! + //! \param destination + //! \param allocation + //! \param description (optional) + //! \param status + //! + MandatorySideStake(CTxDestination destination, Allocation allocation, std::string description, MandatorySideStakeStatus status); + + //! + //! \brief Initialize a sidestake instance with the provided parameters. This form is normally used to construct a + //! mandatory sidestake from a contract. + //! + //! \param destination + //! \param allocation + //! \param description (optional) + //! \param timestamp + //! \param hash + //! \param status + //! + MandatorySideStake(CTxDestination destination, Allocation allocation, std::string description, int64_t timestamp, + uint256 hash, MandatorySideStakeStatus status); + + //! + //! \brief Determine whether a sidestake contains each of the required elements. + //! \return true if the sidestake is well-formed. + //! + bool WellFormed() const; + + //! + //! \brief This is the standardized method that returns the key value (in this case the destination) for the sidestake entry (for + //! the registry_db.h template.) + //! + //! \return CTxDestination key value for the sidestake entry + //! + CTxDestination Key() const; + + //! + //! \brief Provides the sidestake destination address and status as a pair of strings. + //! \return std::pair of strings + //! + std::pair KeyValueToString() const; + + //! + //! \brief Returns the string representation of the current sidestake status + //! + //! \return Translated string representation of sidestake status + //! + std::string StatusToString() const; + + //! + //! \brief Returns the translated or untranslated string of the input sidestake status + //! + //! \param status. SideStake status + //! \param translated. True for translated, false for not translated. Defaults to true. + //! + //! \return SideStake status string. + //! + std::string StatusToString(const MandatorySideStakeStatus& status, const bool& translated = true) const; + + //! + //! \brief Comparison operator overload used in the unit test harness. + //! + //! \param b The right hand side sidestake to compare for equality. + //! + //! \return Equal or not. + //! + bool operator==(MandatorySideStake b); + + //! + //! \brief Comparison operator overload used in the unit test harness. + //! + //! \param b The right hand side sidestake to compare for equality. + //! + //! \return Equal or not. + //! + bool operator!=(MandatorySideStake b); + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) + { + READWRITE(m_destination); + READWRITE(m_allocation); + READWRITE(m_description); + READWRITE(m_timestamp); + READWRITE(m_hash); + READWRITE(m_previous_hash); + READWRITE(m_status); + } +}; + +//! +//! \brief The type that defines a shared pointer to a mandatory sidestake +//! +typedef std::shared_ptr MandatorySideStake_ptr; + +//! +//! \brief This is a facade that combines the mandatory and local sidestake classes into one for use in the registry +//! and the GUI code. +//! +class SideStake +{ +public: + enum class Type { + UNKNOWN, + LOCAL, + MANDATORY, + OUT_OF_BOUND + }; + + enum FilterFlag : uint8_t { + NONE = 0b00, + LOCAL = 0b01, + MANDATORY = 0b10, + ALL = 0b11, + }; + + //! + //! \brief A variant to hold the two different types of sidestake status enums. + //! + typedef std::variant Status; + + SideStake(); + + SideStake(LocalSideStake_ptr sidestake_ptr); + + SideStake(MandatorySideStake_ptr sidestake_ptr); + + //! + //! \brief IsMandatory returns true if the sidestake is mandatory + //! \return true or false + //! + bool IsMandatory() const; + + //! + //! \brief Gets the destination of the sidestake + //! \return CTxDestination of the sidestake + //! + CTxDestination GetDestination() const; + //! + //! \brief Gets the allocation of the sidestake + //! \return A Fraction representing the allocation fraction of the sidestake. + //! + Allocation GetAllocation() const; + //! + //! \brief Gets the description of the sidestake + //! \return The description string of the sidestake + //! + std::string GetDescription() const; + //! + //! \brief Gets a variant containing either the mandatory sidestake status or local sidestake status, whichever + //! is applicable. + //! \return std::variant of the applicable sidestake status + //! + Status GetStatus() const; + //! + //! \brief Gets the status string associated with the applicable sidestake status. + //! \return String of the applicable sidestake status + //! + std::string StatusToString() const; + +private: + //! + //! \brief m_local_sidestake_ptr that points to the local sidestake object if this sidestake object is local; + //! nullptr otherwise. + //! + LocalSideStake_ptr m_local_sidestake_ptr; + //! + //! \brief m_mandatory_sidestake_ptr that points to the mandatory sidestake object if this sidestake object is mandatory; + //! nullptr otherwise. + //! + MandatorySideStake_ptr m_mandatory_sidestake_ptr; + //! + //! \brief m_type holds the type of the sidestake, either mandatory or local. + //! + Type m_type; +}; + +//! +//! \brief The type that defines a shared pointer to a sidestake. This is the facade and in turn will point to either a +//! mandatory or local sidestake as applicable. +//! +typedef std::shared_ptr SideStake_ptr; + +//! +//! \brief The body of a sidestake entry contract. This payload does NOT support +//! legacy payload formatting, as this contract/payload type is introduced after +//! legacy payloads are retired. +//! +class SideStakePayload : public IContractPayload +{ +public: + //! + //! \brief Version number of the current format for a serialized sidestake entry. + //! + //! CONSENSUS: Increment this value when introducing a breaking change and + //! ensure that the serialization/deserialization routines also handle all + //! of the previous versions. + //! + static constexpr uint32_t CURRENT_VERSION = 1; + + //! + //! \brief Version number of the serialized sidestake entry format. + //! + //! Version 1: Initial version: + //! + uint32_t m_version = CURRENT_VERSION; + + MandatorySideStake m_entry; //!< The sidestake entry in the payload. + + //! + //! \brief Initialize an empty, invalid sidestake entry payload. + //! + SideStakePayload(uint32_t version = CURRENT_VERSION); + + //! + //! \brief Initialize a sidestakeEntryPayload from a sidestake destination, allocation, + //! description, and status. + //! + //! \param destination. Destination for the sidestake entry + //! \param allocation. Allocation for the sidestake entry + //! \param description. Description string for the sidstake entry + //! \param status. Status of the sidestake entry + //! + SideStakePayload(const uint32_t version, CTxDestination destination, Allocation allocation, + std::string description, MandatorySideStake::MandatorySideStakeStatus status); + + //! + //! \brief Initialize a sidestake entry payload from the given sidestake entry + //! with the provided version number (and format). + //! + //! \param version Version of the serialized sidestake entry format. + //! \param sidestake_entry The sidestake entry itself. + //! + SideStakePayload(const uint32_t version, MandatorySideStake sidestake_entry); + + //! + //! \brief Initialize a sidestake entry payload from the given sidestake entry + //! with the CURRENT_VERSION. + //! + //! \param sidestake_entry The sidestake entry itself. + //! + SideStakePayload(MandatorySideStake sidestake_entry); + + //! + //! \brief Get the type of contract that this payload contains data for. + //! + GRC::ContractType ContractType() const override + { + return GRC::ContractType::SIDESTAKE; + } + + //! + //! \brief Determine whether the instance represents a complete payload. + //! + //! \return \c true if the payload contains each of the required elements. + //! + bool WellFormed(const ContractAction action) const override + { + bool valid = !(m_version <= 0 || m_version > CURRENT_VERSION); + + if (!valid) { + LogPrint(BCLog::LogFlags::CONTRACT, "WARN: %s: Payload is not well formed. " + "m_version = %u, CURRENT_VERSION = %u", + __func__, + m_version, + CURRENT_VERSION); + + return false; + } + + valid = m_entry.WellFormed(); + + if (!valid) { + LogPrint(BCLog::LogFlags::CONTRACT, "WARN: %s: Sidestake entry is not well-formed. " + "m_entry.WellFormed = %u, m_entry.m_key = %s, m_entry.m_allocation = %f, " + "m_entry.StatusToString() = %s", + __func__, + valid, + CBitcoinAddress(m_entry.m_destination).ToString(), + m_entry.m_allocation.ToPercent(), + m_entry.StatusToString() + ); + + return false; + } + + return valid; + } + + //! + //! \brief Get a string for the key used to construct a legacy contract. + //! + std::string LegacyKeyString() const override + { + return CBitcoinAddress(m_entry.m_destination).ToString(); + } + + //! + //! \brief Get a string for the value used to construct a legacy contract. + //! + std::string LegacyValueString() const override + { + return ToString(m_entry.m_allocation.ToDouble()); + } + + //! + //! \brief Get the burn fee amount required to send a particular contract. This + //! is the same as the LegacyPayload to insure compatibility between the sidestake + //! registry and non-upgraded nodes before the block v13/contract version 3 height + //! + //! \return Burn fee in units of 1/100000000 GRC. + //! + CAmount RequiredBurnAmount() const override + { + return Contract::STANDARD_BURN_AMOUNT; + } + + ADD_CONTRACT_PAYLOAD_SERIALIZE_METHODS; + + template + inline void SerializationOp( + Stream& s, + Operation ser_action, + const ContractAction contract_action) + { + READWRITE(m_version); + READWRITE(m_entry); + } +}; // SideStakePayload + +//! +//! \brief Stores and manages sidestake entries. Note that the mandatory sidestakes are stored in leveldb using +//! the registry db template. The local sidestakes are maintained in sync with the read-write gridcoinsettings.json file. +//! +class SideStakeRegistry : public IContractHandler +{ +public: + //! + //! \brief sidestakeRegistry constructor. The parameter is the version number of the underlying + //! sidestake entry db. This must be incremented when implementing format changes to the sidestake + //! entries to force a reinit. + //! + //! Version 1: 5.4.5.5+ + //! + SideStakeRegistry() + : m_sidestake_db(1) + { + }; + + //! + //! \brief The type that keys local sidestake entries by their destinations. + //! + typedef std::map LocalSideStakeMap; + + //! + //! \brief The type that keys mandatory sidestake entries by their destinations. Note that the entries + //! in this map are actually smart shared pointer wrappers, so that the same actual object + //! can be held by both this map and the historical map without object duplication. + //! + typedef std::map MandatorySideStakeMap; + + //! + //! \brief PendingSideStakeMap. This is not actually used but defined to satisfy the template. + //! + typedef MandatorySideStakeMap PendingSideStakeMap; + + //! + //! \brief The type that keys historical sidestake entries by the contract hash (txid). + //! Note that the entries in this map are actually smart shared pointer wrappers, so that + //! the same actual object can be held by both this map and the (current) sidestake entry map + //! without object duplication. + //! + typedef std::map HistoricalSideStakeMap; + + //! + //! \brief Get the collection of current sidestake entries. Note that this INCLUDES deleted + //! sidestake entries. + //! + //! \return \c A reference to the current sidestake entries stored in the registry. + //! + const std::vector SideStakeEntries() const; + + //! + //! \brief Get the collection of active sidestake entries. This is presented as a vector of + //! smart pointers to the relevant sidestake entries in the database. The entries included have + //! the status of active (for local sidestakes) and/or mandatory (for contract sidestakes). + //! Mandatory sidestakes come before local ones, and the method ensures that the mandatory sidestakes + //! returned do not total an allocation greater than MaxMandatorySideStakeTotalAlloc, and all of the + //! sidestakes combined do not total an allocation greater than 1.0. + //! + //! \param bitmask filter to return mandatory only, local only, or all + //! + //! \return A vector of smart pointers to sidestake entries. + //! + const std::vector ActiveSideStakeEntries(const SideStake::FilterFlag& filter, const bool& include_zero_alloc) const; + + //! + //! \brief Get the current sidestake entry for the specified destination. + //! + //! \param key The destination of the sidestake entry. + //! \param bitmask filter to try mandatory only, local only, or all + //! + //! \return A vector of smart pointers to entries matching the provided destination. Up to two elements + //! are returned, mandatory entry first, depending on the filter set. + //! + std::vector Try(const CTxDestination& key, const SideStake::FilterFlag& filter) const; + + //! + //! \brief Get the current sidestake entry for the specified destination if it has a status of ACTIVE or MANDATORY. + //! + //! \param key The destination of the sidestake entry. + //! \param bitmask filter to try mandatory only, local only, or all + //! + //! \return A vector of smart pointers to entries matching the provided destination that are in status of + //! MANDATORY or ACTIVE. Up to two elements are returned, mandatory entry first,, depending on the filter set. + //! + std::vector TryActive(const CTxDestination& key, const SideStake::FilterFlag& filter) const; + + //! + //! \brief Destroy the contract handler state in case of an error in loading + //! the sidestake entry registry state from LevelDB to prepare for reload from contract + //! replay. This is not used for sidestake entries, unless -clearsidestakehistory is specified + //! as a startup argument, because contract replay storage and full reversion has + //! been implemented for sidestake entries. + //! + void Reset() override; + + //! + //! \brief Determine whether a sidestake entry contract is valid. + //! + //! \param contract Contains the sidestake entry contract to validate. + //! \param tx Transaction that contains the contract. + //! \param DoS Misbehavior out. + //! + //! \return \c true if the contract contains a valid sidestake entry. + //! + bool Validate(const Contract& contract, const CTransaction& tx, int& DoS) const override; + + //! + //! \brief Determine whether a sidestake entry contract is valid including block context. This is used + //! in ConnectBlock. Note that for sidestake entries this simply calls Validate as there is no + //! block level specific validation to be done at the current time. + //! + //! \param ctx ContractContext containing the sidestake entry data to validate. + //! \param DoS Misbehavior score out. + //! + //! \return \c false If the contract fails validation. + //! + bool BlockValidate(const ContractContext& ctx, int& DoS) const override; + + //! + //! \brief Add a sidestake entry to the registry from contract data. For the sidestake registry + //! both Add and Delete actually call a common helper function AddDelete, because the action + //! is actually symmetric to both. + //! + //! \param ctx + //! + void Add(const ContractContext& ctx) override; + + //! + //! \brief Mark a sidestake entry deleted in the registry from contract data. For the sidestake registry + //! both Add and Delete actually call a common helper function AddDelete, because the action + //! is actually symmetric to both. + //! \param ctx + //! + void Delete(const ContractContext& ctx) override; + + //! + //! \brief Allows local (voluntary) sidestakes to be added to the in-memory local map and not persisted to + //! the registry db. + //! + //! \param SideStake object to add + //! \param bool save_to_file if true causes SaveLocalSideStakesToConfig() to be called. + //! + void NonContractAdd(const LocalSideStake& sidestake, const bool& save_to_file = true); + + //! + //! \brief Provides for deletion of local (voluntary) sidestakes from the in-memory local map that are not persisted + //! to the registry db. Deletion is by the map key (CTxDestination). + //! + //! \param destination + //! \param bool save_to_file if true causes SaveLocalSideStakesToConfig() to be called. + //! + void NonContractDelete(const CTxDestination& destination, const bool& save_to_file = true); + + //! + //! \brief Revert the registry state for the sidestake entry to the state prior + //! to this ContractContext application. This is typically used + //! during reorganizations, where blocks are disconnected. + //! + //! \param ctx References the sidestake entry contract and associated context. + //! + void Revert(const ContractContext& ctx) override; + + //! + //! \brief Initialize the sidestakeRegistry, which now includes restoring the state of the sidestakeRegistry from + //! LevelDB on wallet start. + //! + //! \return Block height of the database restored from LevelDB. Zero if no LevelDB sidestake entry data is found or + //! there is some issue in LevelDB sidestake entry retrieval. (This will cause the contract replay to change scope + //! and initialize the sidestakeRegistry from contract replay and store in LevelDB.) + //! + int Initialize() override; + + //! + //! \brief Gets the block height through which is stored in the sidestake entry registry database. + //! + //! \return block height. + //! + int GetDBHeight() override; + + //! + //! \brief Function normally only used after a series of reverts during block disconnects, because + //! block disconnects are done in groups back to a common ancestor, and will include a series of reverts. + //! This is essentially atomic, and therefore the final (common) height only needs to be set once. TODO: + //! reversion should be done with a vector argument of the contract contexts, along with a final height to + //! clean this up and move the logic to here from the calling function. + //! + //! \param height to set the storage DB bookmark. + //! + void SetDBHeight(int& height) override; + + //! + //! \brief Resets the maps in the sidestakeRegistry but does not disturb the underlying LevelDB + //! storage. This is only used during testing in the testing harness. + //! + void ResetInMemoryOnly(); + + //! + //! \brief Passivates the elements in the sidestake db, which means remove from memory elements in the + //! historical map that are not referenced by the active entry map. The backing store of the element removed + //! from memory is retained and will be transparently restored if find() is called on the hash key + //! for the element. + //! + //! \return The number of elements passivated. + //! + uint64_t PassivateDB(); + + //! + //! \brief This method parses the config file for local sidestakes. It is based on the original GetSideStakingStatusAndAlloc() + //! that was in miner.cpp prior to the implementation of the SideStake class. + //! + void LoadLocalSideStakesFromConfig(); + + //! + //! \brief A static function that is called by the scheduler to run the sidestake entry database passivation. + //! + static void RunDBPassivation(); + + //! + //! \brief Specializes the template RegistryDB for the SideStake class + //! + typedef RegistryDB SideStakeDB; + +private: + //! + //! \brief Protects the registry with multithreaded access. This is implemented INTERNAL to the registry class. + //! + mutable CCriticalSection cs_lock; + + //! + //! \brief Private helper method for the Add and Delete methods above. They both use identical code (with + //! different input statuses). + //! + //! \param ctx The contract context for the add or delete. + //! + void AddDelete(const ContractContext& ctx); + + //! + //! \brief Private helper function for non-contract add and delete to align the config r-w file with + //! in memory local sidestake map. + //! + //! \return bool true if successful. + //! + bool SaveLocalSideStakesToConfig(); + + //! + //! \brief Provides the total allocation for all active mandatory sidestakes as a Fraction. + //! \return total active mandatory sidestake allocation as a Fraction. + //! + Allocation GetMandatoryAllocationsTotal() const; + + void SubscribeToCoreSignals(); + void UnsubscribeFromCoreSignals(); + + LocalSideStakeMap m_local_sidestake_entries; //!< Contains the local (non-contract) sidestake entries. + MandatorySideStakeMap m_mandatory_sidestake_entries; //!< Contains the mandatory sidestake entries, including DELETED. + PendingSideStakeMap m_pending_sidestake_entries {}; //!< Not used. Only to satisfy the template. + + SideStakeDB m_sidestake_db; //!< The internal sidestake db object for leveldb persistence. + + bool m_local_entry_already_saved_to_config = false; //!< Flag to prevent reload on signal if individual entry saved already. + +public: + + SideStakeDB& GetSideStakeDB(); +}; // sidestakeRegistry + +//! +//! \brief Get the global sidestake entry registry. +//! +//! \return Current global sidestake entry registry instance. +//! +SideStakeRegistry& GetSideStakeRegistry(); +} // namespace GRC + +#endif // GRIDCOIN_SIDESTAKE_H diff --git a/src/init.cpp b/src/init.cpp index 6493b44231..286d5d11eb 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -433,6 +433,13 @@ void SetupServerArgs() "if -enablesidestaking is set. If set along with -sidestakeaddresses " "overrides the -sidestake entries.", ArgsManager::ALLOW_ANY | ArgsManager::IMMEDIATE_EFFECT, OptionsCategory::STAKING); + argsman.AddArg("-sidestakedescriptions=string1,string2,...,stringN>", "Sidestake entry description. There can be as many " + "specified as desired. Only six per stake can be sent. " + "If more than six are specified. Six are randomly chosen " + "for each stake. Only active if -enablesidestaking is set. " + "If set along with -sidestakeaddresses overrides the " + "-sidestake entries.", + ArgsManager::ALLOW_ANY | ArgsManager::IMMEDIATE_EFFECT, OptionsCategory::STAKING); argsman.AddArg("-enablestakesplit", "Enable unspent output spitting when staking to optimize staking efficiency " "(default: 0", ArgsManager::ALLOW_ANY | ArgsManager::IMMEDIATE_EFFECT, OptionsCategory::STAKING); diff --git a/src/miner.cpp b/src/miner.cpp index 21f6a73597..1c45764078 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -205,7 +205,7 @@ bool CreateMRCRewards(CBlock &blocknew, std::mapId().TryCpid(); + // This boolean will be used to ensure that there is only one mandatory sidestake transaction bound into a block. This + // in combination with the transaction level validation for the maximum mandatory allocation perfects that rule. + bool mandatory_sidestake_bound = false; + // Largest block you're willing to create: unsigned int nBlockMaxSize = gArgs.GetArg("-blockmaxsize", MAX_BLOCK_SIZE_GEN/2); // Limit to between 1K and MAX_BLOCK_SIZE-1K for sanity: @@ -601,6 +605,18 @@ bool CreateRestOfTheBlock(CBlock &block, CBlockIndex* pindexPrev, } //TryCpid() } // output limit } // contract type is MRC + + // If a mandatory sidestake contract has not already been bound into the block, then set mandatory_sidestake_bound + // to true. The ignore_transaction flag is still false, so this mandatory sidestake contract will be bound into the + // block. Any more mandatory sidestakes in the transaction loop will be ignored because the mandatory_sidestake_bound + // will be set to true in the second and succeeding iterations in the loop. + if (contract.m_type == GRC::ContractType::SIDESTAKE) { + if (!mandatory_sidestake_bound) { + mandatory_sidestake_bound = true; + } else { + ignore_transaction = true; + } + } // contract type is SIDESTAKE } // contracts not empty if (ignore_transaction) continue; @@ -829,14 +845,14 @@ bool CreateCoinStake(CBlock &blocknew, CKey &key, } void SplitCoinStakeOutput(CBlock &blocknew, int64_t &nReward, bool &fEnableStakeSplit, bool &fEnableSideStaking, - SideStakeAlloc &vSideStakeAlloc, int64_t &nMinStakeSplitValue, double &dEfficiency) + int64_t &nMinStakeSplitValue, double &dEfficiency) { // When this function is called, CreateCoinStake and CreateGridcoinReward have already been called // and there will be a single coinstake output (besides the empty one) that has the combined stake + research // reward. This function does the following... // 1. Perform reward payment to specified addresses ("sidestaking") in the following manner... // a. Check if both flags false and if so return with no action. - // b. Limit number of outputs based on bv. 3 for <=9 and 8 for >= 10. + // b. Limit number of outputs based on bv: 3 for <= v9, 8 for v10 & v11, and 10 for >= v12. // c. Pull the nValue from the original output and store locally. (nReward was passed in.) // d. Pop the existing outputs. // e. Validate each address provided for redirection in turn. If valid, create an output of the @@ -869,7 +885,7 @@ void SplitCoinStakeOutput(CBlock &blocknew, int64_t &nReward, bool &fEnableStake // 8 for 10 and above (excluding MRC outputs). The first one must be empty, so that gives 2 and 7 usable ones, // respectively. MRC outputs are excluded here. They are addressed in CreateMRC separately. Unlike in other areas, // the foundation sidestake IS COUNTED in the GetMRCOutputLimit because it is a sidestake, but handled in the - // CreateMRCRewards function and not here. + // CreateMRCRewards function and not here. For block version 12+ nMaxOutputs is 10, which gives 9 usable. unsigned int nMaxOutputs = GetCoinstakeOutputLimit(blocknew.nVersion) - GetMRCOutputLimit(blocknew.nVersion, true); // Set the maximum number of sidestake outputs to two less than the maximum allowable coinstake outputs @@ -881,7 +897,7 @@ void SplitCoinStakeOutput(CBlock &blocknew, int64_t &nReward, bool &fEnableStake unsigned int nOutputsUsed = 1; // If the number of sidestaking allocation entries exceeds nMaxSideStakeOutputs, then shuffle the vSideStakeAlloc - // to support sidestaking with more than six entries. This is a super simple solution but has some disadvantages. + // to support sidestaking with more than eight entries. This is a super simple solution but has some disadvantages. // If the person made a mistake and has the entries in the config file add up to more than 100%, then those entries // resulting a cumulative total over 100% will always be excluded, not just randomly excluded, because the cumulative // check is done in the order of the entries in the config file. This is not regarded as a big issue, because @@ -889,9 +905,19 @@ void SplitCoinStakeOutput(CBlock &blocknew, int64_t &nReward, bool &fEnableStake // mMaxSideStakeOutput entries, the residual returned to the coinstake will vary when the entries are shuffled, // because the total percentage of the selected entries will be randomized. No attempt to renormalize // the percentages is done. - if (vSideStakeAlloc.size() > nMaxSideStakeOutputs) - { - Shuffle(vSideStakeAlloc.begin(), vSideStakeAlloc.end(), FastRandomContext()); + SideStakeAlloc mandatory_sidestakes + = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(GRC::SideStake::FilterFlag::MANDATORY, false); + SideStakeAlloc local_sidestakes + = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(GRC::SideStake::FilterFlag::LOCAL, false); + + if (mandatory_sidestakes.size() > GetMandatorySideStakeOutputLimit(blocknew.nVersion)) { + Shuffle(mandatory_sidestakes.begin(), mandatory_sidestakes.end(), FastRandomContext()); + } + + if (local_sidestakes.size() > nMaxSideStakeOutputs + - std::min(GetMandatorySideStakeOutputLimit(blocknew.nVersion), + mandatory_sidestakes.size())) { + Shuffle(local_sidestakes.begin(), local_sidestakes.end(), FastRandomContext()); } // Initialize remaining stake output value to the total value of output for stake, which also includes @@ -911,34 +937,46 @@ void SplitCoinStakeOutput(CBlock &blocknew, int64_t &nReward, bool &fEnableStake blocknew.vtx[1].vout.clear(); CScript SideStakeScriptPubKey; - double dSumAllocation = 0.0; - - if (fEnableSideStaking) - { - // Iterate through passed in SideStake vector until either all elements processed, the maximum number of - // sidestake outputs is reached, or accumulated allocation will exceed 100%. - for(auto iterSideStake = vSideStakeAlloc.begin(); - (iterSideStake != vSideStakeAlloc.end()) && (nOutputsUsed <= nMaxSideStakeOutputs); - ++iterSideStake) + GRC::Allocation SumAllocation; + + // Lambda for sidestake allocation. This iterates throught the provided sidestake vector until either all elements processed, + // the maximum number of sidestake outputs is reached via the provided output_limit, or accumulated allocation will exceed 100%. + const auto allocate_sidestakes = [&](SideStakeAlloc sidestakes, unsigned int output_limit) { + for (auto iterSideStake = sidestakes.begin(); + (iterSideStake != sidestakes.end()) + && (nOutputsUsed <= output_limit); + ++iterSideStake) { - CBitcoinAddress address(iterSideStake->first); + CBitcoinAddress address(iterSideStake->get()->GetDestination()); + GRC::Allocation allocation = iterSideStake->get()->GetAllocation(); + if (!address.IsValid()) { LogPrintf("WARN: SplitCoinStakeOutput: ignoring sidestake invalid address %s.", - iterSideStake->first.c_str()); + address.ToString()); continue; } // Do not process a distribution that would result in an output less than 1 CENT. This will flow back into // the coinstake below. Prevents dust build-up. - if (nReward * iterSideStake->second < CENT) + // + // This is extremely important for mandatory sidestakes when validating this on a receiving node. + // This allows the validator to retrace the dust elimination for the coinstake mandatory sidestakes, and + // verify that EITHER the residual number of mandatory outputs after dust elimination is less than or equal to the + // maximum, in which case they all must be present and valid, OR, the residual number of outputs is greater than the + // maximum, which means that the maximum number of mandatory outputs MUST be present and valid. + // + // Note that nOutputsUsed is NOT incremented if the output is suppressed by this check. + if (allocation * nReward < CENT) { LogPrintf("WARN: SplitCoinStakeOutput: distribution %f too small to address %s.", - CoinToDouble(nReward * iterSideStake->second), iterSideStake->first.c_str()); + CoinToDouble(static_cast(allocation * nReward).ToCAmount()), + address.ToString() + ); continue; } - if (dSumAllocation + iterSideStake->second > 1.0) + if (SumAllocation + allocation > 1) { LogPrintf("WARN: SplitCoinStakeOutput: allocation percentage over 100 percent, " "ending sidestake allocations."); @@ -961,11 +999,11 @@ void SplitCoinStakeOutput(CBlock &blocknew, int64_t &nReward, bool &fEnableStake int64_t nSideStake = 0; // For allocations ending less than 100% assign using sidestake allocation. - if (dSumAllocation + iterSideStake->second < 1.0) - nSideStake = nReward * iterSideStake->second; + if (SumAllocation + allocation < 1) + nSideStake = static_cast(allocation * nReward).ToCAmount(); // We need to handle the final sidestake differently in the case it brings the total allocation up to 100%, // because testing showed in corner cases the output return to the staking address could be off by one Halford. - else if (dSumAllocation + iterSideStake->second == 1.0) + else if (SumAllocation + allocation == 1) // Simply assign the special case final nSideStake the remaining output value minus input value to ensure // a match on the output flowing down. nSideStake = nRemainingStakeOutputValue - nInputValue; @@ -973,18 +1011,24 @@ void SplitCoinStakeOutput(CBlock &blocknew, int64_t &nReward, bool &fEnableStake blocknew.vtx[1].vout.push_back(CTxOut(nSideStake, SideStakeScriptPubKey)); LogPrintf("SplitCoinStakeOutput: create sidestake UTXO %i value %f to address %s", - nOutputsUsed, CoinToDouble(nReward * iterSideStake->second), iterSideStake->first.c_str()); - dSumAllocation += iterSideStake->second; + nOutputsUsed, + CoinToDouble(static_cast(allocation * nReward).ToCAmount()), + address.ToString() + ); + SumAllocation += allocation; nRemainingStakeOutputValue -= nSideStake; nOutputsUsed++; } - // If we get here and dSumAllocation is zero then the enablesidestaking flag was set, but no VALID distribution - // was in the vSideStakeAlloc vector. (Note that this is also in the parsing routine in StakeMiner, so it will show - // up when the wallet is first started, but also needs to be here, to remind the user periodically that something - // is amiss.) - if (dSumAllocation == 0.0) - LogPrintf("WARN: %s: enablesidestaking was set in config but nothing has been allocated for" - " distribution!", __func__); + }; + + if (fEnableSideStaking) { + // Iterate through mandatory SideStake vector until either all elements processed, the maximum number of + // mandatory sidestake outputs is reached, or accumulated allocation will exceed 100%. + allocate_sidestakes(mandatory_sidestakes, GetMandatorySideStakeOutputLimit(blocknew.nVersion)); + + // Iterate through local SideStake vector until either all elements processed, the maximum number of + // sidestake outputs is reached, or accumulated allocation will exceed 100%. + allocate_sidestakes(local_sidestakes, nMaxSideStakeOutputs); } // By this point, if SideStaking was used and 100% was allocated nRemainingStakeOutputValue will be @@ -1050,23 +1094,27 @@ void SplitCoinStakeOutput(CBlock &blocknew, int64_t &nReward, bool &fEnableStake // The final state here of the coinstake blocknew.vtx[1].vout is // [empty], // [reward split 1], [reward split 2], ... , [reward split m], - // [sidestake 1], ... , [sidestake n], - // [MRC 1], ..., [MRC p]. + // [mandatory sidestake 1], ... , [mandatory sidestake n] + // [sidestake 1], ... , [sidestake p], + // [MRC 1], ..., [MRC q]. // // Currently according to the output limit rules encoded in CreateMRC and here: // For block version 10-11: - // one empty, m <= 6, m + n <= 7, and p = 0. + // one empty, m <= 7, n = 0, n + p <= 6, m + n + p <= 7 (i.e. empty + m + n + p <= 8), and q = 0, total <= 8.. // // For block version 12: - // one empty, m <= 6, m + n <= 10, and p <= 10. + // one empty, m <= 9, n = 0, n + p <= 8, m + n + p <= 9 (i.e. empty + m + n + p <= 10), and q <= 10, total <= 20. + // (On testnet q <= 3, total <= 13.) + + // For block version 13+: + // one empty, m <= 9, n <= 4, n + p <= 8, m + n + p <= 9 (i.e. empty + m + n + p <= 10), and q <= 10, total <= 20. + // (On testnet q <= 3, total <= 13.) // The total generated GRC is the total of the reward splits - the fees (the original GridcoinReward which is the // research reward + CBR), plus the total of the MRC outputs 2 to p (these outputs already have the fees subtracted) // MRC output 1 is always to the foundation (it is essentially a sidestake) and represents a cut of the MRC fees. } - - unsigned int GetNumberOfStakeOutputs(int64_t &nValue, int64_t &nMinStakeSplitValue, double &dEfficiency) { int64_t nDesiredStakeOutputValue = 0; @@ -1248,110 +1296,6 @@ bool IsMiningAllowed(CWallet *pwallet) return g_miner_status.StakingEnabled(); } -// This function parses the config file for the directives for side staking. It is used -// in StakeMiner for the miner loop and also called by rpc getstakinginfo. -SideStakeAlloc GetSideStakingStatusAndAlloc() -{ - SideStakeAlloc vSideStakeAlloc; - std::vector> raw_vSideStakeAlloc; - double dSumAllocation = 0.0; - - // Parse destinations and allocations. We don't need to worry about any that are rejected other than a warning - // message, because any unallocated rewards will go back into the coinstake output(s). - - // If -sidestakeaddresses and -sidestakeallocations is set in either the config file or the r-w settings file - // and the settings are not empty and they are the same size, this will take precedence over the multiple entry - // -sidestake format. - std::vector addresses; - std::vector allocations; - - ParseString(gArgs.GetArg("-sidestakeaddresses", ""), ',', addresses); - ParseString(gArgs.GetArg("-sidestakeallocations", ""), ',', allocations); - - if (addresses.size() != allocations.size()) - { - LogPrintf("WARN: %s: Malformed new style sidestaking configuration entries. Reverting to original format.", - __func__); - } - - if (addresses.size() && addresses.size() == allocations.size()) - { - for (unsigned int i = 0; i < addresses.size(); ++i) - { - raw_vSideStakeAlloc.push_back(std::make_pair(addresses[i], allocations[i])); - } - } - else if (gArgs.GetArgs("-sidestake").size()) - { - for (auto const& sSubParam : gArgs.GetArgs("-sidestake")) - { - std::vector vSubParam; - - ParseString(sSubParam, ',', vSubParam); - if (vSubParam.size() != 2) - { - LogPrintf("WARN: %s: Incomplete SideStake Allocation specified. Skipping SideStake entry.", __func__); - continue; - } - - raw_vSideStakeAlloc.push_back(std::make_pair(vSubParam[0], vSubParam[1])); - } - } - - for (auto const& entry : raw_vSideStakeAlloc) - { - std::string sAddress; - double dAllocation = 0.0; - - sAddress = entry.first; - - CBitcoinAddress address(sAddress); - if (!address.IsValid()) - { - LogPrintf("WARN: %s: ignoring sidestake invalid address %s.", __func__, sAddress); - continue; - } - - if (!ParseDouble(entry.second, &dAllocation)) - { - LogPrintf("WARN: %s: Invalid allocation %s provided. Skipping allocation.", __func__, entry.second); - continue; - } - - dAllocation /= 100.0; - - if (dAllocation <= 0) - { - LogPrintf("WARN: %s: Negative or zero allocation provided. Skipping allocation.", __func__); - continue; - } - - // The below will stop allocations if someone has made a mistake and the total adds up to more than 100%. - // Note this same check is also done in SplitCoinStakeOutput, but it needs to be done here for two reasons: - // 1. Early alertment in the debug log, rather than when the first kernel is found, and 2. When the UI is - // hooked up, the SideStakeAlloc vector will be filled in by other than reading the config file and will - // skip the above code. - dSumAllocation += dAllocation; - if (dSumAllocation > 1.0) - { - LogPrintf("WARN: %s: allocation percentage over 100 percent, ending sidestake allocations.", __func__); - break; - } - - vSideStakeAlloc.push_back(std::pair(sAddress, dAllocation)); - LogPrint(BCLog::LogFlags::MINER, "INFO: %s: SideStakeAlloc Address %s, Allocation %f", - __func__, sAddress, dAllocation); - } - - // If we get here and dSumAllocation is zero then the enablesidestaking flag was set, but no VALID distribution - // was provided in the config file, so warn in the debug log. - if (!dSumAllocation) - LogPrintf("WARN: %s: enablesidestaking was set in config but nothing has been allocated for" - " distribution!", __func__); - - return vSideStakeAlloc; -} - // This function parses the config file for the directives for stake splitting. It is used // in StakeMiner for the miner loop and also called by rpc getstakinginfo. bool GetStakeSplitStatusAndParams(int64_t& nMinStakeSplitValue, double& dEfficiency, int64_t& nDesiredStakeOutputValue) @@ -1415,12 +1359,10 @@ void StakeMiner(CWallet *pwallet) // nMinStakeSplitValue and dEfficiency are out parameters. bool fEnableStakeSplit = GetStakeSplitStatusAndParams(nMinStakeSplitValue, dEfficiency, nDesiredStakeOutputValue); - bool fEnableSideStaking = gArgs.GetBoolArg("-enablesidestaking"); - - LogPrint(BCLog::LogFlags::MINER, "INFO: %s: fEnableSideStaking = %u", __func__, fEnableSideStaking); - - // vSideStakeAlloc is an out parameter. - if (fEnableSideStaking) vSideStakeAlloc = GetSideStakingStatusAndAlloc(); + // If the vSideStakeAlloc is not empty, then set fEnableSideStaking to true. Note that vSideStakeAlloc will not be empty + // if non-zero allocation mandatory sidestakes are set OR local sidestaking is turned on by the -enablesidestaking config + // option. + bool fEnableSideStaking = (!GRC::GetSideStakeRegistry().ActiveSideStakeEntries(GRC::SideStake::FilterFlag::ALL, false).empty()); // wait for next round if (!MilliSleep(nMinerSleep)) return; @@ -1508,7 +1450,7 @@ void StakeMiner(CWallet *pwallet) // * If argument is supplied desiring stake output splitting or side staking, then call SplitCoinStakeOutput. if (fEnableStakeSplit || fEnableSideStaking) SplitCoinStakeOutput(StakeBlock, nReward, fEnableStakeSplit, fEnableSideStaking, - vSideStakeAlloc, nMinStakeSplitValue, dEfficiency); + nMinStakeSplitValue, dEfficiency); g_timer.GetTimes(function + "SplitCoinStakeOutput", "miner"); diff --git a/src/miner.h b/src/miner.h index 75223078e3..c713a8de89 100644 --- a/src/miner.h +++ b/src/miner.h @@ -8,11 +8,13 @@ #define BITCOIN_MINER_H #include "main.h" +#include "gridcoin/sidestake.h" + class CWallet; class CWalletTx; -typedef std::vector< std::pair > SideStakeAlloc; +typedef std::vector SideStakeAlloc; extern unsigned int nMinerSleep; @@ -24,7 +26,6 @@ static const int64_t MIN_STAKE_SPLIT_VALUE_GRC = 800; void SplitCoinStakeOutput(CBlock &blocknew, int64_t &nReward, bool &fEnableStakeSplit, bool &fEnableSideStaking, SideStakeAlloc &vSideStakeAlloc, double &dEfficiency); unsigned int GetNumberOfStakeOutputs(int64_t &nValue, int64_t &nMinStakeSplitValue, double &dEfficiency); -SideStakeAlloc GetSideStakingStatusAndAlloc(); bool GetStakeSplitStatusAndParams(int64_t& nMinStakeSplitValue, double& dEfficiency, int64_t& nDesiredStakeOutputValue); bool CreateMRCRewards(CBlock &blocknew, diff --git a/src/node/ui_interface.cpp b/src/node/ui_interface.cpp index 2aaab60d6f..c412d77269 100644 --- a/src/node/ui_interface.cpp +++ b/src/node/ui_interface.cpp @@ -34,6 +34,7 @@ struct UISignals { boost::signals2::signal Translate; boost::signals2::signal NotifyBlocksChanged; boost::signals2::signal UpdateMessageBox; + boost::signals2::signal RwSettingsUpdated; }; static UISignals g_ui_signals; @@ -63,6 +64,7 @@ ADD_SIGNALS_IMPL_WRAPPER(QueueShutdown); ADD_SIGNALS_IMPL_WRAPPER(Translate); ADD_SIGNALS_IMPL_WRAPPER(NotifyBlocksChanged); ADD_SIGNALS_IMPL_WRAPPER(UpdateMessageBox); +ADD_SIGNALS_IMPL_WRAPPER(RwSettingsUpdated); void CClientUIInterface::ThreadSafeMessageBox(const std::string& message, const std::string& caption, int style) { return g_ui_signals.ThreadSafeMessageBox(message, caption, style); } void CClientUIInterface::UpdateMessageBox(const std::string& version, const std::string& message) { return g_ui_signals.UpdateMessageBox(version, message); } @@ -84,7 +86,7 @@ void CClientUIInterface::NewPollReceived(int64_t poll_time) { return g_ui_signal void CClientUIInterface::NewVoteReceived(const uint256& poll_txid) { return g_ui_signals.NewVoteReceived(poll_txid); } void CClientUIInterface::NotifyAlertChanged(const uint256 &hash, ChangeType status) { return g_ui_signals.NotifyAlertChanged(hash, status); } void CClientUIInterface::NotifyScraperEvent(const scrapereventtypes& ScraperEventtype, ChangeType status, const std::string& message) { return g_ui_signals.NotifyScraperEvent(ScraperEventtype, status, message); } - +void CClientUIInterface::RwSettingsUpdated() { return g_ui_signals.RwSettingsUpdated(); } bool InitError(const std::string &str) { diff --git a/src/node/ui_interface.h b/src/node/ui_interface.h index 7cbcffd5e6..a0917c573a 100644 --- a/src/node/ui_interface.h +++ b/src/node/ui_interface.h @@ -138,6 +138,9 @@ class CClientUIInterface /** New vote received **/ ADD_SIGNALS_DECL_WRAPPER(NewVoteReceived, void, const uint256& poll_txid); + /** Read-write settings file updated **/ + ADD_SIGNALS_DECL_WRAPPER(RwSettingsUpdated, void); + /** * New, updated or cancelled alert. * @note called with lock cs_mapAlerts held. diff --git a/src/qt/CMakeLists.txt b/src/qt/CMakeLists.txt index 3302a30599..b62d55953b 100644 --- a/src/qt/CMakeLists.txt +++ b/src/qt/CMakeLists.txt @@ -29,6 +29,7 @@ add_library(gridcoinqt STATIC decoration.cpp diagnosticsdialog.cpp editaddressdialog.cpp + editsidestakedialog.cpp favoritespage.cpp guiutil.cpp intro.cpp @@ -61,6 +62,7 @@ add_library(gridcoinqt STATIC rpcconsole.cpp sendcoinsdialog.cpp sendcoinsentry.cpp + sidestaketablemodel.cpp signverifymessagedialog.cpp trafficgraphwidget.cpp transactiondesc.cpp @@ -122,6 +124,7 @@ set_source_files_properties( mrcmodel.cpp qtipcserver.cpp researcher/researchermodel.cpp + sidestaketablemodel.cpp transactiondesc.cpp transactiontablemodel.cpp voting/votingmodel.cpp diff --git a/src/qt/aboutdialog.cpp b/src/qt/aboutdialog.cpp index 9748a8087a..b8707c941c 100755 --- a/src/qt/aboutdialog.cpp +++ b/src/qt/aboutdialog.cpp @@ -8,7 +8,7 @@ AboutDialog::AboutDialog(QWidget *parent) : ui(new Ui::AboutDialog) { ui->setupUi(this); - ui->copyrightLabel->setText("Copyright 2009-2023 The Bitcoin/Peercoin/Black-Coin/Gridcoin developers"); + ui->copyrightLabel->setText("Copyright 2009-2024 The Bitcoin/Peercoin/Black-Coin/Gridcoin developers"); resize(GRC::ScaleSize(this, width(), height())); } diff --git a/src/qt/editsidestakedialog.cpp b/src/qt/editsidestakedialog.cpp new file mode 100644 index 0000000000..18596ac7ca --- /dev/null +++ b/src/qt/editsidestakedialog.cpp @@ -0,0 +1,156 @@ +// Copyright (c) 2014-2024 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or https://opensource.org/licenses/mit-license.php. + +#include "editsidestakedialog.h" +#include "ui_editsidestakedialog.h" +#include "sidestaketablemodel.h" +#include "guiutil.h" +#include "qt/decoration.h" + +#include + +EditSideStakeDialog::EditSideStakeDialog(Mode mode, QWidget* parent) + : QDialog(parent) + , ui(new Ui::EditSideStakeDialog) + , mode(mode) + , model(nullptr) +{ + ui->setupUi(this); + + resize(GRC::ScaleSize(this, width(), height())); + + GUIUtil::setupAddressWidget(ui->addressLineEdit, this); + + switch (mode) + { + case NewSideStake: + setWindowTitle(tr("New SideStake")); + ui->statusLineEdit->setEnabled(false); + ui->statusLabel->setHidden(true); + ui->statusLineEdit->setHidden(true); + break; + case EditSideStake: + setWindowTitle(tr("Edit SideStake")); + ui->addressLineEdit->setEnabled(false); + ui->statusLabel->setHidden(false); + ui->statusLineEdit->setHidden(false); + ui->statusLineEdit->setEnabled(false); + break; + } + +} + +EditSideStakeDialog::~EditSideStakeDialog() +{ + delete ui; +} + +void EditSideStakeDialog::setModel(SideStakeTableModel* model) +{ + this->model = model; + if (!model) { + return; + } + +} + +void EditSideStakeDialog::loadRow(int row) +{ + m_row = row; + + ui->addressLineEdit->setText(model->index(row, SideStakeTableModel::Address, QModelIndex()).data(Qt::EditRole).toString()); + ui->allocationLineEdit->setText(model->index(row, SideStakeTableModel::Allocation, QModelIndex()).data(Qt::EditRole).toString()); + ui->descriptionLineEdit->setText(model->index(row, SideStakeTableModel::Description, QModelIndex()).data(Qt::EditRole).toString()); + ui->statusLineEdit->setText(model->index(row, SideStakeTableModel::Status, QModelIndex()).data(Qt::EditRole).toString()); +} + +bool EditSideStakeDialog::saveCurrentRow() +{ + if (!model) { + return false; + } + + bool success = true; + + switch (mode) + { + case NewSideStake: + address = model->addRow(ui->addressLineEdit->text(), + ui->allocationLineEdit->text(), + ui->descriptionLineEdit->text()); + + if (address.isEmpty()) { + success = false; + } + + break; + case EditSideStake: + QModelIndex index = model->index(m_row, SideStakeTableModel::Allocation, QModelIndex()); + model->setData(index, ui->allocationLineEdit->text(), Qt::EditRole); + + if (model->getEditStatus() == SideStakeTableModel::OK || model->getEditStatus() == SideStakeTableModel::NO_CHANGES) { + index = model->index(m_row, SideStakeTableModel::Description, QModelIndex()); + model->setData(index, ui->descriptionLineEdit->text(), Qt::EditRole); + + if (model->getEditStatus() == SideStakeTableModel::OK || model->getEditStatus() == SideStakeTableModel::NO_CHANGES) { + break; + } + } + + success = false; + + break; + } + + return success; +} + +void EditSideStakeDialog::accept() +{ + if (!model) { + return; + } + + if (!saveCurrentRow()) + { + switch (model->getEditStatus()) + { + case SideStakeTableModel::OK: + // Failed with unknown reason. Just reject. + break; + case SideStakeTableModel::NO_CHANGES: + // No changes were made during edit operation. Just reject. + break; + case SideStakeTableModel::INVALID_ADDRESS: + QMessageBox::warning(this, windowTitle(), + tr("The entered address \"%1\" is not " + "a valid Gridcoin address.").arg(ui->addressLineEdit->text()), + QMessageBox::Ok, QMessageBox::Ok); + break; + case SideStakeTableModel::DUPLICATE_ADDRESS: + QMessageBox::warning(this, windowTitle(), + tr("The entered address \"%1\" already " + "has a local sidestake entry.").arg(ui->addressLineEdit->text()), + QMessageBox::Ok, QMessageBox::Ok); + break; + case SideStakeTableModel::INVALID_ALLOCATION: + QMessageBox::warning(this, windowTitle(), + tr("The entered allocation is not valid. Check to make sure that the " + "allocation is greater than zero and when added to the other allocations " + "totals less than 100."), + QMessageBox::Ok, QMessageBox::Ok); + break; + case SideStakeTableModel::INVALID_DESCRIPTION: + QMessageBox::warning(this, windowTitle(), + tr("The entered description is not valid. Check to make sure that the " + "description only contains letters, numbers, spaces, periods, or " + "underscores."), + QMessageBox::Ok, QMessageBox::Ok); + } + + return; + } + + QDialog::accept(); +} diff --git a/src/qt/editsidestakedialog.h b/src/qt/editsidestakedialog.h new file mode 100644 index 0000000000..2da58052ad --- /dev/null +++ b/src/qt/editsidestakedialog.h @@ -0,0 +1,50 @@ +// Copyright (c) 2014-2024 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or https://opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QT_EDITSIDESTAKEDIALOG_H +#define BITCOIN_QT_EDITSIDESTAKEDIALOG_H + +#include + +QT_BEGIN_NAMESPACE +class QDataWidgetMapper; +QT_END_NAMESPACE + +namespace Ui { +class EditSideStakeDialog; +} +class SideStakeTableModel; + +/** Dialog for editing an address and associated information. + */ +class EditSideStakeDialog : public QDialog +{ + Q_OBJECT + +public: + enum Mode { + NewSideStake, + EditSideStake + }; + + explicit EditSideStakeDialog(Mode mode, QWidget* parent = nullptr); + ~EditSideStakeDialog(); + + void setModel(SideStakeTableModel* model); + void loadRow(int row); + +public slots: + void accept(); + +private: + bool saveCurrentRow(); + + Ui::EditSideStakeDialog *ui; + Mode mode; + SideStakeTableModel *model; + int m_row; + + QString address; +}; +#endif // BITCOIN_QT_EDITSIDESTAKEDIALOG_H diff --git a/src/qt/forms/editsidestakedialog.ui b/src/qt/forms/editsidestakedialog.ui new file mode 100644 index 0000000000..a2dd71f36b --- /dev/null +++ b/src/qt/forms/editsidestakedialog.ui @@ -0,0 +1,173 @@ + + + EditSideStakeDialog + + + + 0 + 0 + 400 + 300 + + + + Add or Edit SideStake + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Address + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Allocation + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Description + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + Status + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + EditSideStakeDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + EditSideStakeDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/qt/forms/optionsdialog.ui b/src/qt/forms/optionsdialog.ui index acd91271bd..c1c7d8038a 100644 --- a/src/qt/forms/optionsdialog.ui +++ b/src/qt/forms/optionsdialog.ui @@ -16,8 +16,8 @@ true - - + + QTabWidget::North @@ -245,83 +245,147 @@ Staking - - - - 10 - 10 - 651 - 135 - - - - - - - This enables or disables staking (the default is enabled). Note that a change to this setting will permanently override the config file with an entry in the settings file. - - - Enable Staking - - - - - - - This enables or disables splitting of stake outputs to optimize staking (default disabled). Note that a change to this setting will permanently override the config file with an entry in the settings file. - - - Enable Stake Splitting - - - - - - - - - Target Efficiency - - - - - - - Valid values are between 75 and 98 percent. Note that a change to this setting will permanently override the config file with an entry in the settings file. - - - - - - - Min Post Split UTXO - - - - - - - Valid values are 800 or greater. Note that a change to this setting will permanently override the config file with an entry in the settings file. - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - + + + + + + + This enables or disables staking (the default is enabled). Note that a change to this setting will permanently override the config file with an entry in the settings file. + + + Enable Staking + + + + + + + This enables or disables splitting of stake outputs to optimize staking (default disabled). Note that a change to this setting will permanently override the config file with an entry in the settings file. + + + Enable Stake Splitting + + + + + + + + + Target Efficiency + + + + + + + Valid values are between 75 and 98 percent. Note that a change to this setting will permanently override the config file with an entry in the settings file. + + + + + + + Min Post Split UTXO + + + + + + + Valid values are 800 or greater. Note that a change to this setting will permanently override the config file with an entry in the settings file. + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Enable Locally Specified Sidestaking + + + + + + + true + + + + + + + + + New + + + + :/icons/add:/icons/add + + + + + + + false + + + Edit + + + + :/icons/edit:/icons/edit + + + + + + + false + + + Delete + + + + :/icons/remove:/icons/remove + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + @@ -534,7 +598,7 @@ - + @@ -624,6 +688,8 @@
qvaluecombobox.h
- + + + diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp index aecbd97885..26bd13b1d3 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -9,7 +9,11 @@ #include "qt/decoration.h" #include "init.h" #include "miner.h" +#include "sidestaketablemodel.h" +#include "editsidestakedialog.h" +#include "logging.h" +#include #include #include #include @@ -27,6 +31,8 @@ OptionsDialog::OptionsDialog(QWidget* parent) , fStakingEfficiencyValid(true) , fMinStakeSplitValueValid(true) , fPollExpireNotifyValid(true) + , m_init_column_sizes_set(false) + , m_resize_columns_in_progress(false) { ui->setupUi(this); @@ -152,6 +158,55 @@ void OptionsDialog::setModel(OptionsModel *model) mapper->setModel(model); setMapper(); mapper->toFirst(); + + SideStakeTableModel* sidestake_model = model->getSideStakeTableModel(); + + sidestake_model->refresh(); + + ui->sidestakingTableView->setModel(sidestake_model); + ui->sidestakingTableView->verticalHeader()->hide(); + ui->sidestakingTableView->setSelectionBehavior(QAbstractItemView::SelectRows); + ui->sidestakingTableView->setSelectionMode(QAbstractItemView::ExtendedSelection); + ui->sidestakingTableView->setContextMenuPolicy(Qt::CustomContextMenu); + + // Scale column widths by the logical DPI over 96.0 to deal with hires displays. + ui->sidestakingTableView->setColumnWidth(SideStakeTableModel::Address, GRC::ScalePx(this, ADDRESS_COLUMN_WIDTH)); + ui->sidestakingTableView->setColumnWidth(SideStakeTableModel::Allocation, GRC::ScalePx(this, ALLOCATION_COLUMN_WIDTH)); + ui->sidestakingTableView->setColumnWidth(SideStakeTableModel::Description, GRC::ScalePx(this, DESCRIPTION_COLUMN_WIDTH)); + ui->sidestakingTableView->setColumnWidth(SideStakeTableModel::Status, GRC::ScalePx(this, STATUS_COLUMN_WIDTH)); + ui->sidestakingTableView->setShowGrid(true); + + // Set table column sizes vector for sidestake table proportional resize algorithm. + m_table_column_sizes = {GRC::ScalePx(this, ADDRESS_COLUMN_WIDTH), + GRC::ScalePx(this, ALLOCATION_COLUMN_WIDTH), + GRC::ScalePx(this, DESCRIPTION_COLUMN_WIDTH), + GRC::ScalePx(this, STATUS_COLUMN_WIDTH)}; + + ui->sidestakingTableView->sortByColumn(0, Qt::AscendingOrder); + + // Insures initial size of sidestake table and (header) columns are correct as of the context directly + // after tab selection. + connect(ui->tabWidget, &QTabWidget::currentChanged, this, &OptionsDialog::tabWidgetSelectionChanged); + + // Insures that header width remains constant and columns are resized correctly when a column delimiter is + // dragged to resize one column. + connect(ui->sidestakingTableView->horizontalHeader(), &QHeaderView::sectionResized, + this, &OptionsDialog::sidestakeTableSectionResized); + + connect(ui->enableSideStaking, &QCheckBox::toggled, this, &OptionsDialog::hideSideStakeEdit); + connect(ui->enableSideStaking, &QCheckBox::toggled, this, &OptionsDialog::refreshSideStakeTableModel); + + connect(ui->pushButtonNewSideStake, &QPushButton::clicked, this, &OptionsDialog::newSideStakeButton_clicked); + connect(ui->pushButtonEditSideStake, &QPushButton::clicked, this, &OptionsDialog::editSideStakeButton_clicked); + connect(ui->pushButtonDeleteSideStake, &QPushButton::clicked, this, &OptionsDialog::deleteSideStakeButton_clicked); + + connect(ui->sidestakingTableView->selectionModel(), &QItemSelectionModel::selectionChanged, + this, &OptionsDialog::sidestakeSelectionChanged); + + ui->sidestakingTableView->installEventFilter(this); + + connect(this, &OptionsDialog::sidestakeAllocationInvalid, this, &OptionsDialog::handleSideStakeAllocationInvalid); + connect(this, &OptionsDialog::sidestakeDescriptionInvalid, this, &OptionsDialog::handleSideStakeDescriptionInvalid); } /* update the display unit, to not use the default ("BTC") */ @@ -188,6 +243,7 @@ void OptionsDialog::setMapper() mapper->addMapping(ui->enableStakeSplit, OptionsModel::EnableStakeSplit); mapper->addMapping(ui->stakingEfficiency, OptionsModel::StakingEfficiency); mapper->addMapping(ui->minPostSplitOutputValue, OptionsModel::MinStakeSplitValue); + mapper->addMapping(ui->enableSideStaking, OptionsModel::EnableSideStaking); /* Window */ mapper->addMapping(ui->disableTransactionNotifications, OptionsModel::DisableTrxNotifications); @@ -240,7 +296,8 @@ void OptionsDialog::setSaveButtonState(bool fState) void OptionsDialog::on_okButton_clicked() { - mapper->submit(); + refreshSideStakeTableModel(); + accept(); } @@ -251,15 +308,72 @@ void OptionsDialog::on_cancelButton_clicked() void OptionsDialog::on_applyButton_clicked() { - mapper->submit(); + refreshSideStakeTableModel(); + disableApplyButton(); } +void OptionsDialog::newSideStakeButton_clicked() +{ + if (!model) { + return; + } + + EditSideStakeDialog dialog(EditSideStakeDialog::NewSideStake, this); + + dialog.setModel(model->getSideStakeTableModel()); + + dialog.exec(); +} + +void OptionsDialog::editSideStakeButton_clicked() +{ + if (!model || !ui->sidestakingTableView->selectionModel()) { + return; + } + + QModelIndexList indexes = ui->sidestakingTableView->selectionModel()->selectedRows(); + + if (indexes.isEmpty()) { + return; + } + + if (indexes.size() > 1) { + QMessageBox::warning(this, tr("Error"), tr("You can only edit one sidestake at a time."), QMessageBox::Ok); + } + + EditSideStakeDialog dialog(EditSideStakeDialog::EditSideStake, this); + + dialog.setModel(model->getSideStakeTableModel()); + dialog.loadRow(indexes.at(0).row()); + dialog.exec(); +} + +void OptionsDialog::deleteSideStakeButton_clicked() +{ + if (!model || !ui->sidestakingTableView->selectionModel()) { + return; + } + + QModelIndexList indexes = ui->sidestakingTableView->selectionModel()->selectedRows(); + + if (indexes.isEmpty()) { + return; + } + + if (indexes.size() > 1) { + QMessageBox::warning(this, tr("Error"), tr("You can only delete one sidestake at a time."), QMessageBox::Ok); + } + + model->getSideStakeTableModel()->removeRows(indexes.at(0).row(), 1); +} + void OptionsDialog::showRestartWarning_Proxy() { if(!fRestartWarningDisplayed_Proxy) { - QMessageBox::warning(this, tr("Warning"), tr("This setting will take effect after restarting Gridcoin."), QMessageBox::Ok); + QMessageBox::warning(this, tr("Warning"), tr("This setting will take effect" + " after restarting Gridcoin."), QMessageBox::Ok); fRestartWarningDisplayed_Proxy = true; } } @@ -332,6 +446,16 @@ void OptionsDialog::hideStakeSplitting() } } +void OptionsDialog::hideSideStakeEdit() +{ + if (model) { + bool local_side_staking_enabled = ui->enableSideStaking->isChecked(); + + ui->pushButtonNewSideStake->setHidden(!local_side_staking_enabled); + ui->pushButtonEditSideStake->setHidden(!local_side_staking_enabled); + } +} + void OptionsDialog::handleProxyIpValid(QValidatedLineEdit *object, bool fState) { // this is used in a check before re-enabling the save buttons @@ -406,6 +530,16 @@ void OptionsDialog::handlePollExpireNotifyValid(QValidatedLineEdit *object, bool } } +void OptionsDialog::refreshSideStakeTableModel() +{ + if (!mapper->submit() + && model->getSideStakeTableModel()->getEditStatus() == SideStakeTableModel::INVALID_ALLOCATION) { + emit sidestakeAllocationInvalid(); + } else { + model->getSideStakeTableModel()->refresh(); + } +} + bool OptionsDialog::eventFilter(QObject *object, QEvent *event) { bool filter_event = false; @@ -492,5 +626,169 @@ bool OptionsDialog::eventFilter(QObject *object, QEvent *event) } } - return QDialog::eventFilter(object, event); + // This is required to provide immediate feedback on invalid allocation entries on in place editing. + if (object == ui->sidestakingTableView) { + if (model->getSideStakeTableModel()->getEditStatus() == SideStakeTableModel::INVALID_ALLOCATION) { + emit sidestakeAllocationInvalid(); + } + + if (model->getSideStakeTableModel()->getEditStatus() == SideStakeTableModel::INVALID_DESCRIPTION) { + emit sidestakeDescriptionInvalid(); + } + } + + return QDialog::eventFilter(object, event); +} + +void OptionsDialog::sidestakeSelectionChanged() +{ + QTableView *table = ui->sidestakingTableView; + + if (table->selectionModel()->hasSelection()) { + QModelIndexList indexes = ui->sidestakingTableView->selectionModel()->selectedRows(); + + if (indexes.size() > 1) { + ui->pushButtonEditSideStake->setEnabled(false); + ui->pushButtonDeleteSideStake->setEnabled(false); + } else if (static_cast(indexes.at(0).internalPointer())->IsMandatory()) { + ui->pushButtonEditSideStake->setEnabled(false); + ui->pushButtonDeleteSideStake->setEnabled(false); + } else { + ui->pushButtonEditSideStake->setEnabled(true); + ui->pushButtonDeleteSideStake->setEnabled(true); + } + } +} + +void OptionsDialog::handleSideStakeAllocationInvalid() +{ + model->getSideStakeTableModel()->refresh(); + + QMessageBox::warning(this, windowTitle(), + tr("The entered allocation is not valid and is reverted. Check to make sure " + "that the allocation is greater than or equal to zero and when added to the other " + "allocations totals less than 100."), + QMessageBox::Ok, QMessageBox::Ok); +} + +void OptionsDialog::handleSideStakeDescriptionInvalid() +{ + model->getSideStakeTableModel()->refresh(); + + QMessageBox::warning(this, windowTitle(), + tr("The entered description is not valid. Check to make sure that the " + "description only contains letters, numbers, spaces, periods, or " + "underscores."), + QMessageBox::Ok, QMessageBox::Ok); +} + +void OptionsDialog::updateSideStakeTableView() +{ + ui->sidestakingTableView->update(); +} + +void OptionsDialog::resizeSideStakeTableColumns(const bool& neighbor_pair_adjust, const int& index, + const int& old_size, const int& new_size) +{ + // This prevents unwanted recursion to here from addressBookSectionResized. + m_resize_columns_in_progress = true; + + if (!model) { + m_resize_columns_in_progress = false; + + return; + } + + if (!m_init_column_sizes_set) { + for (int i = 0; i < (int) m_table_column_sizes.size(); ++i) { + ui->sidestakingTableView->horizontalHeader()->resizeSection(i, m_table_column_sizes[i]); + + + LogPrint(BCLog::LogFlags::VERBOSE, "INFO: %s: section size = %i", + __func__, + ui->sidestakingTableView->horizontalHeader()->sectionSize(i)); + } + + LogPrint(BCLog::LogFlags::VERBOSE, "INFO: %s: header width = %i", + __func__, + ui->sidestakingTableView->horizontalHeader()->width() + ); + + m_init_column_sizes_set = true; + m_resize_columns_in_progress = false; + + return; + } + + if (neighbor_pair_adjust) { + if (index != SideStakeTableModel::all_ColumnIndex.size() - 1) { + int new_neighbor_section_size = ui->sidestakingTableView->horizontalHeader()->sectionSize(index + 1) + + old_size - new_size; + + ui->sidestakingTableView->horizontalHeader()->resizeSection( + index + 1, new_neighbor_section_size); + + // This detects and deals with the case where the resize of a column tries to force the neighbor + // to a size below its minimum, in which case we have to reverse out the attempt. + if (ui->sidestakingTableView->horizontalHeader()->sectionSize(index + 1) + != new_neighbor_section_size) { + ui->sidestakingTableView->horizontalHeader()->resizeSection( + index, + ui->sidestakingTableView->horizontalHeader()->sectionSize(index) + + new_neighbor_section_size + - ui->sidestakingTableView->horizontalHeader()->sectionSize(index + 1)); + } + } else { + // Do not allow the last column to be resized because there is no adjoining neighbor to the right + // and we are maintaining the total width fixed to the size of the containing frame. + ui->sidestakingTableView->horizontalHeader()->resizeSection(index, old_size); + } + + m_resize_columns_in_progress = false; + + return; + } + + // This is the proportional resize case when the window is resized. + const int width = ui->sidestakingTableView->horizontalHeader()->width() - 5; + + int orig_header_width = 0; + + for (const auto& iter : SideStakeTableModel::all_ColumnIndex) { + orig_header_width += ui->sidestakingTableView->horizontalHeader()->sectionSize(iter); + } + + if (!width || !orig_header_width) return; + + for (const auto& iter : SideStakeTableModel::all_ColumnIndex) { + int section_size = ui->sidestakingTableView->horizontalHeader()->sectionSize(iter); + + ui->sidestakingTableView->horizontalHeader()->resizeSection( + iter, section_size * width / orig_header_width); + } + + m_resize_columns_in_progress = false; +} + +void OptionsDialog::resizeEvent(QResizeEvent *event) +{ + resizeSideStakeTableColumns(); + + QWidget::resizeEvent(event); +} + +void OptionsDialog::sidestakeTableSectionResized(int index, int old_size, int new_size) +{ + // Avoid implicit recursion between resizeTableColumns and addressBookSectionResized + if (m_resize_columns_in_progress) return; + + resizeSideStakeTableColumns(true, index, old_size, new_size); +} + +void OptionsDialog::tabWidgetSelectionChanged(int index) +{ + // Index = 2 is the sidestaking tab for the current tab order. + if (index == 2) { + resizeSideStakeTableColumns(); + } } diff --git a/src/qt/optionsdialog.h b/src/qt/optionsdialog.h index ae7d2adf6e..64a2deaa10 100644 --- a/src/qt/optionsdialog.h +++ b/src/qt/optionsdialog.h @@ -22,8 +22,13 @@ class OptionsDialog : public QDialog void setModel(OptionsModel *model); void setMapper(); +public slots: + void resizeSideStakeTableColumns(const bool& neighbor_pair_adjust = false, const int& index = 0, + const int& old_size = 0, const int& new_size = 0); + protected: - bool eventFilter(QObject *object, QEvent *event); + bool eventFilter(QObject *object, QEvent *event) override; + void resizeEvent(QResizeEvent *event) override; private slots: /* enable only apply button */ @@ -40,6 +45,10 @@ private slots: void on_cancelButton_clicked(); void on_applyButton_clicked(); + void newSideStakeButton_clicked(); + void editSideStakeButton_clicked(); + void deleteSideStakeButton_clicked(); + void showRestartWarning_Proxy(); void showRestartWarning_Lang(); void updateDisplayUnit(); @@ -48,16 +57,25 @@ private slots: void hideLimitTxnDisplayDate(); void hideStakeSplitting(); void hidePollExpireNotify(); + void hideSideStakeEdit(); void handleProxyIpValid(QValidatedLineEdit *object, bool fState); void handleStakingEfficiencyValid(QValidatedLineEdit *object, bool fState); void handleMinStakeSplitValueValid(QValidatedLineEdit *object, bool fState); void handlePollExpireNotifyValid(QValidatedLineEdit *object, bool fState); + void handleSideStakeAllocationInvalid(); + void handleSideStakeDescriptionInvalid(); + + void refreshSideStakeTableModel(); + + void tabWidgetSelectionChanged(int index); signals: void proxyIpValid(QValidatedLineEdit *object, bool fValid); void stakingEfficiencyValid(QValidatedLineEdit *object, bool fValid); void minStakeSplitValueValid(QValidatedLineEdit *object, bool fValid); void pollExpireNotifyValid(QValidatedLineEdit *object, bool fValid); + void sidestakeAllocationInvalid(); + void sidestakeDescriptionInvalid(); private: Ui::OptionsDialog *ui; @@ -69,6 +87,26 @@ private slots: bool fStakingEfficiencyValid; bool fMinStakeSplitValueValid; bool fPollExpireNotifyValid; + + std::vector m_table_column_sizes; + bool m_init_column_sizes_set; + bool m_resize_columns_in_progress; + + enum SideStakeTableColumnWidths + { + ADDRESS_COLUMN_WIDTH = 200, + ALLOCATION_COLUMN_WIDTH = 60, + DESCRIPTION_COLUMN_WIDTH = 150, + STATUS_COLUMN_WIDTH = 50 + }; + +private slots: + void sidestakeSelectionChanged(); + void updateSideStakeTableView(); + + /** Resize address book table columns based on incoming signal */ + void sidestakeTableSectionResized(int index, int old_size, int new_size); + }; #endif // BITCOIN_QT_OPTIONSDIALOG_H diff --git a/src/qt/optionsmodel.cpp b/src/qt/optionsmodel.cpp index 666b60371b..1d86e919ae 100644 --- a/src/qt/optionsmodel.cpp +++ b/src/qt/optionsmodel.cpp @@ -79,6 +79,8 @@ void OptionsModel::Init() if (settings.contains("dataDir") && dataDir != GUIUtil::getDefaultDataDirectory()) { gArgs.SoftSetArg("-datadir", GUIUtil::qstringToBoostPath(settings.value("dataDir").toString()).string()); } + + m_sidestake_model = new SideStakeTableModel(this); } int OptionsModel::rowCount(const QModelIndex & parent) const @@ -155,6 +157,9 @@ QVariant OptionsModel::data(const QModelIndex & index, int role) const case EnableStakeSplit: // This comes from the core and is a read-write setting (see below). return QVariant(gArgs.GetBoolArg("-enablestakesplit")); + case EnableSideStaking: + // This comes from the core and is a read-write setting (see below). + return QVariant(gArgs.GetBoolArg("-enablesidestaking")); case StakingEfficiency: // This comes from the core and is a read-write setting (see below). return QVariant((double) gArgs.GetArg("-stakingefficiency", (int64_t) 90)); @@ -310,10 +315,15 @@ bool OptionsModel::setData(const QModelIndex & index, const QVariant & value, in case EnableStakeSplit: // This is a core setting stored in the read-write settings file and once set will override the read-only //config file. - //fStakeSplitEnabled = value.toBool(); gArgs.ForceSetArg("-enablestakesplit", value.toBool() ? "1" : "0"); updateRwSetting("enablestakesplit", gArgs.GetBoolArg("-enablestakesplit")); break; + case EnableSideStaking: + // This is a core setting stored in the read-write settings file and once set will override the read-only + //config file. + gArgs.ForceSetArg("-enablesidestaking", value.toBool() ? "1" : "0"); + updateRwSetting("enablesidestaking", gArgs.GetBoolArg("-enablesidestaking")); + break; case StakingEfficiency: // This is a core setting stored in the read-write settings file and once set will override the read-only //config file. @@ -461,3 +471,8 @@ QString OptionsModel::getDataDir() { return dataDir; } + +SideStakeTableModel* OptionsModel::getSideStakeTableModel() +{ + return m_sidestake_model; +} diff --git a/src/qt/optionsmodel.h b/src/qt/optionsmodel.h index d80009e66f..25dceccda4 100644 --- a/src/qt/optionsmodel.h +++ b/src/qt/optionsmodel.h @@ -1,6 +1,7 @@ #ifndef BITCOIN_QT_OPTIONSMODEL_H #define BITCOIN_QT_OPTIONSMODEL_H +#include "sidestaketablemodel.h" #include #include @@ -41,6 +42,7 @@ class OptionsModel : public QAbstractListModel DataDir, // QString EnableStaking, // bool EnableStakeSplit, // bool + EnableSideStaking, // bool StakingEfficiency, // double MinStakeSplitValue, // int PollExpireNotification, // double @@ -77,6 +79,8 @@ class OptionsModel : public QAbstractListModel QString getCurrentStyle(); QString getDataDir(); + SideStakeTableModel* getSideStakeTableModel(); + /* Explicit setters */ void setCurrentStyle(QString theme); void setMaskValues(bool privacy_mode); @@ -101,6 +105,8 @@ class OptionsModel : public QAbstractListModel QString walletStylesheet; QString dataDir; + SideStakeTableModel* m_sidestake_model; + signals: void displayUnitChanged(int unit); void reserveBalanceChanged(qint64); diff --git a/src/qt/researcher/researchermodel.cpp b/src/qt/researcher/researchermodel.cpp index d19a57b320..18a4f7bef4 100644 --- a/src/qt/researcher/researchermodel.cpp +++ b/src/qt/researcher/researchermodel.cpp @@ -85,7 +85,8 @@ BeaconStatus MapAdvertiseBeaconError(const BeaconError error) case BeaconError::PENDING: return BeaconStatus::PENDING; case BeaconError::TX_FAILED: return BeaconStatus::ERROR_TX_FAILED; case BeaconError::WALLET_LOCKED: return BeaconStatus::ERROR_WALLET_LOCKED; - } + case BeaconError::ALEADY_IN_MEMPOOL: return BeaconStatus::ALREADY_IN_MEMPOOL; + } assert(false); // Suppress warning } @@ -150,6 +151,8 @@ QString ResearcherModel::mapBeaconStatus(const BeaconStatus status) return tr("Beacon expires soon. Renew immediately."); case BeaconStatus::RENEWAL_POSSIBLE: return tr("Beacon eligible for renewal."); + case BeaconStatus::ALREADY_IN_MEMPOOL: + return tr("Beacon advertisement transaction already in mempool."); case BeaconStatus::UNKNOWN: return tr("Waiting for sync..."); } @@ -181,6 +184,7 @@ QIcon ResearcherModel::mapBeaconStatusIcon(const BeaconStatus status) const case BeaconStatus::PENDING: return make_icon(warning); case BeaconStatus::RENEWAL_NEEDED: return make_icon(danger); case BeaconStatus::RENEWAL_POSSIBLE: return make_icon(warning); + case BeaconStatus::ALREADY_IN_MEMPOOL: return make_icon(warning); case BeaconStatus::UNKNOWN: return make_icon(inactive); } diff --git a/src/qt/researcher/researchermodel.h b/src/qt/researcher/researchermodel.h index 125de50fe9..81309fc0b1 100644 --- a/src/qt/researcher/researchermodel.h +++ b/src/qt/researcher/researchermodel.h @@ -44,6 +44,7 @@ enum class BeaconStatus PENDING, RENEWAL_NEEDED, RENEWAL_POSSIBLE, + ALREADY_IN_MEMPOOL, UNKNOWN, }; diff --git a/src/qt/sidestaketablemodel.cpp b/src/qt/sidestaketablemodel.cpp new file mode 100644 index 0000000000..018cb0977e --- /dev/null +++ b/src/qt/sidestaketablemodel.cpp @@ -0,0 +1,463 @@ +// Copyright (c) 2014-2024 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or https://opensource.org/licenses/mit-license.php. + +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +static void RwSettingsUpdated(SideStakeTableModel* sidestake_model) +{ + qDebug() << QString("%1").arg(__func__); + QMetaObject::invokeMethod(sidestake_model, "updateSideStakeTableModel", Qt::QueuedConnection); +} + +} // anonymous namespace + +SideStakeLessThan::SideStakeLessThan(int column, Qt::SortOrder order) + : m_column(column) + , m_order(order) +{} + +bool SideStakeLessThan::operator()(const GRC::SideStake& left, const GRC::SideStake& right) const +{ + const GRC::SideStake* pLeft = &left; + const GRC::SideStake* pRight = &right; + + if (m_order == Qt::DescendingOrder) { + std::swap(pLeft, pRight); + } + + // For the purposes of sorting mandatory and local sidestakes in the GUI table, we will shift the local status enum to int + // values that are above the mandatory enum values by OUT_OF_BOUND on the mandatory status enum. + int left_status, right_status; + + if (pLeft->IsMandatory()) { + left_status = static_cast(std::get(pLeft->GetStatus()).Value()); + } else { + // For purposes of comparison, the enum value for local sidestake is shifted by the max entry of the mandatory + // status enum. + left_status = static_cast(std::get(pLeft->GetStatus()).Value()) + + static_cast(GRC::MandatorySideStake::MandatorySideStakeStatus::OUT_OF_BOUND); + } + + if (pRight->IsMandatory()) { + right_status = static_cast(std::get(pRight->GetStatus()).Value()); + } else { + // For purposes of comparison, the enum value for local sidestake is shifted by the max entry of the mandatory + // status enum. + right_status = static_cast(std::get(pRight->GetStatus()).Value()) + + static_cast(GRC::MandatorySideStake::MandatorySideStakeStatus::OUT_OF_BOUND); + } + + switch (static_cast(m_column)) { + case SideStakeTableModel::Address: + return pLeft->GetDestination() < pRight->GetDestination(); + case SideStakeTableModel::Allocation: + return pLeft->GetAllocation() < pRight->GetAllocation(); + case SideStakeTableModel::Description: + return pLeft->GetDescription().compare(pRight->GetDescription()) < 0; + case SideStakeTableModel::Status: + return left_status < right_status; + } // no default case, so the compiler can warn about missing cases + assert(false); +} + +class SideStakeTablePriv +{ +public: + QList m_cached_sidestakes; + int m_sort_column{-1}; + Qt::SortOrder m_sort_order; + + void refreshSideStakes() + { + m_cached_sidestakes.clear(); + + std::vector core_sidestakes + = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(GRC::SideStake::FilterFlag::ALL, true); + + m_cached_sidestakes.reserve(core_sidestakes.size()); + + for (const auto& entry : core_sidestakes) { + m_cached_sidestakes.append(*entry); + } + + if (m_sort_column >= 0) { + std::stable_sort(m_cached_sidestakes.begin(), m_cached_sidestakes.end(), SideStakeLessThan(m_sort_column, m_sort_order)); + } + } + + int size() + { + return m_cached_sidestakes.size(); + } + + GRC::SideStake* index(int idx) + { + if (idx >= 0 && idx < m_cached_sidestakes.size()) { + return &m_cached_sidestakes[idx]; + } + + return nullptr; + } +}; + +SideStakeTableModel::SideStakeTableModel(OptionsModel* parent) + : QAbstractTableModel(parent) +{ + m_columns << tr("Address") << tr("Allocation") << tr("Description") << tr("Status"); + m_priv.reset(new SideStakeTablePriv()); + + subscribeToCoreSignals(); + + // load initial data + refresh(); +} + +SideStakeTableModel::~SideStakeTableModel() +{ + // Intentionally left empty +} + +int SideStakeTableModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return m_priv->size(); +} + +int SideStakeTableModel::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return m_columns.length(); +} + +QVariant SideStakeTableModel::data(const QModelIndex &index, int role) const +{ + if(!index.isValid()) + return QVariant(); + + GRC::SideStake* rec = static_cast(index.internalPointer()); + + const auto column = static_cast(index.column()); + if (role == Qt::DisplayRole) { + switch (column) { + case Address: + return QString::fromStdString(CBitcoinAddress(rec->GetDestination()).ToString()); + case Allocation: + return QString().setNum(rec->GetAllocation().ToPercent(), 'f', 2) + QString("\%"); + case Description: + return QString::fromStdString(rec->GetDescription()); + case Status: + return QString::fromStdString(rec->StatusToString()); + } // no default case, so the compiler can warn about missing cases + assert(false); + } else if (role == Qt::EditRole) { + switch (column) { + case Address: + return QString::fromStdString(CBitcoinAddress(rec->GetDestination()).ToString()); + case Allocation: + return QString().setNum(rec->GetAllocation().ToPercent(), 'f', 2); + case Description: + return QString::fromStdString(rec->GetDescription()); + case Status: + return QString::fromStdString(rec->StatusToString()); + } // no default case, so the compiler can warn about missing cases + } else if (role == Qt::TextAlignmentRole) { + switch (column) { + case Address: + return QVariant(Qt::AlignLeft | Qt::AlignVCenter); + case Allocation: + return QVariant(Qt::AlignRight | Qt::AlignVCenter); + case Description: + return QVariant(Qt::AlignLeft | Qt::AlignVCenter); + case Status: + return QVariant(Qt::AlignCenter | Qt::AlignVCenter); + default: + return QVariant(); + } + } + + return QVariant(); +} + +bool SideStakeTableModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid()) { + return false; + } + + GRC::SideStakeRegistry& registry = GRC::GetSideStakeRegistry(); + + GRC::SideStake* rec = static_cast(index.internalPointer()); + + if (role != Qt::EditRole) { + return false; + } + + m_edit_status = OK; + + switch (index.column()) + { + case Address: + { + // The address of a sidestake entry is not editable. + return false; + } + case Allocation: + { + GRC::Allocation prior_total_allocation; + + // Save the original local sidestake (also in the core). + GRC::SideStake orig_sidestake = *rec; + + if (orig_sidestake.GetAllocation().ToPercent() == value.toDouble()) { + m_edit_status = NO_CHANGES; + return false; + } + + for (const auto& entry : registry.ActiveSideStakeEntries(GRC::SideStake::FilterFlag::ALL, true)) { + CTxDestination destination = entry->GetDestination(); + GRC::Allocation allocation = entry->GetAllocation(); + + if (destination == orig_sidestake.GetDestination()) { + continue; + } + + prior_total_allocation += allocation; + } + + bool parse_ok = false; + double read_allocation = value.toDouble(&parse_ok) / 100.0; + + GRC::Allocation modified_allocation(read_allocation); + + if (!parse_ok || modified_allocation < 0 || prior_total_allocation + modified_allocation > 1) { + m_edit_status = INVALID_ALLOCATION; + + LogPrint(BCLog::LogFlags::VERBOSE, "INFO: %s: m_edit_status = %i", + __func__, + (int) m_edit_status); + + return false; + } + + // Overwrite the existing sidestake entry with the modified allocation + registry.NonContractAdd(GRC::LocalSideStake(orig_sidestake.GetDestination(), + modified_allocation, + orig_sidestake.GetDescription(), + std::get(orig_sidestake.GetStatus()).Value()), + true); + + break; + } + case Description: + { + std::string orig_value = value.toString().toStdString(); + std::string san_value = SanitizeString(orig_value, SAFE_CHARS_CSV); + + if (rec->GetDescription() == orig_value) { + m_edit_status = NO_CHANGES; + return false; + } + + if (san_value != orig_value) { + m_edit_status = INVALID_DESCRIPTION; + return false; + } + + // Save the original local sidestake (also in the core). + GRC::SideStake orig_sidestake = *rec; + + // Overwrite the existing sidestake entry with the modified description + registry.NonContractAdd(GRC::LocalSideStake(orig_sidestake.GetDestination(), + orig_sidestake.GetAllocation(), + san_value, + std::get(orig_sidestake.GetStatus()).Value()), + true); + + break; + } + case Status: + // Status is not editable + return false; + } + + updateSideStakeTableModel(); + + return true; +} + +QVariant SideStakeTableModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(orientation == Qt::Horizontal) + { + if(role == Qt::DisplayRole && section < m_columns.size()) + { + return m_columns[section]; + } + } + return QVariant(); +} + +Qt::ItemFlags SideStakeTableModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) { + return Qt::NoItemFlags; + } + + GRC::SideStake* rec = static_cast(index.internalPointer()); + + Qt::ItemFlags retval = Qt::ItemIsSelectable | Qt::ItemIsEnabled; + + if (!rec->IsMandatory() && (index.column() == Allocation || index.column() == Description)) { + retval |= Qt::ItemIsEditable; + } + + return retval; +} + +QModelIndex SideStakeTableModel::index(int row, int column, const QModelIndex &parent) const +{ + Q_UNUSED(parent); + GRC::SideStake* data = m_priv->index(row); + + if (data) + return createIndex(row, column, data); + return QModelIndex(); +} + +QString SideStakeTableModel::addRow(const QString &address, const QString &allocation, const QString description) +{ + GRC::SideStakeRegistry& registry = GRC::GetSideStakeRegistry(); + + CBitcoinAddress sidestake_address; + sidestake_address.SetString(address.toStdString()); + + m_edit_status = OK; + + if (!sidestake_address.IsValid()) { + m_edit_status = INVALID_ADDRESS; + return QString(); + } + + // Check for duplicate local sidestakes. Here we use the actual core sidestake registry rather than the + // UI model. + std::vector core_local_sidestake = registry.Try(sidestake_address.Get(), GRC::SideStake::FilterFlag::LOCAL); + + if (!core_local_sidestake.empty()) { + m_edit_status = DUPLICATE_ADDRESS; + return QString(); + } + + GRC::Allocation prior_total_allocation; + + // Get total allocation of all active/mandatory sidestake entries + for (const auto& entry : registry.ActiveSideStakeEntries(GRC::SideStake::FilterFlag::ALL, true)) { + prior_total_allocation += entry->GetAllocation(); + } + + // The new allocation must be parseable as a double, must be greater than or equal to 0, and + // must result in a total allocation of less than or equal to 100%. + bool parse_ok = false; + double read_allocation = allocation.toDouble(&parse_ok) / 100.0; + + GRC::Allocation sidestake_allocation(read_allocation); + + if (!parse_ok || sidestake_allocation < 0 || prior_total_allocation + sidestake_allocation > 1) { + m_edit_status = INVALID_ALLOCATION; + + LogPrint(BCLog::LogFlags::VERBOSE, "INFO: %s: m_edit_status = %i", + __func__, + (int) m_edit_status); + + return QString(); + } + + std::string sidestake_description = description.toStdString(); + std::string sanitized_description = SanitizeString(sidestake_description, SAFE_CHARS_CSV); + + if (sanitized_description != sidestake_description) { + m_edit_status = INVALID_DESCRIPTION; + return QString(); + } + + registry.NonContractAdd(GRC::LocalSideStake(sidestake_address.Get(), + sidestake_allocation, + sanitized_description, + GRC::LocalSideStake::LocalSideStakeStatus::ACTIVE)); + + updateSideStakeTableModel(); + + return QString::fromStdString(sidestake_address.ToString()); +} + +bool SideStakeTableModel::removeRows(int row, int count, const QModelIndex &parent) +{ + Q_UNUSED(parent); + GRC::SideStake* rec = m_priv->index(row); + + if (count != 1 || !rec || rec->IsMandatory()) + { + // Can only remove one row at a time, and cannot remove rows not in model. + // Also refuse to remove mandatory sidestakes. + return false; + } + + GRC::GetSideStakeRegistry().NonContractDelete(rec->GetDestination()); + + updateSideStakeTableModel(); + + return true; +} + +SideStakeTableModel::EditStatus SideStakeTableModel::getEditStatus() const +{ + return m_edit_status; +} + +void SideStakeTableModel::refresh() +{ + Q_EMIT layoutAboutToBeChanged(); + m_priv->refreshSideStakes(); + + m_edit_status = OK; + + Q_EMIT layoutChanged(); +} + +void SideStakeTableModel::sort(int column, Qt::SortOrder order) +{ + m_priv->m_sort_column = column; + m_priv->m_sort_order = order; + refresh(); +} + +void SideStakeTableModel::updateSideStakeTableModel() +{ + refresh(); + + emit updateSideStakeTableModelSig(); +} + +void SideStakeTableModel::subscribeToCoreSignals() +{ + // Connect signals to client + uiInterface.RwSettingsUpdated_connect(boost::bind(RwSettingsUpdated, this)); +} + +void SideStakeTableModel::unsubscribeFromCoreSignals() +{ + // Disconnect signals from client (currently no-op). +} diff --git a/src/qt/sidestaketablemodel.h b/src/qt/sidestaketablemodel.h new file mode 100644 index 0000000000..2c496f373c --- /dev/null +++ b/src/qt/sidestaketablemodel.h @@ -0,0 +1,103 @@ +// Copyright (c) 2014-2024 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or https://opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QT_SIDESTAKETABLEMODEL_H +#define BITCOIN_QT_SIDESTAKETABLEMODEL_H + +#include +#include +#include "gridcoin/sidestake.h" + +class OptionsModel; +class SideStakeTablePriv; + +QT_BEGIN_NAMESPACE +class QTimer; +QT_END_NAMESPACE + +class SideStakeLessThan +{ +public: + SideStakeLessThan(int column, Qt::SortOrder order); + + bool operator()(const GRC::SideStake& left, const GRC::SideStake& right) const; + +private: + int m_column; + Qt::SortOrder m_order; +}; + +//! +//! \brief The SideStakeTableModel class represents the core sidestake registry as a model which can be consumed +//! and updated by the GUI. +//! +class SideStakeTableModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + explicit SideStakeTableModel(OptionsModel* parent = nullptr); + ~SideStakeTableModel(); + + enum ColumnIndex { + Address, + Allocation, + Description, + Status + }; + + static constexpr std::initializer_list all_ColumnIndex = {Address, + Allocation, + Description, + Status}; + + /** Return status of edit/insert operation */ + enum EditStatus { + OK, /**< Everything ok */ + NO_CHANGES, /**< No changes were made during edit operation */ + INVALID_ADDRESS, /**< Unparseable address */ + DUPLICATE_ADDRESS, /**< Address already in sidestake registry */ + INVALID_ALLOCATION, /**< Allocation is invalid (i.e. not parseable or not between 0.0 and 100.0) */ + INVALID_DESCRIPTION /**< Description contains an invalid character */ + }; + + /** @name Methods overridden from QAbstractTableModel + @{*/ + int rowCount(const QModelIndex &parent) const; + int columnCount(const QModelIndex &parent) const; + QVariant data(const QModelIndex &index, int role) const; + bool setData(const QModelIndex &index, const QVariant &value, int role); + QVariant headerData(int section, Qt::Orientation orientation, int role) const; + QModelIndex index(int row, int column, const QModelIndex &parent) const; + Qt::ItemFlags flags(const QModelIndex &index) const; + bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()); + void sort(int column, Qt::SortOrder order); + /*@}*/ + + /** Add a sidestake to the model. + Returns the added address on success, and an empty string otherwise. + */ + QString addRow(const QString &address, const QString &allocation, const QString description); + + EditStatus getEditStatus() const; + +public Q_SLOTS: + void refresh(); + +private: + QStringList m_columns; + std::unique_ptr m_priv; + EditStatus m_edit_status; + void subscribeToCoreSignals(); + void unsubscribeFromCoreSignals(); + +signals: + + void updateSideStakeTableModelSig(); + +public slots: + void updateSideStakeTableModel(); +}; + +#endif // BITCOIN_QT_SIDESTAKETABLEMODEL_H diff --git a/src/qt/voting/additionalfieldstableview.cpp b/src/qt/voting/additionalfieldstableview.cpp index f184a9cca1..114b967e01 100644 --- a/src/qt/voting/additionalfieldstableview.cpp +++ b/src/qt/voting/additionalfieldstableview.cpp @@ -20,6 +20,11 @@ AdditionalFieldsTableView::~AdditionalFieldsTableView() void AdditionalFieldsTableView::resizeEvent(QResizeEvent* event) { int height = horizontalHeader()->height(); + + if (!model()) { + return; + } + for (int i = 0; i < model()->rowCount(); ++i) { height += rowHeight(i); diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 1ca483654a..5adab057b5 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -7,6 +7,7 @@ #include "blockchain.h" #include "gridcoin/protocol.h" #include "gridcoin/scraper/scraper_registry.h" +#include "gridcoin/sidestake.h" #include "node/blockstorage.h" #include #include "gridcoin/mrc.h" @@ -2268,6 +2269,17 @@ UniValue addkey(const UniValue& params, bool fHelp) } } + // For add a mandatory sidestake, the 4th parameter is the allocation and the description (5th parameter) is optional. + if (type == GRC::ContractType::SIDESTAKE) { + if (action == GRC::ContractAction::ADD) { + required_param_count = 4; + param_count_max = 5; + } else { + required_param_count = 3; + param_count_max = 3; + } + } + if (fHelp || params.size() < required_param_count || params.size() > param_count_max) { std::string error_string; @@ -2304,7 +2316,8 @@ UniValue addkey(const UniValue& params, bool fHelp) if (!(type == GRC::ContractType::PROJECT || type == GRC::ContractType::SCRAPER - || type == GRC::ContractType::PROTOCOL)) { + || type == GRC::ContractType::PROTOCOL + || type == GRC::ContractType::SIDESTAKE)) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid contract type for addkey."); } @@ -2318,22 +2331,45 @@ UniValue addkey(const UniValue& params, bool fHelp) case GRC::ContractType::PROJECT: { if (action == GRC::ContractAction::ADD) { + bool gdpr_export_control = false; + if (block_v13_enabled) { + // We must do our own conversion to boolean here, because the 5th parameter can either be + // a boolean for project or a string for sidestake, which means the client.cpp entry cannot contain a + // unicode type specifier for the 5th parameter. + if (ToLower(params[4].get_str()) == "true") { + gdpr_export_control = true; + } else if (ToLower(params[4].get_str()) != "false") { + // Neither true or false - throw an exception. + throw JSONRPCError(RPC_INVALID_PARAMETER, "GDPR export parameter invalid. Must be true or false."); + } + contract = GRC::MakeContract( contract_version, action, uint32_t{3}, // Contract payload version number, 3 params[2].get_str(), // Name params[3].get_str(), // URL - params[4].getBool()); // GDPR stats export protection enforced boolean + gdpr_export_control); // GDPR stats export protection enforced boolean + } else if (project_v2_enabled) { + // We must do our own conversion to boolean here, because the 5th parameter can either be + // a boolean for project or a string for sidestake, which means the client.cpp entry cannot contain a + // unicode type specifier for the 5th parameter. + if (ToLower(params[4].get_str()) == "true") { + gdpr_export_control = true; + } else if (ToLower(params[4].get_str()) != "false") { + // Neither true or false - throw an exception. + throw JSONRPCError(RPC_INVALID_PARAMETER, "GDPR export parameter invalid. Must be true or false."); + } + contract = GRC::MakeContract( contract_version, action, uint32_t{2}, // Contract payload version number, 2 params[2].get_str(), // Name params[3].get_str(), // URL - params[4].getBool()); // GDPR stats export protection enforced boolean + gdpr_export_control); // GDPR stats export protection enforced boolean } else { contract = GRC::MakeContract( @@ -2433,6 +2469,47 @@ UniValue addkey(const UniValue& params, bool fHelp) params[2].get_str(), // key params[3].get_str()); // value break; + case GRC::ContractType::SIDESTAKE: + { + if (block_v13_enabled) { + CBitcoinAddress sidestake_address; + if (!sidestake_address.SetString(params[2].get_str())) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Address specified for the sidestake is invalid."); + } + + std::string description; + if (params.size() > 4) { + description = params[4].get_str(); + } + + // We have to do our own conversion here because the 4th parameter type specifier cannot be set other + // than string in the client.cpp file. + double allocation = 0.0; + if (params.size() > 3 && !ParseDouble(params[3].get_str(), &allocation)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid allocation specified."); + } + + allocation /= 100.0; + + if (allocation > 1.0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Allocation specified is greater than 100.0%."); + } + + contract = GRC::MakeContract( + contract_version, // Contract version number (3+) + action, // Contract action + uint32_t {1}, // Contract payload version number + sidestake_address.Get(), // Sidestake destination + allocation, // Sidestake allocation + description, // Sidestake description + GRC::MandatorySideStake::MandatorySideStakeStatus::MANDATORY // sidestake status + ); + } else { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Sidestake contracts are not valid for block version less than v13."); + } + + break; + } case GRC::ContractType::BEACON: [[fallthrough]]; case GRC::ContractType::CLAIM: diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 8bc4bbca8d..b1a76da8f8 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -201,7 +201,6 @@ static const CRPCConvertParam vRPCConvertParams[] = { "superblocks" , 1 }, // Developer - { "addkey" , 4 }, { "auditsnapshotaccrual" , 1 }, { "auditsnapshotaccruals" , 0 }, { "beaconaudit" , 0 }, diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index 49f787e1b1..69eb2eaae9 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -98,8 +98,6 @@ UniValue getstakinginfo(const UniValue& params, bool fHelp) bool fEnableSideStaking = gArgs.GetBoolArg("-enablesidestaking"); - if (fEnableSideStaking) vSideStakeAlloc = GetSideStakingStatusAndAlloc(); - stakesplitting.pushKV("stake-splitting-enabled", fEnableStakeSplit); if (fEnableStakeSplit) { @@ -110,19 +108,23 @@ UniValue getstakinginfo(const UniValue& params, bool fHelp) } obj.pushKV("stake-splitting", stakesplitting); - sidestaking.pushKV("side-staking-enabled", fEnableSideStaking); - if (fEnableSideStaking) + // This is what the miner sees... + vSideStakeAlloc = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(GRC::SideStake::FilterFlag::ALL, false); + + sidestaking.pushKV("local_side_staking_enabled", fEnableSideStaking); + + // Note that if local_side_staking_enabled is true, then local sidestakes will be applicable and shown. Mandatory + // sidestakes are always included. + for (const auto& alloc : vSideStakeAlloc) { - for (const auto& alloc : vSideStakeAlloc) - { - sidestakingalloc.pushKV("address", alloc.first); - sidestakingalloc.pushKV("allocation-pct", alloc.second * 100); + sidestakingalloc.pushKV("address", CBitcoinAddress(alloc->GetDestination()).ToString()); + sidestakingalloc.pushKV("allocation_pct", alloc->GetAllocation().ToPercent()); + sidestakingalloc.pushKV("status", alloc->StatusToString()); - vsidestakingalloc.push_back(sidestakingalloc); - } - sidestaking.pushKV("side-staking-allocations", vsidestakingalloc); + vsidestakingalloc.push_back(sidestakingalloc); } - obj.pushKV("side-staking", sidestaking); + sidestaking.pushKV("side_staking_allocations", vsidestakingalloc); + obj.pushKV("side_staking", sidestaking); obj.pushKV("difficulty", diff); obj.pushKV("errors", GetWarnings("statusbar")); diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index c47e0debc7..d3688e459b 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -11,6 +11,7 @@ #include "gridcoin/contract/contract.h" #include "gridcoin/mrc.h" #include "gridcoin/project.h" +#include "gridcoin/sidestake.h" #include "gridcoin/staking/difficulty.h" #include "gridcoin/superblock.h" #include "gridcoin/support/block_finder.h" @@ -245,6 +246,20 @@ UniValue VotePayloadToJson(const GRC::ContractPayload& payload) return out; } +UniValue SideStakePayloadToJson (const GRC::ContractPayload& payload) +{ + const auto& sidestake = payload.As(); + + UniValue out(UniValue::VOBJ); + + out.pushKV("address", CBitcoinAddress(sidestake.m_entry.m_destination).ToString()); + out.pushKV("allocation", sidestake.m_entry.m_allocation.ToPercent()); + out.pushKV("description", sidestake.m_entry.m_description); + out.pushKV("status", sidestake.m_entry.StatusToString()); + + return out; +} + UniValue LegacyVotePayloadToJson(const GRC::ContractPayload& payload) { const auto& vote = payload.As(); @@ -295,6 +310,9 @@ UniValue ContractToJson(const GRC::Contract& contract) case GRC::ContractType::MRC: out.pushKV("body", MRCToJson(contract.CopyPayloadAs())); break; + case GRC::ContractType::SIDESTAKE: + out.pushKV("body", SideStakePayloadToJson(contract.SharePayload())); + break; default: out.pushKV("body", LegacyContractPayloadToJson(contract.SharePayload())); break; diff --git a/src/script.h b/src/script.h index 5cf9feafe3..c65169c25c 100644 --- a/src/script.h +++ b/src/script.h @@ -17,6 +17,7 @@ #include "keystore.h" #include "prevector.h" #include +#include "serialize.h" #include "wallet/ismine.h" typedef std::vector valtype; @@ -30,7 +31,14 @@ class CScriptID : public BaseHash CScriptID() : BaseHash() {} explicit CScriptID(const CScript& in); explicit CScriptID(const uint160& in) : BaseHash(in) {} -// explicit CScriptID(const ScriptHash& in); + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) + { + READWRITE(m_hash); + } }; static const unsigned int MAX_SCRIPT_ELEMENT_SIZE = 520; // bytes @@ -85,6 +93,13 @@ class CNoDestination { friend bool operator==(const CNoDestination &a, const CNoDestination &b) { return true; } friend bool operator!=(const CNoDestination &a, const CNoDestination &b) { return false; } friend bool operator<(const CNoDestination &a, const CNoDestination &b) { return true; } + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) + {} + }; /** A txout script template with a specific destination. It is either: diff --git a/src/serialize.h b/src/serialize.h index 3c300b1e94..a09d5243d8 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -724,6 +725,10 @@ template void Unserialize(Stream& os, std::shared_p template void Serialize(Stream& os, const std::unique_ptr& p); template void Unserialize(Stream& os, std::unique_ptr& p); +// variant +template void Serialize(Stream& os, const std::variant& v); +template void Unserialize(Stream& is, const std::variant& v); + /** @@ -1071,6 +1076,46 @@ void Unserialize(Stream& is, std::shared_ptr& p) +// variant +template +void Serialize(Stream& os, const std::variant& v) { + // The 253 limit here is for that if there's a need for more than 253 type variant in the + // future, someone can replace the index uint8 with a var-int without sacrificing backwards + // compatibility. + static_assert(sizeof...(Args) < 253, "variants should hold less than 253 types."); + + Serialize(os, (uint8_t)v.index()); + + std::visit([&](auto& v2) { Serialize(os, v2); }, v); +} + +template +void unserialize_variant_helper(Stream& is, uint8_t index, V& v) {} + +template +void unserialize_variant_helper(Stream& is, uint8_t index, V& v) { + if (index == n) { + T o; + Unserialize(is, o); + v = o; + } else { + unserialize_variant_helper(is, index, v); + } +} + +template +void Unserialize(Stream& is, std::variant& v) { + // The 253 limit here is for that if there's a need for more than 253 type variant in the + // future, someone can replace the index uint8 with a var-int without sacrificing backwards + // compatibility. + static_assert(sizeof...(Args) < 253, "variants should hold less than 253 types."); + + uint8_t index; + Unserialize(is, index); + + unserialize_variant_helper, Args...>(is, index, v); +} + /** * Support for ADD_SERIALIZE_METHODS and READWRITE macro */ diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 7bed59e789..ebc70baa79 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -30,6 +30,7 @@ add_executable(test_gridcoin gridcoin/protocol_tests.cpp gridcoin/researcher_tests.cpp gridcoin/scraper_registry_tests.cpp + gridcoin/sidestake_tests.cpp gridcoin/superblock_tests.cpp key_tests.cpp merkle_tests.cpp diff --git a/src/test/gridcoin/sidestake_tests.cpp b/src/test/gridcoin/sidestake_tests.cpp new file mode 100644 index 0000000000..590d2a43f9 --- /dev/null +++ b/src/test/gridcoin/sidestake_tests.cpp @@ -0,0 +1,147 @@ +// Copyright (c) 2024 The Gridcoin developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://opensource.org/licenses/mit-license.php. + +#include + +#include +#include + +BOOST_AUTO_TEST_SUITE(sidestake_tests) + +BOOST_AUTO_TEST_CASE(sidestake_Allocation_Initialization_trivial) +{ + GRC::Allocation allocation; + + BOOST_CHECK_EQUAL(allocation.GetNumerator(), 0); + BOOST_CHECK_EQUAL(allocation.GetDenominator(), 1); + BOOST_CHECK_EQUAL(allocation.IsSimplified(), true); + BOOST_CHECK_EQUAL(allocation.IsZero(), true); + BOOST_CHECK_EQUAL(allocation.IsPositive(), false); + BOOST_CHECK_EQUAL(allocation.IsNonNegative(), true); + BOOST_CHECK_EQUAL(allocation.ToCAmount(), (CAmount) 0); +} + +BOOST_AUTO_TEST_CASE(sidestake_Allocation_Initialization_from_double_below_minimum) +{ + GRC::Allocation allocation((double) 0.0000499999); + + BOOST_CHECK_EQUAL(allocation.IsSimplified(), true); + BOOST_CHECK_EQUAL(allocation.IsZero(), true); + BOOST_CHECK_EQUAL(allocation.IsPositive(), false); + BOOST_CHECK_EQUAL(allocation.IsNonNegative(), true); + BOOST_CHECK_EQUAL(allocation.ToCAmount(), (CAmount) 0); +} + +BOOST_AUTO_TEST_CASE(sidestake_Allocation_Initialization_from_double_minimum) +{ + GRC::Allocation allocation((double) 0.0001); + + BOOST_CHECK_EQUAL(allocation.GetNumerator(), 1); + BOOST_CHECK_EQUAL(allocation.GetDenominator(), 10000); + BOOST_CHECK_EQUAL(allocation.IsSimplified(), true); + BOOST_CHECK_EQUAL(allocation.IsZero(), false); + BOOST_CHECK_EQUAL(allocation.IsPositive(), true); + BOOST_CHECK_EQUAL(allocation.IsNonNegative(), true); + BOOST_CHECK_EQUAL(allocation.ToCAmount(), (CAmount) 0); +} + +BOOST_AUTO_TEST_CASE(sidestake_Allocation_Initialization_from_double) +{ + GRC::Allocation allocation((double) 0.0005); + + BOOST_CHECK_EQUAL(allocation.GetNumerator(), 1); + BOOST_CHECK_EQUAL(allocation.GetDenominator(), 2000); + BOOST_CHECK_EQUAL(allocation.IsSimplified(), true); + BOOST_CHECK_EQUAL(allocation.IsZero(), false); + BOOST_CHECK_EQUAL(allocation.IsPositive(), true); + BOOST_CHECK_EQUAL(allocation.IsNonNegative(), true); + BOOST_CHECK_EQUAL(allocation.ToCAmount(), (CAmount) 0); +} + +BOOST_AUTO_TEST_CASE(sidestake_Allocation_Initialization_from_double_one_percent) +{ + GRC::Allocation allocation((double) 0.01); + + BOOST_CHECK_EQUAL(allocation.GetNumerator(), 1); + BOOST_CHECK_EQUAL(allocation.GetDenominator(), 100); + BOOST_CHECK_EQUAL(allocation.IsSimplified(), true); + BOOST_CHECK_EQUAL(allocation.IsZero(), false); + BOOST_CHECK_EQUAL(allocation.IsPositive(), true); + BOOST_CHECK_EQUAL(allocation.IsNonNegative(), true); + BOOST_CHECK_EQUAL(allocation.ToCAmount(), (CAmount) 0); +} + +BOOST_AUTO_TEST_CASE(sidestake_Allocation_Initialization_from_double_just_below_unity) +{ + GRC::Allocation allocation((double) 0.9999); + + BOOST_CHECK_EQUAL(allocation.GetNumerator(), 9999); + BOOST_CHECK_EQUAL(allocation.GetDenominator(), 10000); + BOOST_CHECK_EQUAL(allocation.IsSimplified(), true); + BOOST_CHECK_EQUAL(allocation.IsZero(), false); + BOOST_CHECK_EQUAL(allocation.IsPositive(), true); + BOOST_CHECK_EQUAL(allocation.IsNonNegative(), true); + BOOST_CHECK_EQUAL(allocation.ToCAmount(), (CAmount) 0); +} + +BOOST_AUTO_TEST_CASE(sidestake_Allocation_Initialization_from_double_maximum_before_multiplication) +{ + GRC::Allocation allocation((double) 1.0); + + BOOST_CHECK_EQUAL(allocation.GetNumerator(), 1); + BOOST_CHECK_EQUAL(allocation.GetDenominator(), 1); + BOOST_CHECK_EQUAL(allocation.IsSimplified(), true); + BOOST_CHECK_EQUAL(allocation.IsZero(), false); + BOOST_CHECK_EQUAL(allocation.IsPositive(), true); + BOOST_CHECK_EQUAL(allocation.IsNonNegative(), true); + BOOST_CHECK_EQUAL(allocation.ToCAmount(), (CAmount) 1); +} + +BOOST_AUTO_TEST_CASE(sidestake_Allocation_Initialization_from_fraction) +{ + GRC::Allocation allocation(Fraction(2500, 10000)); + + BOOST_CHECK_EQUAL(allocation.GetNumerator(), 2500); + BOOST_CHECK_EQUAL(allocation.GetDenominator(), 10000); + BOOST_CHECK_EQUAL(allocation.IsSimplified(), false); + BOOST_CHECK_EQUAL(allocation.IsZero(), false); + BOOST_CHECK_EQUAL(allocation.IsPositive(), true); + BOOST_CHECK_EQUAL(allocation.IsNonNegative(), true); + BOOST_CHECK_EQUAL(allocation.ToCAmount(), (CAmount) 0); + + allocation.Simplify(); + + BOOST_CHECK_EQUAL(allocation.GetNumerator(), 1); + BOOST_CHECK_EQUAL(allocation.GetDenominator(), 4); + BOOST_CHECK_EQUAL(allocation.IsSimplified(), true); +} + +BOOST_AUTO_TEST_CASE(sidestake_Allocation_ToPercent) +{ + GRC::Allocation allocation((double) 0.0005); + + BOOST_CHECK(std::abs(allocation.ToPercent() - (double) 0.05) < 1e-08); +} + +BOOST_AUTO_TEST_CASE(sidestake_Allocation_multiplication_and_derivation_of_allocation) +{ + // Multiplication is a very common operation with Allocations, because + // the general pattern is to multiply the allocation times a CAmount rewards + // to determine the rewards in Halfords (CAmount) to put on the output. + + // Allocations that are initialized from doubles are rounded to the nearest 1/10000. This is the worst case + // therefore, in terms of numerator and denominator. + GRC::Allocation allocation(0.9999); + + BOOST_CHECK_EQUAL(allocation.GetNumerator(), 9999); + BOOST_CHECK_EQUAL(allocation.GetDenominator(), 10000); + BOOST_CHECK_EQUAL(allocation.IsSimplified(), true); + + CAmount max_accrual = 16384 * COIN; + + CAmount actual_output = static_cast(allocation * max_accrual).ToCAmount(); + BOOST_CHECK_EQUAL(actual_output, int64_t {1638236160000}); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/serialize_tests.cpp b/src/test/serialize_tests.cpp index f21936cef2..e191133de6 100644 --- a/src/test/serialize_tests.cpp +++ b/src/test/serialize_tests.cpp @@ -330,4 +330,36 @@ BOOST_AUTO_TEST_CASE(class_methods) BOOST_CHECK(methodtest3 == methodtest4); } +BOOST_AUTO_TEST_CASE(variants) +{ + CDataStream ss(SER_DISK, PROTOCOL_VERSION); + using p_t = std::pair; + std::variant v; + CTransaction txval; + const char charstrval[16] = "testing charstr"; + CSerializeMethodsTestSingle csmts(-3, false, "testing", charstrval, txval); + + v = 42; + ss << v; + v = "sel"; + ss << v; + v = 3.1415; + ss << v; + v = std::make_pair(14, 48); + ss << v; + v = csmts; + ss << v; + + ss >> v; + BOOST_CHECK_EQUAL(std::get(v), 42); + ss >> v; + BOOST_CHECK_EQUAL(std::get(v), "sel"); + ss >> v; + BOOST_CHECK_EQUAL(std::get(v), 3.1415); + ss >> v; + BOOST_CHECK(std::get(v) == std::make_pair(14, 48)); + ss >> v; + BOOST_CHECK(std::get(v) == csmts); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/util_tests.cpp b/src/test/util_tests.cpp index c7da69d74a..8f8bd204f2 100755 --- a/src/test/util_tests.cpp +++ b/src/test/util_tests.cpp @@ -1,4 +1,5 @@ // Copyright (c) 2011-2020 The Bitcoin Core developers +// Copyright (c) 2024 The Gridcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or https://opensource.org/licenses/mit-license.php. @@ -14,6 +15,58 @@ #include +namespace { +// This version, which is recommended by some resources on the web, is actually slower, and has several issues. See +// the unit tests below. +int msb(const int64_t& n) +{ + // Can't take the log of 0. + if (n == 0) { + return 0; + } + + // Log2 is O(1) both time and space-wise. + return (static_cast(floor(log2(std::abs(n)))) + 1); +} + +int msb2(const int64_t& n_in) +{ + int64_t n = std::abs(n_in); + + int index = 0; + + if (n == 0) { + return 0; + } + + for (int i = 0; i <= 63; ++i) { + if (n % 2 == 1) { + index = i; + } + + n /= 2; + } + + return index + 1; +} + +// This is the one currently used in the Fraction class +int msb3(const int64_t& n_in) +{ + int64_t n = std::abs(n_in); + + int index = 0; + + for (; index <= 63; ++index) { + if (n >> index == 0) { + break; + } + } + + return index; +} +} //anonymous namespace + BOOST_AUTO_TEST_SUITE(util_tests) BOOST_AUTO_TEST_CASE(util_criticalsection) @@ -1024,4 +1077,662 @@ BOOST_AUTO_TEST_CASE(util_TrimString) BOOST_CHECK_EQUAL(TrimString(std::string("\x05\x04\x03\x02\x01\x00", 6), std::string("\x05\x04\x03\x02\x01\x00", 6)), ""); } +BOOST_AUTO_TEST_CASE(Fraction_msb_algorithm_equivalence) +{ + for (unsigned int i = 0; i <= 63; ++i) { + int64_t n = 0; + + if (i > 0) { + n = (int64_t {1} << (i - 1)); + } + + BOOST_CHECK_EQUAL(msb(n), i); + } + + int bias_for_msb_result_63 = 0; + + // msb ugly, ugly, ugly. Log2 looses resolution near the top of the range... + for (int i = 0; i < 16; ++i) { + bias_for_msb_result_63 = (int64_t {1} << i); + + int msb_result = msb(std::numeric_limits::max() - bias_for_msb_result_63); + + if (msb_result == 63) { + LogPrintf("INFO: %s: bias_for_msb_result_63 = %i, msb_result = %i", __func__, bias_for_msb_result_63, msb_result); + break; + } else { + } + } + + // bias_for_msb_result_63 is currently 32768! It should be zero! This disqualifies the log2 based approach based on + // a correctness check. + BOOST_CHECK_EQUAL(msb(std::numeric_limits::max() - bias_for_msb_result_63), 63); + + BOOST_CHECK_EQUAL(msb2(std::numeric_limits::max()), 63); + BOOST_CHECK_EQUAL(msb3(std::numeric_limits::max()), 63); + + std::vector> msb_results, msb2_results, msb3_results; + + unsigned int iterations = 1000; + + FastRandomContext rand(uint256 {0}); + + for (unsigned int i = 0; i < iterations; ++i) { + int64_t n = rand.rand32(); + + msb_results.push_back(std::make_pair(n, msb(n))); + } + + FastRandomContext rand2(uint256 {0}); + + for (unsigned int i = 0; i < iterations; ++i) { + int64_t n = rand2.rand32(); + + msb2_results.push_back(std::make_pair(n, msb2(n))); + } + + FastRandomContext rand3(uint256 {0}); + + for (unsigned int i = 0; i < iterations; ++i) { + int64_t n = rand3.rand32(); + + msb3_results.push_back(std::make_pair(n, msb3(n))); + } + + bool success = true; + + for (unsigned int i = 0; i < iterations; ++i) { + if (msb_results[i] != msb2_results[i] || msb_results[i] != msb3_results[i]) { + success = false; + error("%s: iteration %u: mismatch: %" PRId64 ", msb = %i, %" PRId64 " msb2 = %i, %" PRId64 " msb3 = %i", + __func__, + i, + msb_results[i].first, + msb_results[i].second, + msb2_results[i].first, + msb2_results[i].second, + msb3_results[i].first, + msb3_results[i].second + ); + } + } + + BOOST_CHECK(success); +} + +BOOST_AUTO_TEST_CASE(Fraction_msb_performance_test) +{ + // This is a test to bracket the three different algorithms above in anonymous namespace for doing msb calcs. The first is O(1), + // the second and third are O(log n), but the O(1) straight from the C++ library is pretty heavyweight and highly dependent on CPU + // architecture. + + FastRandomContext rand(uint256 {0}); + + unsigned int iterations = 10000000; + + g_timer.InitTimer("msb_test", true); + + for (unsigned int i = 0; i < iterations; ++i) { + msb(rand.rand64()); + } + + int64_t msb_test_time = g_timer.GetTimes(strprintf("msb %u iterations", iterations), "msb_test").time_since_last_check; + + FastRandomContext rand2(uint256 {0}); + + for (unsigned int i = 0; i < iterations; ++i) { + msb2(rand2.rand64()); + } + + int64_t msb2_test_time = g_timer.GetTimes(strprintf("msb2 %u iterations", iterations), "msb_test").time_since_last_check; + + FastRandomContext rand3(uint256 {0}); + + for (unsigned int i = 0; i < iterations; ++i) { + msb3(rand3.rand64()); + } + + int64_t msb3_test_time = g_timer.GetTimes(strprintf("msb3 %u iterations", iterations), "msb_test").time_since_last_check; + + // The execution time of the above on a 13900K is + + // INFO: GetTimes: timer msb_test: msb 10000000 iterations: elapsed time: 86 ms, time since last check: 86 ms. + // INFO: GetTimes: timer msb_test: msb2 10000000 iterations: elapsed time: 166 ms, time since last check: 80 ms. + // INFO: GetTimes: timer msb_test: msb3 10000000 iterations: elapsed time: 246 ms, time since last check: 80 ms. + + // Which is almost identical. msb appears to be much slower on 32 bit architectures. + + // One can easily have T1 = k1 * O(1) and T2 = k2 * O(n) = k2 * n * O(1) where T1 > T2 for n < q if k1 > q * k2, so the O(1) + // algorithm is by no means the best choice. + + // This test makes sure that the three algorithms are within 20x of the one with the minimum execution time. If not, it will + // fail to prompt us to look at this again. + + double minimum_time = std::min(std::min(msb_test_time, msb2_test_time), msb3_test_time); + + BOOST_CHECK((double) msb_test_time / minimum_time < 20.0); + BOOST_CHECK((double) msb2_test_time / minimum_time < 20.0); + BOOST_CHECK((double) msb3_test_time / minimum_time < 20.0); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_Initialization_trivial) +{ + Fraction fraction; + + BOOST_CHECK_EQUAL(fraction.GetNumerator(), 0); + BOOST_CHECK_EQUAL(fraction.GetDenominator(), 1); + BOOST_CHECK_EQUAL(fraction.IsSimplified(), true); + BOOST_CHECK_EQUAL(fraction.IsZero(), true); + BOOST_CHECK_EQUAL(fraction.IsPositive(), false); + BOOST_CHECK_EQUAL(fraction.IsNonNegative(), true); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_Initialization_from_num_denom_already_simplified) +{ + Fraction fraction(2, 3); + + BOOST_CHECK_EQUAL(fraction.GetNumerator(), 2); + BOOST_CHECK_EQUAL(fraction.GetDenominator(), 3); + BOOST_CHECK_EQUAL(fraction.IsSimplified(), true); + BOOST_CHECK_EQUAL(fraction.IsZero(), false); + BOOST_CHECK_EQUAL(fraction.IsPositive(), true); + BOOST_CHECK_EQUAL(fraction.IsNonNegative(), true); +} + + +BOOST_AUTO_TEST_CASE(util_Fraction_Initialization_from_num_denom_not_simplified) +{ + Fraction fraction(4, 6); + + BOOST_CHECK_EQUAL(fraction.GetNumerator(), 4); + BOOST_CHECK_EQUAL(fraction.GetDenominator(), 6); + BOOST_CHECK_EQUAL(fraction.IsSimplified(), false); + BOOST_CHECK_EQUAL(fraction.IsZero(), false); + BOOST_CHECK_EQUAL(fraction.IsPositive(), true); + BOOST_CHECK_EQUAL(fraction.IsNonNegative(), true); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_Initialization_from_num_denom_with_simplification) +{ + Fraction fraction(4, 6, true); + + BOOST_CHECK_EQUAL(fraction.GetNumerator(), 2); + BOOST_CHECK_EQUAL(fraction.GetDenominator(), 3); + BOOST_CHECK_EQUAL(fraction.IsSimplified(), true); + BOOST_CHECK_EQUAL(fraction.IsZero(), false); + BOOST_CHECK_EQUAL(fraction.IsPositive(), true); + BOOST_CHECK_EQUAL(fraction.IsNonNegative(), true); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_Initialization_from_num_denom_with_simplification_neg_pos) +{ + Fraction fraction(-4, 6, true); + + BOOST_CHECK_EQUAL(fraction.GetNumerator(), -2); + BOOST_CHECK_EQUAL(fraction.GetDenominator(), 3); + BOOST_CHECK_EQUAL(fraction.IsSimplified(), true); + BOOST_CHECK_EQUAL(fraction.IsZero(), false); + BOOST_CHECK_EQUAL(fraction.IsPositive(), false); + BOOST_CHECK_EQUAL(fraction.IsNonNegative(), false); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_Initialization_from_num_denom_with_simplification_pos_neg) +{ + Fraction fraction(4, -6, true); + + BOOST_CHECK_EQUAL(fraction.GetNumerator(), -2); + BOOST_CHECK_EQUAL(fraction.GetDenominator(), 3); + BOOST_CHECK_EQUAL(fraction.IsSimplified(), true); + BOOST_CHECK_EQUAL(fraction.IsZero(), false); + BOOST_CHECK_EQUAL(fraction.IsPositive(), false); + BOOST_CHECK_EQUAL(fraction.IsNonNegative(), false); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_Initialization_from_num_denom_with_simplification_neg_neg) +{ + Fraction fraction(-4, -6, true); + + BOOST_CHECK_EQUAL(fraction.GetNumerator(), 2); + BOOST_CHECK_EQUAL(fraction.GetDenominator(), 3); + BOOST_CHECK_EQUAL(fraction.IsSimplified(), true); + BOOST_CHECK_EQUAL(fraction.IsZero(), false); + BOOST_CHECK_EQUAL(fraction.IsPositive(), true); + BOOST_CHECK_EQUAL(fraction.IsNonNegative(), true); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_Copy_Constructor) +{ + Fraction fraction(4, 6); + + Fraction fraction2(fraction); + + BOOST_CHECK_EQUAL(fraction2.GetNumerator(), 4); + BOOST_CHECK_EQUAL(fraction2.GetDenominator(), 6); + BOOST_CHECK_EQUAL(fraction2.IsSimplified(), false); + BOOST_CHECK_EQUAL(fraction.IsZero(), false); + BOOST_CHECK_EQUAL(fraction.IsPositive(), true); + BOOST_CHECK_EQUAL(fraction.IsNonNegative(), true); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_Initialization_from_int64_t) +{ + Fraction fraction((int64_t) -2); + + BOOST_CHECK_EQUAL(fraction.GetNumerator(), -2); + BOOST_CHECK_EQUAL(fraction.GetDenominator(), 1); + BOOST_CHECK_EQUAL(fraction.IsSimplified(), true); + BOOST_CHECK_EQUAL(fraction.IsZero(), false); + BOOST_CHECK_EQUAL(fraction.IsPositive(), false); + BOOST_CHECK_EQUAL(fraction.IsNonNegative(), false); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_Simplify) +{ + Fraction fraction(-4, -6); + + BOOST_CHECK_EQUAL(fraction.IsSimplified(), false); + + fraction.Simplify(); + + BOOST_CHECK_EQUAL(fraction.GetNumerator(), 2); + BOOST_CHECK_EQUAL(fraction.GetDenominator(), 3); + BOOST_CHECK_EQUAL(fraction.IsSimplified(), true); + BOOST_CHECK_EQUAL(fraction.IsZero(), false); + BOOST_CHECK_EQUAL(fraction.IsPositive(), true); + BOOST_CHECK_EQUAL(fraction.IsNonNegative(), true); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_ToDouble) +{ + Fraction fraction (1, 4); + + BOOST_CHECK_EQUAL(fraction.ToDouble(), 0.25); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_addition) +{ + Fraction lhs(2, 3); + Fraction rhs(3, 4); + + Fraction sum = lhs + rhs; + + BOOST_CHECK_EQUAL(sum.GetNumerator(), 17); + BOOST_CHECK_EQUAL(sum.GetDenominator(), 12); + BOOST_CHECK_EQUAL(sum.IsSimplified(), true); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_addition_with_internal_simplification_common_denominator) +{ + Fraction lhs(3, 10); + Fraction rhs(2, 10); + + Fraction sum = lhs + rhs; + + BOOST_CHECK_EQUAL(sum.GetNumerator(), 1); + BOOST_CHECK_EQUAL(sum.GetDenominator(), 2); + BOOST_CHECK_EQUAL(sum.IsSimplified(), true); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_addition_with_internal_simplification) +{ + Fraction lhs(3, 10); + Fraction rhs(1, 5); + + Fraction sum = lhs + rhs; + + BOOST_CHECK_EQUAL(sum.GetNumerator(), 1); + BOOST_CHECK_EQUAL(sum.GetDenominator(), 2); + BOOST_CHECK_EQUAL(sum.IsSimplified(), true); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_addition_with_internal_gcd_simplification) +{ + Fraction lhs(1, 6); + Fraction rhs(2, 15); + + // gcd(6, 15) = 3, so this really is + // + // 1 * (15/3) + 2 * (6/3) 1 * 5 + 2 * 2 3 + // ---------------------- = ------------- = -- + // 3 * (6/3) * (15/3) 3 * 2 * 5 10 + + Fraction sum = lhs + rhs; + + BOOST_CHECK_EQUAL(sum.GetNumerator(), 3); + BOOST_CHECK_EQUAL(sum.GetDenominator(), 10); + BOOST_CHECK_EQUAL(sum.IsSimplified(), true); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_subtraction) +{ + Fraction lhs(2, 3); + Fraction rhs(3, 4); + + Fraction difference = lhs - rhs; + + BOOST_CHECK_EQUAL(difference.GetNumerator(), -1); + BOOST_CHECK_EQUAL(difference.GetDenominator(), 12); + BOOST_CHECK_EQUAL(difference.IsSimplified(), true); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_subtraction_with_internal_simplification) +{ + Fraction lhs(2, 10); + Fraction rhs(7, 10); + + Fraction difference = lhs - rhs; + + BOOST_CHECK_EQUAL(difference.GetNumerator(), -1); + BOOST_CHECK_EQUAL(difference.GetDenominator(), 2); + BOOST_CHECK_EQUAL(difference.IsSimplified(), true); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_multiplication_with_internal_simplification) +{ + Fraction lhs(-2, 3); + Fraction rhs(3, 4); + + Fraction product = lhs * rhs; + + BOOST_CHECK_EQUAL(product.GetNumerator(), -1); + BOOST_CHECK_EQUAL(product.GetDenominator(), 2); + BOOST_CHECK_EQUAL(product.IsSimplified(), true); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_multiplication_with_cross_simplification_overflow_resistance) +{ + + Fraction lhs(std::numeric_limits::max() - 3, std::numeric_limits::max() - 1, false); + Fraction rhs((std::numeric_limits::max() - 1) / (int64_t) 2, (std::numeric_limits::max() - 3) / (int64_t) 2); + + Fraction product; + + // This should NOT overflow + bool overflow = false; + try { + product = lhs * rhs; + } catch (std::overflow_error& e) { + overflow = true; + } + + BOOST_CHECK_EQUAL(overflow, false); + + if (!overflow) { + BOOST_CHECK(product == Fraction(1)); + } +} + +BOOST_AUTO_TEST_CASE(util_Fraction_division_with_internal_simplification) +{ + Fraction lhs(-2, 3); + Fraction rhs(4, 3); + + Fraction quotient = lhs / rhs; + + BOOST_CHECK_EQUAL(quotient.GetNumerator(), -1); + BOOST_CHECK_EQUAL(quotient.GetDenominator(), 2); + BOOST_CHECK_EQUAL(quotient.IsSimplified(), true); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_self_addition_with_internal_simplification) +{ + Fraction fraction(3, 10); + + fraction += Fraction(2, 10); + + BOOST_CHECK_EQUAL(fraction.GetNumerator(), 1); + BOOST_CHECK_EQUAL(fraction.GetDenominator(), 2); + BOOST_CHECK_EQUAL(fraction.IsSimplified(), true); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_self_subtraction_with_internal_simplification) +{ + Fraction fraction(7, 10); + + fraction -= Fraction(2, 10); + + BOOST_CHECK_EQUAL(fraction.GetNumerator(), 1); + BOOST_CHECK_EQUAL(fraction.GetDenominator(), 2); + BOOST_CHECK_EQUAL(fraction.IsSimplified(), true); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_self_multiplication_with_internal_simplification) +{ + Fraction fraction(-2, 3); + + fraction *= Fraction(3, 4); + + BOOST_CHECK_EQUAL(fraction.GetNumerator(), -1); + BOOST_CHECK_EQUAL(fraction.GetDenominator(), 2); + BOOST_CHECK_EQUAL(fraction.IsSimplified(), true); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_self_division_with_internal_simplification) +{ + Fraction fraction(-2, 3); + + fraction /= Fraction(4, 3); + + BOOST_CHECK_EQUAL(fraction.GetNumerator(), -1); + BOOST_CHECK_EQUAL(fraction.GetDenominator(), 2); + BOOST_CHECK_EQUAL(fraction.IsSimplified(), true); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_multiplication_by_zero_Fraction) +{ + Fraction lhs(-2, 3); + Fraction rhs(0); + + Fraction product = lhs * rhs; + + BOOST_CHECK_EQUAL(product.GetNumerator(), 0); + BOOST_CHECK_EQUAL(product.GetDenominator(), 1); + BOOST_CHECK_EQUAL(product.IsSimplified(), true); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_division_by_zero_Fraction) +{ + Fraction lhs(-2, 3); + Fraction rhs(0); + + std::string err; + + try { + Fraction quotient = lhs / rhs; + } catch (std::out_of_range& e) { + err = e.what(); + } + + BOOST_CHECK_EQUAL(err, "denominator specified is zero"); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_division_by_zero_int64_t) +{ + Fraction lhs(-2, 3); + int64_t rhs = 0; + + std::string err; + + try { + Fraction quotient = lhs / rhs; + } catch (std::out_of_range& e) { + err = e.what(); + } + + BOOST_CHECK_EQUAL(err, std::string{"denominator specified is zero"}); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_multiplication_overflow_1) +{ + Fraction lhs((int64_t) 1 << 30, 1); + Fraction rhs((int64_t) 1 << 31, 1); + + LogPrintf("INFO: %s: msb((int64_t) 1 << 30) = %i", __func__, msb3((int64_t) 1 << 30)); + LogPrintf("INFO: %s: msb((int64_t) 1 << 31) = %i", __func__, msb3((int64_t) 1 << 31)); + + std::string err; + + try { + Fraction product = lhs * rhs; + } catch (std::overflow_error& e) { + err = e.what(); + } + + BOOST_CHECK_EQUAL(err, std::string {}); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_multiplication_overflow_2) +{ + Fraction lhs(((int64_t) 1 << 31) - 1, 1); + Fraction rhs(((int64_t) 1 << 31) - 1, 1); + + std::string err; + + try { + Fraction product = lhs * rhs; + } catch (std::overflow_error& e) { + err = e.what(); + } + + BOOST_CHECK_EQUAL(err, std::string {""}); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_multiplication_overflow_3) +{ + Fraction lhs((int64_t) 1 << 31, 1); + Fraction rhs((int64_t) 1 << 31, 1); + + std::string err; + + try { + Fraction product = lhs * rhs; + } catch (std::overflow_error& e) { + err = e.what(); + } + + BOOST_CHECK_EQUAL(err, std::string {"fraction multiplication results in an overflow"}); +} + + +BOOST_AUTO_TEST_CASE(util_Fraction_addition_overflow_1) +{ + Fraction lhs(std::numeric_limits::max() / 2, 1); + Fraction rhs(std::numeric_limits::max() / 2 + 1, 1); + + std::string err; + + try { + Fraction addition = lhs + rhs; + } catch (std::overflow_error& e) { + err = e.what(); + } + + BOOST_CHECK_EQUAL(err, std::string {}); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_addition_overflow_2) +{ + Fraction lhs(std::numeric_limits::max() / 2 + 1, 1); + Fraction rhs(std::numeric_limits::max() / 2 + 1, 1); + + std::string err; + + try { + Fraction addition = lhs + rhs; + } catch (std::overflow_error& e) { + err = e.what(); + } + + BOOST_CHECK_EQUAL(err, std::string {"fraction addition of a + b where a > 0 and b > 0 results in an overflow"}); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_addition_overflow_3) +{ + Fraction lhs(-(std::numeric_limits::max() / 2 + 1), 1); + Fraction rhs(-(std::numeric_limits::max() / 2 + 1), 1); + + std::string err; + + try { + Fraction addition = lhs + rhs; + } catch (std::overflow_error& e) { + err = e.what(); + } + + BOOST_CHECK_EQUAL(err, std::string {}); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_addition_overflow_4) +{ + Fraction lhs(-(std::numeric_limits::max() / 2 + 1), 1); + Fraction rhs(-(std::numeric_limits::max() / 2 + 2), 1); + + std::string err; + + try { + Fraction addition = lhs + rhs; + } catch (std::overflow_error& e) { + err = e.what(); + } + + BOOST_CHECK_EQUAL(err, std::string {"fraction addition of a + b where a < 0 and b < 0 results in an overflow"}); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_equal) +{ + BOOST_CHECK_EQUAL(Fraction(1, 2) == Fraction(2, 4), true); + BOOST_CHECK_EQUAL(Fraction(-1, 2) == Fraction(1, -2), true); + BOOST_CHECK_EQUAL(Fraction(-1, 2) == Fraction(1, 2), false); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_not_equal) +{ + BOOST_CHECK_EQUAL(Fraction(1, 2) != Fraction(2, 4), false); + BOOST_CHECK_EQUAL(Fraction(-1, 2) != Fraction(1, -2), false); + BOOST_CHECK_EQUAL(Fraction(-1, 2) != Fraction(1, 2), true); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_less_than_or_equal) +{ + BOOST_CHECK_EQUAL(Fraction(3, 4) <= Fraction(4, 5), true); + BOOST_CHECK_EQUAL(Fraction(3, 4) <= Fraction(6, 8), true); + BOOST_CHECK_EQUAL(Fraction(3, 4) <= Fraction(2, 3), false); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_greater_than_or_equal) +{ + BOOST_CHECK_EQUAL(Fraction(4, 5) >= Fraction(3, 4), true); + BOOST_CHECK_EQUAL(Fraction(6, 8) >= Fraction(3, 4), true); + BOOST_CHECK_EQUAL(Fraction(2, 3) >= Fraction(3, 4), false); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_less_than) +{ + BOOST_CHECK_EQUAL(Fraction(3, 4) < Fraction(4, 5), true); + BOOST_CHECK_EQUAL(Fraction(3, 4) < Fraction(6, 8), false); + BOOST_CHECK_EQUAL(Fraction(3, 4) < Fraction(2, 3), false); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_greater_than) +{ + BOOST_CHECK_EQUAL(Fraction(4, 5) > Fraction(3, 4), true); + BOOST_CHECK_EQUAL(Fraction(6, 8) > Fraction(3, 4), false); + BOOST_CHECK_EQUAL(Fraction(2, 3) > Fraction(3, 4), false); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_logic_negation) +{ + BOOST_CHECK_EQUAL(!Fraction(1, 2), false); + BOOST_CHECK_EQUAL(!Fraction(-1, 2), false); + BOOST_CHECK_EQUAL(!Fraction(), true); +} + +BOOST_AUTO_TEST_CASE(util_Fraction_ToString) +{ + Fraction fraction(123, 10000); + + BOOST_CHECK_EQUAL(fraction.IsSimplified(), true); + BOOST_CHECK_EQUAL(fraction.ToString(),"123/10000"); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/util.h b/src/util.h index 43480785ee..66288783ad 100644 --- a/src/util.h +++ b/src/util.h @@ -6,11 +6,13 @@ #ifndef BITCOIN_UTIL_H #define BITCOIN_UTIL_H +#include "arith_uint256.h" #include "uint256.h" #include "fwd.h" #include "hash.h" #include +#include #include #include #include @@ -160,27 +162,118 @@ inline int64_t abs64(int64_t n) return (n >= 0 ? n : -n); } -// Small class to represent fractions. We could do more sophisticated things like reduction using GCD, and overloaded -// multiplication, but we don't need it, because this is used in very limited places, and we actually in many of the -// algorithms where this needs to be used need to carefully control the order of multiplication and division using the -// numerator and denominator. +//! +//! \brief Class to represent fractions and common fraction operations with built in simplification. This supports integer operations +//! for consensus critical code where floating point would cause problems across different architectures and/or compiler +//! implementations. +//! +//! In particular this class is used for sidestake allocations, both the allocation "percentage", and the CAmount allocations +//! resulting from muliplying the allocation (fraction) times the CAmount rewards. +//! class Fraction { public: - Fraction() {} - + //! + //! \brief Trivial zero fraction constructor + //! + Fraction() + : m_numerator(0) + , m_denominator(1) + , m_simplified(true) + {} + + //! + //! \brief Copy constructor + //! + //! \param Fraction f + //! + Fraction(const Fraction& f) + : Fraction(f.GetNumerator(), f.GetDenominator()) + {} + + //! + //! \brief Constructor with simplification boolean directive + //! + //! \param Fraction f + //! \param boolean simplify + //! + Fraction(const Fraction& f, const bool& simplify) + : Fraction(f.GetNumerator(), f.GetDenominator(), simplify) + {} + + //! + //! \brief Constructor from numerator and denominator + //! + //! \param in64t_t numerator + //! \param int64_t denominator + //! Fraction(const int64_t& numerator, const int64_t& denominator) : m_numerator(numerator) , m_denominator(denominator) + , m_simplified(false) { if (m_denominator == 0) { throw std::out_of_range("denominator specified is zero"); } + + if (std::gcd(m_numerator, m_denominator) == 1 && m_denominator > 0) { + m_simplified = true; + } + } + + //! + //! \brief Constructor from numerator and denominator with simplification boolean directive + //! + //! \param int64_t numerator + //! \param int64_t denominator + //! \param boolean simplify + //! + Fraction(const int64_t& numerator, + const int64_t& denominator, + const bool& simplify) + : Fraction(numerator, denominator) + { + if (!m_simplified && simplify) { + Simplify(); + } + } + + ~Fraction() + {} + + //! + //! \brief Constructor from input int64_t integer (i.e. denominator = 1). + //! + //! \param numerator + //! + Fraction(const int64_t& numerator) + : Fraction(numerator, 1) + {} + + bool IsZero() const + { + // The denominator cannot be zero by construction rules. + return m_numerator == 0; + } + + bool IsNonZero() const + { + return !IsZero(); + } + + bool IsPositive() const + { + return (m_denominator > 0 && m_numerator > 0) || (m_denominator < 0 && m_numerator < 0); + } + + bool IsNonNegative() const + { + return IsPositive() || IsZero(); } - bool isNonZero() + bool IsNegative() const { - return m_denominator != 0 && m_numerator != 0; + return !IsNonNegative(); } constexpr int64_t GetNumerator() const @@ -193,9 +286,418 @@ class Fraction { return m_denominator; } + bool IsSimplified() const + { + return m_simplified; + } + + void Simplify() + { + // Check whether already simplified, if so, nothing to do. + if (m_simplified) { + return; + } + + // Nice that we are at C++17! :) + int64_t gcd = std::gcd(m_numerator, m_denominator); + + // If both numerator and denominator are negative, + // change the sign of gcd to flip both to positive. + if (m_numerator < 0 && m_denominator < 0) { + gcd = -gcd; + } + + m_numerator = m_numerator / gcd; + m_denominator = m_denominator / gcd; + + // Since the case where both are less than zero has already been changed to +/+, + // If we have m_denominator < 0, we must have m_numerator >= 0. So move the negative + // sign to the numerator and make the denominator positive. This simplifies the equality + // comparison. + if (m_denominator < 0) { + m_denominator = -m_denominator; + m_numerator = -m_numerator; + } + + m_simplified = true; + } + + double ToDouble() const + { + return (double) m_numerator / (double) m_denominator; + } + + Fraction operator=(const Fraction& rhs) + { + m_numerator = rhs.GetNumerator(); + m_denominator = rhs.GetDenominator(); + + return *this; + } + + std::string ToString() const + { + return strprintf("%" PRId64 "/" "%" PRId64, m_numerator, m_denominator); + } + + bool operator!() + { + return IsZero(); + } + + Fraction operator+(const Fraction& rhs) const + { + Fraction slhs(*this, true); + Fraction srhs(rhs, true); + + // If the same denominator (and remember these are already reduced to simplest form) just add the numerators and put + // over the common denominator... + if (slhs.GetDenominator() == srhs.GetDenominator()) { + return Fraction(overflow_add(slhs.GetNumerator(), srhs.GetNumerator()), slhs.GetDenominator(), true); + } + + // Now the more complex case. In general, fraction addition follows this pattern: + // + // a c a * (d/g) + c * (b/g) + // - + - , g = gcd(b, d) => --------------------- where {(b/g), (d/g)} will be elements of the counting numbers. + // b d g * (b/g) * (d/g) + // + // (b/g) and (d/g) are divisible with no remainders precisely because of the definition of gcd. + // + // We have already covered the trivial common denominator case above before bothering to compute the gcd of the + // denominator. + int64_t denom_gcd = std::gcd(slhs.GetDenominator(), srhs.GetDenominator()); + + // We have two special cases. One is where g = b (i.e. d is actually a multiple of b). In this case, + // the expression simplifies to + // + // a * (d/b) + c + // ------------- + // d + if (denom_gcd == slhs.GetDenominator()) { + return Fraction(overflow_add(overflow_mult(slhs.GetNumerator(), srhs.GetDenominator() / slhs.GetDenominator()), + srhs.GetNumerator()), + srhs.GetDenominator(), + true); + } + + // The other is where g = d (i.e. b is actually a multiple of d). In this case, + // the expression simplifies to + // + // a + c * (b/d) + // ------------- + // b + if (denom_gcd == srhs.GetDenominator()) { + return Fraction(overflow_add(overflow_mult(srhs.GetNumerator(), slhs.GetDenominator() / srhs.GetDenominator()), + slhs.GetNumerator()), + slhs.GetDenominator(), + true); + } + + // Otherwise do the full pattern of getting a common denominator (pulling out the gcd of the denominators), + // and adding, then simplify... + // + // This approach is more complex than + // + // a * d + c * b + // ------------- + // b * d + // + // but has the advantage of being more resistant to overflows, especially when the two denominators are related by a large + // gcd. In particular in Gridcoin's application with Allocations, the largest denominator of the allocations is 10000, so + // every allocation denominator in reduced form must be divisible evenly into 10000. This means the majority of fraction + // additions will be the two simpler cases above. + return Fraction(overflow_add(overflow_mult(slhs.GetNumerator(), srhs.GetDenominator() / denom_gcd), + overflow_mult(srhs.GetNumerator(), slhs.GetDenominator() / denom_gcd)), + overflow_mult(denom_gcd, overflow_mult(slhs.GetDenominator() / denom_gcd, srhs.GetDenominator() / denom_gcd)), + true); + } + + Fraction operator+(const int64_t& rhs) const + { + Fraction slhs(*this, true); + + return Fraction(overflow_add(slhs.GetNumerator(), overflow_mult(slhs.GetDenominator(), rhs)), slhs.GetDenominator(), true); + } + + Fraction operator-(const Fraction& rhs) const + { + return (*this + Fraction(-rhs.GetNumerator(), rhs.GetDenominator())); + } + + Fraction operator-(const int64_t& rhs) const + { + return (*this + -rhs); + } + + Fraction operator*(const Fraction& rhs) const + { + Fraction slhs(*this, true); + Fraction srhs(rhs, true); + + // Gcd's can be used in multiplication for better overflow resistance as well. + // + // Consider + // a c + // - * -, where a/b and c/d are already simplified (i.e. gcd(a, b) = gcd(c, d) = 1. + // b d + // + // We can have g = gcd(a, d) and h = gcd(c, b), which is with the numerators reversed, since multiplication is + // commutative. This means we have + // + // (c / h) (a / g) + // ------- * ------- . + // (b / h) (d / g) + // + // If we form Fraction(c, b, true) and Fraction(a, d, true), the simplification will determine and divide the numerator and + // denominator by h and g respectively. + // + // A specific example is instructive. + // + // 1998 1000 999 1000 1000 999 1 1 + // ---- * ---- = ---- * ---- = ---- * --- = - * - + // 2000 999 1000 999 1000 999 1 1 + // + // This is a formal form of what grade school teachers called factor cancellation. :). + + Fraction sxlhs(srhs.GetNumerator(), slhs.GetDenominator(), true); + Fraction sxrhs(slhs.GetNumerator(), srhs.GetDenominator(), true); + + return Fraction(overflow_mult(sxlhs.GetNumerator(), sxrhs.GetNumerator()), + overflow_mult(sxlhs.GetDenominator(), sxrhs.GetDenominator()), + true); + } + + Fraction operator*(const int64_t& rhs) const + { + Fraction slhs(*this, true); + + return Fraction(overflow_mult(slhs.GetNumerator(), rhs), slhs.GetDenominator(), true); + } + + Fraction operator/(const Fraction& rhs) const + { + return (*this * Fraction(rhs.GetDenominator(), rhs.GetNumerator())); + } + + Fraction operator/(const int64_t& rhs) const + { + Fraction slhs(*this, true); + + return Fraction(slhs.GetNumerator(), overflow_mult(slhs.GetDenominator(), rhs), true); + } + + Fraction operator+=(const Fraction& rhs) + { + Simplify(); + + *this = *this + rhs; + + return *this; + } + + Fraction operator+=(const int64_t& rhs) + { + Simplify(); + + *this = *this + rhs; + + return *this; + } + + Fraction operator-=(const Fraction& rhs) + { + Simplify(); + + *this = *this - rhs; + + return *this; + } + + Fraction operator-=(const int64_t& rhs) + { + Simplify(); + + *this = *this - rhs; + + return *this; + } + + Fraction operator*=(const Fraction& rhs) + { + Simplify(); + + *this = *this * rhs; + + return *this; + } + + Fraction operator*=(const int64_t& rhs) + { + Simplify(); + + *this = *this * rhs; + + return *this; + } + + Fraction operator/=(const Fraction& rhs) + { + Simplify(); + + *this = *this / rhs; + + return *this; + } + + Fraction operator/=(const int64_t& rhs) + { + Simplify(); + + *this = *this / rhs; + + return *this; + } + + bool operator==(const Fraction& rhs) const + { + Fraction slhs(*this, true); + Fraction srhs(rhs, true); + + return (slhs.GetNumerator() == srhs.GetNumerator() && slhs.GetDenominator() == slhs.GetDenominator()); + } + + bool operator!=(const Fraction& rhs) const + { + return !(*this == rhs); + } + + bool operator<=(const Fraction& rhs) const + { + return (rhs - *this).IsNonNegative(); + } + + bool operator>=(const Fraction& rhs) const + { + return (*this - rhs).IsNonNegative(); + } + + bool operator<(const Fraction& rhs) const + { + return (rhs - *this).IsPositive(); + } + + bool operator>(const Fraction& rhs) const + { + return (*this - rhs).IsPositive(); + } + + bool operator==(const int64_t& rhs) const + { + return (*this == Fraction(rhs)); + } + + bool operator!=(const int64_t& rhs) const + { + return !(*this == rhs); + } + + bool operator<=(const int64_t& rhs) const + { + return *this <= Fraction(rhs); + } + + bool operator>=(const int64_t& rhs) const + { + return *this >= Fraction(rhs); + } + + bool operator<(const int64_t& rhs) const + { + return *this < Fraction(rhs); + } + + bool operator>(const int64_t& rhs) const + { + return *this > Fraction(rhs); + } + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) + { + READWRITE(m_numerator); + READWRITE(m_denominator); + READWRITE(m_simplified); + } + private: - int64_t m_numerator = 0; - int64_t m_denominator = 1; + int msb(const int64_t& n) const + { + int64_t abs_n = std::abs(n); + + int index = 0; + + for (; index <= 63; ++index) { + if (abs_n >> index == 0) { + break; + } + } + + return index; + } + + int64_t overflow_mult(const int64_t& a, const int64_t& b) const + { + if (a == 0 || b == 0) { + return 0; + } + + // A 64-bit integer with the lower 32 bits filled has value 2^32 - 1. Multiplying two of these, a * b, together + // is (2^32 - 1) * (2^32 - 1) = 2^64 - 2^33 + 1 > 2^63. Log2(2^63) = msb(a) + msb(b) - 1. So a quick overflow limit... + + if (msb(a) + msb(b) > 63) { + throw std::overflow_error("fraction multiplication results in an overflow"); + } + + return a * b; + } + + int64_t overflow_add(const int64_t& a, const int64_t& b) const + { + if (a == 0) { + return b; + } + + if (b == 0) { + return a; + } + + if (a > 0 && b > 0) { + if (a <= std::numeric_limits::max() - b) { + return a + b; + } else { + throw std::overflow_error("fraction addition of a + b where a > 0 and b > 0 results in an overflow"); + } + } + + if (a < 0 && b < 0) { + // Remember b is negative here, so the difference below is GREATER than std::numeric_limits::min(). + if (a >= std::numeric_limits::min() - b) { + return a + b; + } else { + throw std::overflow_error("fraction addition of a + b where a < 0 and b < 0 results in an overflow"); + } + } + + // The only thing left is that a and b are opposite in sign, so addition cannot overflow. + return a + b; + } + + int64_t m_numerator; + int64_t m_denominator; + bool m_simplified; }; inline std::string leftTrim(std::string src, char chr) diff --git a/src/util/strencodings.cpp b/src/util/strencodings.cpp index 34fe5f2aba..db89881f26 100644 --- a/src/util/strencodings.cpp +++ b/src/util/strencodings.cpp @@ -21,6 +21,7 @@ static const std::string SAFE_CHARS[] = CHARS_ALPHA_NUM + " .,;-_?@", // SAFE_CHARS_UA_COMMENT CHARS_ALPHA_NUM + ".-_", // SAFE_CHARS_FILENAME CHARS_ALPHA_NUM + "!*'();:@&=+$,/?#[]-_.~%", // SAFE_CHARS_URI + CHARS_ALPHA_NUM + " .-_" // SAFE_CHARS_CSV }; std::string SanitizeString(const std::string& str, int rule) diff --git a/src/util/strencodings.h b/src/util/strencodings.h index 1671bd1f9a..1e8834d8e5 100644 --- a/src/util/strencodings.h +++ b/src/util/strencodings.h @@ -28,6 +28,7 @@ enum SafeChars SAFE_CHARS_UA_COMMENT, //!< BIP-0014 subset SAFE_CHARS_FILENAME, //!< Chars allowed in filenames SAFE_CHARS_URI, //!< Chars allowed in URIs (RFC 3986) + SAFE_CHARS_CSV //!< Chars allowed in fields stored as comma separated values }; /** diff --git a/src/util/system.cpp b/src/util/system.cpp index 8832e5e493..e31bc297b7 100644 --- a/src/util/system.cpp +++ b/src/util/system.cpp @@ -3,6 +3,7 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or https://opensource.org/licenses/mit-license.php. +#include "node/ui_interface.h" #include #include #include @@ -1154,6 +1155,9 @@ bool updateRwSetting(const std::string& name, const util::SettingsValue& value) settings.rw_settings[name] = value; } }); + + uiInterface.RwSettingsUpdated(); + return gArgs.WriteSettingsFile(); } @@ -1169,6 +1173,9 @@ bool updateRwSettings(const std::vector= 13) ? 4 : 0; +} + Fraction FoundationSideStakeAllocation() { // Note that the 4/5 (80%) for mainnet was approved by a validated poll, @@ -651,7 +657,7 @@ unsigned int GetMRCOutputLimit(const int& block_version, bool include_foundation // in the returned limit) AND the foundation sidestake allocation is greater than zero, then reduce the reported // output limit by 1. If the foundation sidestake allocation is zero, then there will be no foundation sidestake // output, so the output_limit should be as above. If the output limit was already zero then it remains zero. - if (!include_foundation_sidestake && FoundationSideStakeAllocation().isNonZero() && output_limit) { + if (!include_foundation_sidestake && FoundationSideStakeAllocation().IsNonZero() && output_limit) { --output_limit; } @@ -766,7 +772,8 @@ class ClaimValidator bool CheckReward(const CAmount& research_owed, CAmount& out_stake_owed, const CAmount& mrc_staker_fees_owed, const CAmount& mrc_fees, - const CAmount& mrc_rewards, const unsigned int& mrc_non_zero_outputs) const + const CAmount& mrc_rewards, const unsigned int& mrc_non_zero_outputs, + std::string& error_out) const { out_stake_owed = GRC::GetProofOfStakeReward(m_coin_age, m_block.nTime, m_pindex); @@ -774,6 +781,8 @@ class ClaimValidator // For block version 11, mrc_fees_owed and mrc_rewards are both zero, and there are no MRC outputs, so this is // the only check necessary. if (m_total_claimed > research_owed + out_stake_owed + m_fees + mrc_fees + mrc_rewards) { + error_out = "Claim too high"; + return error("%s: CheckReward FAILED: m_total_claimed of %s > %s = research_owed %s + out_stake_owed %s + m_fees %s + " "mrc_fees %s + mrc_rewards = %s", __func__, @@ -799,7 +808,7 @@ class ClaimValidator // sidestake even though there will not be a corresponding mrc rewards output. (Zero value outputs are // suppressed because that is wasteful. bool foundation_mrc_sidestake_present = (m_claim.m_mrc_tx_map.size() - && FoundationSideStakeAllocation().isNonZero()) ? true : false; + && FoundationSideStakeAllocation().IsNonZero()) ? true : false; // If there is no mrc, then this is coinstake.vout.size() - 0 - 0, which is one beyond the last coinstake // element. @@ -823,6 +832,8 @@ class ClaimValidator } if (total_owed_to_staker > research_owed + out_stake_owed + m_fees + mrc_staker_fees_owed) { + error_out = "Total owed to staker too high"; + return error("%s: FAILED: total_owed_to_staker of %s > %s = research_owed %s + out_stake_owed %s + " "mrc_fees %s + mrc_rewards = %s", __func__, @@ -835,11 +846,120 @@ class ClaimValidator ); } + // For block version 13 and higher, check to ensure that mandatory sidestakes appear as outputs with the correct + // allocations. + if (m_block.nVersion >= 13) { + // Record the base coinstake destination. + CTxDestination coinstake_destination; + ExtractDestination(m_block.vtx[1].vout[1].scriptPubKey, coinstake_destination); + + // Get mandatory sidestakes + std::vector mandatory_sidestakes + = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(GRC::SideStake::FilterFlag::MANDATORY, false); + + // This is exactly the same as the dust elimination in the SplitCoinStakeOutput function in the miner, with + // the addition that a mandatory sidestake that is degenerate, i.e. eliminated by the miner because it is + // to an address that staked the coinstake (i.e. local to the staker's wallet), in favor of simply returning + // the funds back to the staker on the coinstake return, is also removed from the vector here. + for (auto iter = mandatory_sidestakes.begin(); iter != mandatory_sidestakes.end();) { + if (iter->get()->GetAllocation() * total_owed_to_staker < CENT + || iter->get()->GetDestination() == coinstake_destination) { + iter = mandatory_sidestakes.erase(iter); + } else { + ++iter; + } + } + + unsigned int validated_mandatory_sidestakes = 0; + + // Skip the empty output at index 0, stop at the index before the start of MRC's. + for (unsigned int i = 1; i < mrc_start_index; ++i) { + CTxDestination output_destination; + + if (!ExtractDestination(coinstake.vout[i].scriptPubKey, output_destination)) { + return error("%s: FAILED: coinstake output has invalid destination."); + } + + std::vector mandatory_sidestake + = GRC::GetSideStakeRegistry().TryActive(output_destination, + GRC::SideStake::FilterFlag::MANDATORY);; + + // The output is deemed to match if the destination matches AND the computed allocation matches or exceeds + // what is required by the mandatory sidestake. Note that the test uses the GRC::Allocation class, which + // extends the Fraction class, and provides comparison operators. This is now a precise calculation as it + // is integer arithmetic. + if (!mandatory_sidestake.empty()) { + CAmount actual_output = coinstake.vout[i].nValue; + + CAmount required_output = static_cast(mandatory_sidestake[0]->GetAllocation() + * total_owed_to_staker).ToCAmount(); + + if (actual_output >= required_output) { + + ++validated_mandatory_sidestakes; + } else { + error_out = "Mandatory sidestake failed validation"; + + error("%s: vout[%u] is mandatory sidestake destination %s, but failed validation: " + "actual_output = %" PRId64 ", required_output = %" PRId64, + __func__, + i, + CBitcoinAddress(output_destination).ToString(), + actual_output, + required_output); + } + } + + // This should not happen, but include the check for thoroughness. + if (validated_mandatory_sidestakes > GetMandatorySideStakeOutputLimit(m_block.nVersion)) { + error_out = "Number of mandatory sidestakes in the coinstake exceeds the protocol limit."; + + return error("%s: FAILED: The number of mandatory sidestakes in the coinstake is %u, which is above " + "the limit of %u", + __func__, + validated_mandatory_sidestakes, + GetMandatorySideStakeOutputLimit(m_block.nVersion)); + } + } + + // See the comments in SplitCoinStakeOutput regarding dust elimination in mandatory sidestake selection. Note + // that in the miner for mandatory sidestakes, the shuffle is done AFTER the dust elimination, if the number of + // residual elements is greater than the maximum allowed number of mandatory sidestakes. This leads to the + // following check. + // + // If the residual number of mandatory sidestakes after dust elimination is GREATER than or equal + // GetMandatorySideStakeOutputLimit, then number of outputs matched to mandatory sidestakes should be equal + // to GetMandatorySideStakeOutputLimit, because the shuffle in combination with the allocation lambda operating + // on non-dust outputs will result in exactly GetMandatorySideStakeOutputLimit mandatory sidestakes, which means + // it will pass above, and also pass the check below. We do not have to worry about a cutoff above + // MaxMandatorySideStakeTotalAlloc because that is handled IN ActiveSideStakeEntries, which is used as the + // starting point in the miner (and of course here). + // + // If the residual number of mandatory sidestakes after dust elimination is less than + // GetMandatorySideStakeOutputLimit, it should be equal in number to the mandatory_sidestakes size from above + // after the elimination of outputs less than 1 CENT. + // + // The combination of these constraints means that the number of validated mandatory sidestakes MUST match + // the minimum of GetMandatorySideStakeOutputLimit and mandatory_sidestakes. + if (validated_mandatory_sidestakes < std::min(GetMandatorySideStakeOutputLimit(m_block.nVersion), + mandatory_sidestakes.size())) { + error_out = "Number of mandatory sidestakes is less than required."; + + return error("%s: FAILED: The number of validated sidestakes, %u, is less than required, %u.", + __func__, + validated_mandatory_sidestakes, + std::min(GetMandatorySideStakeOutputLimit(m_block.nVersion), + mandatory_sidestakes.size())); + } + } // v13+ + // If the foundation mrc sidestake is present, we check the foundation sidestake specifically. The MRC // outputs were already checked by CheckMRCRewards. if (foundation_mrc_sidestake_present) { // The fee amount to the foundation must be correct. if (coinstake.vout[mrc_start_index].nValue != mrc_fees - mrc_staker_fees_owed) { + error_out = "MRC Foundation sidestake amount is incorrect"; + return error("%s: FAILED: foundation output value of %s != mrc_fees %s - " "mrc_staker_fees_owed %s", __func__, @@ -854,11 +974,15 @@ class ClaimValidator // The foundation sidestake destination must be able to be extracted. if (!ExtractDestination(coinstake.vout[mrc_start_index].scriptPubKey, foundation_sidestake_destination)) { + error_out = "MRC Foundation sidestake destination is invalid"; + return error("%s: FAILED: foundation MRC sidestake destination not valid", __func__); } // The sidestake destination must match that specified by FoundationSideStakeAddress(). if (foundation_sidestake_destination != FoundationSideStakeAddress().Get()) { + error_out = "MRC Foundation sidestake destination is incorrect."; + return error("%s: FAILED: foundation MRC sidestake destination does not match protocol", __func__); } @@ -866,7 +990,7 @@ class ClaimValidator } } // v12+ - // If we get here, we are done with v11 and v12 validation so return true. + // If we get here, we are done with v11, v12, and v13 validation so return true. return true; } //v11+ @@ -894,6 +1018,7 @@ class ClaimValidator CAmount mrc_fees = 0; CAmount out_stake_owed; unsigned int mrc_non_zero_outputs = 0; + std::string error_out; // Even if the block is staked by an investor, the claim can include MRC payments to researchers... // @@ -905,7 +1030,7 @@ class ClaimValidator return false; } - if (CheckReward(0, out_stake_owed, mrc_staker_fees, mrc_fees, mrc_rewards, mrc_non_zero_outputs)) { + if (CheckReward(0, out_stake_owed, mrc_staker_fees, mrc_fees, mrc_rewards, mrc_non_zero_outputs, error_out)) { LogPrint(BCLog::LogFlags::VERBOSE, "INFO: %s: CheckReward passed: m_total_claimed = %s, research_owed = %s, " "out_stake_owed = %s, m_fees = %s, mrc_staker_fees = %s, mrc_fees = %s, " "mrc_rewards = %s", @@ -933,12 +1058,12 @@ class ClaimValidator } return m_block.DoS(10, error( - "ConnectBlock[%s]: investor claim %s exceeds %s. Expected %s, fees %s", - __func__, - FormatMoney(m_total_claimed), - FormatMoney(out_stake_owed + m_fees), - FormatMoney(out_stake_owed), - FormatMoney(m_fees))); + "ConnectBlock[%s]: investor claim %s, expected %s, fees %: %s", + __func__, + FormatMoney(m_total_claimed), + FormatMoney(out_stake_owed), + FormatMoney(m_fees), + error_out)); } bool CheckResearcherClaim() const @@ -1093,6 +1218,7 @@ class ClaimValidator CAmount mrc_staker_fees = 0; CAmount mrc_fees = 0; unsigned int mrc_non_zero_outputs = 0; + std::string error_out; const GRC::CpidOption cpid = m_claim.m_mining_id.TryCpid(); @@ -1109,7 +1235,7 @@ class ClaimValidator } CAmount out_stake_owed; - if (CheckReward(research_owed, out_stake_owed, mrc_staker_fees, mrc_fees, mrc_rewards, mrc_non_zero_outputs)) { + if (CheckReward(research_owed, out_stake_owed, mrc_staker_fees, mrc_fees, mrc_rewards, mrc_non_zero_outputs, error_out)) { LogPrint(BCLog::LogFlags::VERBOSE, "INFO: %s: Post CheckReward: m_total_claimed = %s, research_owed = %s, " "out_stake_owed = %s, mrc_staker_fees = %s, mrc_fees = %s, mrc_rewards = %s", __func__, @@ -1130,7 +1256,7 @@ class ClaimValidator GRC::Quorum::CurrentSuperblock()); research_owed += newbie_correction; - if (CheckReward(research_owed, out_stake_owed, mrc_staker_fees, mrc_fees, mrc_rewards, mrc_non_zero_outputs)) { + if (CheckReward(research_owed, out_stake_owed, mrc_staker_fees, mrc_fees, mrc_rewards, mrc_non_zero_outputs, error_out)) { LogPrintf("WARNING: ConnectBlock[%s]: Added newbie_correction of %s to calculated research owed. " "Total calculated research with correction matches claim of %s in %s.", __func__, @@ -1146,7 +1272,7 @@ class ClaimValidator // by research age short 10-block-span pending accrual: if (fTestNet && m_block.nVersion <= 9 - && !CheckReward(0, out_stake_owed, 0, 0, 0, 0)) + && !CheckReward(0, out_stake_owed, 0, 0, 0, 0, error_out)) { LogPrintf( "WARNING: ConnectBlock[%s]: ignored bad testnet claim in %s", @@ -1166,18 +1292,19 @@ class ClaimValidator } return m_block.DoS(10, error( - "ConnectBlock[%s]: researcher claim %s exceeds %s for CPID %s. " - "Expected research %s, stake %s, fees %s. " - "Claimed research %s, stake %s", - __func__, - FormatMoney(m_total_claimed), - FormatMoney(research_owed + out_stake_owed + m_fees), - m_claim.m_mining_id.ToString(), - FormatMoney(research_owed), - FormatMoney(out_stake_owed), - FormatMoney(m_fees), - FormatMoney(m_claim.m_research_subsidy), - FormatMoney(m_claim.m_block_subsidy))); + "ConnectBlock[%s]: researcher claim %s compared to expected %s for CPID %s. " + "Expected research %s, stake %s, fees %s. " + "Claimed research %s, stake %s: %s", + __func__, + FormatMoney(m_total_claimed), + FormatMoney(research_owed + out_stake_owed + m_fees), + m_claim.m_mining_id.ToString(), + FormatMoney(research_owed), + FormatMoney(out_stake_owed), + FormatMoney(m_fees), + FormatMoney(m_claim.m_research_subsidy), + FormatMoney(m_claim.m_block_subsidy), + error_out)); } // Cf. CreateMRCRewards which is this method's conjugate. Note the parameters are out parameters. @@ -1934,6 +2061,7 @@ bool AcceptBlock(CBlock& block, bool generated_by_me) EXCLUSIVE_LOCKS_REQUIRED(c || (IsV10Enabled(nHeight) && block.nVersion < 10) || (IsV11Enabled(nHeight) && block.nVersion < 11) || (IsV12Enabled(nHeight) && block.nVersion < 12) + || (IsV13Enabled(nHeight) && block.nVersion < 13) ) { return block.DoS(20, error("%s: reject too old nVersion = %d", __func__, block.nVersion)); } else if ((!IsProtocolV2(nHeight) && block.nVersion >= 7) @@ -1942,6 +2070,7 @@ bool AcceptBlock(CBlock& block, bool generated_by_me) EXCLUSIVE_LOCKS_REQUIRED(c || (!IsV10Enabled(nHeight) && block.nVersion >= 10) || (!IsV11Enabled(nHeight) && block.nVersion >= 11) || (!IsV12Enabled(nHeight) && block.nVersion >= 12) + || (!IsV13Enabled(nHeight) && block.nVersion >= 13) ) { return block.DoS(100, error("%s: reject too new nVersion = %d", __func__, block.nVersion)); } diff --git a/src/validation.h b/src/validation.h index 86ff2421ce..3564409a2e 100644 --- a/src/validation.h +++ b/src/validation.h @@ -109,6 +109,7 @@ bool AcceptBlock(CBlock& block, bool generated_by_me); bool CheckBlockSignature(const CBlock& block); unsigned int GetCoinstakeOutputLimit(const int& block_version); +unsigned int GetMandatorySideStakeOutputLimit(const int& block_version); Fraction FoundationSideStakeAllocation(); CBitcoinAddress FoundationSideStakeAddress(); unsigned int GetMRCOutputLimit(const int& block_version, bool include_foundation_sidestake); diff --git a/src/wallet/diagnose.h b/src/wallet/diagnose.h index 4df1e972ea..f931fc45e1 100644 --- a/src/wallet/diagnose.h +++ b/src/wallet/diagnose.h @@ -14,6 +14,7 @@ #include "net.h" #include "util.h" #include +#include #include #include #include @@ -147,7 +148,7 @@ class Diagnose m_hasEligibleProjects = researcher->Id().Which() == GRC::MiningId::Kind::CPID; m_hasPoolProjects = researcher->Projects().ContainsPool(); - m_researcher_mode = !(configured_for_investor_mode || (!m_hasEligibleProjects && m_hasPoolProjects)); + m_researcher_mode = !(configured_for_investor_mode || (!m_hasEligibleProjects && !m_hasPoolProjects)); } /** @@ -532,15 +533,16 @@ class VerifyCPIDHasRAC : public Diagnose */ const GRC::BeaconRegistry& beacons = GRC::GetBeaconRegistry(); - const GRC::CpidOption cpid = GRC::Researcher::Get()->Id().TryCpid(); - if (const GRC::BeaconOption beacon = beacons.Try(*cpid)) { - if (!beacon->Expired(GetAdjustedTime())) { - return true; - } - for (const auto& beacon_ptr : beacons.FindPending(*cpid)) { - if (!beacon_ptr->Expired(GetAdjustedTime())) { + if (const GRC::CpidOption cpid = GRC::Researcher::Get()->Id().TryCpid()) { + if (const GRC::BeaconOption beacon = beacons.Try(*cpid)) { + if (!beacon->Expired(GetAdjustedTime())) { return true; } + for (const auto& beacon_ptr : beacons.FindPending(*cpid)) { + if (!beacon_ptr->Expired(GetAdjustedTime())) { + return true; + } + } } } return false; diff --git a/test/lint/lint-includes.sh b/test/lint/lint-includes.sh index efcb72f57a..2c7a56f5bc 100755 --- a/test/lint/lint-includes.sh +++ b/test/lint/lint-includes.sh @@ -57,6 +57,7 @@ EXPECTED_BOOST_INCLUDES=( boost/algorithm/string/predicate.hpp boost/algorithm/string/replace.hpp boost/algorithm/string/split.hpp + boost/array.hpp boost/asio.hpp boost/asio/ip/udp.hpp boost/asio/ip/v6_only.hpp