From aecd15ef19da9992106bb01ebb5d614f43837218 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Thu, 28 Sep 2023 17:59:54 -0400 Subject: [PATCH 01/47] Initial skeletons for SideStake class --- src/CMakeLists.txt | 1 + src/Makefile.am | 2 + src/gridcoin/sidestake.cpp | 104 +++++++++++++++++++++++++++ src/gridcoin/sidestake.h | 141 +++++++++++++++++++++++++++++++++++++ 4 files changed, 248 insertions(+) create mode 100644 src/gridcoin/sidestake.cpp create mode 100644 src/gridcoin/sidestake.h 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/gridcoin/sidestake.cpp b/src/gridcoin/sidestake.cpp new file mode 100644 index 0000000000..4301ca53a6 --- /dev/null +++ b/src/gridcoin/sidestake.cpp @@ -0,0 +1,104 @@ +// Copyright (c) 2014-2023 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" + +using namespace GRC; + +// ----------------------------------------------------------------------------- +// Class: SideStake +// ----------------------------------------------------------------------------- +SideStake::SideStake() + : m_address() + , m_allocation() + , m_timestamp(0) + , m_hash() + , m_previous_hash() + , m_status(SideStakeStatus::UNKNOWN) +{} + +SideStake::SideStake(CBitcoinAddress address, double allocation) + : m_address(address) + , m_allocation(allocation) + , m_timestamp(0) + , m_hash() + , m_previous_hash() + , m_status(SideStakeStatus::UNKNOWN) +{} + +SideStake::SideStake(CBitcoinAddress address, double allocation, int64_t timestamp, uint256 hash) + : m_address(address) + , m_allocation(allocation) + , m_timestamp(timestamp) + , m_hash(hash) + , m_previous_hash() + , m_status(SideStakeStatus::UNKNOWN) +{} + +bool SideStake::WellFormed() const +{ + return m_address.IsValid() && m_allocation >= 0.0 && m_allocation <= 1.0; +} + +std::pair SideStake::KeyValueToString() const +{ + return std::make_pair(m_address.ToString(), StatusToString()); +} + +std::string SideStake::StatusToString() const +{ + return StatusToString(m_status.Value()); +} + +std::string SideStake::StatusToString(const SideStakeStatus& status, const bool& translated) const +{ + if (translated) { + switch(status) { + case SideStakeStatus::UNKNOWN: return _("Unknown"); + case SideStakeStatus::ACTIVE: return _("Active"); + case SideStakeStatus::INACTIVE: return _("Inactive"); + case SideStakeStatus::DELETED: return _("Deleted"); + case SideStakeStatus::MANDATORY: return _("Mandatory"); + case SideStakeStatus::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 SideStakeStatus::UNKNOWN: return "Unknown"; + case SideStakeStatus::ACTIVE: return "Active"; + case SideStakeStatus::INACTIVE: return "Inactive"; + case SideStakeStatus::DELETED: return "Deleted"; + case SideStakeStatus::MANDATORY: return "Mandatory"; + case SideStakeStatus::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 SideStake::operator==(SideStake b) +{ + bool result = true; + + result &= (m_address == b.m_address); + result &= (m_allocation == b.m_allocation); + 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 SideStake::operator!=(SideStake b) +{ + return !(*this == b); +} diff --git a/src/gridcoin/sidestake.h b/src/gridcoin/sidestake.h new file mode 100644 index 0000000000..d32bd7214c --- /dev/null +++ b/src/gridcoin/sidestake.h @@ -0,0 +1,141 @@ +// Copyright (c) 2014-2023 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/support/enumbytes.h" +#include "serialize.h" + +namespace GRC { + +enum class SideStakeStatus +{ + UNKNOWN, + ACTIVE, //!< A user specified sidestake that is active + INACTIVE, //!< A user specified sidestake that is inactive + DELETED, //!< A mandatory sidestake that has been deleted by contract + MANDATORY, //!< An active mandatory sidetake by contract + OUT_OF_BOUND +}; + +class SideStake +{ + using Status = EnumByte; + + CBitcoinAddress m_address; + + double m_allocation; + + 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 address. + + 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. + //! + SideStake(); + + //! + //! \brief Initialize a sidestake instance with the provided address and allocation. This is used to construct a user + //! specified sidestake. + //! + //! \param address + //! \param allocation + //! + SideStake(CBitcoinAddress address, double allocation); + + //! + //! \brief Initial a sidestake instance with the provided parameters. This form is normally used to construct a + //! mandatory sidestake from a contract. + //! + //! \param address + //! \param allocation + //! \param timestamp + //! \param hash + //! + SideStake(CBitcoinAddress address, double allocation, int64_t timestamp, uint256 hash); + + //! + //! \brief Determine whether a sidestake contains each of the required elements. + //! \return true if the sidestake is well-formed. + //! + bool WellFormed() const; + + //! + //! \brief Provides the sidestake address and status (value) 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 SideStakeStatus& 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==(SideStake 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!=(SideStake b); + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) + { + READWRITE(m_address); + READWRITE(m_allocation); + READWRITE(m_timestamp); + READWRITE(m_hash); + READWRITE(m_previous_hash); + READWRITE(m_status); + } +}; + +//! +//! \brief The type that defines a shared pointer to a sidestake +//! +typedef std::shared_ptr SideStake_ptr; + +//! +//! \brief A type that either points to some sidestake or does not. +//! +typedef const SideStake_ptr SideStakeOption; + + +} // namespace GRC + +#endif // GRIDCOIN_SIDESTAKE_H From b8a1393650f9486b696de7343a324f5a39358df6 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Fri, 29 Sep 2023 12:25:24 -0400 Subject: [PATCH 02/47] Add payload and registry machinery for SideStake class This adds the machinery to support mandatory sidestakes in the future. It does not include the contract handling part. --- src/gridcoin/contract/payload.h | 2 + src/gridcoin/sidestake.cpp | 373 +++++++++++++++++++++++++- src/gridcoin/sidestake.h | 454 ++++++++++++++++++++++++++++++-- 3 files changed, 798 insertions(+), 31 deletions(-) diff --git a/src/gridcoin/contract/payload.h b/src/gridcoin/contract/payload.h index ed48ef0240..6a1c7bde7b 100644 --- a/src/gridcoin/contract/payload.h +++ b/src/gridcoin/contract/payload.h @@ -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/sidestake.cpp b/src/gridcoin/sidestake.cpp index 4301ca53a6..02f25fd853 100644 --- a/src/gridcoin/sidestake.cpp +++ b/src/gridcoin/sidestake.cpp @@ -6,12 +6,26 @@ #include "node/ui_interface.h" using namespace GRC; +using LogFlags = BCLog::LogFlags; + +namespace { +SideStakeRegistry g_sidestake_entries; +} // anonymous namespace + +// ----------------------------------------------------------------------------- +// Global Functions +// ----------------------------------------------------------------------------- + +SideStakeRegistry& GRC::GetSideStakeRegistry() +{ + return g_sidestake_entries; +} // ----------------------------------------------------------------------------- // Class: SideStake // ----------------------------------------------------------------------------- SideStake::SideStake() - : m_address() + : m_key() , m_allocation() , m_timestamp(0) , m_hash() @@ -19,8 +33,8 @@ SideStake::SideStake() , m_status(SideStakeStatus::UNKNOWN) {} -SideStake::SideStake(CBitcoinAddress address, double allocation) - : m_address(address) +SideStake::SideStake(CBitcoinAddressForStorage address, double allocation) + : m_key(address) , m_allocation(allocation) , m_timestamp(0) , m_hash() @@ -28,8 +42,8 @@ SideStake::SideStake(CBitcoinAddress address, double allocation) , m_status(SideStakeStatus::UNKNOWN) {} -SideStake::SideStake(CBitcoinAddress address, double allocation, int64_t timestamp, uint256 hash) - : m_address(address) +SideStake::SideStake(CBitcoinAddressForStorage address, double allocation, int64_t timestamp, uint256 hash) + : m_key(address) , m_allocation(allocation) , m_timestamp(timestamp) , m_hash(hash) @@ -39,12 +53,17 @@ SideStake::SideStake(CBitcoinAddress address, double allocation, int64_t timesta bool SideStake::WellFormed() const { - return m_address.IsValid() && m_allocation >= 0.0 && m_allocation <= 1.0; + return m_key.IsValid() && m_allocation >= 0.0 && m_allocation <= 1.0; +} + +CBitcoinAddressForStorage SideStake::Key() const +{ + return m_key; } std::pair SideStake::KeyValueToString() const { - return std::make_pair(m_address.ToString(), StatusToString()); + return std::make_pair(m_key.ToString(), StatusToString()); } std::string SideStake::StatusToString() const @@ -88,7 +107,7 @@ bool SideStake::operator==(SideStake b) { bool result = true; - result &= (m_address == b.m_address); + result &= (m_key == b.m_key); result &= (m_allocation == b.m_allocation); result &= (m_timestamp == b.m_timestamp); result &= (m_hash == b.m_hash); @@ -102,3 +121,341 @@ bool SideStake::operator!=(SideStake b) { return !(*this == b); } + +// ----------------------------------------------------------------------------- +// 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, CBitcoinAddressForStorage key, double value, SideStakeStatus status) + : IContractPayload() + , m_version(version) + , m_entry(SideStake(key, value, status)) +{ +} + +SideStakePayload::SideStakePayload(const uint32_t version, SideStake entry) + : IContractPayload() + , m_version(version) + , m_entry(std::move(entry)) +{ +} + +SideStakePayload::SideStakePayload(SideStake entry) + : SideStakePayload(CURRENT_VERSION, std::move(entry)) +{ +} + +// ----------------------------------------------------------------------------- +// Class: SideStakeRegistry +// ----------------------------------------------------------------------------- +const SideStakeRegistry::SideStakeMap& SideStakeRegistry::SideStakeEntries() const +{ + return m_sidestake_entries; +} + +SideStakeOption SideStakeRegistry::Try(const CBitcoinAddressForStorage& key) const +{ + LOCK(cs_lock); + + const auto iter = m_sidestake_entries.find(key); + + if (iter == m_sidestake_entries.end()) { + return nullptr; + } + + return iter->second; +} + +SideStakeOption SideStakeRegistry::TryActive(const CBitcoinAddressForStorage& key) const +{ + LOCK(cs_lock); + + if (const SideStakeOption SideStake_entry = Try(key)) { + if (SideStake_entry->m_status == SideStakeStatus::ACTIVE || SideStake_entry->m_status == SideStakeStatus::MANDATORY) { + return SideStake_entry; + } + } + + return nullptr; +} + +void SideStakeRegistry::Reset() +{ + LOCK(cs_lock); + + m_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 = SideStakeStatus::DELETED; + } + + LOCK(cs_lock); + + auto sidestake_entry_pair_iter = m_sidestake_entries.find(payload.m_entry.m_key); + + SideStake_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_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, key = %s, value = %f, m_timestamp = %" PRId64 ", " + "m_hash = %s, m_previous_hash = %s, m_status = %s", + __func__, + ctx->m_version, + payload.m_version, + payload.m_entry.m_key.ToString(), + payload.m_entry.m_allocation, + payload.m_entry.m_timestamp, + payload.m_entry.m_hash.ToString(), + payload.m_entry.m_previous_hash.ToString(), + payload.m_entry.StatusToString() + ); + + SideStake& 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__, + historical.m_key.ToString(), + historical.m_allocation, + historical.m_hash.GetHex()); + } + + // Finally, insert the new SideStake entry (payload) smart pointer into the m_sidestake_entries map. + m_sidestake_entries[payload.m_entry.m_key] = m_sidestake_db.find(ctx.m_tx.GetHash())->second; + + return; +} + +void SideStakeRegistry::NonContractAdd(SideStake& sidestake) +{ + // Using this form of insert because we want the latest record with the same key to override any previous one. + m_sidestake_entries[sidestake.m_key] = std::make_shared(sidestake); +} + +void SideStakeRegistry::Add(const ContractContext& ctx) +{ + AddDelete(ctx); +} + +void SideStakeRegistry::NonContractDelete(CBitcoinAddressForStorage& address) +{ + auto sidestake_entry_pair_iter = m_sidestake_entries.find(address); + + if (sidestake_entry_pair_iter != m_sidestake_entries.end()) { + m_sidestake_entries.erase(sidestake_entry_pair_iter); + } +} + +void SideStakeRegistry::Delete(const ContractContext& ctx) +{ + AddDelete(ctx); +} + +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_sidestake_entries.find(payload->m_entry.m_key); + + if (entry_to_revert == m_sidestake_entries.end()) { + error("%s: The SideStake entry for key %s to revert was not found in the SideStake entry map.", + __func__, + entry_to_revert->second->m_key.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. + CBitcoinAddressForStorage key = entry_to_revert->second->m_key; + 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_sidestake_entries.erase(payload->m_entry.m_key) == 0) { + error("%s: The SideStake entry to erase during a SideStake entry revert for key %s was not found.", + __func__, + 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__, + 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__, + 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_sidestake_entries[resurrect_entry->second->m_key] = resurrect_entry->second; + } +} + +bool SideStakeRegistry::Validate(const Contract& contract, const CTransaction& tx, int &DoS) const +{ + if (contract.m_version < 1) { + return true; + } + + const auto payload = contract.SharePayloadAs(); + + if (contract.m_version >= 3 && payload->m_version < 2) { + DoS = 25; + error("%s: Legacy SideStake entry contract in contract v3", __func__); + return false; + } + + if (!payload->WellFormed(contract.m_action.Value())) { + DoS = 25; + error("%s: Malformed SideStake entry contract", __func__); + return false; + } + + return true; +} + +bool SideStakeRegistry::BlockValidate(const ContractContext& ctx, int& DoS) const +{ + return Validate(ctx.m_contract, ctx.m_tx, DoS); +} + +int SideStakeRegistry::Initialize() +{ + LOCK(cs_lock); + + int height = m_sidestake_db.Initialize(m_sidestake_entries, m_pending_sidestake_entries); + + 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_sidestake_entries.size()); + + 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_sidestake_entries.clear(); + m_sidestake_db.clear_in_memory_only(); +} + +uint64_t SideStakeRegistry::PassivateDB() +{ + LOCK(cs_lock); + + return m_sidestake_db.passivate_db(); +} + +SideStakeRegistry::SideStakeDB &SideStakeRegistry::GetSideStakeDB() +{ + 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 index d32bd7214c..59893cbaf2 100644 --- a/src/gridcoin/sidestake.h +++ b/src/gridcoin/sidestake.h @@ -6,11 +6,31 @@ #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" namespace GRC { +class CBitcoinAddressForStorage : public CBitcoinAddress +{ +public: + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) + { + // Note that (de)serializing the raw underlying vector char data for the address is safe here + // because this is only used in this module and validations were performed before serialization into + // storage. + READWRITE(vchData); + } +}; + + enum class SideStakeStatus { UNKNOWN, @@ -21,21 +41,36 @@ enum class SideStakeStatus OUT_OF_BOUND }; +//! +//! \brief The SideStake class. This class formalizes the "sidestake", which is a directive to apportion +//! a percentage of the total stake value to a designated address. This address must be a valid address, but +//! may or may not be owned by the staker. This is the primary mechanism to do automatic "donations" to +//! defined network addresses. +//! +//! The class supports two modes of operation. 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 to +//! in any GUI implementation on top of this. Mandatory entries will be picked up by contract handlers similar to +//! other contract types (cf. protocol entries). +//! class SideStake { +public: + //! + //! \brief Wrapped Enumeration of sidestake entry status, mainly for serialization/deserialization. + //! using Status = EnumByte; - CBitcoinAddress m_address; + CBitcoinAddressForStorage m_key; //!< The key here is the Gridcoin Address of the sidestake destination. - double m_allocation; + double m_allocation; //!< The allocation is a double precision floating point between 0.0 and 1.0 inclusive - int64_t m_timestamp; //!< Time of the sidestake contract transaction. + 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_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 address. + uint256 m_previous_hash; //!< The m_hash of the previous mandatory sidestake allocation with the same address. - Status m_status; //!< The status of the sidestake. It is of type int instead of enum for serialization. + 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. @@ -49,10 +84,19 @@ class SideStake //! \param address //! \param allocation //! - SideStake(CBitcoinAddress address, double allocation); + SideStake(CBitcoinAddressForStorage address, double allocation); + + //! + //! \brief Initialize a sidestake instance with the provided parameters. + //! + //! \param address + //! \param allocation + //! \param status + //! + SideStake(CBitcoinAddressForStorage address, double allocation, SideStakeStatus status); //! - //! \brief Initial a sidestake instance with the provided parameters. This form is normally used to construct a + //! \brief Initialize a sidestake instance with the provided parameters. This form is normally used to construct a //! mandatory sidestake from a contract. //! //! \param address @@ -60,7 +104,7 @@ class SideStake //! \param timestamp //! \param hash //! - SideStake(CBitcoinAddress address, double allocation, int64_t timestamp, uint256 hash); + SideStake(CBitcoinAddressForStorage address, double allocation, int64_t timestamp, uint256 hash); //! //! \brief Determine whether a sidestake contains each of the required elements. @@ -68,27 +112,35 @@ class SideStake //! bool WellFormed() const; + //! + //! \brief This is the standardized method that returns the key value for the sidestake entry (for + //! the registry_db.h template.) + //! + //! \return CBitcoinAddress key value for the sidestake entry + //! + CBitcoinAddressForStorage Key() const; + //! //! \brief Provides the sidestake address and status (value) 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 - //! + //! + //! \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. - //! + //! + //! \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 SideStakeStatus& status, const bool& translated = true) const; //! @@ -116,7 +168,7 @@ class SideStake template inline void SerializationOp(Stream& s, Operation ser_action) { - READWRITE(m_address); + READWRITE(m_key); READWRITE(m_allocation); READWRITE(m_timestamp); READWRITE(m_hash); @@ -135,7 +187,363 @@ typedef std::shared_ptr SideStake_ptr; //! typedef const SideStake_ptr SideStakeOption; +//! +//! \brief The body of a sidestake entry contract. Note that this body is bimodal. It +//! supports both the personality of the "LegacyPayload", and also the new native +//! sidestakeEntry format. In the Contract::Body::ConvertFromLegacy call, by the time +//! this call has been reached, the contract will have already been deserialized. +//! This will follow the legacy mode. For contracts at version 3+, the +//! Contract::SharePayload() will NOT call the ConvertFromLegacy. Note that because +//! the existing legacyPayloads are not versioned, the deserialization of +//! the payload first (de)serializes m_key, which is guaranteed to exist in either +//! legacy or native. If the key is empty, then payload v2+ is being deserialized +//! and the m_version and m_value are (de)serialized. This is ugly +//! but necessary to deal with the unversioned Legacy Payloads and maintain +//! compatibility. +//! +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; + + SideStake 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 entry constructed from + //! string key and value. Not to be used for version 1 payloads. Will assert. Does NOT + //! initialize hash fields. + //! + //! \param key. Key string for the sidestake entry + //! \param value. Value string for the sidestake entry + //! \param status. Status of the sidestake entry + //! + SideStakePayload(const uint32_t version, CBitcoinAddressForStorage key, double value, SideStakeStatus 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, SideStake 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(SideStake 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 + { + if (m_version <= 0 || m_version > CURRENT_VERSION) { + return false; + } + + return m_entry.WellFormed(); + } + + //! + //! \brief Get a string for the key used to construct a legacy contract. + //! + std::string LegacyKeyString() const override + { + return m_entry.m_key.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); + } + + //! + //! \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. +//! +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: TBD. + //! + SideStakeRegistry() + : m_sidestake_db(1) + { + }; + + //! + //! \brief The type that keys sidestake entries by their key strings. 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 SideStakeMap; + + //! + //! \brief PendingSideStakeMap. This is not actually used but defined to satisfy the template. + //! + typedef SideStakeMap 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 SideStakeMap& SideStakeEntries() const; + + //! + //! \brief Get the current sidestake entry for the specified key string. + //! + //! \param key The key string of the sidestake entry. + //! + //! \return An object that either contains a reference to some sidestake entry if it exists + //! for the key or does not. + //! + SideStakeOption Try(const CBitcoinAddressForStorage& key) const; + + //! + //! \brief Get the current sidestake entry for the specified key string if it has a status of ACTIVE or MANDATORY. + //! + //! \param key The key string of the sidestake entry. + //! + //! \return An object that either contains a reference to some sidestake entry if it exists + //! for the key and is in the required status or does not. + //! + SideStakeOption TryActive(const CBitcoinAddressForStorage& key) 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. + //! + //! \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 Allows local (voluntary) sidestakes to be added to the in-memory map and not persisted to + //! the registry db. + //! \param SideStake object to add + //! + void NonContractAdd(SideStake& sidestake); + + //! + //! \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 Provides for deletion of local (voluntary) sidestakes from the in-memory map that are not persisted + //! to the registry db. Deletion is by the map key (CBitcoinAddress). + //! \param address + //! + void NonContractDelete(CBitcoinAddressForStorage& address); + + //! + //! \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 Revert the registry state for the sidestake entry to the state prior + //! to this ContractContext application. This is typically an issue + //! 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 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); + + SideStakeMap m_sidestake_entries; //!< Contains the current sidestake entries including entries marked DELETED. + PendingSideStakeMap m_pending_sidestake_entries {}; //!< Not used. Only to satisfy the template. + + SideStakeDB m_sidestake_db; + +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 From 4a8d5054b58708997cd15bbf4d420eb0b8212230 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Fri, 29 Sep 2023 19:05:08 -0400 Subject: [PATCH 03/47] The initial implementation of LoadLocalSideStakesFromConfig() --- src/gridcoin/sidestake.cpp | 133 +++++++++++++++++++++++++++++++++++++ src/gridcoin/sidestake.h | 11 +++ 2 files changed, 144 insertions(+) diff --git a/src/gridcoin/sidestake.cpp b/src/gridcoin/sidestake.cpp index 02f25fd853..de996ee234 100644 --- a/src/gridcoin/sidestake.cpp +++ b/src/gridcoin/sidestake.cpp @@ -21,6 +21,17 @@ SideStakeRegistry& GRC::GetSideStakeRegistry() return g_sidestake_entries; } +// ----------------------------------------------------------------------------- +// Class: CBitcoinAddressForStorage +// ----------------------------------------------------------------------------- +CBitcoinAddressForStorage::CBitcoinAddressForStorage() + : CBitcoinAddress() +{} + +CBitcoinAddressForStorage::CBitcoinAddressForStorage(CBitcoinAddress address) + : CBitcoinAddress(address) +{} + // ----------------------------------------------------------------------------- // Class: SideStake // ----------------------------------------------------------------------------- @@ -434,6 +445,128 @@ uint64_t SideStakeRegistry::PassivateDB() return m_sidestake_db.passivate_db(); } +void SideStakeRegistry::LoadLocalSideStakesFromConfig() +{ + std::vector vSideStakes; + 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 (const auto& 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; + } + + SideStake sidestake(static_cast(address), dAllocation, 0, uint256{}); + + // This will add or update (replace) a non-contract entry in the registry for the local sidestake. + NonContractAdd(sidestake); + + // 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. + vSideStakes.push_back(sidestake); + + LogPrint(BCLog::LogFlags::MINER, "INFO: %s: SideStakeAlloc Address %s, Allocation %f", + __func__, sAddress, dAllocation); + } + + for (auto& entry : m_sidestake_entries) + { + // Only look at active entries. The others are NA for this alignment. + if (entry.second->m_status == SideStakeStatus::ACTIVE) { + auto iter = std::find(vSideStakes.begin(), vSideStakes.end(), *entry.second); + + if (iter == vSideStakes.end()) { + // Entry in map is no longer found in config files, so mark map entry deleted. + + entry.second->m_status = SideStakeStatus::DELETED; + } + } + } + + // 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__); +} + SideStakeRegistry::SideStakeDB &SideStakeRegistry::GetSideStakeDB() { return m_sidestake_db; diff --git a/src/gridcoin/sidestake.h b/src/gridcoin/sidestake.h index 59893cbaf2..c63b8ab28c 100644 --- a/src/gridcoin/sidestake.h +++ b/src/gridcoin/sidestake.h @@ -17,6 +17,9 @@ namespace GRC { class CBitcoinAddressForStorage : public CBitcoinAddress { public: + CBitcoinAddressForStorage(); + + CBitcoinAddressForStorage(CBitcoinAddress address); ADD_SERIALIZE_METHODS; @@ -419,6 +422,7 @@ class SideStakeRegistry : public IContractHandler //! //! \brief Allows local (voluntary) sidestakes to be added to the in-memory map and not persisted to //! the registry db. + //! //! \param SideStake object to add //! void NonContractAdd(SideStake& sidestake); @@ -427,6 +431,7 @@ class SideStakeRegistry : public IContractHandler //! \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; @@ -499,6 +504,12 @@ class SideStakeRegistry : public IContractHandler //! 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. //! From ea86469efe87c515d5fb1733d6753ffc34614e4c Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Fri, 29 Sep 2023 23:19:44 -0400 Subject: [PATCH 04/47] Implement ActiveSideStakeEntries() --- src/gridcoin/sidestake.cpp | 14 ++++++++++++++ src/gridcoin/sidestake.h | 9 +++++++++ 2 files changed, 23 insertions(+) diff --git a/src/gridcoin/sidestake.cpp b/src/gridcoin/sidestake.cpp index de996ee234..45eea5e7f1 100644 --- a/src/gridcoin/sidestake.cpp +++ b/src/gridcoin/sidestake.cpp @@ -172,6 +172,20 @@ const SideStakeRegistry::SideStakeMap& SideStakeRegistry::SideStakeEntries() con return m_sidestake_entries; } +const std::vector SideStakeRegistry::ActiveSideStakeEntries() const +{ + std::vector sidestakes; + + for (const auto& entry : m_sidestake_entries) + { + if (entry.second->m_status == SideStakeStatus::ACTIVE || entry.second->m_status == SideStakeStatus::MANDATORY) { + sidestakes.push_back(entry.second); + } + } + + return sidestakes; +} + SideStakeOption SideStakeRegistry::Try(const CBitcoinAddressForStorage& key) const { LOCK(cs_lock); diff --git a/src/gridcoin/sidestake.h b/src/gridcoin/sidestake.h index c63b8ab28c..9ac2090236 100644 --- a/src/gridcoin/sidestake.h +++ b/src/gridcoin/sidestake.h @@ -367,6 +367,15 @@ class SideStakeRegistry : public IContractHandler //! const SideStakeMap& 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). + //! + //! \return A vector of smart pointers to sidestake entries. + //! + const std::vector ActiveSideStakeEntries() const; + //! //! \brief Get the current sidestake entry for the specified key string. //! From 66f70b76bcec37e87b7a8ca072585ff130c82d8a Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Sat, 30 Sep 2023 00:01:04 -0400 Subject: [PATCH 05/47] Change miner to use ActiveSideStakeEntries() --- src/gridcoin/sidestake.cpp | 28 ++++++-- src/gridcoin/sidestake.h | 5 +- src/miner.cpp | 131 +++++-------------------------------- src/miner.h | 5 +- src/rpc/mining.cpp | 6 +- 5 files changed, 47 insertions(+), 128 deletions(-) diff --git a/src/gridcoin/sidestake.cpp b/src/gridcoin/sidestake.cpp index 45eea5e7f1..c06ad5e234 100644 --- a/src/gridcoin/sidestake.cpp +++ b/src/gridcoin/sidestake.cpp @@ -53,13 +53,17 @@ SideStake::SideStake(CBitcoinAddressForStorage address, double allocation) , m_status(SideStakeStatus::UNKNOWN) {} -SideStake::SideStake(CBitcoinAddressForStorage address, double allocation, int64_t timestamp, uint256 hash) +SideStake::SideStake(CBitcoinAddressForStorage address, + double allocation, + int64_t timestamp, + uint256 hash, + SideStakeStatus status) : m_key(address) , m_allocation(allocation) , m_timestamp(timestamp) , m_hash(hash) , m_previous_hash() - , m_status(SideStakeStatus::UNKNOWN) + , m_status(status) {} bool SideStake::WellFormed() const @@ -145,10 +149,13 @@ SideStakePayload::SideStakePayload(uint32_t version) { } -SideStakePayload::SideStakePayload(const uint32_t version, CBitcoinAddressForStorage key, double value, SideStakeStatus status) +SideStakePayload::SideStakePayload(const uint32_t version, + CBitcoinAddressForStorage key, + double value, + SideStakeStatus status) : IContractPayload() , m_version(version) - , m_entry(SideStake(key, value, status)) + , m_entry(SideStake(key, value, 0, uint256{}, status)) { } @@ -172,10 +179,15 @@ const SideStakeRegistry::SideStakeMap& SideStakeRegistry::SideStakeEntries() con return m_sidestake_entries; } -const std::vector SideStakeRegistry::ActiveSideStakeEntries() const +const std::vector SideStakeRegistry::ActiveSideStakeEntries() { std::vector sidestakes; + // For right now refresh sidestakes from config file. This is about the same overhead as the original + // function in the miner. Perhaps replace with a signal to only refresh when r-w config file is + // actually changed. + LoadLocalSideStakesFromConfig(); + for (const auto& entry : m_sidestake_entries) { if (entry.second->m_status == SideStakeStatus::ACTIVE || entry.second->m_status == SideStakeStatus::MANDATORY) { @@ -547,7 +559,11 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() break; } - SideStake sidestake(static_cast(address), dAllocation, 0, uint256{}); + SideStake sidestake(static_cast(address), + dAllocation, + 0, + uint256{}, + SideStakeStatus::ACTIVE); // This will add or update (replace) a non-contract entry in the registry for the local sidestake. NonContractAdd(sidestake); diff --git a/src/gridcoin/sidestake.h b/src/gridcoin/sidestake.h index 9ac2090236..f0fc8b4687 100644 --- a/src/gridcoin/sidestake.h +++ b/src/gridcoin/sidestake.h @@ -106,8 +106,9 @@ class SideStake //! \param allocation //! \param timestamp //! \param hash + //! \param status //! - SideStake(CBitcoinAddressForStorage address, double allocation, int64_t timestamp, uint256 hash); + SideStake(CBitcoinAddressForStorage address, double allocation, int64_t timestamp, uint256 hash, SideStakeStatus status); //! //! \brief Determine whether a sidestake contains each of the required elements. @@ -374,7 +375,7 @@ class SideStakeRegistry : public IContractHandler //! //! \return A vector of smart pointers to sidestake entries. //! - const std::vector ActiveSideStakeEntries() const; + const std::vector ActiveSideStakeEntries(); //! //! \brief Get the current sidestake entry for the specified key string. diff --git a/src/miner.cpp b/src/miner.cpp index 21f6a73597..e017aa595c 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -921,24 +921,26 @@ void SplitCoinStakeOutput(CBlock &blocknew, int64_t &nReward, bool &fEnableStake (iterSideStake != vSideStakeAlloc.end()) && (nOutputsUsed <= nMaxSideStakeOutputs); ++iterSideStake) { - CBitcoinAddress address(iterSideStake->first); + CBitcoinAddress& address = iterSideStake->get()->m_key; if (!address.IsValid()) { LogPrintf("WARN: SplitCoinStakeOutput: ignoring sidestake invalid address %s.", - iterSideStake->first.c_str()); + iterSideStake->get()->m_key.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) + if (nReward * iterSideStake->get()->m_allocation < CENT) { LogPrintf("WARN: SplitCoinStakeOutput: distribution %f too small to address %s.", - CoinToDouble(nReward * iterSideStake->second), iterSideStake->first.c_str()); + CoinToDouble(nReward * iterSideStake->get()->m_allocation), + iterSideStake->get()->m_key.ToString() + ); continue; } - if (dSumAllocation + iterSideStake->second > 1.0) + if (dSumAllocation + iterSideStake->get()->m_allocation > 1.0) { LogPrintf("WARN: SplitCoinStakeOutput: allocation percentage over 100 percent, " "ending sidestake allocations."); @@ -961,11 +963,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 (dSumAllocation + iterSideStake->get()->m_allocation < 1.0) + nSideStake = nReward * iterSideStake->get()->m_allocation; // 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 (dSumAllocation + iterSideStake->get()->m_allocation == 1.0) // 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,8 +975,11 @@ 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(nReward * iterSideStake->get()->m_allocation), + iterSideStake->get()->m_key.ToString() + ); + dSumAllocation += iterSideStake->get()->m_allocation; nRemainingStakeOutputValue -= nSideStake; nOutputsUsed++; } @@ -1248,110 +1253,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) @@ -1420,7 +1321,7 @@ void StakeMiner(CWallet *pwallet) LogPrint(BCLog::LogFlags::MINER, "INFO: %s: fEnableSideStaking = %u", __func__, fEnableSideStaking); // vSideStakeAlloc is an out parameter. - if (fEnableSideStaking) vSideStakeAlloc = GetSideStakingStatusAndAlloc(); + if (fEnableSideStaking) vSideStakeAlloc = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(); // wait for next round if (!MilliSleep(nMinerSleep)) return; 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/rpc/mining.cpp b/src/rpc/mining.cpp index 49f787e1b1..e692030356 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -98,7 +98,7 @@ UniValue getstakinginfo(const UniValue& params, bool fHelp) bool fEnableSideStaking = gArgs.GetBoolArg("-enablesidestaking"); - if (fEnableSideStaking) vSideStakeAlloc = GetSideStakingStatusAndAlloc(); + if (fEnableSideStaking) vSideStakeAlloc = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(); stakesplitting.pushKV("stake-splitting-enabled", fEnableStakeSplit); if (fEnableStakeSplit) @@ -115,8 +115,8 @@ UniValue getstakinginfo(const UniValue& params, bool fHelp) { for (const auto& alloc : vSideStakeAlloc) { - sidestakingalloc.pushKV("address", alloc.first); - sidestakingalloc.pushKV("allocation-pct", alloc.second * 100); + sidestakingalloc.pushKV("address", alloc->m_key.ToString()); + sidestakingalloc.pushKV("allocation-pct", alloc->m_allocation * 100); vsidestakingalloc.push_back(sidestakingalloc); } From 3a4e946006a0660f3728f095bc23a54b680a53b0 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Sat, 30 Sep 2023 22:52:30 -0400 Subject: [PATCH 06/47] Wire up the rest of the sidestake contract machinery --- src/gridcoin/contract/contract.cpp | 46 +++++++++++++++++++++++------- src/gridcoin/contract/registry.cpp | 2 ++ src/gridcoin/contract/registry.h | 3 ++ src/gridcoin/sidestake.cpp | 36 +++++++++++++++++------ src/gridcoin/sidestake.h | 32 +++++++++++++++++++-- src/rpc/blockchain.cpp | 37 +++++++++++++++++++++++- src/validation.cpp | 2 ++ 7 files changed, 136 insertions(+), 22 deletions(-) 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/registry.cpp b/src/gridcoin/contract/registry.cpp index cb7e85e5b5..aa618ef50e 100644 --- a/src/gridcoin/contract/registry.cpp +++ b/src/gridcoin/contract/registry.cpp @@ -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..4132f465eb 100644 --- a/src/gridcoin/contract/registry.h +++ b/src/gridcoin/contract/registry.h @@ -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]]; diff --git a/src/gridcoin/sidestake.cpp b/src/gridcoin/sidestake.cpp index c06ad5e234..bee4eeeea4 100644 --- a/src/gridcoin/sidestake.cpp +++ b/src/gridcoin/sidestake.cpp @@ -182,16 +182,30 @@ const SideStakeRegistry::SideStakeMap& SideStakeRegistry::SideStakeEntries() con const std::vector SideStakeRegistry::ActiveSideStakeEntries() { std::vector sidestakes; + double allocation_sum = 0.0; // For right now refresh sidestakes from config file. This is about the same overhead as the original // function in the miner. Perhaps replace with a signal to only refresh when r-w config file is // actually changed. LoadLocalSideStakesFromConfig(); + // The loops below prevent sidestakes from being added that cause a total allocation above 1.0 (100%). + + // Do mandatory sidestakes first. + for (const auto& entry : m_sidestake_entries) + { + if (entry.second->m_status == SideStakeStatus::MANDATORY && allocation_sum + entry.second->m_allocation <= 1.0) { + sidestakes.push_back(entry.second); + allocation_sum += entry.second->m_allocation; + } + } + + // Followed by local active sidestakes for (const auto& entry : m_sidestake_entries) { - if (entry.second->m_status == SideStakeStatus::ACTIVE || entry.second->m_status == SideStakeStatus::MANDATORY) { + if (entry.second->m_status == SideStakeStatus::ACTIVE && allocation_sum + entry.second->m_allocation <= 1.0) { sidestakes.push_back(entry.second); + allocation_sum += entry.second->m_allocation; } } @@ -400,15 +414,11 @@ void SideStakeRegistry::Revert(const ContractContext& ctx) bool SideStakeRegistry::Validate(const Contract& contract, const CTransaction& tx, int &DoS) const { - if (contract.m_version < 1) { - return true; - } - const auto payload = contract.SharePayloadAs(); - if (contract.m_version >= 3 && payload->m_version < 2) { + if (contract.m_version < 3) { DoS = 25; - error("%s: Legacy SideStake entry contract in contract v3", __func__); + error("%s: Sidestake entries only valid in contract v3 and above", __func__); return false; } @@ -519,9 +529,17 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() } } + // First, determine allocation already taken by mandatory sidestakes, because they must be allocated first. + for (const auto& entry : SideStakeEntries()) { + if (entry.second->m_status == SideStakeStatus::MANDATORY) { + dSumAllocation += entry.second->m_allocation; + } + } + for (const auto& entry : raw_vSideStakeAlloc) { std::string sAddress; + double dAllocation = 0.0; sAddress = entry.first; @@ -583,9 +601,9 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() auto iter = std::find(vSideStakes.begin(), vSideStakes.end(), *entry.second); if (iter == vSideStakes.end()) { - // Entry in map is no longer found in config files, so mark map entry deleted. + // Entry in map is no longer found in config files, so mark map entry inactive. - entry.second->m_status = SideStakeStatus::DELETED; + entry.second->m_status = SideStakeStatus::INACTIVE; } } } diff --git a/src/gridcoin/sidestake.h b/src/gridcoin/sidestake.h index f0fc8b4687..5018a438e6 100644 --- a/src/gridcoin/sidestake.h +++ b/src/gridcoin/sidestake.h @@ -11,6 +11,7 @@ #include "gridcoin/contract/registry_db.h" #include "gridcoin/support/enumbytes.h" #include "serialize.h" +#include "logging.h" namespace GRC { @@ -29,6 +30,7 @@ class CBitcoinAddressForStorage : public CBitcoinAddress // Note that (de)serializing the raw underlying vector char data for the address is safe here // because this is only used in this module and validations were performed before serialization into // storage. + READWRITE(nVersion); READWRITE(vchData); } }; @@ -274,11 +276,35 @@ class SideStakePayload : public IContractPayload //! bool WellFormed(const ContractAction action) const override { - if (m_version <= 0 || m_version > CURRENT_VERSION) { + 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, + m_entry.m_key.ToString(), + m_entry.m_allocation, + m_entry.StatusToString() + ); + return false; } - return m_entry.WellFormed(); + return valid; } //! @@ -372,6 +398,8 @@ class SideStakeRegistry : public IContractHandler //! \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 sidestakes + //! returned do not total an allocation greater than 1.0. //! //! \return A vector of smart pointers to sidestake entries. //! diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 1ca483654a..bb8129d55f 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" @@ -2304,7 +2305,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."); } @@ -2433,6 +2435,39 @@ 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) { + GRC::CBitcoinAddressForStorage sidestake_address; + if (!sidestake_address.SetString(params[2].get_str())) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Address specified for the sidestake is invalid."); + } + + double allocation = 0.0; + if (!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, // Sidestake address + allocation, // Sidestake allocation + GRC::SideStakeStatus::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/validation.cpp b/src/validation.cpp index d9b12eb165..23398d8eee 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1934,6 +1934,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 +1943,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)); } From 0bc173f88d081f440eef80b693fa83a57a95959a Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Mon, 2 Oct 2023 00:42:29 -0400 Subject: [PATCH 07/47] Add sidestaking entry status to getstakinginfo. --- src/rpc/mining.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index e692030356..e4befd87e3 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -117,6 +117,7 @@ UniValue getstakinginfo(const UniValue& params, bool fHelp) { sidestakingalloc.pushKV("address", alloc->m_key.ToString()); sidestakingalloc.pushKV("allocation-pct", alloc->m_allocation * 100); + sidestakingalloc.pushKV("status", alloc->StatusToString()); vsidestakingalloc.push_back(sidestakingalloc); } From 0b4f5ab2b31d6c22b04ce6866c449e02674541b1 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Mon, 2 Oct 2023 16:56:09 -0400 Subject: [PATCH 08/47] Implement RwSettingsUpdated core signal --- src/gridcoin/sidestake.cpp | 74 ++++++++++++++++++++++++++------------ src/gridcoin/sidestake.h | 3 ++ src/node/ui_interface.cpp | 4 ++- src/node/ui_interface.h | 3 ++ src/util/system.cpp | 7 ++++ 5 files changed, 68 insertions(+), 23 deletions(-) diff --git a/src/gridcoin/sidestake.cpp b/src/gridcoin/sidestake.cpp index bee4eeeea4..ebed05a63e 100644 --- a/src/gridcoin/sidestake.cpp +++ b/src/gridcoin/sidestake.cpp @@ -5,6 +5,17 @@ #include "sidestake.h" #include "node/ui_interface.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; @@ -184,13 +195,13 @@ const std::vector SideStakeRegistry::ActiveSideStakeEntries() std::vector sidestakes; double allocation_sum = 0.0; - // For right now refresh sidestakes from config file. This is about the same overhead as the original - // function in the miner. Perhaps replace with a signal to only refresh when r-w config file is - // actually changed. - LoadLocalSideStakesFromConfig(); + // 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); + // Do mandatory sidestakes first. for (const auto& entry : m_sidestake_entries) { @@ -258,13 +269,13 @@ void SideStakeRegistry::AddDelete(const ContractContext& ctx) SideStakePayload payload = ctx->CopyPayloadAs(); - // Fill this in from the transaction context because these are not done during payload - // initialization. + // 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. + // 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 = SideStakeStatus::DELETED; } @@ -275,14 +286,14 @@ void SideStakeRegistry::AddDelete(const ContractContext& ctx) SideStake_ptr current_sidestake_entry_ptr = nullptr; - // Is there an existing SideStake entry in the map? + // Is there an existing SideStake entry in the map? bool current_sidestake_entry_present = (sidestake_entry_pair_iter != m_sidestake_entries.end()); - // If so, then get a smart pointer to it. + // 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. + // 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 {}; @@ -315,7 +326,7 @@ void SideStakeRegistry::AddDelete(const ContractContext& ctx) historical.m_hash.GetHex()); } - // Finally, insert the new SideStake entry (payload) smart pointer into the m_sidestake_entries map. + // Finally, insert the new SideStake entry (payload) smart pointer into the m_sidestake_entries map. m_sidestake_entries[payload.m_entry.m_key] = m_sidestake_db.find(ctx.m_tx.GetHash())->second; return; @@ -350,9 +361,9 @@ 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. + // 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_sidestake_entries.find(payload->m_entry.m_key); @@ -362,8 +373,8 @@ void SideStakeRegistry::Revert(const ContractContext& ctx) __func__, entry_to_revert->second->m_key.ToString()); - // If there is no record in the current m_sidestake_entries map, then there is nothing to do here. This - // should not occur. + // If there is no record in the current m_sidestake_entries map, then there is nothing to do here. This + // should not occur. return; } @@ -388,9 +399,9 @@ void SideStakeRegistry::Revert(const ContractContext& ctx) __func__, 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. + // 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()) { @@ -406,8 +417,8 @@ void SideStakeRegistry::Revert(const ContractContext& ctx) 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. + // 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_sidestake_entries[resurrect_entry->second->m_key] = resurrect_entry->second; } } @@ -442,9 +453,14 @@ int SideStakeRegistry::Initialize() int height = m_sidestake_db.Initialize(m_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_sidestake_entries.size()); + // Add the local sidestakes specified in the config file(s) to the mandatory sidestakes. + LoadLocalSideStakesFromConfig(); + return height; } @@ -536,6 +552,8 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() } } + LOCK(cs_lock); + for (const auto& entry : raw_vSideStakeAlloc) { std::string sAddress; @@ -615,8 +633,20 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() " distribution!", __func__); } +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; } diff --git a/src/gridcoin/sidestake.h b/src/gridcoin/sidestake.h index 5018a438e6..25d5de8574 100644 --- a/src/gridcoin/sidestake.h +++ b/src/gridcoin/sidestake.h @@ -577,6 +577,9 @@ class SideStakeRegistry : public IContractHandler //! void AddDelete(const ContractContext& ctx); + void SubscribeToCoreSignals(); + void UnsubscribeFromCoreSignals(); + SideStakeMap m_sidestake_entries; //!< Contains the current sidestake entries including entries marked DELETED. PendingSideStakeMap m_pending_sidestake_entries {}; //!< Not used. Only to satisfy the template. 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/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 Date: Mon, 2 Oct 2023 17:41:35 -0400 Subject: [PATCH 09/47] Change operation of fEnableSideStaking flag The fEnableSideStaking flag should only control local sidestakes, not mandatory ones. --- src/gridcoin/sidestake.cpp | 18 ++++++++++++------ src/miner.cpp | 7 +++---- src/rpc/mining.cpp | 22 +++++++++++----------- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/gridcoin/sidestake.cpp b/src/gridcoin/sidestake.cpp index ebed05a63e..c310c625c9 100644 --- a/src/gridcoin/sidestake.cpp +++ b/src/gridcoin/sidestake.cpp @@ -211,12 +211,18 @@ const std::vector SideStakeRegistry::ActiveSideStakeEntries() } } - // Followed by local active sidestakes - for (const auto& entry : m_sidestake_entries) - { - if (entry.second->m_status == SideStakeStatus::ACTIVE && allocation_sum + entry.second->m_allocation <= 1.0) { - sidestakes.push_back(entry.second); - allocation_sum += entry.second->m_allocation; + // 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_sidestake_entries) + { + if (entry.second->m_status == SideStakeStatus::ACTIVE && allocation_sum + entry.second->m_allocation <= 1.0) { + sidestakes.push_back(entry.second); + allocation_sum += entry.second->m_allocation; + } } } diff --git a/src/miner.cpp b/src/miner.cpp index e017aa595c..2c11645982 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -1318,10 +1318,9 @@ void StakeMiner(CWallet *pwallet) bool fEnableSideStaking = gArgs.GetBoolArg("-enablesidestaking"); - LogPrint(BCLog::LogFlags::MINER, "INFO: %s: fEnableSideStaking = %u", __func__, fEnableSideStaking); - - // vSideStakeAlloc is an out parameter. - if (fEnableSideStaking) vSideStakeAlloc = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(); + // Note that fEnableSideStaking is now processed internal to ActiveSideStakeEntries. The sidestaking flag only + // controls local sidestakes. If there exists mandatory sidestakes, they occur regardless of the flag. + vSideStakeAlloc = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(); // wait for next round if (!MilliSleep(nMinerSleep)) return; diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index e4befd87e3..f7b9123a3f 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -110,20 +110,20 @@ UniValue getstakinginfo(const UniValue& params, bool fHelp) } obj.pushKV("stake-splitting", stakesplitting); - sidestaking.pushKV("side-staking-enabled", fEnableSideStaking); - if (fEnableSideStaking) + 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->m_key.ToString()); - sidestakingalloc.pushKV("allocation-pct", alloc->m_allocation * 100); - sidestakingalloc.pushKV("status", alloc->StatusToString()); + sidestakingalloc.pushKV("address", alloc->m_key.ToString()); + sidestakingalloc.pushKV("allocation_pct", alloc->m_allocation * 100); + 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")); From fe18cc825a01f2af36215c8231c703d4ae6418d3 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Mon, 2 Oct 2023 18:11:01 -0400 Subject: [PATCH 10/47] Implement enable sidestaking user checkbox in GUI options. --- src/qt/forms/optionsdialog.ui | 7 +++++++ src/qt/optionsdialog.cpp | 1 + src/qt/optionsmodel.cpp | 10 +++++++++- src/qt/optionsmodel.h | 1 + 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/qt/forms/optionsdialog.ui b/src/qt/forms/optionsdialog.ui index acd91271bd..f9f2a1e69d 100644 --- a/src/qt/forms/optionsdialog.ui +++ b/src/qt/forms/optionsdialog.ui @@ -320,6 +320,13 @@ + + + + Enable Locally Specified Sidestaking + + + diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp index aecbd97885..1e211749e8 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -188,6 +188,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); diff --git a/src/qt/optionsmodel.cpp b/src/qt/optionsmodel.cpp index 666b60371b..a0de553ea9 100644 --- a/src/qt/optionsmodel.cpp +++ b/src/qt/optionsmodel.cpp @@ -155,6 +155,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 +313,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. diff --git a/src/qt/optionsmodel.h b/src/qt/optionsmodel.h index d80009e66f..f6e7f37bdf 100644 --- a/src/qt/optionsmodel.h +++ b/src/qt/optionsmodel.h @@ -41,6 +41,7 @@ class OptionsModel : public QAbstractListModel DataDir, // QString EnableStaking, // bool EnableStakeSplit, // bool + EnableSideStaking, // bool StakingEfficiency, // double MinStakeSplitValue, // int PollExpireNotification, // double From 24ad31cce223024482edd0941c869468af4197dd Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Mon, 2 Oct 2023 18:20:34 -0400 Subject: [PATCH 11/47] Adjust size of staking frame in options --- src/qt/forms/optionsdialog.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qt/forms/optionsdialog.ui b/src/qt/forms/optionsdialog.ui index f9f2a1e69d..a24ede03e9 100644 --- a/src/qt/forms/optionsdialog.ui +++ b/src/qt/forms/optionsdialog.ui @@ -251,7 +251,7 @@ 10 10 651 - 135 + 171 From 24c5d8728feadd869fbc0cb8c7dc8c297ba7b3a9 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Tue, 3 Oct 2023 17:27:08 -0400 Subject: [PATCH 12/47] Add description field for sidestake. --- src/gridcoin/sidestake.cpp | 10 +++++++-- src/gridcoin/sidestake.h | 15 +++++++++---- src/rpc/blockchain.cpp | 46 ++++++++++++++++++++++++++++++++++++-- src/rpc/client.cpp | 1 - 4 files changed, 63 insertions(+), 9 deletions(-) diff --git a/src/gridcoin/sidestake.cpp b/src/gridcoin/sidestake.cpp index c310c625c9..5378af713b 100644 --- a/src/gridcoin/sidestake.cpp +++ b/src/gridcoin/sidestake.cpp @@ -49,15 +49,17 @@ CBitcoinAddressForStorage::CBitcoinAddressForStorage(CBitcoinAddress address) SideStake::SideStake() : m_key() , m_allocation() + , m_description() , m_timestamp(0) , m_hash() , m_previous_hash() , m_status(SideStakeStatus::UNKNOWN) {} -SideStake::SideStake(CBitcoinAddressForStorage address, double allocation) +SideStake::SideStake(CBitcoinAddressForStorage address, double allocation, std::string description) : m_key(address) , m_allocation(allocation) + , m_description(description) , m_timestamp(0) , m_hash() , m_previous_hash() @@ -66,11 +68,13 @@ SideStake::SideStake(CBitcoinAddressForStorage address, double allocation) SideStake::SideStake(CBitcoinAddressForStorage address, double allocation, + std::string description, int64_t timestamp, uint256 hash, SideStakeStatus status) : m_key(address) , m_allocation(allocation) + , m_description(description) , m_timestamp(timestamp) , m_hash(hash) , m_previous_hash() @@ -163,10 +167,11 @@ SideStakePayload::SideStakePayload(uint32_t version) SideStakePayload::SideStakePayload(const uint32_t version, CBitcoinAddressForStorage key, double value, + std::string description, SideStakeStatus status) : IContractPayload() , m_version(version) - , m_entry(SideStake(key, value, 0, uint256{}, status)) + , m_entry(SideStake(key, value, description, 0, uint256{}, status)) { } @@ -603,6 +608,7 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() SideStake sidestake(static_cast(address), dAllocation, + std::string {}, 0, uint256{}, SideStakeStatus::ACTIVE); diff --git a/src/gridcoin/sidestake.h b/src/gridcoin/sidestake.h index 25d5de8574..afebd8faed 100644 --- a/src/gridcoin/sidestake.h +++ b/src/gridcoin/sidestake.h @@ -69,6 +69,8 @@ class SideStake double m_allocation; //!< The allocation is a double precision floating point between 0.0 and 1.0 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. @@ -88,17 +90,19 @@ class SideStake //! //! \param address //! \param allocation + //! \param description (optional) //! - SideStake(CBitcoinAddressForStorage address, double allocation); + SideStake(CBitcoinAddressForStorage address, double allocation, std::string description); //! //! \brief Initialize a sidestake instance with the provided parameters. //! //! \param address //! \param allocation + //! \param description (optional) //! \param status //! - SideStake(CBitcoinAddressForStorage address, double allocation, SideStakeStatus status); + SideStake(CBitcoinAddressForStorage address, double allocation, std::string description, SideStakeStatus status); //! //! \brief Initialize a sidestake instance with the provided parameters. This form is normally used to construct a @@ -106,11 +110,12 @@ class SideStake //! //! \param address //! \param allocation + //! \param description (optional) //! \param timestamp //! \param hash //! \param status //! - SideStake(CBitcoinAddressForStorage address, double allocation, int64_t timestamp, uint256 hash, SideStakeStatus status); + SideStake(CBitcoinAddressForStorage address, double allocation, std::string description, int64_t timestamp, uint256 hash, SideStakeStatus status); //! //! \brief Determine whether a sidestake contains each of the required elements. @@ -176,6 +181,7 @@ class SideStake { READWRITE(m_key); READWRITE(m_allocation); + READWRITE(m_description); READWRITE(m_timestamp); READWRITE(m_hash); READWRITE(m_previous_hash); @@ -242,7 +248,8 @@ class SideStakePayload : public IContractPayload //! \param value. Value string for the sidestake entry //! \param status. Status of the sidestake entry //! - SideStakePayload(const uint32_t version, CBitcoinAddressForStorage key, double value, SideStakeStatus status); + SideStakePayload(const uint32_t version, CBitcoinAddressForStorage key, double value, + std::string description, SideStakeStatus status); //! //! \brief Initialize a sidestake entry payload from the given sidestake entry diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index bb8129d55f..c06cbec093 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -2269,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; @@ -2320,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( @@ -2443,6 +2477,13 @@ UniValue addkey(const UniValue& params, bool fHelp) 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 (!ParseDouble(params[3].get_str(), &allocation)) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid allocation specified."); @@ -2460,6 +2501,7 @@ UniValue addkey(const UniValue& params, bool fHelp) uint32_t {1}, // Contract payload version number sidestake_address, // Sidestake address allocation, // Sidestake allocation + description, // Sidestake description GRC::SideStakeStatus::MANDATORY // sidestake status ); } else { 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 }, From 95ffea9c610426495118912c00bfc3bf103883b1 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Thu, 5 Oct 2023 12:44:59 -0400 Subject: [PATCH 13/47] Split out map for local sidestakes I realized that a change in design was necessary to split out the map to hold local sidestakes, because it is possible that a local sidestake could exist that pays to the same address as a mandatory sidestake. It is much more correct, and simpler, to retain both instead of coming up with complicated override rules. --- src/gridcoin/sidestake.cpp | 98 +++++++++++++++++++++++++++++++------- src/gridcoin/sidestake.h | 30 ++++++++---- src/rpc/mining.cpp | 4 +- 3 files changed, 105 insertions(+), 27 deletions(-) diff --git a/src/gridcoin/sidestake.cpp b/src/gridcoin/sidestake.cpp index 5378af713b..5133510183 100644 --- a/src/gridcoin/sidestake.cpp +++ b/src/gridcoin/sidestake.cpp @@ -4,6 +4,7 @@ #include "sidestake.h" #include "node/ui_interface.h" +#include "univalue.h" //! //! \brief Model callback bound to the \c RwSettingsUpdated core signal. @@ -222,7 +223,7 @@ const std::vector SideStakeRegistry::ActiveSideStakeEntries() if (fEnableSideStaking) { LogPrint(BCLog::LogFlags::MINER, "INFO: %s: fEnableSideStaking = %u", __func__, fEnableSideStaking); - for (const auto& entry : m_sidestake_entries) + for (const auto& entry : m_local_sidestake_entries) { if (entry.second->m_status == SideStakeStatus::ACTIVE && allocation_sum + entry.second->m_allocation <= 1.0) { sidestakes.push_back(entry.second); @@ -234,30 +235,42 @@ const std::vector SideStakeRegistry::ActiveSideStakeEntries() return sidestakes; } -SideStakeOption SideStakeRegistry::Try(const CBitcoinAddressForStorage& key) const +std::vector SideStakeRegistry::Try(const CBitcoinAddressForStorage& key, const bool& local_only) const { LOCK(cs_lock); - const auto iter = m_sidestake_entries.find(key); + std::vector result; - if (iter == m_sidestake_entries.end()) { - return nullptr; + if (!local_only) { + const auto mandatory_entry = m_sidestake_entries.find(key); + + if (mandatory_entry != m_sidestake_entries.end()) { + result.push_back(mandatory_entry->second); + } + } + + const auto local_entry = m_local_sidestake_entries.find(key); + + if (local_entry != m_sidestake_entries.end()) { + result.push_back(local_entry->second); } - return iter->second; + return result; } -SideStakeOption SideStakeRegistry::TryActive(const CBitcoinAddressForStorage& key) const +std::vector SideStakeRegistry::TryActive(const CBitcoinAddressForStorage& key, const bool& local_only) const { LOCK(cs_lock); - if (const SideStakeOption SideStake_entry = Try(key)) { - if (SideStake_entry->m_status == SideStakeStatus::ACTIVE || SideStake_entry->m_status == SideStakeStatus::MANDATORY) { - return SideStake_entry; + std::vector result; + + for (const auto& iter : Try(key, local_only)) { + if (iter->m_status == SideStakeStatus::MANDATORY || iter->m_status == SideStakeStatus::ACTIVE) { + result.push_back(iter); } } - return nullptr; + return result; } void SideStakeRegistry::Reset() @@ -346,7 +359,9 @@ void SideStakeRegistry::AddDelete(const ContractContext& ctx) void SideStakeRegistry::NonContractAdd(SideStake& sidestake) { // Using this form of insert because we want the latest record with the same key to override any previous one. - m_sidestake_entries[sidestake.m_key] = std::make_shared(sidestake); + m_local_sidestake_entries[sidestake.m_key] = std::make_shared(sidestake); + + } void SideStakeRegistry::Add(const ContractContext& ctx) @@ -356,10 +371,10 @@ void SideStakeRegistry::Add(const ContractContext& ctx) void SideStakeRegistry::NonContractDelete(CBitcoinAddressForStorage& address) { - auto sidestake_entry_pair_iter = m_sidestake_entries.find(address); + auto sidestake_entry_pair_iter = m_local_sidestake_entries.find(address); - if (sidestake_entry_pair_iter != m_sidestake_entries.end()) { - m_sidestake_entries.erase(sidestake_entry_pair_iter); + if (sidestake_entry_pair_iter != m_local_sidestake_entries.end()) { + m_local_sidestake_entries.erase(sidestake_entry_pair_iter); } } @@ -469,9 +484,11 @@ int SideStakeRegistry::Initialize() 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_sidestake_entries.size()); - // Add the local sidestakes specified in the config file(s) to the mandatory sidestakes. + // 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; } @@ -497,6 +514,7 @@ void SideStakeRegistry::ResetInMemoryOnly() { LOCK(cs_lock); + m_local_sidestake_entries.clear(); m_sidestake_entries.clear(); m_sidestake_db.clear_in_memory_only(); } @@ -510,6 +528,16 @@ uint64_t SideStakeRegistry::PassivateDB() 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 vSideStakes; std::vector> raw_vSideStakeAlloc; double dSumAllocation = 0.0; @@ -624,7 +652,7 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() __func__, sAddress, dAllocation); } - for (auto& entry : m_sidestake_entries) + for (auto& entry : m_local_sidestake_entries) { // Only look at active entries. The others are NA for this alignment. if (entry.second->m_status == SideStakeStatus::ACTIVE) { @@ -645,6 +673,42 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() " distribution!", __func__); } +bool SideStakeRegistry::SaveLocalSideStakesToConfig() +{ + bool status = false; + + std::string addresses; + std::string allocations; + std::string descriptions; + + std::string separator; + + std::vector> settings; + + unsigned int i = 0; + for (const auto& iter : m_local_sidestake_entries) { + if (i) { + separator = ","; + } + + addresses += separator + iter.second->m_key.ToString(); + allocations += separator + ToString(iter.second->m_allocation * 100.0); + descriptions += separator + iter.second->m_description; + + ++i; + } + + settings.push_back(std::make_pair("addresses", addresses)); + settings.push_back(std::make_pair("allocations", allocations)); + settings.push_back(std::make_pair("descriptions", descriptions)); + + status = updateRwSettings(settings); + + m_local_entry_already_saved_to_config = true; + + return status; +} + void SideStakeRegistry::SubscribeToCoreSignals() { uiInterface.RwSettingsUpdated_connect(std::bind(RwSettingsUpdated, this)); diff --git a/src/gridcoin/sidestake.h b/src/gridcoin/sidestake.h index afebd8faed..964f933b5c 100644 --- a/src/gridcoin/sidestake.h +++ b/src/gridcoin/sidestake.h @@ -197,7 +197,7 @@ typedef std::shared_ptr SideStake_ptr; //! //! \brief A type that either points to some sidestake or does not. //! -typedef const SideStake_ptr SideStakeOption; +//typedef const SideStake_ptr SideStakeOption; //! //! \brief The body of a sidestake entry contract. Note that this body is bimodal. It @@ -416,21 +416,24 @@ class SideStakeRegistry : public IContractHandler //! \brief Get the current sidestake entry for the specified key string. //! //! \param key The key string of the sidestake entry. + //! \param local_only If true causes Try to only check the local sidestake map. Defaults to false. //! - //! \return An object that either contains a reference to some sidestake entry if it exists - //! for the key or does not. + //! \return A vector of smart pointers to entries matching the provided key (address). Up to two elements + //! are returned, mandatory entry first, unless local only boolean is set true. //! - SideStakeOption Try(const CBitcoinAddressForStorage& key) const; + std::vector Try(const CBitcoinAddressForStorage& key, const bool& local_only = false) const; //! //! \brief Get the current sidestake entry for the specified key string if it has a status of ACTIVE or MANDATORY. //! //! \param key The key string of the sidestake entry. + //! \param local_only If true causes Try to only check the local sidestake map. Defaults to false. //! - //! \return An object that either contains a reference to some sidestake entry if it exists - //! for the key and is in the required status or does not. + //! \return A vector of smart pointers to entries matching the provided key (address) that are in status of + //! MANDATORY or ACTIVE. Up to two elements are returned, mandatory entry first, unless local only boolean + //! is set true. //! - SideStakeOption TryActive(const CBitcoinAddressForStorage& key) const; + std::vector TryActive(const CBitcoinAddressForStorage& key, const bool& local_only = false) const; //! //! \brief Destroy the contract handler state in case of an error in loading @@ -584,14 +587,25 @@ class SideStakeRegistry : public IContractHandler //! 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(); + void SubscribeToCoreSignals(); void UnsubscribeFromCoreSignals(); - SideStakeMap m_sidestake_entries; //!< Contains the current sidestake entries including entries marked DELETED. + SideStakeMap m_local_sidestake_entries; //!< Contains the local (non-contract) sidestake entries. + SideStakeMap m_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; + bool m_local_entry_already_saved_to_config = false; //!< Flag to prevent reload on signal if individual entry saved already. + public: SideStakeDB& GetSideStakeDB(); diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index f7b9123a3f..373ba3e98b 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 = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(); - stakesplitting.pushKV("stake-splitting-enabled", fEnableStakeSplit); if (fEnableStakeSplit) { @@ -110,6 +108,8 @@ UniValue getstakinginfo(const UniValue& params, bool fHelp) } obj.pushKV("stake-splitting", stakesplitting); + vSideStakeAlloc = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(); + 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 From 4da4867b325901daa257e4eacfe122d2bdc89f33 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Thu, 5 Oct 2023 13:43:01 -0400 Subject: [PATCH 14/47] Create skeletons for sidestaketablemodel.h/cpp --- src/Makefile.qt.include | 2 ++ src/qt/sidestaketablemodel.cpp | 3 +++ src/qt/sidestaketablemodel.h | 8 ++++++++ 3 files changed, 13 insertions(+) create mode 100644 src/qt/sidestaketablemodel.cpp create mode 100644 src/qt/sidestaketablemodel.h diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index f6fd404e71..072fec2f65 100755 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -285,6 +285,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 \ @@ -372,6 +373,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/qt/sidestaketablemodel.cpp b/src/qt/sidestaketablemodel.cpp new file mode 100644 index 0000000000..b2f0d3e20c --- /dev/null +++ b/src/qt/sidestaketablemodel.cpp @@ -0,0 +1,3 @@ +// Copyright (c) 2014-2023 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/qt/sidestaketablemodel.h b/src/qt/sidestaketablemodel.h new file mode 100644 index 0000000000..c5cfc47676 --- /dev/null +++ b/src/qt/sidestaketablemodel.h @@ -0,0 +1,8 @@ +// Copyright (c) 2014-2023 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 + +#endif // BITCOIN_QT_SIDESTAKETABLEMODEL_H From c988ce39261969f79522f75b8788705c230a7c8b Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Thu, 5 Oct 2023 13:43:46 -0400 Subject: [PATCH 15/47] Add sidestakingTableWidget to optionsdialog form --- src/qt/forms/optionsdialog.ui | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/qt/forms/optionsdialog.ui b/src/qt/forms/optionsdialog.ui index a24ede03e9..f7d4ab6d25 100644 --- a/src/qt/forms/optionsdialog.ui +++ b/src/qt/forms/optionsdialog.ui @@ -251,7 +251,7 @@ 10 10 651 - 171 + 291 @@ -327,6 +327,9 @@ + + + From 09aff4e81cfd9ae6e2134f951363de035c56e304 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Fri, 6 Oct 2023 12:52:11 -0400 Subject: [PATCH 16/47] Implementation of SideStakeTableModel and sidestakeTableView in options --- src/Makefile.qt.include | 1 + src/gridcoin/sidestake.cpp | 20 ++- src/gridcoin/sidestake.h | 2 +- src/qt/CMakeLists.txt | 2 + src/qt/forms/optionsdialog.ui | 209 +++++++++++++++----------- src/qt/optionsdialog.cpp | 52 +++++++ src/qt/optionsdialog.h | 15 ++ src/qt/optionsmodel.cpp | 7 + src/qt/optionsmodel.h | 5 + src/qt/sidestaketablemodel.cpp | 264 +++++++++++++++++++++++++++++++++ src/qt/sidestaketablemodel.h | 82 ++++++++++ 11 files changed, 564 insertions(+), 95 deletions(-) diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 072fec2f65..de442f7f93 100755 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -161,6 +161,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 \ diff --git a/src/gridcoin/sidestake.cpp b/src/gridcoin/sidestake.cpp index 5133510183..36ee082253 100644 --- a/src/gridcoin/sidestake.cpp +++ b/src/gridcoin/sidestake.cpp @@ -191,9 +191,21 @@ SideStakePayload::SideStakePayload(SideStake entry) // ----------------------------------------------------------------------------- // Class: SideStakeRegistry // ----------------------------------------------------------------------------- -const SideStakeRegistry::SideStakeMap& SideStakeRegistry::SideStakeEntries() const +const std::vector SideStakeRegistry::SideStakeEntries() const { - return m_sidestake_entries; + std::vector sidestakes; + + LOCK(cs_lock); + + for (const auto& entry : m_sidestake_entries) { + sidestakes.push_back(entry.second); + } + + for (const auto& entry : m_local_sidestake_entries) { + sidestakes.push_back(entry.second); + } + + return sidestakes; } const std::vector SideStakeRegistry::ActiveSideStakeEntries() @@ -586,8 +598,8 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() // First, determine allocation already taken by mandatory sidestakes, because they must be allocated first. for (const auto& entry : SideStakeEntries()) { - if (entry.second->m_status == SideStakeStatus::MANDATORY) { - dSumAllocation += entry.second->m_allocation; + if (entry->m_status == SideStakeStatus::MANDATORY) { + dSumAllocation += entry->m_allocation; } } diff --git a/src/gridcoin/sidestake.h b/src/gridcoin/sidestake.h index 964f933b5c..245873c211 100644 --- a/src/gridcoin/sidestake.h +++ b/src/gridcoin/sidestake.h @@ -399,7 +399,7 @@ class SideStakeRegistry : public IContractHandler //! //! \return \c A reference to the current sidestake entries stored in the registry. //! - const SideStakeMap& SideStakeEntries() const; + const std::vector SideStakeEntries() const; //! //! \brief Get the collection of active sidestake entries. This is presented as a vector of diff --git a/src/qt/CMakeLists.txt b/src/qt/CMakeLists.txt index 3302a30599..03b06569f7 100644 --- a/src/qt/CMakeLists.txt +++ b/src/qt/CMakeLists.txt @@ -61,6 +61,7 @@ add_library(gridcoinqt STATIC rpcconsole.cpp sendcoinsdialog.cpp sendcoinsentry.cpp + sidestaketablemodel.cpp signverifymessagedialog.cpp trafficgraphwidget.cpp transactiondesc.cpp @@ -122,6 +123,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/forms/optionsdialog.ui b/src/qt/forms/optionsdialog.ui index f7d4ab6d25..2ab8be9f2d 100644 --- a/src/qt/forms/optionsdialog.ui +++ b/src/qt/forms/optionsdialog.ui @@ -16,8 +16,8 @@ true - - + + QTabWidget::North @@ -245,93 +245,122 @@ Staking - - - - 10 - 10 - 651 - 291 - - - - - - - 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 - - - - - - - - + + + + + + + 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 + + + + + + + Edit + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + @@ -544,7 +573,7 @@ - + diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp index 1e211749e8..a5af0b1d63 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -9,6 +9,7 @@ #include "qt/decoration.h" #include "init.h" #include "miner.h" +#include "sidestaketablemodel.h" #include #include @@ -152,6 +153,27 @@ 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->horizontalHeader()->setStretchLastSection(true); + ui->sidestakingTableView->setShowGrid(true); + + connect(ui->enableSideStaking, &QCheckBox::toggled, this, &OptionsDialog::hideSideStakeEdit); + connect(ui->enableSideStaking, &QCheckBox::toggled, this, &OptionsDialog::refreshSideStakeTableModel); } /* update the display unit, to not use the default ("BTC") */ @@ -253,9 +275,22 @@ void OptionsDialog::on_cancelButton_clicked() void OptionsDialog::on_applyButton_clicked() { mapper->submit(); + + refreshSideStakeTableModel(); + disableApplyButton(); } +void OptionsDialog::newSideStakeButton_clicked() +{ + +} + +void OptionsDialog::editSideStakeButton_clicked() +{ + +} + void OptionsDialog::showRestartWarning_Proxy() { if(!fRestartWarningDisplayed_Proxy) @@ -333,6 +368,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 @@ -407,6 +452,13 @@ void OptionsDialog::handlePollExpireNotifyValid(QValidatedLineEdit *object, bool } } +void OptionsDialog::refreshSideStakeTableModel() +{ + mapper->submit(); + + model->getSideStakeTableModel()->refresh(); +} + bool OptionsDialog::eventFilter(QObject *object, QEvent *event) { bool filter_event = false; diff --git a/src/qt/optionsdialog.h b/src/qt/optionsdialog.h index ae7d2adf6e..8e04400daa 100644 --- a/src/qt/optionsdialog.h +++ b/src/qt/optionsdialog.h @@ -40,6 +40,9 @@ private slots: void on_cancelButton_clicked(); void on_applyButton_clicked(); + void newSideStakeButton_clicked(); + void editSideStakeButton_clicked(); + void showRestartWarning_Proxy(); void showRestartWarning_Lang(); void updateDisplayUnit(); @@ -48,11 +51,14 @@ 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 refreshSideStakeTableModel(); + signals: void proxyIpValid(QValidatedLineEdit *object, bool fValid); void stakingEfficiencyValid(QValidatedLineEdit *object, bool fValid); @@ -69,6 +75,15 @@ private slots: bool fStakingEfficiencyValid; bool fMinStakeSplitValueValid; bool fPollExpireNotifyValid; + + enum SideStakeTableColumnWidths + { + ADDRESS_COLUMN_WIDTH = 200, + ALLOCATION_COLUMN_WIDTH = 80, + DESCRIPTION_COLUMN_WIDTH = 130, + BANSUBNET_COLUMN_WIDTH = 150, + STATUS_COLUMN_WIDTH = 150 + }; }; #endif // BITCOIN_QT_OPTIONSDIALOG_H diff --git a/src/qt/optionsmodel.cpp b/src/qt/optionsmodel.cpp index a0de553ea9..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 @@ -469,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 f6e7f37bdf..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 @@ -78,6 +79,8 @@ class OptionsModel : public QAbstractListModel QString getCurrentStyle(); QString getDataDir(); + SideStakeTableModel* getSideStakeTableModel(); + /* Explicit setters */ void setCurrentStyle(QString theme); void setMaskValues(bool privacy_mode); @@ -102,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/sidestaketablemodel.cpp b/src/qt/sidestaketablemodel.cpp index b2f0d3e20c..8514b10df9 100644 --- a/src/qt/sidestaketablemodel.cpp +++ b/src/qt/sidestaketablemodel.cpp @@ -1,3 +1,267 @@ // Copyright (c) 2014-2023 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 + +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); + } + + switch (static_cast(m_column)) { + case SideStakeTableModel::Address: + return pLeft->m_key < pRight->m_key; + case SideStakeTableModel::Allocation: + return pLeft->m_allocation < pRight->m_allocation; + case SideStakeTableModel::Description: + return pLeft->m_description.compare(pRight->m_description) < 0; + case SideStakeTableModel::Status: + return pLeft->m_status < pRight->m_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(); + + 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()); + + // 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(rec->m_key.ToString()); + case Allocation: + return rec->m_allocation * 100.0; + case Description: + return QString::fromStdString(rec->m_description); + case Status: + return QString::fromStdString(rec->StatusToString()); + } // no default case, so the compiler can warn about missing cases + assert(false); + } 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(); +} + +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; + + Qt::ItemFlags retval = Qt::ItemIsSelectable | Qt::ItemIsEnabled; + 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()); + + double sidestake_allocation = 0.0; + + std::string sidestake_description = description.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, true); + + if (!core_local_sidestake.empty()) { + m_edit_status = DUPLICATE_ADDRESS; + return QString(); + } + + if (!ParseDouble(allocation.toStdString(), &sidestake_allocation) + && (sidestake_allocation < 0.0 || sidestake_allocation > 1.0)) { + m_edit_status = INVALID_ALLOCATION; + return QString(); + } + + sidestake_allocation /= 100.0; + + registry.NonContractAdd(GRC::SideStake(sidestake_address, + sidestake_allocation, + sidestake_description, + int64_t {0}, + uint256 {}, + GRC::SideStakeStatus::ACTIVE)); + + updateSideStakeTableModel(); + + return QString::fromStdString(sidestake_address.ToString()); +} + +SideStakeTableModel::EditStatus SideStakeTableModel::getEditStatus() const +{ + return m_edit_status; +} + +void SideStakeTableModel::refresh() +{ + Q_EMIT layoutAboutToBeChanged(); + m_priv->refreshSideStakes(); + 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(); +} + +static void RwSettingsUpdated(SideStakeTableModel* sidestake_model) +{ + qDebug() << QString("%1").arg(__func__); + QMetaObject::invokeMethod(sidestake_model, "updateSideStakeTableModel", Qt::QueuedConnection); +} + +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 index c5cfc47676..24d7263e72 100644 --- a/src/qt/sidestaketablemodel.h +++ b/src/qt/sidestaketablemodel.h @@ -5,4 +5,86 @@ #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; +}; + +class SideStakeTableModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + explicit SideStakeTableModel(OptionsModel* parent = nullptr); + ~SideStakeTableModel(); + + enum 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) */ + }; + + /** @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; + 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; + 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(); + + void updateSideStakeTableModel(); + +signals: + + void updateSideStakeTableModelSig(); +}; + #endif // BITCOIN_QT_SIDESTAKETABLEMODEL_H From a1a349bcc724fcff639a6d1c4f2da17001890f37 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Fri, 6 Oct 2023 13:55:47 -0400 Subject: [PATCH 17/47] Add missing logic to pick up descriptions in LoadLocalSideStakesFromConfig() --- src/gridcoin/sidestake.cpp | 41 ++++++++++++++++++++++++-------------- src/init.cpp | 7 +++++++ 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/gridcoin/sidestake.cpp b/src/gridcoin/sidestake.cpp index 36ee082253..90a00f3c25 100644 --- a/src/gridcoin/sidestake.cpp +++ b/src/gridcoin/sidestake.cpp @@ -551,7 +551,7 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() } std::vector vSideStakes; - std::vector> raw_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 @@ -559,40 +559,51 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() // 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. + // -sidestake format. Note that -descriptions is optional; however, if descriptions is used, the number 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); - if (addresses.size() != allocations.size()) + bool new_format_valid = false; + + if (addresses.size() != allocations.size() || (!descriptions.empty() && descriptions.size() != addresses.size())) { - LogPrintf("WARN: %s: Malformed new style sidestaking configuration entries. Reverting to original format.", + 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; - 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])); + raw_vSideStakeAlloc.push_back(std::make_tuple(addresses[i], allocations[i], descriptions[i])); } } - else if (gArgs.GetArgs("-sidestake").size()) + + 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) + 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])); + // 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], "")); + } } } @@ -611,7 +622,7 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() double dAllocation = 0.0; - sAddress = entry.first; + sAddress = std::get<0>(entry); CBitcoinAddress address(sAddress); if (!address.IsValid()) @@ -620,9 +631,9 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() continue; } - if (!ParseDouble(entry.second, &dAllocation)) + if (!ParseDouble(std::get<1>(entry), &dAllocation)) { - LogPrintf("WARN: %s: Invalid allocation %s provided. Skipping allocation.", __func__, entry.second); + LogPrintf("WARN: %s: Invalid allocation %s provided. Skipping allocation.", __func__, std::get<1>(entry)); continue; } @@ -648,7 +659,7 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() SideStake sidestake(static_cast(address), dAllocation, - std::string {}, + std::get<2>(entry), 0, uint256{}, SideStakeStatus::ACTIVE); 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); From 1a20fd0d84e717209ff6cd56fc1e27d85b10e3fa Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Fri, 6 Oct 2023 16:56:04 -0400 Subject: [PATCH 18/47] Add icons to sidestake new and edit buttons --- src/qt/forms/optionsdialog.ui | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/qt/forms/optionsdialog.ui b/src/qt/forms/optionsdialog.ui index 2ab8be9f2d..242a402d3c 100644 --- a/src/qt/forms/optionsdialog.ui +++ b/src/qt/forms/optionsdialog.ui @@ -334,6 +334,10 @@ New + + + :/icons/add:/icons/add + @@ -341,6 +345,10 @@ Edit + + + :/icons/edit:/icons/edit + @@ -663,6 +671,8 @@
qvaluecombobox.h
- + + + From 19a7ea6463697ca5bfc6c0b031fcc7b2070b5203 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Fri, 6 Oct 2023 19:11:52 -0400 Subject: [PATCH 19/47] Implementation of EditSideStakeDialog --- src/Makefile.qt.include | 4 + src/gridcoin/sidestake.cpp | 80 +++++++----- src/gridcoin/sidestake.h | 11 +- src/miner.cpp | 2 +- src/qt/CMakeLists.txt | 1 + src/qt/editsidestakedialog.cpp | 149 ++++++++++++++++++++++ src/qt/editsidestakedialog.h | 50 ++++++++ src/qt/forms/editsidestakedialog.ui | 173 +++++++++++++++++++++++++ src/qt/forms/optionsdialog.ui | 17 +++ src/qt/optionsdialog.cpp | 130 +++++++++++++++++-- src/qt/optionsdialog.h | 7 ++ src/qt/sidestaketablemodel.cpp | 187 ++++++++++++++++++++++++++-- src/qt/sidestaketablemodel.h | 11 +- src/rpc/mining.cpp | 3 +- 14 files changed, 773 insertions(+), 52 deletions(-) create mode 100644 src/qt/editsidestakedialog.cpp create mode 100644 src/qt/editsidestakedialog.h create mode 100644 src/qt/forms/editsidestakedialog.ui diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index de442f7f93..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 \ @@ -250,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 \ @@ -342,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 \ diff --git a/src/gridcoin/sidestake.cpp b/src/gridcoin/sidestake.cpp index 90a00f3c25..0220a15b87 100644 --- a/src/gridcoin/sidestake.cpp +++ b/src/gridcoin/sidestake.cpp @@ -208,7 +208,8 @@ const std::vector SideStakeRegistry::SideStakeEntries() const return sidestakes; } -const std::vector SideStakeRegistry::ActiveSideStakeEntries() +const std::vector SideStakeRegistry::ActiveSideStakeEntries(const bool& local_only, + const bool& include_zero_alloc) { std::vector sidestakes; double allocation_sum = 0.0; @@ -221,11 +222,15 @@ const std::vector SideStakeRegistry::ActiveSideStakeEntries() LOCK(cs_lock); // Do mandatory sidestakes first. - for (const auto& entry : m_sidestake_entries) - { - if (entry.second->m_status == SideStakeStatus::MANDATORY && allocation_sum + entry.second->m_allocation <= 1.0) { - sidestakes.push_back(entry.second); - allocation_sum += entry.second->m_allocation; + if (!local_only) { + for (const auto& entry : m_sidestake_entries) + { + if (entry.second->m_status == SideStakeStatus::MANDATORY && allocation_sum + entry.second->m_allocation <= 1.0) { + if ((include_zero_alloc && entry.second->m_allocation == 0.0) || entry.second->m_allocation > 0.0) { + sidestakes.push_back(entry.second); + allocation_sum += entry.second->m_allocation; + } + } } } @@ -238,8 +243,10 @@ const std::vector SideStakeRegistry::ActiveSideStakeEntries() for (const auto& entry : m_local_sidestake_entries) { if (entry.second->m_status == SideStakeStatus::ACTIVE && allocation_sum + entry.second->m_allocation <= 1.0) { - sidestakes.push_back(entry.second); - allocation_sum += entry.second->m_allocation; + if ((include_zero_alloc && entry.second->m_allocation == 0.0) || entry.second->m_allocation > 0.0) { + sidestakes.push_back(entry.second); + allocation_sum += entry.second->m_allocation; + } } } } @@ -263,7 +270,7 @@ std::vector SideStakeRegistry::Try(const CBitcoinAddressForStorag const auto local_entry = m_local_sidestake_entries.find(key); - if (local_entry != m_sidestake_entries.end()) { + if (local_entry != m_local_sidestake_entries.end()) { result.push_back(local_entry->second); } @@ -368,12 +375,16 @@ void SideStakeRegistry::AddDelete(const ContractContext& ctx) return; } -void SideStakeRegistry::NonContractAdd(SideStake& sidestake) +void SideStakeRegistry::NonContractAdd(const SideStake& 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_key] = std::make_shared(sidestake); - + if (save_to_file) { + SaveLocalSideStakesToConfig(); + } } void SideStakeRegistry::Add(const ContractContext& ctx) @@ -381,13 +392,19 @@ void SideStakeRegistry::Add(const ContractContext& ctx) AddDelete(ctx); } -void SideStakeRegistry::NonContractDelete(CBitcoinAddressForStorage& address) +void SideStakeRegistry::NonContractDelete(const CBitcoinAddressForStorage& address, const bool& save_to_file) { + LOCK(cs_lock); + auto sidestake_entry_pair_iter = m_local_sidestake_entries.find(address); 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::Delete(const ContractContext& ctx) @@ -571,17 +588,22 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() bool new_format_valid = false; - if (addresses.size() != allocations.size() || (!descriptions.empty() && descriptions.size() != addresses.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; + 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) - { - raw_vSideStakeAlloc.push_back(std::make_tuple(addresses[i], allocations[i], descriptions[i])); + 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])); + } + } } } @@ -639,9 +661,9 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() dAllocation /= 100.0; - if (dAllocation <= 0) + if (dAllocation < 0) { - LogPrintf("WARN: %s: Negative or zero allocation provided. Skipping allocation.", __func__); + LogPrintf("WARN: %s: Negative allocation provided. Skipping allocation.", __func__); continue; } @@ -665,7 +687,7 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() SideStakeStatus::ACTIVE); // This will add or update (replace) a non-contract entry in the registry for the local sidestake. - NonContractAdd(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. @@ -708,6 +730,8 @@ bool SideStakeRegistry::SaveLocalSideStakesToConfig() std::vector> settings; + LOCK(cs_lock); + unsigned int i = 0; for (const auto& iter : m_local_sidestake_entries) { if (i) { @@ -721,9 +745,9 @@ bool SideStakeRegistry::SaveLocalSideStakesToConfig() ++i; } - settings.push_back(std::make_pair("addresses", addresses)); - settings.push_back(std::make_pair("allocations", allocations)); - settings.push_back(std::make_pair("descriptions", descriptions)); + 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); diff --git a/src/gridcoin/sidestake.h b/src/gridcoin/sidestake.h index 245873c211..dccb6b216a 100644 --- a/src/gridcoin/sidestake.h +++ b/src/gridcoin/sidestake.h @@ -408,9 +408,11 @@ class SideStakeRegistry : public IContractHandler //! Mandatory sidestakes come before local ones, and the method ensures that the sidestakes //! returned do not total an allocation greater than 1.0. //! + //! \param bool true to return local sidestakes only + //! //! \return A vector of smart pointers to sidestake entries. //! - const std::vector ActiveSideStakeEntries(); + const std::vector ActiveSideStakeEntries(const bool& local_only, const bool& include_zero_alloc); //! //! \brief Get the current sidestake entry for the specified key string. @@ -472,8 +474,9 @@ class SideStakeRegistry : public IContractHandler //! the registry db. //! //! \param SideStake object to add + //! \param bool save_to_file if true causes SaveLocalSideStakesToConfig() to be called. //! - void NonContractAdd(SideStake& sidestake); + void NonContractAdd(const SideStake& sidestake, const bool& save_to_file = true); //! //! \brief Add a sidestake entry to the registry from contract data. For the sidestake registry @@ -487,9 +490,11 @@ class SideStakeRegistry : public IContractHandler //! //! \brief Provides for deletion of local (voluntary) sidestakes from the in-memory map that are not persisted //! to the registry db. Deletion is by the map key (CBitcoinAddress). + //! //! \param address + //! \param bool save_to_file if true causes SaveLocalSideStakesToConfig() to be called. //! - void NonContractDelete(CBitcoinAddressForStorage& address); + void NonContractDelete(const CBitcoinAddressForStorage& address, const bool& save_to_file = true); //! //! \brief Mark a sidestake entry deleted in the registry from contract data. For the sidestake registry diff --git a/src/miner.cpp b/src/miner.cpp index 2c11645982..c47236f80f 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -1320,7 +1320,7 @@ void StakeMiner(CWallet *pwallet) // Note that fEnableSideStaking is now processed internal to ActiveSideStakeEntries. The sidestaking flag only // controls local sidestakes. If there exists mandatory sidestakes, they occur regardless of the flag. - vSideStakeAlloc = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(); + vSideStakeAlloc = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(false, false); // wait for next round if (!MilliSleep(nMinerSleep)) return; diff --git a/src/qt/CMakeLists.txt b/src/qt/CMakeLists.txt index 03b06569f7..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 diff --git a/src/qt/editsidestakedialog.cpp b/src/qt/editsidestakedialog.cpp new file mode 100644 index 0000000000..7d0904d351 --- /dev/null +++ b/src/qt/editsidestakedialog.cpp @@ -0,0 +1,149 @@ +// Copyright (c) 2014-2023 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().toString()); + ui->allocationLineEdit->setText(model->index(row, SideStakeTableModel::Allocation, QModelIndex()).data().toString()); + ui->descriptionLineEdit->setText(model->index(row, SideStakeTableModel::Description, QModelIndex()).data().toString()); + ui->statusLineEdit->setText(model->index(row, SideStakeTableModel::Status, QModelIndex()).data().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.").arg(ui->allocationLineEdit->text()), + 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..b58a44fad6 --- /dev/null +++ b/src/qt/editsidestakedialog.h @@ -0,0 +1,50 @@ +// Copyright (c) 2014-2023 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 242a402d3c..c1c7d8038a 100644 --- a/src/qt/forms/optionsdialog.ui +++ b/src/qt/forms/optionsdialog.ui @@ -342,6 +342,9 @@
+ + false + Edit @@ -351,6 +354,20 @@ + + + + false + + + Delete + + + + :/icons/remove:/icons/remove + + + diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp index a5af0b1d63..250b803c4b 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -10,7 +10,9 @@ #include "init.h" #include "miner.h" #include "sidestaketablemodel.h" +#include "editsidestakedialog.h" +#include #include #include #include @@ -172,8 +174,22 @@ void OptionsDialog::setModel(OptionsModel *model) ui->sidestakingTableView->horizontalHeader()->setStretchLastSection(true); ui->sidestakingTableView->setShowGrid(true); + ui->sidestakingTableView->sortByColumn(0, Qt::AscendingOrder); + 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); + } /* update the display unit, to not use the default ("BTC") */ @@ -263,7 +279,8 @@ void OptionsDialog::setSaveButtonState(bool fState) void OptionsDialog::on_okButton_clicked() { - mapper->submit(); + refreshSideStakeTableModel(); + accept(); } @@ -274,8 +291,6 @@ void OptionsDialog::on_cancelButton_clicked() void OptionsDialog::on_applyButton_clicked() { - mapper->submit(); - refreshSideStakeTableModel(); disableApplyButton(); @@ -283,19 +298,65 @@ void OptionsDialog::on_applyButton_clicked() 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; } } @@ -454,9 +515,12 @@ void OptionsDialog::handlePollExpireNotifyValid(QValidatedLineEdit *object, bool void OptionsDialog::refreshSideStakeTableModel() { - mapper->submit(); - - model->getSideStakeTableModel()->refresh(); + if (!mapper->submit() + && model->getSideStakeTableModel()->getEditStatus() == SideStakeTableModel::INVALID_ALLOCATION) { + emit sidestakeAllocationInvalid(); + } else { + model->getSideStakeTableModel()->refresh(); + } } bool OptionsDialog::eventFilter(QObject *object, QEvent *event) @@ -545,5 +609,57 @@ bool OptionsDialog::eventFilter(QObject *object, QEvent *event) } } +<<<<<<< HEAD +======= + // 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) { + LogPrint(BCLog::LogFlags::VERBOSE, "INFO %s: event type = %i", + __func__, + (int) event->type()); + + emit sidestakeAllocationInvalid(); + } + } + +>>>>>>> 71b6deb3e (Implementation of EditSideStakeDialog) 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())->m_status + == GRC::SideStakeStatus::MANDATORY) { + 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::updateSideStakeTableView() +{ + ui->sidestakingTableView->update(); +} diff --git a/src/qt/optionsdialog.h b/src/qt/optionsdialog.h index 8e04400daa..325dcc2839 100644 --- a/src/qt/optionsdialog.h +++ b/src/qt/optionsdialog.h @@ -42,6 +42,7 @@ private slots: void newSideStakeButton_clicked(); void editSideStakeButton_clicked(); + void deleteSideStakeButton_clicked(); void showRestartWarning_Proxy(); void showRestartWarning_Lang(); @@ -56,6 +57,7 @@ private slots: void handleStakingEfficiencyValid(QValidatedLineEdit *object, bool fState); void handleMinStakeSplitValueValid(QValidatedLineEdit *object, bool fState); void handlePollExpireNotifyValid(QValidatedLineEdit *object, bool fState); + void handleSideStakeAllocationInvalid(); void refreshSideStakeTableModel(); @@ -64,6 +66,7 @@ private slots: void stakingEfficiencyValid(QValidatedLineEdit *object, bool fValid); void minStakeSplitValueValid(QValidatedLineEdit *object, bool fValid); void pollExpireNotifyValid(QValidatedLineEdit *object, bool fValid); + void sidestakeAllocationInvalid(); private: Ui::OptionsDialog *ui; @@ -84,6 +87,10 @@ private slots: BANSUBNET_COLUMN_WIDTH = 150, STATUS_COLUMN_WIDTH = 150 }; + +private slots: + void sidestakeSelectionChanged(); + void updateSideStakeTableView(); }; #endif // BITCOIN_QT_OPTIONSDIALOG_H diff --git a/src/qt/sidestaketablemodel.cpp b/src/qt/sidestaketablemodel.cpp index 8514b10df9..c3df02df21 100644 --- a/src/qt/sidestaketablemodel.cpp +++ b/src/qt/sidestaketablemodel.cpp @@ -10,6 +10,16 @@ #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) @@ -48,7 +58,7 @@ class SideStakeTablePriv { m_cached_sidestakes.clear(); - std::vector core_sidestakes = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(); + std::vector core_sidestakes = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(false, true); m_cached_sidestakes.reserve(core_sidestakes.size()); @@ -83,6 +93,8 @@ SideStakeTableModel::SideStakeTableModel(OptionsModel* parent) m_columns << tr("Address") << tr("Allocation") << tr("Description") << tr("Status"); m_priv.reset(new SideStakeTablePriv()); + subscribeToCoreSignals(); + // load initial data refresh(); } @@ -116,7 +128,7 @@ QVariant SideStakeTableModel::data(const QModelIndex &index, int role) const GRC::SideStake* rec = static_cast(index.internalPointer()); const auto column = static_cast(index.column()); - if (role == Qt::DisplayRole) { + if (role == Qt::DisplayRole || role == Qt::EditRole) { switch (column) { case Address: return QString::fromStdString(rec->m_key.ToString()); @@ -146,6 +158,126 @@ QVariant SideStakeTableModel::data(const QModelIndex &index, int role) const 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: + { + CBitcoinAddress address; + address.SetString(value.toString().toStdString()); + + + if (rec->m_key == address) { + m_edit_status = NO_CHANGES; + return false; + } else if (!address.IsValid()) { + m_edit_status = INVALID_ADDRESS; + return false; + } + + std::vector sidestakes = registry.Try(address, true); + + if (!sidestakes.empty()) { + m_edit_status = DUPLICATE_ADDRESS; + return false; + } + + // There is no valid state change left for address. If you are editing the item, the address field is + // not editable, so will be NO_CHANGES. For a non-matching address, it will be covered by the dialog + // in New mode. + break; + } + case Allocation: + { + double prior_total_allocation = 0.0; + + // Save the original local sidestake (also in the core). + GRC::SideStake orig_sidestake = *rec; + + for (const auto& entry : registry.ActiveSideStakeEntries(false, true)) { + if (entry->m_key == orig_sidestake.m_key) { + continue; + } + + prior_total_allocation += entry->m_allocation * 100.0; + } + + if (rec->m_allocation * 100.0 == value.toDouble()) { + m_edit_status = NO_CHANGES; + return false; + } + + if (value.toDouble() < 0.0 || prior_total_allocation + value.toDouble() > 100.0) { + m_edit_status = INVALID_ALLOCATION; + + LogPrint(BCLog::LogFlags::VERBOSE, "INFO: %s: m_edit_status = %i", + __func__, + (int) m_edit_status); + + return false; + } + + // Delete the original sidestake + registry.NonContractDelete(orig_sidestake.m_key, false); + + // Add back the sidestake with the modified allocation + registry.NonContractAdd(GRC::SideStake(orig_sidestake.m_key, + value.toDouble() / 100.0, + orig_sidestake.m_description, + int64_t {0}, + uint256 {}, + orig_sidestake.m_status.Value()), true); + + break; + } + case Description: + { + if (rec->m_description == value.toString().toStdString()) { + m_edit_status = NO_CHANGES; + return false; + } + + // Save the original local sidestake (also in the core). + GRC::SideStake orig_sidestake = *rec; + + // Delete the original sidestake + registry.NonContractDelete(orig_sidestake.m_key, false); + + // Add back the sidestake with the modified allocation + registry.NonContractAdd(GRC::SideStake(orig_sidestake.m_key, + orig_sidestake.m_allocation, + value.toString().toStdString(), + int64_t {0}, + uint256 {}, + orig_sidestake.m_status.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) @@ -160,9 +292,19 @@ QVariant SideStakeTableModel::headerData(int section, Qt::Orientation orientatio Qt::ItemFlags SideStakeTableModel::flags(const QModelIndex &index) const { - if (!index.isValid()) return Qt::NoItemFlags; + if (!index.isValid()) { + return Qt::NoItemFlags; + } + + GRC::SideStake* rec = static_cast(index.internalPointer()); Qt::ItemFlags retval = Qt::ItemIsSelectable | Qt::ItemIsEnabled; + + if (rec->m_status == GRC::SideStakeStatus::ACTIVE + && (index.column() == Allocation || index.column() == Description)) { + retval |= Qt::ItemIsEditable; + } + return retval; } @@ -198,13 +340,22 @@ QString SideStakeTableModel::addRow(const QString &address, const QString &alloc // UI model. std::vector core_local_sidestake = registry.Try(sidestake_address, true); + double prior_total_allocation = 0.0; + + // Get total allocation of all active/mandatory sidestake entries + for (const auto& entry : registry.ActiveSideStakeEntries(false, true)) { + prior_total_allocation += entry->m_allocation * 100.0; + } + if (!core_local_sidestake.empty()) { m_edit_status = DUPLICATE_ADDRESS; return QString(); } + // 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 100. if (!ParseDouble(allocation.toStdString(), &sidestake_allocation) - && (sidestake_allocation < 0.0 || sidestake_allocation > 1.0)) { + || sidestake_allocation < 0.0 || prior_total_allocation + sidestake_allocation > 100.0) { m_edit_status = INVALID_ALLOCATION; return QString(); } @@ -223,6 +374,25 @@ QString SideStakeTableModel::addRow(const QString &address, const QString &alloc 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->m_status == GRC::SideStakeStatus::MANDATORY) + { + // 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->m_key); + + updateSideStakeTableModel(); + + return true; +} + SideStakeTableModel::EditStatus SideStakeTableModel::getEditStatus() const { return m_edit_status; @@ -232,6 +402,9 @@ void SideStakeTableModel::refresh() { Q_EMIT layoutAboutToBeChanged(); m_priv->refreshSideStakes(); + + m_edit_status = OK; + Q_EMIT layoutChanged(); } @@ -249,12 +422,6 @@ void SideStakeTableModel::updateSideStakeTableModel() emit updateSideStakeTableModelSig(); } -static void RwSettingsUpdated(SideStakeTableModel* sidestake_model) -{ - qDebug() << QString("%1").arg(__func__); - QMetaObject::invokeMethod(sidestake_model, "updateSideStakeTableModel", Qt::QueuedConnection); -} - void SideStakeTableModel::subscribeToCoreSignals() { // Connect signals to client diff --git a/src/qt/sidestaketablemodel.h b/src/qt/sidestaketablemodel.h index 24d7263e72..cd265c7461 100644 --- a/src/qt/sidestaketablemodel.h +++ b/src/qt/sidestaketablemodel.h @@ -28,6 +28,10 @@ class SideStakeLessThan 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 @@ -57,9 +61,11 @@ class SideStakeTableModel : public 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); /*@}*/ @@ -80,11 +86,12 @@ public Q_SLOTS: void subscribeToCoreSignals(); void unsubscribeFromCoreSignals(); - void updateSideStakeTableModel(); - signals: void updateSideStakeTableModelSig(); + +public slots: + void updateSideStakeTableModel(); }; #endif // BITCOIN_QT_SIDESTAKETABLEMODEL_H diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index 373ba3e98b..dc0cc887ce 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -108,7 +108,8 @@ UniValue getstakinginfo(const UniValue& params, bool fHelp) } obj.pushKV("stake-splitting", stakesplitting); - vSideStakeAlloc = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(); + // This is what the miner sees... + vSideStakeAlloc = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(false, false); sidestaking.pushKV("local_side_staking_enabled", fEnableSideStaking); From 09d92168ff961eeacc0d648382fadca3c0f63a09 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Thu, 12 Oct 2023 17:51:52 -0400 Subject: [PATCH 20/47] Implement proportional column resizing for SideStakeTableView This is based on the same approach used for the AddressBook. --- src/qt/optionsdialog.cpp | 137 ++++++++++++++++++++++++++++++++--- src/qt/optionsdialog.h | 22 +++++- src/qt/sidestaketablemodel.h | 5 ++ 3 files changed, 149 insertions(+), 15 deletions(-) diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp index 250b803c4b..ce993396c1 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -11,6 +11,7 @@ #include "miner.h" #include "sidestaketablemodel.h" #include "editsidestakedialog.h" +#include "logging.h" #include #include @@ -30,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); @@ -171,11 +174,25 @@ void OptionsDialog::setModel(OptionsModel *model) 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->horizontalHeader()->setStretchLastSection(true); 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); @@ -609,22 +626,14 @@ bool OptionsDialog::eventFilter(QObject *object, QEvent *event) } } -<<<<<<< HEAD -======= // This is required to provide immediate feedback on invalid allocation entries on in place editing. - if (object == ui->sidestakingTableView) - { + if (object == ui->sidestakingTableView) { if (model->getSideStakeTableModel()->getEditStatus() == SideStakeTableModel::INVALID_ALLOCATION) { - LogPrint(BCLog::LogFlags::VERBOSE, "INFO %s: event type = %i", - __func__, - (int) event->type()); - emit sidestakeAllocationInvalid(); } } ->>>>>>> 71b6deb3e (Implementation of EditSideStakeDialog) - return QDialog::eventFilter(object, event); + return QDialog::eventFilter(object, event); } void OptionsDialog::sidestakeSelectionChanged() @@ -663,3 +672,109 @@ 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 325dcc2839..17e017b013 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 */ @@ -61,6 +66,8 @@ private slots: void refreshSideStakeTableModel(); + void tabWidgetSelectionChanged(int index); + signals: void proxyIpValid(QValidatedLineEdit *object, bool fValid); void stakingEfficiencyValid(QValidatedLineEdit *object, bool fValid); @@ -79,18 +86,25 @@ private slots: 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 = 80, + ALLOCATION_COLUMN_WIDTH = 50, DESCRIPTION_COLUMN_WIDTH = 130, - BANSUBNET_COLUMN_WIDTH = 150, - STATUS_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/sidestaketablemodel.h b/src/qt/sidestaketablemodel.h index cd265c7461..5bbe0f69f9 100644 --- a/src/qt/sidestaketablemodel.h +++ b/src/qt/sidestaketablemodel.h @@ -47,6 +47,11 @@ class SideStakeTableModel : public QAbstractTableModel Status }; + static constexpr std::initializer_list all_ColumnIndex = {Address, + Allocation, + Description, + Status}; + /** Return status of edit/insert operation */ enum EditStatus { OK, /**< Everything ok */ From 68310ef97efaf7714fb20458611dc4930c6bdee6 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Sat, 14 Oct 2023 18:38:49 -0400 Subject: [PATCH 21/47] Implement sidestake description validation --- src/qt/editsidestakedialog.cpp | 9 ++++++++- src/qt/optionsdialog.cpp | 17 ++++++++++++++++- src/qt/optionsdialog.h | 6 ++++-- src/qt/sidestaketablemodel.cpp | 24 +++++++++++++++++++----- src/qt/sidestaketablemodel.h | 3 ++- src/util/strencodings.cpp | 1 + src/util/strencodings.h | 1 + 7 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/qt/editsidestakedialog.cpp b/src/qt/editsidestakedialog.cpp index 7d0904d351..2b8bc3f46f 100644 --- a/src/qt/editsidestakedialog.cpp +++ b/src/qt/editsidestakedialog.cpp @@ -138,7 +138,14 @@ void EditSideStakeDialog::accept() 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.").arg(ui->allocationLineEdit->text()), + "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); } diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp index ce993396c1..98c898ac4b 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -206,7 +206,7 @@ void OptionsDialog::setModel(OptionsModel *model) 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") */ @@ -631,6 +631,10 @@ bool OptionsDialog::eventFilter(QObject *object, QEvent *event) if (model->getSideStakeTableModel()->getEditStatus() == SideStakeTableModel::INVALID_ALLOCATION) { emit sidestakeAllocationInvalid(); } + + if (model->getSideStakeTableModel()->getEditStatus() == SideStakeTableModel::INVALID_DESCRIPTION) { + emit sidestakeDescriptionInvalid(); + } } return QDialog::eventFilter(object, event); @@ -668,6 +672,17 @@ void OptionsDialog::handleSideStakeAllocationInvalid() 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(); diff --git a/src/qt/optionsdialog.h b/src/qt/optionsdialog.h index 17e017b013..64a2deaa10 100644 --- a/src/qt/optionsdialog.h +++ b/src/qt/optionsdialog.h @@ -63,6 +63,7 @@ private slots: void handleMinStakeSplitValueValid(QValidatedLineEdit *object, bool fState); void handlePollExpireNotifyValid(QValidatedLineEdit *object, bool fState); void handleSideStakeAllocationInvalid(); + void handleSideStakeDescriptionInvalid(); void refreshSideStakeTableModel(); @@ -74,6 +75,7 @@ private slots: void minStakeSplitValueValid(QValidatedLineEdit *object, bool fValid); void pollExpireNotifyValid(QValidatedLineEdit *object, bool fValid); void sidestakeAllocationInvalid(); + void sidestakeDescriptionInvalid(); private: Ui::OptionsDialog *ui; @@ -93,8 +95,8 @@ private slots: enum SideStakeTableColumnWidths { ADDRESS_COLUMN_WIDTH = 200, - ALLOCATION_COLUMN_WIDTH = 50, - DESCRIPTION_COLUMN_WIDTH = 130, + ALLOCATION_COLUMN_WIDTH = 60, + DESCRIPTION_COLUMN_WIDTH = 150, STATUS_COLUMN_WIDTH = 50 }; diff --git a/src/qt/sidestaketablemodel.cpp b/src/qt/sidestaketablemodel.cpp index c3df02df21..9beabbdf78 100644 --- a/src/qt/sidestaketablemodel.cpp +++ b/src/qt/sidestaketablemodel.cpp @@ -247,11 +247,19 @@ bool SideStakeTableModel::setData(const QModelIndex &index, const QVariant &valu } case Description: { - if (rec->m_description == value.toString().toStdString()) { + std::string orig_value = value.toString().toStdString(); + std::string san_value = SanitizeString(orig_value, SAFE_CHARS_CSV); + + if (rec->m_description == 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; @@ -261,7 +269,7 @@ bool SideStakeTableModel::setData(const QModelIndex &index, const QVariant &valu // Add back the sidestake with the modified allocation registry.NonContractAdd(GRC::SideStake(orig_sidestake.m_key, orig_sidestake.m_allocation, - value.toString().toStdString(), + san_value, int64_t {0}, uint256 {}, orig_sidestake.m_status.Value()), true); @@ -327,8 +335,6 @@ QString SideStakeTableModel::addRow(const QString &address, const QString &alloc double sidestake_allocation = 0.0; - std::string sidestake_description = description.toStdString(); - m_edit_status = OK; if (!sidestake_address.IsValid()) { @@ -362,9 +368,17 @@ QString SideStakeTableModel::addRow(const QString &address, const QString &alloc sidestake_allocation /= 100.0; + 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::SideStake(sidestake_address, sidestake_allocation, - sidestake_description, + sanitized_description, int64_t {0}, uint256 {}, GRC::SideStakeStatus::ACTIVE)); diff --git a/src/qt/sidestaketablemodel.h b/src/qt/sidestaketablemodel.h index 5bbe0f69f9..60320c36f3 100644 --- a/src/qt/sidestaketablemodel.h +++ b/src/qt/sidestaketablemodel.h @@ -58,7 +58,8 @@ class SideStakeTableModel : public QAbstractTableModel 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_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 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 }; /** From 8b5f0cb0bafa9fbbf8f00a00811b55fe79fa355d Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Sat, 21 Oct 2023 19:49:42 -0400 Subject: [PATCH 22/47] Sidestake class cleanup. --- src/gridcoin/sidestake.cpp | 46 ++++++++--------- src/gridcoin/sidestake.h | 91 +++++++++++++++------------------- src/miner.cpp | 8 +-- src/qt/sidestaketablemodel.cpp | 18 +++---- src/rpc/mining.cpp | 2 +- 5 files changed, 77 insertions(+), 88 deletions(-) diff --git a/src/gridcoin/sidestake.cpp b/src/gridcoin/sidestake.cpp index 0220a15b87..fd4c9030e6 100644 --- a/src/gridcoin/sidestake.cpp +++ b/src/gridcoin/sidestake.cpp @@ -48,7 +48,7 @@ CBitcoinAddressForStorage::CBitcoinAddressForStorage(CBitcoinAddress address) // Class: SideStake // ----------------------------------------------------------------------------- SideStake::SideStake() - : m_key() + : m_address() , m_allocation() , m_description() , m_timestamp(0) @@ -58,7 +58,7 @@ SideStake::SideStake() {} SideStake::SideStake(CBitcoinAddressForStorage address, double allocation, std::string description) - : m_key(address) + : m_address(address) , m_allocation(allocation) , m_description(description) , m_timestamp(0) @@ -73,7 +73,7 @@ SideStake::SideStake(CBitcoinAddressForStorage address, int64_t timestamp, uint256 hash, SideStakeStatus status) - : m_key(address) + : m_address(address) , m_allocation(allocation) , m_description(description) , m_timestamp(timestamp) @@ -84,17 +84,17 @@ SideStake::SideStake(CBitcoinAddressForStorage address, bool SideStake::WellFormed() const { - return m_key.IsValid() && m_allocation >= 0.0 && m_allocation <= 1.0; + return m_address.IsValid() && m_allocation >= 0.0 && m_allocation <= 1.0; } CBitcoinAddressForStorage SideStake::Key() const { - return m_key; + return m_address; } std::pair SideStake::KeyValueToString() const { - return std::make_pair(m_key.ToString(), StatusToString()); + return std::make_pair(m_address.ToString(), StatusToString()); } std::string SideStake::StatusToString() const @@ -138,7 +138,7 @@ bool SideStake::operator==(SideStake b) { bool result = true; - result &= (m_key == b.m_key); + result &= (m_address == b.m_address); result &= (m_allocation == b.m_allocation); result &= (m_timestamp == b.m_timestamp); result &= (m_hash == b.m_hash); @@ -166,13 +166,13 @@ SideStakePayload::SideStakePayload(uint32_t version) } SideStakePayload::SideStakePayload(const uint32_t version, - CBitcoinAddressForStorage key, - double value, + CBitcoinAddressForStorage address, + double allocation, std::string description, SideStakeStatus status) : IContractPayload() , m_version(version) - , m_entry(SideStake(key, value, description, 0, uint256{}, status)) + , m_entry(SideStake(address, allocation, description, 0, uint256{}, status)) { } @@ -325,7 +325,7 @@ void SideStakeRegistry::AddDelete(const ContractContext& ctx) LOCK(cs_lock); - auto sidestake_entry_pair_iter = m_sidestake_entries.find(payload.m_entry.m_key); + auto sidestake_entry_pair_iter = m_sidestake_entries.find(payload.m_entry.m_address); SideStake_ptr current_sidestake_entry_ptr = nullptr; @@ -343,12 +343,12 @@ void SideStakeRegistry::AddDelete(const ContractContext& ctx) } LogPrint(LogFlags::CONTRACT, "INFO: %s: SideStake entry add/delete: contract m_version = %u, payload " - "m_version = %u, key = %s, value = %f, m_timestamp = %" PRId64 ", " + "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, - payload.m_entry.m_key.ToString(), + payload.m_entry.m_address.ToString(), payload.m_entry.m_allocation, payload.m_entry.m_timestamp, payload.m_entry.m_hash.ToString(), @@ -364,13 +364,13 @@ void SideStakeRegistry::AddDelete(const ContractContext& ctx) "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__, - historical.m_key.ToString(), + historical.m_address.ToString(), historical.m_allocation, historical.m_hash.GetHex()); } // Finally, insert the new SideStake entry (payload) smart pointer into the m_sidestake_entries map. - m_sidestake_entries[payload.m_entry.m_key] = m_sidestake_db.find(ctx.m_tx.GetHash())->second; + m_sidestake_entries[payload.m_entry.m_address] = m_sidestake_db.find(ctx.m_tx.GetHash())->second; return; } @@ -380,7 +380,7 @@ void SideStakeRegistry::NonContractAdd(const SideStake& sidestake, const bool& s 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_key] = std::make_shared(sidestake); + m_local_sidestake_entries[sidestake.m_address] = std::make_shared(sidestake); if (save_to_file) { SaveLocalSideStakesToConfig(); @@ -421,12 +421,12 @@ void SideStakeRegistry::Revert(const ContractContext& ctx) // resurrect. LOCK(cs_lock); - auto entry_to_revert = m_sidestake_entries.find(payload->m_entry.m_key); + auto entry_to_revert = m_sidestake_entries.find(payload->m_entry.m_address); if (entry_to_revert == m_sidestake_entries.end()) { error("%s: The SideStake entry for key %s to revert was not found in the SideStake entry map.", __func__, - entry_to_revert->second->m_key.ToString()); + entry_to_revert->second->m_address.ToString()); // If there is no record in the current m_sidestake_entries map, then there is nothing to do here. This // should not occur. @@ -434,13 +434,13 @@ void SideStakeRegistry::Revert(const ContractContext& ctx) } // If this is not a null hash, then there will be a prior entry to resurrect. - CBitcoinAddressForStorage key = entry_to_revert->second->m_key; + CBitcoinAddressForStorage key = entry_to_revert->second->m_address; 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_sidestake_entries.erase(payload->m_entry.m_key) == 0) { + if (m_sidestake_entries.erase(payload->m_entry.m_address) == 0) { error("%s: The SideStake entry to erase during a SideStake entry revert for key %s was not found.", __func__, key.ToString()); @@ -474,7 +474,7 @@ void SideStakeRegistry::Revert(const ContractContext& ctx) // 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_sidestake_entries[resurrect_entry->second->m_key] = resurrect_entry->second; + m_sidestake_entries[resurrect_entry->second->m_address] = resurrect_entry->second; } } @@ -576,7 +576,7 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() // 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 number must + // -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; @@ -738,7 +738,7 @@ bool SideStakeRegistry::SaveLocalSideStakesToConfig() separator = ","; } - addresses += separator + iter.second->m_key.ToString(); + addresses += separator + iter.second->m_address.ToString(); allocations += separator + ToString(iter.second->m_allocation * 100.0); descriptions += separator + iter.second->m_description; diff --git a/src/gridcoin/sidestake.h b/src/gridcoin/sidestake.h index dccb6b216a..6705b1c527 100644 --- a/src/gridcoin/sidestake.h +++ b/src/gridcoin/sidestake.h @@ -15,6 +15,10 @@ namespace GRC { +//! +//! \brief The CBitcoinAddressForStorage class. This is a very small extension of the CBitcoinAddress class that +//! provides serialization/deserialization. +//! class CBitcoinAddressForStorage : public CBitcoinAddress { public: @@ -65,19 +69,19 @@ class SideStake //! using Status = EnumByte; - CBitcoinAddressForStorage m_key; //!< The key here is the Gridcoin Address of the sidestake destination. + CBitcoinAddressForStorage m_address; //!< The Gridcoin Address of the sidestake destination. - double m_allocation; //!< The allocation is a double precision floating point between 0.0 and 1.0 inclusive + double m_allocation; //!< The allocation is a double precision floating point between 0.0 and 1.0 inclusive - std::string m_description; //!< The description of the sidestake (optional) + std::string m_description; //!< The description of the sidestake (optional) - int64_t m_timestamp; //!< Time of the sidestake contract transaction. + 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_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 address. + uint256 m_previous_hash; //!< The m_hash of the previous mandatory sidestake allocation with the same address. - Status m_status; //!< The status of the sidestake. It is of type int instead of enum for serialization. + 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. @@ -115,7 +119,8 @@ class SideStake //! \param hash //! \param status //! - SideStake(CBitcoinAddressForStorage address, double allocation, std::string description, int64_t timestamp, uint256 hash, SideStakeStatus status); + SideStake(CBitcoinAddressForStorage address, double allocation, std::string description, int64_t timestamp, + uint256 hash, SideStakeStatus status); //! //! \brief Determine whether a sidestake contains each of the required elements. @@ -124,7 +129,7 @@ class SideStake bool WellFormed() const; //! - //! \brief This is the standardized method that returns the key value for the sidestake entry (for + //! \brief This is the standardized method that returns the key value (in this case the address) for the sidestake entry (for //! the registry_db.h template.) //! //! \return CBitcoinAddress key value for the sidestake entry @@ -132,7 +137,7 @@ class SideStake CBitcoinAddressForStorage Key() const; //! - //! \brief Provides the sidestake address and status (value) as a pair of strings. + //! \brief Provides the sidestake address and status as a pair of strings. //! \return std::pair of strings //! std::pair KeyValueToString() const; @@ -161,7 +166,6 @@ class SideStake //! //! \return Equal or not. //! - bool operator==(SideStake b); //! @@ -171,7 +175,6 @@ class SideStake //! //! \return Equal or not. //! - bool operator!=(SideStake b); ADD_SERIALIZE_METHODS; @@ -179,7 +182,7 @@ class SideStake template inline void SerializationOp(Stream& s, Operation ser_action) { - READWRITE(m_key); + READWRITE(m_address); READWRITE(m_allocation); READWRITE(m_description); READWRITE(m_timestamp); @@ -195,23 +198,9 @@ class SideStake typedef std::shared_ptr SideStake_ptr; //! -//! \brief A type that either points to some sidestake or does not. -//! -//typedef const SideStake_ptr SideStakeOption; - -//! -//! \brief The body of a sidestake entry contract. Note that this body is bimodal. It -//! supports both the personality of the "LegacyPayload", and also the new native -//! sidestakeEntry format. In the Contract::Body::ConvertFromLegacy call, by the time -//! this call has been reached, the contract will have already been deserialized. -//! This will follow the legacy mode. For contracts at version 3+, the -//! Contract::SharePayload() will NOT call the ConvertFromLegacy. Note that because -//! the existing legacyPayloads are not versioned, the deserialization of -//! the payload first (de)serializes m_key, which is guaranteed to exist in either -//! legacy or native. If the key is empty, then payload v2+ is being deserialized -//! and the m_version and m_value are (de)serialized. This is ugly -//! but necessary to deal with the unversioned Legacy Payloads and maintain -//! compatibility. +//! \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 { @@ -240,15 +229,15 @@ class SideStakePayload : public IContractPayload SideStakePayload(uint32_t version = CURRENT_VERSION); //! - //! \brief Initialize a sidestakeEntryPayload from a sidestake entry constructed from - //! string key and value. Not to be used for version 1 payloads. Will assert. Does NOT - //! initialize hash fields. + //! \brief Initialize a sidestakeEntryPayload from a sidestake address, allocation, + //! description, and status. //! - //! \param key. Key string for the sidestake entry - //! \param value. Value string for the sidestake entry + //! \param address. Address 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, CBitcoinAddressForStorage key, double value, + SideStakePayload(const uint32_t version, CBitcoinAddressForStorage address, double allocation, std::string description, SideStakeStatus status); //! @@ -303,7 +292,7 @@ class SideStakePayload : public IContractPayload "m_entry.StatusToString() = %s", __func__, valid, - m_entry.m_key.ToString(), + m_entry.m_address.ToString(), m_entry.m_allocation, m_entry.StatusToString() ); @@ -319,7 +308,7 @@ class SideStakePayload : public IContractPayload //! std::string LegacyKeyString() const override { - return m_entry.m_key.ToString(); + return m_entry.m_address.ToString(); } //! @@ -366,7 +355,7 @@ class SideStakeRegistry : public IContractHandler //! sidestake entry db. This must be incremented when implementing format changes to the sidestake //! entries to force a reinit. //! - //! Version 1: TBD. + //! Version 1: 5.4.5.5+ //! SideStakeRegistry() : m_sidestake_db(1) @@ -374,7 +363,7 @@ class SideStakeRegistry : public IContractHandler }; //! - //! \brief The type that keys sidestake entries by their key strings. Note that the entries + //! \brief The type that keys sidestake entries by their addresses. 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. //! @@ -415,23 +404,23 @@ class SideStakeRegistry : public IContractHandler const std::vector ActiveSideStakeEntries(const bool& local_only, const bool& include_zero_alloc); //! - //! \brief Get the current sidestake entry for the specified key string. + //! \brief Get the current sidestake entry for the specified address. //! - //! \param key The key string of the sidestake entry. + //! \param key The address of the sidestake entry. //! \param local_only If true causes Try to only check the local sidestake map. Defaults to false. //! - //! \return A vector of smart pointers to entries matching the provided key (address). Up to two elements + //! \return A vector of smart pointers to entries matching the provided address. Up to two elements //! are returned, mandatory entry first, unless local only boolean is set true. //! std::vector Try(const CBitcoinAddressForStorage& key, const bool& local_only = false) const; //! - //! \brief Get the current sidestake entry for the specified key string if it has a status of ACTIVE or MANDATORY. + //! \brief Get the current sidestake entry for the specified address if it has a status of ACTIVE or MANDATORY. //! - //! \param key The key string of the sidestake entry. + //! \param key The address of the sidestake entry. //! \param local_only If true causes Try to only check the local sidestake map. Defaults to false. //! - //! \return A vector of smart pointers to entries matching the provided key (address) that are in status of + //! \return A vector of smart pointers to entries matching the provided address that are in status of //! MANDATORY or ACTIVE. Up to two elements are returned, mandatory entry first, unless local only boolean //! is set true. //! @@ -440,7 +429,7 @@ class SideStakeRegistry : public IContractHandler //! //! \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 + //! 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. //! @@ -460,7 +449,7 @@ class SideStakeRegistry : public IContractHandler //! //! \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. + //! 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. @@ -470,7 +459,7 @@ class SideStakeRegistry : public IContractHandler bool BlockValidate(const ContractContext& ctx, int& DoS) const override; //! - //! \brief Allows local (voluntary) sidestakes to be added to the in-memory map and not persisted to + //! \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 @@ -488,7 +477,7 @@ class SideStakeRegistry : public IContractHandler void Add(const ContractContext& ctx) override; //! - //! \brief Provides for deletion of local (voluntary) sidestakes from the in-memory map that are not persisted + //! \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 (CBitcoinAddress). //! //! \param address @@ -506,7 +495,7 @@ class SideStakeRegistry : public IContractHandler //! //! \brief Revert the registry state for the sidestake entry to the state prior - //! to this ContractContext application. This is typically an issue + //! to this ContractContext application. This is typically used //! during reorganizations, where blocks are disconnected. //! //! \param ctx References the sidestake entry contract and associated context. diff --git a/src/miner.cpp b/src/miner.cpp index c47236f80f..a79be4ab23 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -921,11 +921,11 @@ void SplitCoinStakeOutput(CBlock &blocknew, int64_t &nReward, bool &fEnableStake (iterSideStake != vSideStakeAlloc.end()) && (nOutputsUsed <= nMaxSideStakeOutputs); ++iterSideStake) { - CBitcoinAddress& address = iterSideStake->get()->m_key; + CBitcoinAddress& address = iterSideStake->get()->m_address; if (!address.IsValid()) { LogPrintf("WARN: SplitCoinStakeOutput: ignoring sidestake invalid address %s.", - iterSideStake->get()->m_key.ToString()); + iterSideStake->get()->m_address.ToString()); continue; } @@ -935,7 +935,7 @@ void SplitCoinStakeOutput(CBlock &blocknew, int64_t &nReward, bool &fEnableStake { LogPrintf("WARN: SplitCoinStakeOutput: distribution %f too small to address %s.", CoinToDouble(nReward * iterSideStake->get()->m_allocation), - iterSideStake->get()->m_key.ToString() + iterSideStake->get()->m_address.ToString() ); continue; } @@ -977,7 +977,7 @@ void SplitCoinStakeOutput(CBlock &blocknew, int64_t &nReward, bool &fEnableStake LogPrintf("SplitCoinStakeOutput: create sidestake UTXO %i value %f to address %s", nOutputsUsed, CoinToDouble(nReward * iterSideStake->get()->m_allocation), - iterSideStake->get()->m_key.ToString() + iterSideStake->get()->m_address.ToString() ); dSumAllocation += iterSideStake->get()->m_allocation; nRemainingStakeOutputValue -= nSideStake; diff --git a/src/qt/sidestaketablemodel.cpp b/src/qt/sidestaketablemodel.cpp index 9beabbdf78..85d7ce9b9c 100644 --- a/src/qt/sidestaketablemodel.cpp +++ b/src/qt/sidestaketablemodel.cpp @@ -36,7 +36,7 @@ bool SideStakeLessThan::operator()(const GRC::SideStake& left, const GRC::SideSt switch (static_cast(m_column)) { case SideStakeTableModel::Address: - return pLeft->m_key < pRight->m_key; + return pLeft->m_address < pRight->m_address; case SideStakeTableModel::Allocation: return pLeft->m_allocation < pRight->m_allocation; case SideStakeTableModel::Description: @@ -131,7 +131,7 @@ QVariant SideStakeTableModel::data(const QModelIndex &index, int role) const if (role == Qt::DisplayRole || role == Qt::EditRole) { switch (column) { case Address: - return QString::fromStdString(rec->m_key.ToString()); + return QString::fromStdString(rec->m_address.ToString()); case Allocation: return rec->m_allocation * 100.0; case Description: @@ -182,7 +182,7 @@ bool SideStakeTableModel::setData(const QModelIndex &index, const QVariant &valu address.SetString(value.toString().toStdString()); - if (rec->m_key == address) { + if (rec->m_address == address) { m_edit_status = NO_CHANGES; return false; } else if (!address.IsValid()) { @@ -210,7 +210,7 @@ bool SideStakeTableModel::setData(const QModelIndex &index, const QVariant &valu GRC::SideStake orig_sidestake = *rec; for (const auto& entry : registry.ActiveSideStakeEntries(false, true)) { - if (entry->m_key == orig_sidestake.m_key) { + if (entry->m_address == orig_sidestake.m_address) { continue; } @@ -233,10 +233,10 @@ bool SideStakeTableModel::setData(const QModelIndex &index, const QVariant &valu } // Delete the original sidestake - registry.NonContractDelete(orig_sidestake.m_key, false); + registry.NonContractDelete(orig_sidestake.m_address, false); // Add back the sidestake with the modified allocation - registry.NonContractAdd(GRC::SideStake(orig_sidestake.m_key, + registry.NonContractAdd(GRC::SideStake(orig_sidestake.m_address, value.toDouble() / 100.0, orig_sidestake.m_description, int64_t {0}, @@ -264,10 +264,10 @@ bool SideStakeTableModel::setData(const QModelIndex &index, const QVariant &valu GRC::SideStake orig_sidestake = *rec; // Delete the original sidestake - registry.NonContractDelete(orig_sidestake.m_key, false); + registry.NonContractDelete(orig_sidestake.m_address, false); // Add back the sidestake with the modified allocation - registry.NonContractAdd(GRC::SideStake(orig_sidestake.m_key, + registry.NonContractAdd(GRC::SideStake(orig_sidestake.m_address, orig_sidestake.m_allocation, san_value, int64_t {0}, @@ -400,7 +400,7 @@ bool SideStakeTableModel::removeRows(int row, int count, const QModelIndex &pare return false; } - GRC::GetSideStakeRegistry().NonContractDelete(rec->m_key); + GRC::GetSideStakeRegistry().NonContractDelete(rec->m_address); updateSideStakeTableModel(); diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index dc0cc887ce..55cd761167 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -117,7 +117,7 @@ UniValue getstakinginfo(const UniValue& params, bool fHelp) // sidestakes are always included. for (const auto& alloc : vSideStakeAlloc) { - sidestakingalloc.pushKV("address", alloc->m_key.ToString()); + sidestakingalloc.pushKV("address", alloc->m_address.ToString()); sidestakingalloc.pushKV("allocation_pct", alloc->m_allocation * 100); sidestakingalloc.pushKV("status", alloc->StatusToString()); From e33416aa707221d64f666af43ef6625a4ea0dfda Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Tue, 31 Oct 2023 17:37:23 -0400 Subject: [PATCH 23/47] Add V13 height conditional for sidestake registry in GetLowestRegistryBlockHeight() When below Block13Height, the sidestake registry will report a height of zero when initialized. This causes the wrong clamp to be applied to contract replay. This commit fixes that issue. --- src/gridcoin/contract/registry.h | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/gridcoin/contract/registry.h b/src/gridcoin/contract/registry.h index 4132f465eb..b2bd46d954 100644 --- a/src/gridcoin/contract/registry.h +++ b/src/gridcoin/contract/registry.h @@ -157,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; } } From 2065f769b3d6c36ad771b94c73351b74e489e3f9 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Sun, 24 Dec 2023 19:29:27 -0500 Subject: [PATCH 24/47] Changes to separate local and mandatory sidestakes classwise --- src/gridcoin/sidestake.cpp | 339 ++++++++++++++++++++++++--------- src/gridcoin/sidestake.h | 265 ++++++++++++++++++++------ src/miner.cpp | 26 +-- src/qt/optionsdialog.cpp | 3 +- src/qt/sidestaketablemodel.cpp | 100 ++++++---- src/rpc/blockchain.cpp | 16 +- src/rpc/mining.cpp | 4 +- 7 files changed, 547 insertions(+), 206 deletions(-) diff --git a/src/gridcoin/sidestake.cpp b/src/gridcoin/sidestake.cpp index fd4c9030e6..32d59dd289 100644 --- a/src/gridcoin/sidestake.cpp +++ b/src/gridcoin/sidestake.cpp @@ -45,34 +45,34 @@ CBitcoinAddressForStorage::CBitcoinAddressForStorage(CBitcoinAddress address) {} // ----------------------------------------------------------------------------- -// Class: SideStake +// Class: MandatorySideStake // ----------------------------------------------------------------------------- -SideStake::SideStake() +MandatorySideStake::MandatorySideStake() : m_address() , m_allocation() , m_description() , m_timestamp(0) , m_hash() , m_previous_hash() - , m_status(SideStakeStatus::UNKNOWN) + , m_status(MandatorySideStakeStatus::UNKNOWN) {} -SideStake::SideStake(CBitcoinAddressForStorage address, double allocation, std::string description) +MandatorySideStake::MandatorySideStake(CBitcoinAddressForStorage address, double allocation, std::string description) : m_address(address) , m_allocation(allocation) , m_description(description) , m_timestamp(0) , m_hash() , m_previous_hash() - , m_status(SideStakeStatus::UNKNOWN) + , m_status(MandatorySideStakeStatus::UNKNOWN) {} -SideStake::SideStake(CBitcoinAddressForStorage address, - double allocation, - std::string description, - int64_t timestamp, - uint256 hash, - SideStakeStatus status) +MandatorySideStake::MandatorySideStake(CBitcoinAddressForStorage address, + double allocation, + std::string description, + int64_t timestamp, + uint256 hash, + MandatorySideStakeStatus status) : m_address(address) , m_allocation(allocation) , m_description(description) @@ -82,48 +82,44 @@ SideStake::SideStake(CBitcoinAddressForStorage address, , m_status(status) {} -bool SideStake::WellFormed() const +bool MandatorySideStake::WellFormed() const { return m_address.IsValid() && m_allocation >= 0.0 && m_allocation <= 1.0; } -CBitcoinAddressForStorage SideStake::Key() const +CBitcoinAddressForStorage MandatorySideStake::Key() const { return m_address; } -std::pair SideStake::KeyValueToString() const +std::pair MandatorySideStake::KeyValueToString() const { return std::make_pair(m_address.ToString(), StatusToString()); } -std::string SideStake::StatusToString() const +std::string MandatorySideStake::StatusToString() const { return StatusToString(m_status.Value()); } -std::string SideStake::StatusToString(const SideStakeStatus& status, const bool& translated) const +std::string MandatorySideStake::StatusToString(const MandatorySideStakeStatus& status, const bool& translated) const { if (translated) { switch(status) { - case SideStakeStatus::UNKNOWN: return _("Unknown"); - case SideStakeStatus::ACTIVE: return _("Active"); - case SideStakeStatus::INACTIVE: return _("Inactive"); - case SideStakeStatus::DELETED: return _("Deleted"); - case SideStakeStatus::MANDATORY: return _("Mandatory"); - case SideStakeStatus::OUT_OF_BOUND: break; + 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 SideStakeStatus::UNKNOWN: return "Unknown"; - case SideStakeStatus::ACTIVE: return "Active"; - case SideStakeStatus::INACTIVE: return "Inactive"; - case SideStakeStatus::DELETED: return "Deleted"; - case SideStakeStatus::MANDATORY: return "Mandatory"; - case SideStakeStatus::OUT_OF_BOUND: break; + 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 @@ -134,12 +130,13 @@ std::string SideStake::StatusToString(const SideStakeStatus& status, const bool& return std::string{}; } -bool SideStake::operator==(SideStake b) +bool MandatorySideStake::operator==(MandatorySideStake b) { bool result = true; result &= (m_address == b.m_address); 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); @@ -148,11 +145,168 @@ bool SideStake::operator==(SideStake b) return result; } -bool SideStake::operator!=(SideStake b) +bool MandatorySideStake::operator!=(MandatorySideStake b) +{ + return !(*this == b); +} + +// ----------------------------------------------------------------------------- +// Class: LocalSideStake +// ----------------------------------------------------------------------------- +LocalSideStake::LocalSideStake() + : m_address() + , m_allocation() + , m_description() + , m_status(LocalSideStakeStatus::UNKNOWN) +{} + +LocalSideStake::LocalSideStake(CBitcoinAddressForStorage address, double allocation, std::string description) + : m_address(address) + , m_allocation(allocation) + , m_description(description) + , m_status(LocalSideStakeStatus::UNKNOWN) +{} + +LocalSideStake::LocalSideStake(CBitcoinAddressForStorage address, + double allocation, + std::string description, + LocalSideStakeStatus status) + : m_address(address) + , m_allocation(allocation) + , m_description(description) + , m_status(status) +{} + +bool LocalSideStake::WellFormed() const +{ + return m_address.IsValid() && m_allocation >= 0.0 && m_allocation <= 1.0; +} + +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_address == b.m_address); + 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: SideStake +// ----------------------------------------------------------------------------- +SideStake::SideStake() + : m_local_sidestake_ptr(nullptr) + , m_mandatory_sidestake_ptr(nullptr) + , m_mandatory(false) +{} + +SideStake::SideStake(LocalSideStake_ptr sidestake_ptr) + : m_local_sidestake_ptr(sidestake_ptr) + , m_mandatory_sidestake_ptr(nullptr) + , m_mandatory(false) +{} + +SideStake::SideStake(MandatorySideStake_ptr sidestake_ptr) + : m_local_sidestake_ptr(nullptr) + , m_mandatory_sidestake_ptr(sidestake_ptr) + , m_mandatory(true) +{} + +bool SideStake::IsMandatory() const +{ + return m_mandatory; +} + +CBitcoinAddress SideStake::GetAddress() const +{ + if (IsMandatory()) { + return m_mandatory_sidestake_ptr->m_address; + } else { + return m_local_sidestake_ptr->m_address; + } +} + +double SideStake::GetAllocation() const +{ + if (IsMandatory()) { + return m_mandatory_sidestake_ptr->m_allocation; + } else { + return m_local_sidestake_ptr->m_allocation; + } +} + +std::string SideStake::GetDescription() const +{ + if (IsMandatory()) { + return m_mandatory_sidestake_ptr->m_description; + } else { + return m_local_sidestake_ptr->m_description; + } +} + +SideStake::Status SideStake::GetStatus() const +{ + Status status; + + if (IsMandatory()) { + status = m_mandatory_sidestake_ptr->m_status; + } else { + status = m_local_sidestake_ptr->m_status; + } + + return status; +} + +std::string SideStake::StatusToString() const +{ + if (IsMandatory()) { + return m_mandatory_sidestake_ptr->StatusToString(); + } else { + return m_local_sidestake_ptr->StatusToString(); + } +} + // ----------------------------------------------------------------------------- // Class: SideStakePayload // ----------------------------------------------------------------------------- @@ -169,21 +323,21 @@ SideStakePayload::SideStakePayload(const uint32_t version, CBitcoinAddressForStorage address, double allocation, std::string description, - SideStakeStatus status) + MandatorySideStake::MandatorySideStakeStatus status) : IContractPayload() , m_version(version) - , m_entry(SideStake(address, allocation, description, 0, uint256{}, status)) + , m_entry(MandatorySideStake(address, allocation, description, 0, uint256{}, status)) { } -SideStakePayload::SideStakePayload(const uint32_t version, SideStake entry) +SideStakePayload::SideStakePayload(const uint32_t version, MandatorySideStake entry) : IContractPayload() , m_version(version) , m_entry(std::move(entry)) { } -SideStakePayload::SideStakePayload(SideStake entry) +SideStakePayload::SideStakePayload(MandatorySideStake entry) : SideStakePayload(CURRENT_VERSION, std::move(entry)) { } @@ -197,12 +351,12 @@ const std::vector SideStakeRegistry::SideStakeEntries() const LOCK(cs_lock); - for (const auto& entry : m_sidestake_entries) { - sidestakes.push_back(entry.second); + 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(entry.second); + sidestakes.push_back(std::make_shared(entry.second)); } return sidestakes; @@ -223,11 +377,12 @@ const std::vector SideStakeRegistry::ActiveSideStakeEntries(const // Do mandatory sidestakes first. if (!local_only) { - for (const auto& entry : m_sidestake_entries) + for (const auto& entry : m_mandatory_sidestake_entries) { - if (entry.second->m_status == SideStakeStatus::MANDATORY && allocation_sum + entry.second->m_allocation <= 1.0) { + if (entry.second->m_status == MandatorySideStake::MandatorySideStakeStatus::MANDATORY + && allocation_sum + entry.second->m_allocation <= 1.0) { if ((include_zero_alloc && entry.second->m_allocation == 0.0) || entry.second->m_allocation > 0.0) { - sidestakes.push_back(entry.second); + sidestakes.push_back(std::make_shared(entry.second)); allocation_sum += entry.second->m_allocation; } } @@ -242,9 +397,10 @@ const std::vector SideStakeRegistry::ActiveSideStakeEntries(const for (const auto& entry : m_local_sidestake_entries) { - if (entry.second->m_status == SideStakeStatus::ACTIVE && allocation_sum + entry.second->m_allocation <= 1.0) { + if (entry.second->m_status == LocalSideStake::LocalSideStakeStatus::ACTIVE + && allocation_sum + entry.second->m_allocation <= 1.0) { if ((include_zero_alloc && entry.second->m_allocation == 0.0) || entry.second->m_allocation > 0.0) { - sidestakes.push_back(entry.second); + sidestakes.push_back(std::make_shared(entry.second)); allocation_sum += entry.second->m_allocation; } } @@ -261,17 +417,17 @@ std::vector SideStakeRegistry::Try(const CBitcoinAddressForStorag std::vector result; if (!local_only) { - const auto mandatory_entry = m_sidestake_entries.find(key); + const auto mandatory_entry = m_mandatory_sidestake_entries.find(key); - if (mandatory_entry != m_sidestake_entries.end()) { - result.push_back(mandatory_entry->second); + if (mandatory_entry != m_mandatory_sidestake_entries.end()) { + result.push_back(std::make_shared(mandatory_entry->second)); } } const auto local_entry = m_local_sidestake_entries.find(key); if (local_entry != m_local_sidestake_entries.end()) { - result.push_back(local_entry->second); + result.push_back(std::make_shared(local_entry->second)); } return result; @@ -284,8 +440,14 @@ std::vector SideStakeRegistry::TryActive(const CBitcoinAddressFor std::vector result; for (const auto& iter : Try(key, local_only)) { - if (iter->m_status == SideStakeStatus::MANDATORY || iter->m_status == SideStakeStatus::ACTIVE) { - result.push_back(iter); + 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); + } } } @@ -296,7 +458,7 @@ void SideStakeRegistry::Reset() { LOCK(cs_lock); - m_sidestake_entries.clear(); + m_mandatory_sidestake_entries.clear(); m_sidestake_db.clear(); } @@ -320,17 +482,17 @@ void SideStakeRegistry::AddDelete(const ContractContext& ctx) // 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 = SideStakeStatus::DELETED; + payload.m_entry.m_status = MandatorySideStake::MandatorySideStakeStatus::DELETED; } LOCK(cs_lock); - auto sidestake_entry_pair_iter = m_sidestake_entries.find(payload.m_entry.m_address); + auto sidestake_entry_pair_iter = m_mandatory_sidestake_entries.find(payload.m_entry.m_address); - SideStake_ptr current_sidestake_entry_ptr = nullptr; + 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_sidestake_entries.end()); + 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) { @@ -356,7 +518,7 @@ void SideStakeRegistry::AddDelete(const ContractContext& ctx) payload.m_entry.StatusToString() ); - SideStake& historical = payload.m_entry; + MandatorySideStake& historical = payload.m_entry; if (!m_sidestake_db.insert(ctx.m_tx.GetHash(), height, historical)) { @@ -370,28 +532,33 @@ void SideStakeRegistry::AddDelete(const ContractContext& ctx) } // Finally, insert the new SideStake entry (payload) smart pointer into the m_sidestake_entries map. - m_sidestake_entries[payload.m_entry.m_address] = m_sidestake_db.find(ctx.m_tx.GetHash())->second; + m_mandatory_sidestake_entries[payload.m_entry.m_address] = m_sidestake_db.find(ctx.m_tx.GetHash())->second; return; } -void SideStakeRegistry::NonContractAdd(const SideStake& sidestake, const bool& save_to_file) +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_address] = std::make_shared(sidestake); + // 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_address] = std::make_shared(sidestake); if (save_to_file) { SaveLocalSideStakesToConfig(); } } -void SideStakeRegistry::Add(const ContractContext& ctx) -{ - AddDelete(ctx); -} - void SideStakeRegistry::NonContractDelete(const CBitcoinAddressForStorage& address, const bool& save_to_file) { LOCK(cs_lock); @@ -407,11 +574,6 @@ void SideStakeRegistry::NonContractDelete(const CBitcoinAddressForStorage& addre } } -void SideStakeRegistry::Delete(const ContractContext& ctx) -{ - AddDelete(ctx); -} - void SideStakeRegistry::Revert(const ContractContext& ctx) { const auto payload = ctx->SharePayloadAs(); @@ -421,9 +583,9 @@ void SideStakeRegistry::Revert(const ContractContext& ctx) // resurrect. LOCK(cs_lock); - auto entry_to_revert = m_sidestake_entries.find(payload->m_entry.m_address); + auto entry_to_revert = m_mandatory_sidestake_entries.find(payload->m_entry.m_address); - if (entry_to_revert == m_sidestake_entries.end()) { + 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__, entry_to_revert->second->m_address.ToString()); @@ -440,7 +602,7 @@ void SideStakeRegistry::Revert(const ContractContext& ctx) // 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_sidestake_entries.erase(payload->m_entry.m_address) == 0) { + if (m_mandatory_sidestake_entries.erase(payload->m_entry.m_address) == 0) { error("%s: The SideStake entry to erase during a SideStake entry revert for key %s was not found.", __func__, key.ToString()); @@ -474,7 +636,7 @@ void SideStakeRegistry::Revert(const ContractContext& ctx) // 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_sidestake_entries[resurrect_entry->second->m_address] = resurrect_entry->second; + m_mandatory_sidestake_entries[resurrect_entry->second->m_address] = resurrect_entry->second; } } @@ -506,12 +668,12 @@ int SideStakeRegistry::Initialize() { LOCK(cs_lock); - int height = m_sidestake_db.Initialize(m_sidestake_entries, m_pending_sidestake_entries); + 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_sidestake_entries.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(); @@ -544,7 +706,7 @@ void SideStakeRegistry::ResetInMemoryOnly() LOCK(cs_lock); m_local_sidestake_entries.clear(); - m_sidestake_entries.clear(); + m_mandatory_sidestake_entries.clear(); m_sidestake_db.clear_in_memory_only(); } @@ -567,7 +729,7 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() return; } - std::vector vSideStakes; + std::vector vLocalSideStakes; std::vector> raw_vSideStakeAlloc; double dSumAllocation = 0.0; @@ -631,8 +793,9 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() // First, determine allocation already taken by mandatory sidestakes, because they must be allocated first. for (const auto& entry : SideStakeEntries()) { - if (entry->m_status == SideStakeStatus::MANDATORY) { - dSumAllocation += entry->m_allocation; + if (entry->IsMandatory() + && std::get(entry->GetStatus()) == MandatorySideStake::MandatorySideStakeStatus::MANDATORY) { + dSumAllocation += entry->GetAllocation(); } } @@ -679,19 +842,17 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() break; } - SideStake sidestake(static_cast(address), - dAllocation, - std::get<2>(entry), - 0, - uint256{}, - SideStakeStatus::ACTIVE); + LocalSideStake sidestake(static_cast(address), + dAllocation, + std::get<2>(entry), + 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. - vSideStakes.push_back(sidestake); + vLocalSideStakes.push_back(sidestake); LogPrint(BCLog::LogFlags::MINER, "INFO: %s: SideStakeAlloc Address %s, Allocation %f", __func__, sAddress, dAllocation); @@ -700,13 +861,13 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() for (auto& entry : m_local_sidestake_entries) { // Only look at active entries. The others are NA for this alignment. - if (entry.second->m_status == SideStakeStatus::ACTIVE) { - auto iter = std::find(vSideStakes.begin(), vSideStakes.end(), *entry.second); + if (entry.second->m_status == LocalSideStake::LocalSideStakeStatus::ACTIVE) { + auto iter = std::find(vLocalSideStakes.begin(), vLocalSideStakes.end(), *entry.second); - if (iter == vSideStakes.end()) { + if (iter == vLocalSideStakes.end()) { // Entry in map is no longer found in config files, so mark map entry inactive. - entry.second->m_status = SideStakeStatus::INACTIVE; + entry.second->m_status = LocalSideStake::LocalSideStakeStatus::INACTIVE; } } } diff --git a/src/gridcoin/sidestake.h b/src/gridcoin/sidestake.h index 6705b1c527..3fe2443dbf 100644 --- a/src/gridcoin/sidestake.h +++ b/src/gridcoin/sidestake.h @@ -39,35 +39,146 @@ class CBitcoinAddressForStorage : public CBitcoinAddress } }; - -enum class SideStakeStatus +//! +//! \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 address. This address must be a valid address, 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 { - UNKNOWN, - ACTIVE, //!< A user specified sidestake that is active - INACTIVE, //!< A user specified sidestake that is inactive - DELETED, //!< A mandatory sidestake that has been deleted by contract - MANDATORY, //!< An active mandatory sidetake by contract - OUT_OF_BOUND +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; + + CBitcoinAddressForStorage m_address; //!< The Gridcoin Address of the sidestake destination. + + double m_allocation; //!< The allocation is a double precision floating point between 0.0 and 1.0 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 address and allocation. This is used to construct a user + //! specified sidestake. + //! + //! \param address + //! \param allocation + //! \param description (optional) + //! + LocalSideStake(CBitcoinAddressForStorage address, double allocation, std::string description); + + //! + //! \brief Initialize a sidestake instance with the provided parameters. + //! + //! \param address + //! \param allocation + //! \param description (optional) + //! \param status + //! + LocalSideStake(CBitcoinAddressForStorage address, double 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_address); + READWRITE(m_allocation); + READWRITE(m_description); + READWRITE(m_status); + } }; //! -//! \brief The SideStake class. This class formalizes the "sidestake", which is a directive to apportion +//! \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 address. This address must be a valid address, but //! may or may not be owned by the staker. This is the primary mechanism to do automatic "donations" to //! defined network addresses. //! -//! The class supports two modes of operation. 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 to -//! in any GUI implementation on top of this. Mandatory entries will be picked up by contract handlers similar to -//! other contract types (cf. protocol entries). +//! Mandatory entries will be picked up by contract handlers similar to other contract types (cf. protocol entries). //! -class SideStake +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; + using Status = EnumByte; CBitcoinAddressForStorage m_address; //!< The Gridcoin Address of the sidestake destination. @@ -86,7 +197,7 @@ class SideStake //! //! \brief Initialize an empty, invalid sidestake instance. //! - SideStake(); + MandatorySideStake(); //! //! \brief Initialize a sidestake instance with the provided address and allocation. This is used to construct a user @@ -96,7 +207,7 @@ class SideStake //! \param allocation //! \param description (optional) //! - SideStake(CBitcoinAddressForStorage address, double allocation, std::string description); + MandatorySideStake(CBitcoinAddressForStorage address, double allocation, std::string description); //! //! \brief Initialize a sidestake instance with the provided parameters. @@ -106,7 +217,7 @@ class SideStake //! \param description (optional) //! \param status //! - SideStake(CBitcoinAddressForStorage address, double allocation, std::string description, SideStakeStatus status); + MandatorySideStake(CBitcoinAddressForStorage address, double allocation, std::string description, MandatorySideStakeStatus status); //! //! \brief Initialize a sidestake instance with the provided parameters. This form is normally used to construct a @@ -119,8 +230,8 @@ class SideStake //! \param hash //! \param status //! - SideStake(CBitcoinAddressForStorage address, double allocation, std::string description, int64_t timestamp, - uint256 hash, SideStakeStatus status); + MandatorySideStake(CBitcoinAddressForStorage address, double allocation, std::string description, int64_t timestamp, + uint256 hash, MandatorySideStakeStatus status); //! //! \brief Determine whether a sidestake contains each of the required elements. @@ -157,7 +268,7 @@ class SideStake //! //! \return SideStake status string. //! - std::string StatusToString(const SideStakeStatus& status, const bool& translated = true) const; + std::string StatusToString(const MandatorySideStakeStatus& status, const bool& translated = true) const; //! //! \brief Comparison operator overload used in the unit test harness. @@ -166,7 +277,7 @@ class SideStake //! //! \return Equal or not. //! - bool operator==(SideStake b); + bool operator==(MandatorySideStake b); //! //! \brief Comparison operator overload used in the unit test harness. @@ -175,7 +286,7 @@ class SideStake //! //! \return Equal or not. //! - bool operator!=(SideStake b); + bool operator!=(MandatorySideStake b); ADD_SERIALIZE_METHODS; @@ -193,7 +304,44 @@ class SideStake }; //! -//! \brief The type that defines a shared pointer to a sidestake +//! \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: + //! + //! \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); + + bool IsMandatory() const; + + CBitcoinAddress GetAddress() const; + double GetAllocation() const; + std::string GetDescription() const; + Status GetStatus() const; + std::string StatusToString() const; + +private: + LocalSideStake_ptr m_local_sidestake_ptr; + MandatorySideStake_ptr m_mandatory_sidestake_ptr; + bool m_mandatory; +}; + +//! +//! \brief The type that defines a shared pointer to a mandatory sidestake //! typedef std::shared_ptr SideStake_ptr; @@ -221,7 +369,7 @@ class SideStakePayload : public IContractPayload //! uint32_t m_version = CURRENT_VERSION; - SideStake m_entry; //!< The sidestake entry in the payload. + MandatorySideStake m_entry; //!< The sidestake entry in the payload. //! //! \brief Initialize an empty, invalid sidestake entry payload. @@ -238,7 +386,7 @@ class SideStakePayload : public IContractPayload //! \param status. Status of the sidestake entry //! SideStakePayload(const uint32_t version, CBitcoinAddressForStorage address, double allocation, - std::string description, SideStakeStatus status); + std::string description, MandatorySideStake::MandatorySideStakeStatus status); //! //! \brief Initialize a sidestake entry payload from the given sidestake entry @@ -247,7 +395,7 @@ class SideStakePayload : public IContractPayload //! \param version Version of the serialized sidestake entry format. //! \param sidestake_entry The sidestake entry itself. //! - SideStakePayload(const uint32_t version, SideStake sidestake_entry); + SideStakePayload(const uint32_t version, MandatorySideStake sidestake_entry); //! //! \brief Initialize a sidestake entry payload from the given sidestake entry @@ -255,7 +403,7 @@ class SideStakePayload : public IContractPayload //! //! \param sidestake_entry The sidestake entry itself. //! - SideStakePayload(SideStake sidestake_entry); + SideStakePayload(MandatorySideStake sidestake_entry); //! //! \brief Get the type of contract that this payload contains data for. @@ -363,16 +511,23 @@ class SideStakeRegistry : public IContractHandler }; //! - //! \brief The type that keys sidestake entries by their addresses. Note that the entries + //! \brief The type that keys local sidestake entries by their addresses. 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 LocalSideStakeMap; + + //! + //! \brief The type that keys mandatory sidestake entries by their addresses. 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 SideStakeMap; + typedef std::map MandatorySideStakeMap; //! //! \brief PendingSideStakeMap. This is not actually used but defined to satisfy the template. //! - typedef SideStakeMap PendingSideStakeMap; + typedef MandatorySideStakeMap PendingSideStakeMap; //! //! \brief The type that keys historical sidestake entries by the contract hash (txid). @@ -380,7 +535,7 @@ class SideStakeRegistry : public IContractHandler //! the same actual object can be held by both this map and the (current) sidestake entry map //! without object duplication. //! - typedef std::map HistoricalSideStakeMap; + typedef std::map HistoricalSideStakeMap; //! //! \brief Get the collection of current sidestake entries. Note that this INCLUDES deleted @@ -459,22 +614,30 @@ class SideStakeRegistry : public IContractHandler bool BlockValidate(const ContractContext& ctx, int& DoS) const override; //! - //! \brief Allows local (voluntary) sidestakes to be added to the in-memory local map and not persisted to - //! the registry db. + //! \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 SideStake object to add - //! \param bool save_to_file if true causes SaveLocalSideStakesToConfig() to be called. + //! \param ctx //! - void NonContractAdd(const SideStake& sidestake, const bool& save_to_file = true); + void Add(const ContractContext& ctx) override; //! - //! \brief Add a sidestake entry to the registry from contract data. For the sidestake registry + //! \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 Add(const ContractContext& ctx) override; + 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 @@ -485,14 +648,6 @@ class SideStakeRegistry : public IContractHandler //! void NonContractDelete(const CBitcoinAddressForStorage& address, const bool& save_to_file = true); - //! - //! \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 Revert the registry state for the sidestake entry to the state prior //! to this ContractContext application. This is typically used @@ -560,10 +715,10 @@ class SideStakeRegistry : public IContractHandler //! //! \brief Specializes the template RegistryDB for the SideStake class //! - typedef RegistryDB SideStakeDB; @@ -592,9 +747,9 @@ class SideStakeRegistry : public IContractHandler void SubscribeToCoreSignals(); void UnsubscribeFromCoreSignals(); - SideStakeMap m_local_sidestake_entries; //!< Contains the local (non-contract) sidestake entries. - SideStakeMap m_sidestake_entries; //!< Contains the mandatory sidestake entries, including DELETED. - PendingSideStakeMap m_pending_sidestake_entries {}; //!< Not used. Only to satisfy the template. + 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; diff --git a/src/miner.cpp b/src/miner.cpp index a79be4ab23..7150ad4e81 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -921,26 +921,28 @@ void SplitCoinStakeOutput(CBlock &blocknew, int64_t &nReward, bool &fEnableStake (iterSideStake != vSideStakeAlloc.end()) && (nOutputsUsed <= nMaxSideStakeOutputs); ++iterSideStake) { - CBitcoinAddress& address = iterSideStake->get()->m_address; + CBitcoinAddress address = iterSideStake->get()->GetAddress(); + double allocation = iterSideStake->get()->GetAllocation(); + if (!address.IsValid()) { LogPrintf("WARN: SplitCoinStakeOutput: ignoring sidestake invalid address %s.", - iterSideStake->get()->m_address.ToString()); + 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->get()->m_allocation < CENT) + if (nReward * allocation < CENT) { LogPrintf("WARN: SplitCoinStakeOutput: distribution %f too small to address %s.", - CoinToDouble(nReward * iterSideStake->get()->m_allocation), - iterSideStake->get()->m_address.ToString() + CoinToDouble(nReward * allocation), + address.ToString() ); continue; } - if (dSumAllocation + iterSideStake->get()->m_allocation > 1.0) + if (dSumAllocation + allocation > 1.0) { LogPrintf("WARN: SplitCoinStakeOutput: allocation percentage over 100 percent, " "ending sidestake allocations."); @@ -963,11 +965,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->get()->m_allocation < 1.0) - nSideStake = nReward * iterSideStake->get()->m_allocation; + if (dSumAllocation + allocation < 1.0) + nSideStake = nReward * allocation; // 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->get()->m_allocation == 1.0) + else if (dSumAllocation + allocation == 1.0) // 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; @@ -976,10 +978,10 @@ void SplitCoinStakeOutput(CBlock &blocknew, int64_t &nReward, bool &fEnableStake LogPrintf("SplitCoinStakeOutput: create sidestake UTXO %i value %f to address %s", nOutputsUsed, - CoinToDouble(nReward * iterSideStake->get()->m_allocation), - iterSideStake->get()->m_address.ToString() + CoinToDouble(nReward * allocation), + address.ToString() ); - dSumAllocation += iterSideStake->get()->m_allocation; + dSumAllocation += allocation; nRemainingStakeOutputValue -= nSideStake; nOutputsUsed++; } diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp index 98c898ac4b..26bd13b1d3 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -650,8 +650,7 @@ void OptionsDialog::sidestakeSelectionChanged() if (indexes.size() > 1) { ui->pushButtonEditSideStake->setEnabled(false); ui->pushButtonDeleteSideStake->setEnabled(false); - } else if (static_cast(indexes.at(0).internalPointer())->m_status - == GRC::SideStakeStatus::MANDATORY) { + } else if (static_cast(indexes.at(0).internalPointer())->IsMandatory()) { ui->pushButtonEditSideStake->setEnabled(false); ui->pushButtonDeleteSideStake->setEnabled(false); } else { diff --git a/src/qt/sidestaketablemodel.cpp b/src/qt/sidestaketablemodel.cpp index 85d7ce9b9c..38dac02dfe 100644 --- a/src/qt/sidestaketablemodel.cpp +++ b/src/qt/sidestaketablemodel.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -34,15 +35,37 @@ bool SideStakeLessThan::operator()(const GRC::SideStake& left, const GRC::SideSt 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->m_address < pRight->m_address; + return pLeft->GetAddress() < pRight->GetAddress(); case SideStakeTableModel::Allocation: - return pLeft->m_allocation < pRight->m_allocation; + return pLeft->GetAllocation() < pRight->GetAllocation(); case SideStakeTableModel::Description: - return pLeft->m_description.compare(pRight->m_description) < 0; + return pLeft->GetDescription().compare(pRight->GetDescription()) < 0; case SideStakeTableModel::Status: - return pLeft->m_status < pRight->m_status; + return left_status < right_status; } // no default case, so the compiler can warn about missing cases assert(false); } @@ -84,7 +107,6 @@ class SideStakeTablePriv return nullptr; } - }; SideStakeTableModel::SideStakeTableModel(OptionsModel* parent) @@ -131,11 +153,11 @@ QVariant SideStakeTableModel::data(const QModelIndex &index, int role) const if (role == Qt::DisplayRole || role == Qt::EditRole) { switch (column) { case Address: - return QString::fromStdString(rec->m_address.ToString()); + return QString::fromStdString(rec->GetAddress().ToString()); case Allocation: - return rec->m_allocation * 100.0; + return rec->GetAllocation() * 100.0; case Description: - return QString::fromStdString(rec->m_description); + return QString::fromStdString(rec->GetDescription()); case Status: return QString::fromStdString(rec->StatusToString()); } // no default case, so the compiler can warn about missing cases @@ -181,8 +203,7 @@ bool SideStakeTableModel::setData(const QModelIndex &index, const QVariant &valu CBitcoinAddress address; address.SetString(value.toString().toStdString()); - - if (rec->m_address == address) { + if (rec->GetAddress() == address) { m_edit_status = NO_CHANGES; return false; } else if (!address.IsValid()) { @@ -209,15 +230,23 @@ bool SideStakeTableModel::setData(const QModelIndex &index, const QVariant &valu // Save the original local sidestake (also in the core). GRC::SideStake orig_sidestake = *rec; + CBitcoinAddress orig_address = rec->GetAddress(); + double orig_allocation = rec->GetAllocation(); + std::string orig_description = rec->GetDescription(); + GRC::SideStake::Status orig_status = rec->GetStatus(); + for (const auto& entry : registry.ActiveSideStakeEntries(false, true)) { - if (entry->m_address == orig_sidestake.m_address) { + CBitcoinAddress address = entry->GetAddress(); + double allocation = entry->GetAllocation(); + + if (address == orig_address) { continue; } - prior_total_allocation += entry->m_allocation * 100.0; + prior_total_allocation += allocation * 100.0; } - if (rec->m_allocation * 100.0 == value.toDouble()) { + if (orig_allocation * 100.0 == value.toDouble()) { m_edit_status = NO_CHANGES; return false; } @@ -233,15 +262,13 @@ bool SideStakeTableModel::setData(const QModelIndex &index, const QVariant &valu } // Delete the original sidestake - registry.NonContractDelete(orig_sidestake.m_address, false); + registry.NonContractDelete(orig_address, false); // Add back the sidestake with the modified allocation - registry.NonContractAdd(GRC::SideStake(orig_sidestake.m_address, - value.toDouble() / 100.0, - orig_sidestake.m_description, - int64_t {0}, - uint256 {}, - orig_sidestake.m_status.Value()), true); + registry.NonContractAdd(GRC::LocalSideStake(orig_address, + value.toDouble() / 100.0, + orig_description, + std::get(orig_status).Value()), true); break; } @@ -250,7 +277,7 @@ bool SideStakeTableModel::setData(const QModelIndex &index, const QVariant &valu std::string orig_value = value.toString().toStdString(); std::string san_value = SanitizeString(orig_value, SAFE_CHARS_CSV); - if (rec->m_description == orig_value) { + if (rec->GetDescription() == orig_value) { m_edit_status = NO_CHANGES; return false; } @@ -264,15 +291,13 @@ bool SideStakeTableModel::setData(const QModelIndex &index, const QVariant &valu GRC::SideStake orig_sidestake = *rec; // Delete the original sidestake - registry.NonContractDelete(orig_sidestake.m_address, false); + registry.NonContractDelete(orig_sidestake.GetAddress(), false); // Add back the sidestake with the modified allocation - registry.NonContractAdd(GRC::SideStake(orig_sidestake.m_address, - orig_sidestake.m_allocation, - san_value, - int64_t {0}, - uint256 {}, - orig_sidestake.m_status.Value()), true); + registry.NonContractAdd(GRC::LocalSideStake(orig_sidestake.GetAddress(), + orig_sidestake.GetAllocation(), + san_value, + std::get(orig_sidestake.GetStatus()).Value()), true); break; } @@ -308,7 +333,8 @@ Qt::ItemFlags SideStakeTableModel::flags(const QModelIndex &index) const Qt::ItemFlags retval = Qt::ItemIsSelectable | Qt::ItemIsEnabled; - if (rec->m_status == GRC::SideStakeStatus::ACTIVE + if (!rec->IsMandatory() + && std::get(rec->GetStatus()) == GRC::LocalSideStake::LocalSideStakeStatus::ACTIVE && (index.column() == Allocation || index.column() == Description)) { retval |= Qt::ItemIsEditable; } @@ -350,7 +376,7 @@ QString SideStakeTableModel::addRow(const QString &address, const QString &alloc // Get total allocation of all active/mandatory sidestake entries for (const auto& entry : registry.ActiveSideStakeEntries(false, true)) { - prior_total_allocation += entry->m_allocation * 100.0; + prior_total_allocation += entry->GetAllocation() * 100.0; } if (!core_local_sidestake.empty()) { @@ -376,12 +402,10 @@ QString SideStakeTableModel::addRow(const QString &address, const QString &alloc return QString(); } - registry.NonContractAdd(GRC::SideStake(sidestake_address, - sidestake_allocation, - sanitized_description, - int64_t {0}, - uint256 {}, - GRC::SideStakeStatus::ACTIVE)); + registry.NonContractAdd(GRC::LocalSideStake(sidestake_address, + sidestake_allocation, + sanitized_description, + GRC::LocalSideStake::LocalSideStakeStatus::ACTIVE)); updateSideStakeTableModel(); @@ -393,14 +417,14 @@ bool SideStakeTableModel::removeRows(int row, int count, const QModelIndex &pare Q_UNUSED(parent); GRC::SideStake* rec = m_priv->index(row); - if(count != 1 || !rec || rec->m_status == GRC::SideStakeStatus::MANDATORY) + 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->m_address); + GRC::GetSideStakeRegistry().NonContractDelete(rec->GetAddress()); updateSideStakeTableModel(); diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index c06cbec093..bc5b0a4c6c 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -2496,14 +2496,14 @@ UniValue addkey(const UniValue& params, bool fHelp) } contract = GRC::MakeContract( - contract_version, // Contract version number (3+) - action, // Contract action - uint32_t {1}, // Contract payload version number - sidestake_address, // Sidestake address - allocation, // Sidestake allocation - description, // Sidestake description - GRC::SideStakeStatus::MANDATORY // sidestake status - ); + contract_version, // Contract version number (3+) + action, // Contract action + uint32_t {1}, // Contract payload version number + sidestake_address, // Sidestake address + 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."); } diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index 55cd761167..de16010f16 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -117,8 +117,8 @@ UniValue getstakinginfo(const UniValue& params, bool fHelp) // sidestakes are always included. for (const auto& alloc : vSideStakeAlloc) { - sidestakingalloc.pushKV("address", alloc->m_address.ToString()); - sidestakingalloc.pushKV("allocation_pct", alloc->m_allocation * 100); + sidestakingalloc.pushKV("address", alloc->GetAddress().ToString()); + sidestakingalloc.pushKV("allocation_pct", alloc->GetAllocation() * 100); sidestakingalloc.pushKV("status", alloc->StatusToString()); vsidestakingalloc.push_back(sidestakingalloc); From 09d2bf6767fee1fa422812f4634ad71850e85224 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Mon, 25 Dec 2023 18:16:14 -0500 Subject: [PATCH 25/47] Change CBitcoinAddress to CTxDestination in sidestake --- src/gridcoin/sidestake.cpp | 206 +++++++++++++++++---------------- src/gridcoin/sidestake.h | 90 +++++++------- src/miner.cpp | 2 +- src/qt/sidestaketablemodel.cpp | 28 ++--- src/rpc/blockchain.cpp | 4 +- src/rpc/mining.cpp | 2 +- 6 files changed, 168 insertions(+), 164 deletions(-) diff --git a/src/gridcoin/sidestake.cpp b/src/gridcoin/sidestake.cpp index 32d59dd289..6b88ff037b 100644 --- a/src/gridcoin/sidestake.cpp +++ b/src/gridcoin/sidestake.cpp @@ -33,6 +33,7 @@ SideStakeRegistry& GRC::GetSideStakeRegistry() return g_sidestake_entries; } +/* // ----------------------------------------------------------------------------- // Class: CBitcoinAddressForStorage // ----------------------------------------------------------------------------- @@ -43,83 +44,63 @@ CBitcoinAddressForStorage::CBitcoinAddressForStorage() CBitcoinAddressForStorage::CBitcoinAddressForStorage(CBitcoinAddress address) : CBitcoinAddress(address) {} +*/ // ----------------------------------------------------------------------------- -// Class: MandatorySideStake +// Class: LocalSideStake // ----------------------------------------------------------------------------- -MandatorySideStake::MandatorySideStake() - : m_address() +LocalSideStake::LocalSideStake() + : m_destination() , m_allocation() , m_description() - , m_timestamp(0) - , m_hash() - , m_previous_hash() - , m_status(MandatorySideStakeStatus::UNKNOWN) + , m_status(LocalSideStakeStatus::UNKNOWN) {} -MandatorySideStake::MandatorySideStake(CBitcoinAddressForStorage address, double allocation, std::string description) - : m_address(address) +LocalSideStake::LocalSideStake(CTxDestination destination, double 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) + , m_status(LocalSideStakeStatus::UNKNOWN) {} -MandatorySideStake::MandatorySideStake(CBitcoinAddressForStorage address, - double allocation, - std::string description, - int64_t timestamp, - uint256 hash, - MandatorySideStakeStatus status) - : m_address(address) +LocalSideStake::LocalSideStake(CTxDestination destination, + double allocation, + std::string description, + LocalSideStakeStatus 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 m_address.IsValid() && m_allocation >= 0.0 && m_allocation <= 1.0; -} - -CBitcoinAddressForStorage MandatorySideStake::Key() const -{ - return m_address; -} - -std::pair MandatorySideStake::KeyValueToString() const +bool LocalSideStake::WellFormed() const { - return std::make_pair(m_address.ToString(), StatusToString()); + return CBitcoinAddress(m_destination).IsValid() && m_allocation >= 0.0 && m_allocation <= 1.0; } -std::string MandatorySideStake::StatusToString() const +std::string LocalSideStake::StatusToString() const { return StatusToString(m_status.Value()); } -std::string MandatorySideStake::StatusToString(const MandatorySideStakeStatus& status, const bool& translated) const +std::string LocalSideStake::StatusToString(const LocalSideStakeStatus& 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; + 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 MandatorySideStakeStatus::UNKNOWN: return "Unknown"; - case MandatorySideStakeStatus::DELETED: return "Deleted"; - case MandatorySideStakeStatus::MANDATORY: return "Mandatory"; - case MandatorySideStakeStatus::OUT_OF_BOUND: break; + 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 @@ -130,81 +111,99 @@ std::string MandatorySideStake::StatusToString(const MandatorySideStakeStatus& s return std::string{}; } -bool MandatorySideStake::operator==(MandatorySideStake b) +bool LocalSideStake::operator==(LocalSideStake b) { bool result = true; - result &= (m_address == b.m_address); + 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) +bool LocalSideStake::operator!=(LocalSideStake b) { return !(*this == b); } // ----------------------------------------------------------------------------- -// Class: LocalSideStake +// Class: MandatorySideStake // ----------------------------------------------------------------------------- -LocalSideStake::LocalSideStake() - : m_address() +MandatorySideStake::MandatorySideStake() + : m_destination() , m_allocation() , m_description() - , m_status(LocalSideStakeStatus::UNKNOWN) + , m_timestamp(0) + , m_hash() + , m_previous_hash() + , m_status(MandatorySideStakeStatus::UNKNOWN) {} -LocalSideStake::LocalSideStake(CBitcoinAddressForStorage address, double allocation, std::string description) - : m_address(address) +MandatorySideStake::MandatorySideStake(CTxDestination destination, double allocation, std::string description) + : m_destination(destination) , m_allocation(allocation) , m_description(description) - , m_status(LocalSideStakeStatus::UNKNOWN) + , m_timestamp(0) + , m_hash() + , m_previous_hash() + , m_status(MandatorySideStakeStatus::UNKNOWN) {} -LocalSideStake::LocalSideStake(CBitcoinAddressForStorage address, - double allocation, - std::string description, - LocalSideStakeStatus status) - : m_address(address) +MandatorySideStake::MandatorySideStake(CTxDestination destination, + double 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 LocalSideStake::WellFormed() const +bool MandatorySideStake::WellFormed() const { - return m_address.IsValid() && m_allocation >= 0.0 && m_allocation <= 1.0; + return CBitcoinAddress(m_destination).IsValid() && m_allocation >= 0.0 && m_allocation <= 1.0; } -std::string LocalSideStake::StatusToString() const +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 LocalSideStake::StatusToString(const LocalSideStakeStatus& status, const bool& translated) const +std::string MandatorySideStake::StatusToString(const MandatorySideStakeStatus& 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; + 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 LocalSideStakeStatus::UNKNOWN: return "Unknown"; - case LocalSideStakeStatus::ACTIVE: return "Active"; - case LocalSideStakeStatus::INACTIVE: return "Inactive"; - case LocalSideStakeStatus::OUT_OF_BOUND: break; + 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 @@ -215,19 +214,22 @@ std::string LocalSideStake::StatusToString(const LocalSideStakeStatus& status, c return std::string{}; } -bool LocalSideStake::operator==(LocalSideStake b) +bool MandatorySideStake::operator==(MandatorySideStake b) { bool result = true; - result &= (m_address == b.m_address); + 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 LocalSideStake::operator!=(LocalSideStake b) +bool MandatorySideStake::operator!=(MandatorySideStake b) { return !(*this == b); } @@ -258,12 +260,12 @@ bool SideStake::IsMandatory() const return m_mandatory; } -CBitcoinAddress SideStake::GetAddress() const +CTxDestination SideStake::GetDestination() const { if (IsMandatory()) { - return m_mandatory_sidestake_ptr->m_address; + return m_mandatory_sidestake_ptr->m_destination; } else { - return m_local_sidestake_ptr->m_address; + return m_local_sidestake_ptr->m_destination; } } @@ -320,13 +322,13 @@ SideStakePayload::SideStakePayload(uint32_t version) } SideStakePayload::SideStakePayload(const uint32_t version, - CBitcoinAddressForStorage address, + CTxDestination destination, double allocation, std::string description, MandatorySideStake::MandatorySideStakeStatus status) : IContractPayload() , m_version(version) - , m_entry(MandatorySideStake(address, allocation, description, 0, uint256{}, status)) + , m_entry(MandatorySideStake(destination, allocation, description, 0, uint256{}, status)) { } @@ -410,7 +412,7 @@ const std::vector SideStakeRegistry::ActiveSideStakeEntries(const return sidestakes; } -std::vector SideStakeRegistry::Try(const CBitcoinAddressForStorage& key, const bool& local_only) const +std::vector SideStakeRegistry::Try(const CTxDestination& key, const bool& local_only) const { LOCK(cs_lock); @@ -433,7 +435,7 @@ std::vector SideStakeRegistry::Try(const CBitcoinAddressForStorag return result; } -std::vector SideStakeRegistry::TryActive(const CBitcoinAddressForStorage& key, const bool& local_only) const +std::vector SideStakeRegistry::TryActive(const CTxDestination& key, const bool& local_only) const { LOCK(cs_lock); @@ -487,7 +489,7 @@ void SideStakeRegistry::AddDelete(const ContractContext& ctx) LOCK(cs_lock); - auto sidestake_entry_pair_iter = m_mandatory_sidestake_entries.find(payload.m_entry.m_address); + auto sidestake_entry_pair_iter = m_mandatory_sidestake_entries.find(payload.m_entry.m_destination); MandatorySideStake_ptr current_sidestake_entry_ptr = nullptr; @@ -510,7 +512,7 @@ void SideStakeRegistry::AddDelete(const ContractContext& ctx) __func__, ctx->m_version, payload.m_version, - payload.m_entry.m_address.ToString(), + CBitcoinAddress(payload.m_entry.m_destination).ToString(), payload.m_entry.m_allocation, payload.m_entry.m_timestamp, payload.m_entry.m_hash.ToString(), @@ -526,13 +528,13 @@ void SideStakeRegistry::AddDelete(const ContractContext& ctx) "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__, - historical.m_address.ToString(), + CBitcoinAddress(historical.m_destination).ToString(), historical.m_allocation, 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_address] = m_sidestake_db.find(ctx.m_tx.GetHash())->second; + m_mandatory_sidestake_entries[payload.m_entry.m_destination] = m_sidestake_db.find(ctx.m_tx.GetHash())->second; return; } @@ -552,18 +554,18 @@ void SideStakeRegistry::NonContractAdd(const LocalSideStake& sidestake, const bo 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_address] = std::make_shared(sidestake); + m_local_sidestake_entries[sidestake.m_destination] = std::make_shared(sidestake); if (save_to_file) { SaveLocalSideStakesToConfig(); } } -void SideStakeRegistry::NonContractDelete(const CBitcoinAddressForStorage& address, const bool& save_to_file) +void SideStakeRegistry::NonContractDelete(const CTxDestination& destination, const bool& save_to_file) { LOCK(cs_lock); - auto sidestake_entry_pair_iter = m_local_sidestake_entries.find(address); + 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); @@ -583,12 +585,12 @@ void SideStakeRegistry::Revert(const ContractContext& ctx) // resurrect. LOCK(cs_lock); - auto entry_to_revert = m_mandatory_sidestake_entries.find(payload->m_entry.m_address); + 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__, - entry_to_revert->second->m_address.ToString()); + 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. @@ -596,16 +598,16 @@ void SideStakeRegistry::Revert(const ContractContext& ctx) } // If this is not a null hash, then there will be a prior entry to resurrect. - CBitcoinAddressForStorage key = entry_to_revert->second->m_address; + 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_address) == 0) { + 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__, - key.ToString()); + CBitcoinAddress(key).ToString()); // If the record to revert is not found in the m_sidestake_entries map, no point in continuing. return; } @@ -614,7 +616,7 @@ void SideStakeRegistry::Revert(const ContractContext& ctx) 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__, - key.ToString()); + 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, @@ -630,13 +632,13 @@ void SideStakeRegistry::Revert(const ContractContext& ctx) 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__, - key.ToString()); + 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_address] = resurrect_entry->second; + m_mandatory_sidestake_entries[resurrect_entry->second->m_destination] = resurrect_entry->second; } } @@ -842,7 +844,7 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() break; } - LocalSideStake sidestake(static_cast(address), + LocalSideStake sidestake(address.Get(), dAllocation, std::get<2>(entry), LocalSideStake::LocalSideStakeStatus::ACTIVE); @@ -899,7 +901,7 @@ bool SideStakeRegistry::SaveLocalSideStakesToConfig() separator = ","; } - addresses += separator + iter.second->m_address.ToString(); + addresses += separator + CBitcoinAddress(iter.second->m_destination).ToString(); allocations += separator + ToString(iter.second->m_allocation * 100.0); descriptions += separator + iter.second->m_description; diff --git a/src/gridcoin/sidestake.h b/src/gridcoin/sidestake.h index 3fe2443dbf..cf6eab9ae9 100644 --- a/src/gridcoin/sidestake.h +++ b/src/gridcoin/sidestake.h @@ -15,6 +15,7 @@ namespace GRC { +/* //! //! \brief The CBitcoinAddressForStorage class. This is a very small extension of the CBitcoinAddress class that //! provides serialization/deserialization. @@ -38,10 +39,11 @@ class CBitcoinAddressForStorage : public CBitcoinAddress READWRITE(vchData); } }; +*/ //! //! \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 address. This address must be a valid address, but +//! 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. //! @@ -64,7 +66,7 @@ class LocalSideStake //! using Status = EnumByte; - CBitcoinAddressForStorage m_address; //!< The Gridcoin Address of the sidestake destination. + CTxDestination m_destination; //!< The destination of the sidestake. double m_allocation; //!< The allocation is a double precision floating point between 0.0 and 1.0 inclusive @@ -79,24 +81,24 @@ class LocalSideStake LocalSideStake(); //! - //! \brief Initialize a sidestake instance with the provided address and allocation. This is used to construct a user + //! \brief Initialize a sidestake instance with the provided destination and allocation. This is used to construct a user //! specified sidestake. //! - //! \param address + //! \param destination //! \param allocation //! \param description (optional) //! - LocalSideStake(CBitcoinAddressForStorage address, double allocation, std::string description); + LocalSideStake(CTxDestination destination, double allocation, std::string description); //! //! \brief Initialize a sidestake instance with the provided parameters. //! - //! \param address + //! \param destination //! \param allocation //! \param description (optional) //! \param status //! - LocalSideStake(CBitcoinAddressForStorage address, double allocation, std::string description, LocalSideStakeStatus status); + LocalSideStake(CTxDestination destination, double allocation, std::string description, LocalSideStakeStatus status); //! //! \brief Determine whether a sidestake contains each of the required elements. @@ -144,7 +146,7 @@ class LocalSideStake template inline void SerializationOp(Stream& s, Operation ser_action) { - READWRITE(m_address); + READWRITE(m_destination); READWRITE(m_allocation); READWRITE(m_description); READWRITE(m_status); @@ -158,7 +160,7 @@ 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 address. This address must be a valid address, but +//! 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. //! @@ -180,7 +182,7 @@ class MandatorySideStake //! using Status = EnumByte; - CBitcoinAddressForStorage m_address; //!< The Gridcoin Address of the sidestake destination. + CTxDestination m_destination; //!< The destination of the sidestake. double m_allocation; //!< The allocation is a double precision floating point between 0.0 and 1.0 inclusive @@ -190,7 +192,7 @@ class MandatorySideStake 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 address. + 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 int instead of enum for serialization. @@ -200,37 +202,37 @@ class MandatorySideStake MandatorySideStake(); //! - //! \brief Initialize a sidestake instance with the provided address and allocation. This is used to construct a user + //! \brief Initialize a sidestake instance with the provided destination and allocation. This is used to construct a user //! specified sidestake. //! - //! \param address + //! \param destination //! \param allocation //! \param description (optional) //! - MandatorySideStake(CBitcoinAddressForStorage address, double allocation, std::string description); + MandatorySideStake(CTxDestination destination, double allocation, std::string description); //! //! \brief Initialize a sidestake instance with the provided parameters. //! - //! \param address + //! \param destination //! \param allocation //! \param description (optional) //! \param status //! - MandatorySideStake(CBitcoinAddressForStorage address, double allocation, std::string description, MandatorySideStakeStatus status); + MandatorySideStake(CTxDestination destination, double 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 address + //! \param destination //! \param allocation //! \param description (optional) //! \param timestamp //! \param hash //! \param status //! - MandatorySideStake(CBitcoinAddressForStorage address, double allocation, std::string description, int64_t timestamp, + MandatorySideStake(CTxDestination destination, double allocation, std::string description, int64_t timestamp, uint256 hash, MandatorySideStakeStatus status); //! @@ -240,15 +242,15 @@ class MandatorySideStake bool WellFormed() const; //! - //! \brief This is the standardized method that returns the key value (in this case the address) for the sidestake entry (for + //! \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 CBitcoinAddress key value for the sidestake entry + //! \return CTxDestination key value for the sidestake entry //! - CBitcoinAddressForStorage Key() const; + CTxDestination Key() const; //! - //! \brief Provides the sidestake address and status as a pair of strings. + //! \brief Provides the sidestake destination address and status as a pair of strings. //! \return std::pair of strings //! std::pair KeyValueToString() const; @@ -293,7 +295,7 @@ class MandatorySideStake template inline void SerializationOp(Stream& s, Operation ser_action) { - READWRITE(m_address); + READWRITE(m_destination); READWRITE(m_allocation); READWRITE(m_description); READWRITE(m_timestamp); @@ -328,7 +330,7 @@ class SideStake bool IsMandatory() const; - CBitcoinAddress GetAddress() const; + CTxDestination GetDestination() const; double GetAllocation() const; std::string GetDescription() const; Status GetStatus() const; @@ -377,15 +379,15 @@ class SideStakePayload : public IContractPayload SideStakePayload(uint32_t version = CURRENT_VERSION); //! - //! \brief Initialize a sidestakeEntryPayload from a sidestake address, allocation, + //! \brief Initialize a sidestakeEntryPayload from a sidestake destination, allocation, //! description, and status. //! - //! \param address. Address for the sidestake entry + //! \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, CBitcoinAddressForStorage address, double allocation, + SideStakePayload(const uint32_t version, CTxDestination destination, double allocation, std::string description, MandatorySideStake::MandatorySideStakeStatus status); //! @@ -440,7 +442,7 @@ class SideStakePayload : public IContractPayload "m_entry.StatusToString() = %s", __func__, valid, - m_entry.m_address.ToString(), + CBitcoinAddress(m_entry.m_destination).ToString(), m_entry.m_allocation, m_entry.StatusToString() ); @@ -456,7 +458,7 @@ class SideStakePayload : public IContractPayload //! std::string LegacyKeyString() const override { - return m_entry.m_address.ToString(); + return CBitcoinAddress(m_entry.m_destination).ToString(); } //! @@ -511,18 +513,18 @@ class SideStakeRegistry : public IContractHandler }; //! - //! \brief The type that keys local sidestake entries by their addresses. Note that the entries + //! \brief The type that keys local 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 LocalSideStakeMap; + typedef std::map LocalSideStakeMap; //! - //! \brief The type that keys mandatory sidestake entries by their addresses. Note that the entries + //! \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; + typedef std::map MandatorySideStakeMap; //! //! \brief PendingSideStakeMap. This is not actually used but defined to satisfy the template. @@ -559,27 +561,27 @@ class SideStakeRegistry : public IContractHandler const std::vector ActiveSideStakeEntries(const bool& local_only, const bool& include_zero_alloc); //! - //! \brief Get the current sidestake entry for the specified address. + //! \brief Get the current sidestake entry for the specified destination. //! - //! \param key The address of the sidestake entry. + //! \param key The destination of the sidestake entry. //! \param local_only If true causes Try to only check the local sidestake map. Defaults to false. //! - //! \return A vector of smart pointers to entries matching the provided address. Up to two elements + //! \return A vector of smart pointers to entries matching the provided destination. Up to two elements //! are returned, mandatory entry first, unless local only boolean is set true. //! - std::vector Try(const CBitcoinAddressForStorage& key, const bool& local_only = false) const; + std::vector Try(const CTxDestination& key, const bool& local_only = false) const; //! - //! \brief Get the current sidestake entry for the specified address if it has a status of ACTIVE or MANDATORY. + //! \brief Get the current sidestake entry for the specified destination if it has a status of ACTIVE or MANDATORY. //! - //! \param key The address of the sidestake entry. + //! \param key The destination of the sidestake entry. //! \param local_only If true causes Try to only check the local sidestake map. Defaults to false. //! - //! \return A vector of smart pointers to entries matching the provided address that are in status of + //! \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, unless local only boolean //! is set true. //! - std::vector TryActive(const CBitcoinAddressForStorage& key, const bool& local_only = false) const; + std::vector TryActive(const CTxDestination& key, const bool& local_only = false) const; //! //! \brief Destroy the contract handler state in case of an error in loading @@ -641,12 +643,12 @@ class SideStakeRegistry : public IContractHandler //! //! \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 (CBitcoinAddress). + //! to the registry db. Deletion is by the map key (CTxDestination). //! - //! \param address + //! \param destination //! \param bool save_to_file if true causes SaveLocalSideStakesToConfig() to be called. //! - void NonContractDelete(const CBitcoinAddressForStorage& address, const bool& save_to_file = true); + void NonContractDelete(const CTxDestination& destination, const bool& save_to_file = true); //! //! \brief Revert the registry state for the sidestake entry to the state prior diff --git a/src/miner.cpp b/src/miner.cpp index 7150ad4e81..13f68de631 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -921,7 +921,7 @@ void SplitCoinStakeOutput(CBlock &blocknew, int64_t &nReward, bool &fEnableStake (iterSideStake != vSideStakeAlloc.end()) && (nOutputsUsed <= nMaxSideStakeOutputs); ++iterSideStake) { - CBitcoinAddress address = iterSideStake->get()->GetAddress(); + CBitcoinAddress address(iterSideStake->get()->GetDestination()); double allocation = iterSideStake->get()->GetAllocation(); if (!address.IsValid()) diff --git a/src/qt/sidestaketablemodel.cpp b/src/qt/sidestaketablemodel.cpp index 38dac02dfe..b02b311c4f 100644 --- a/src/qt/sidestaketablemodel.cpp +++ b/src/qt/sidestaketablemodel.cpp @@ -59,7 +59,7 @@ bool SideStakeLessThan::operator()(const GRC::SideStake& left, const GRC::SideSt switch (static_cast(m_column)) { case SideStakeTableModel::Address: - return pLeft->GetAddress() < pRight->GetAddress(); + return pLeft->GetDestination() < pRight->GetDestination(); case SideStakeTableModel::Allocation: return pLeft->GetAllocation() < pRight->GetAllocation(); case SideStakeTableModel::Description: @@ -153,7 +153,7 @@ QVariant SideStakeTableModel::data(const QModelIndex &index, int role) const if (role == Qt::DisplayRole || role == Qt::EditRole) { switch (column) { case Address: - return QString::fromStdString(rec->GetAddress().ToString()); + return QString::fromStdString(CBitcoinAddress(rec->GetDestination()).ToString()); case Allocation: return rec->GetAllocation() * 100.0; case Description: @@ -203,7 +203,7 @@ bool SideStakeTableModel::setData(const QModelIndex &index, const QVariant &valu CBitcoinAddress address; address.SetString(value.toString().toStdString()); - if (rec->GetAddress() == address) { + if (CBitcoinAddress(rec->GetDestination()) == address) { m_edit_status = NO_CHANGES; return false; } else if (!address.IsValid()) { @@ -211,7 +211,7 @@ bool SideStakeTableModel::setData(const QModelIndex &index, const QVariant &valu return false; } - std::vector sidestakes = registry.Try(address, true); + std::vector sidestakes = registry.Try(address.Get(), true); if (!sidestakes.empty()) { m_edit_status = DUPLICATE_ADDRESS; @@ -230,16 +230,16 @@ bool SideStakeTableModel::setData(const QModelIndex &index, const QVariant &valu // Save the original local sidestake (also in the core). GRC::SideStake orig_sidestake = *rec; - CBitcoinAddress orig_address = rec->GetAddress(); + CTxDestination orig_destination = rec->GetDestination(); double orig_allocation = rec->GetAllocation(); std::string orig_description = rec->GetDescription(); GRC::SideStake::Status orig_status = rec->GetStatus(); for (const auto& entry : registry.ActiveSideStakeEntries(false, true)) { - CBitcoinAddress address = entry->GetAddress(); + CTxDestination destination = entry->GetDestination(); double allocation = entry->GetAllocation(); - if (address == orig_address) { + if (destination == orig_destination) { continue; } @@ -262,10 +262,10 @@ bool SideStakeTableModel::setData(const QModelIndex &index, const QVariant &valu } // Delete the original sidestake - registry.NonContractDelete(orig_address, false); + registry.NonContractDelete(orig_destination, false); // Add back the sidestake with the modified allocation - registry.NonContractAdd(GRC::LocalSideStake(orig_address, + registry.NonContractAdd(GRC::LocalSideStake(orig_destination, value.toDouble() / 100.0, orig_description, std::get(orig_status).Value()), true); @@ -291,10 +291,10 @@ bool SideStakeTableModel::setData(const QModelIndex &index, const QVariant &valu GRC::SideStake orig_sidestake = *rec; // Delete the original sidestake - registry.NonContractDelete(orig_sidestake.GetAddress(), false); + registry.NonContractDelete(orig_sidestake.GetDestination(), false); // Add back the sidestake with the modified allocation - registry.NonContractAdd(GRC::LocalSideStake(orig_sidestake.GetAddress(), + registry.NonContractAdd(GRC::LocalSideStake(orig_sidestake.GetDestination(), orig_sidestake.GetAllocation(), san_value, std::get(orig_sidestake.GetStatus()).Value()), true); @@ -370,7 +370,7 @@ QString SideStakeTableModel::addRow(const QString &address, const QString &alloc // 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, true); + std::vector core_local_sidestake = registry.Try(sidestake_address.Get(), true); double prior_total_allocation = 0.0; @@ -402,7 +402,7 @@ QString SideStakeTableModel::addRow(const QString &address, const QString &alloc return QString(); } - registry.NonContractAdd(GRC::LocalSideStake(sidestake_address, + registry.NonContractAdd(GRC::LocalSideStake(sidestake_address.Get(), sidestake_allocation, sanitized_description, GRC::LocalSideStake::LocalSideStakeStatus::ACTIVE)); @@ -424,7 +424,7 @@ bool SideStakeTableModel::removeRows(int row, int count, const QModelIndex &pare return false; } - GRC::GetSideStakeRegistry().NonContractDelete(rec->GetAddress()); + GRC::GetSideStakeRegistry().NonContractDelete(rec->GetDestination()); updateSideStakeTableModel(); diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index bc5b0a4c6c..76e0f4cbc9 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -2472,7 +2472,7 @@ UniValue addkey(const UniValue& params, bool fHelp) case GRC::ContractType::SIDESTAKE: { if (block_v13_enabled) { - GRC::CBitcoinAddressForStorage sidestake_address; + CBitcoinAddress sidestake_address; if (!sidestake_address.SetString(params[2].get_str())) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Address specified for the sidestake is invalid."); } @@ -2499,7 +2499,7 @@ UniValue addkey(const UniValue& params, bool fHelp) contract_version, // Contract version number (3+) action, // Contract action uint32_t {1}, // Contract payload version number - sidestake_address, // Sidestake address + sidestake_address.Get(), // Sidestake destination allocation, // Sidestake allocation description, // Sidestake description GRC::MandatorySideStake::MandatorySideStakeStatus::MANDATORY // sidestake status diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index de16010f16..1dee8aff77 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -117,7 +117,7 @@ UniValue getstakinginfo(const UniValue& params, bool fHelp) // sidestakes are always included. for (const auto& alloc : vSideStakeAlloc) { - sidestakingalloc.pushKV("address", alloc->GetAddress().ToString()); + sidestakingalloc.pushKV("address", CBitcoinAddress(alloc->GetDestination()).ToString()); sidestakingalloc.pushKV("allocation_pct", alloc->GetAllocation() * 100); sidestakingalloc.pushKV("status", alloc->StatusToString()); From 1c01201dd75183836c63c3a4961ec43f14c74f90 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Mon, 25 Dec 2023 20:24:43 -0500 Subject: [PATCH 26/47] Add serialization support to CScriptID and CNoDestination The serialization added to CScriptID and trivial (no-op) serialization added to CNoDestination allows CTxDestination (variant) to be serialized. --- src/script.h | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) 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: From 01bb8120296f01c410a8b5b2537021464cdcea3b Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Tue, 26 Dec 2023 18:03:41 -0500 Subject: [PATCH 27/47] Add missing IsV13Enabled guard in sidestake BlockValidate --- src/gridcoin/sidestake.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gridcoin/sidestake.cpp b/src/gridcoin/sidestake.cpp index 6b88ff037b..2cffd1bcda 100644 --- a/src/gridcoin/sidestake.cpp +++ b/src/gridcoin/sidestake.cpp @@ -663,7 +663,7 @@ bool SideStakeRegistry::Validate(const Contract& contract, const CTransaction& t bool SideStakeRegistry::BlockValidate(const ContractContext& ctx, int& DoS) const { - return Validate(ctx.m_contract, ctx.m_tx, DoS); + return (IsV13Enabled(ctx.m_pindex->nHeight) && Validate(ctx.m_contract, ctx.m_tx, DoS)); } int SideStakeRegistry::Initialize() From 7f894cf0120f1b26edfbf83937fbf0346a3556a6 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Wed, 27 Dec 2023 19:23:51 -0500 Subject: [PATCH 28/47] Implement protocol rule for maximum mandatory sidestake allocation total --- src/chainparams.cpp | 4 ++++ src/consensus/params.h | 4 ++++ src/gridcoin/contract/message.cpp | 20 +++++++++++++--- src/gridcoin/sidestake.cpp | 38 +++++++++++++++++++++++-------- src/gridcoin/sidestake.h | 8 ++++++- src/miner.cpp | 16 +++++++++++++ 6 files changed, 77 insertions(+), 13 deletions(-) diff --git a/src/chainparams.cpp b/src/chainparams.cpp index 257bf80803..f69ff806ce 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 = 0.25; // 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 = 0.25; // 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..10f5ab230f 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 floating point) that can be used by all of the mandatory sidestakes + */ + double MaxMandatorySideStakeTotalAlloc; int64_t StandardContractReplayLookback; 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/sidestake.cpp b/src/gridcoin/sidestake.cpp index 2cffd1bcda..8017cc5c03 100644 --- a/src/gridcoin/sidestake.cpp +++ b/src/gridcoin/sidestake.cpp @@ -365,7 +365,7 @@ const std::vector SideStakeRegistry::SideStakeEntries() const } const std::vector SideStakeRegistry::ActiveSideStakeEntries(const bool& local_only, - const bool& include_zero_alloc) + const bool& include_zero_alloc) const { std::vector sidestakes; double allocation_sum = 0.0; @@ -382,7 +382,7 @@ const std::vector SideStakeRegistry::ActiveSideStakeEntries(const for (const auto& entry : m_mandatory_sidestake_entries) { if (entry.second->m_status == MandatorySideStake::MandatorySideStakeStatus::MANDATORY - && allocation_sum + entry.second->m_allocation <= 1.0) { + && allocation_sum + entry.second->m_allocation <= Params().GetConsensus().MaxMandatorySideStakeTotalAlloc) { if ((include_zero_alloc && entry.second->m_allocation == 0.0) || entry.second->m_allocation > 0.0) { sidestakes.push_back(std::make_shared(entry.second)); allocation_sum += entry.second->m_allocation; @@ -658,6 +658,17 @@ bool SideStakeRegistry::Validate(const Contract& contract, const CTransaction& t return false; } + double 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; } @@ -793,13 +804,8 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() } } - // First, determine allocation already taken by mandatory sidestakes, because they must be allocated first. - for (const auto& entry : SideStakeEntries()) { - if (entry->IsMandatory() - && std::get(entry->GetStatus()) == MandatorySideStake::MandatorySideStakeStatus::MANDATORY) { - dSumAllocation += entry->GetAllocation(); - } - } + // First, add the allocation already taken by mandatory sidestakes, because they must be allocated first. + dSumAllocation += GetMandatoryAllocationsTotal(); LOCK(cs_lock); @@ -919,6 +925,20 @@ bool SideStakeRegistry::SaveLocalSideStakesToConfig() return status; } +double SideStakeRegistry::GetMandatoryAllocationsTotal() const +{ + std::vector sidestakes = ActiveSideStakeEntries(false, false); + double allocation_total = 0.0; + + for (const auto& entry : sidestakes) { + if (entry->IsMandatory()) { + allocation_total += entry->GetAllocation(); + } + } + + return allocation_total; +} + void SideStakeRegistry::SubscribeToCoreSignals() { uiInterface.RwSettingsUpdated_connect(std::bind(RwSettingsUpdated, this)); diff --git a/src/gridcoin/sidestake.h b/src/gridcoin/sidestake.h index cf6eab9ae9..8c8a222097 100644 --- a/src/gridcoin/sidestake.h +++ b/src/gridcoin/sidestake.h @@ -558,7 +558,7 @@ class SideStakeRegistry : public IContractHandler //! //! \return A vector of smart pointers to sidestake entries. //! - const std::vector ActiveSideStakeEntries(const bool& local_only, const bool& include_zero_alloc); + const std::vector ActiveSideStakeEntries(const bool& local_only, const bool& include_zero_alloc) const; //! //! \brief Get the current sidestake entry for the specified destination. @@ -746,6 +746,12 @@ class SideStakeRegistry : public IContractHandler //! bool SaveLocalSideStakesToConfig(); + //! + //! \brief Provides the total allocation for all active mandatory sidestakes as a floating point fraction. + //! \return total active mandatory sidestake allocation as a double. + //! + double GetMandatoryAllocationsTotal() const; + void SubscribeToCoreSignals(); void UnsubscribeFromCoreSignals(); diff --git a/src/miner.cpp b/src/miner.cpp index 13f68de631..3edcca3c20 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -337,6 +337,10 @@ bool CreateRestOfTheBlock(CBlock &block, CBlockIndex* pindexPrev, const GRC::ResearcherPtr researcher = GRC::Researcher::Get(); const GRC::CpidOption cpid = researcher->Id().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; From dc5fbba3b3ff695fdc50a2decfb37d3c29cab7d3 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Thu, 28 Dec 2023 16:08:27 -0500 Subject: [PATCH 29/47] Fix logic to require mandatory sidestaking in miner.cpp If mandatory sidestakes that have non-zero allocation are set OR local sidestakes with non-zero allocations are set and the -enablesidestaking flag is turned on, then set fEnableSideStaking in the miner to true to include sidestakes. This replaces the old logic of fEnableSideStaking, which simply followed -enablestidestaking. --- src/miner.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/miner.cpp b/src/miner.cpp index 3edcca3c20..b96e0d2699 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -1334,12 +1334,15 @@ void StakeMiner(CWallet *pwallet) // nMinStakeSplitValue and dEfficiency are out parameters. bool fEnableStakeSplit = GetStakeSplitStatusAndParams(nMinStakeSplitValue, dEfficiency, nDesiredStakeOutputValue); - bool fEnableSideStaking = gArgs.GetBoolArg("-enablesidestaking"); - // Note that fEnableSideStaking is now processed internal to ActiveSideStakeEntries. The sidestaking flag only // controls local sidestakes. If there exists mandatory sidestakes, they occur regardless of the flag. vSideStakeAlloc = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(false, false); + // 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 = (!vSideStakeAlloc.empty()); + // wait for next round if (!MilliSleep(nMinerSleep)) return; From 84b90cdda9a5befd309be91a5908157cd1c470fd Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Thu, 28 Dec 2023 18:15:04 -0500 Subject: [PATCH 30/47] Implement mandatory sidestake output checking in ClaimValidator CheckReward. --- src/validation.cpp | 45 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/src/validation.cpp b/src/validation.cpp index 23398d8eee..2c1b90dcfa 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -835,6 +835,49 @@ 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) { + for (const auto& sidestake : GRC::GetSideStakeRegistry().ActiveSideStakeEntries(false, false)) { + if (sidestake->IsMandatory()) { + CTxDestination mandatory_sidestake_destination = sidestake->GetDestination(); + double mandatory_sidestake_allocation = sidestake->GetAllocation(); + + // Check each non-MRC output for a match + bool matched = false; + + 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."); + } + + double computed_output_alloc = (double) coinstake.vout[i].nValue / (double) total_owed_to_staker; + + + // The output is deemed to match if the destination matches AND + // the output amount expressed as a double fraction of the awards owed to staker is within 1% + // of the required mandatory allocation. + if (output_destination == mandatory_sidestake_destination + && abs(computed_output_alloc - mandatory_sidestake_allocation) < 0.01) { + + matched = true; + + break; + } + } // iterate through non-MRC outputs + + if (!matched) { + return error("%s: FAILED: mandatory sidestake for address %s, allocation %f not found on coinstake.", + __func__, + CBitcoinAddress(mandatory_sidestake_destination).ToString(), + sidestake->GetAllocation() * 100.0); + } + } // IsMandatory sidestake? + } // active sidestakes + } // 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) { @@ -866,7 +909,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+ From 96c818967d54179c9a44cc82bd212bc8ec9c96db Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Fri, 29 Dec 2023 15:50:29 -0500 Subject: [PATCH 31/47] Limit number of mandatory sidestakes in miner Also add validation for this in Validator. --- src/gridcoin/sidestake.cpp | 59 ++++++++++--------- src/gridcoin/sidestake.h | 28 ++++++--- src/miner.cpp | 97 +++++++++++++++++++----------- src/qt/sidestaketablemodel.cpp | 11 ++-- src/rpc/mining.cpp | 2 +- src/validation.cpp | 104 +++++++++++++++++++++++---------- src/validation.h | 1 + 7 files changed, 197 insertions(+), 105 deletions(-) diff --git a/src/gridcoin/sidestake.cpp b/src/gridcoin/sidestake.cpp index 8017cc5c03..a9c285b02f 100644 --- a/src/gridcoin/sidestake.cpp +++ b/src/gridcoin/sidestake.cpp @@ -240,24 +240,24 @@ bool MandatorySideStake::operator!=(MandatorySideStake b) SideStake::SideStake() : m_local_sidestake_ptr(nullptr) , m_mandatory_sidestake_ptr(nullptr) - , m_mandatory(false) + , m_type(Type::UNKNOWN) {} SideStake::SideStake(LocalSideStake_ptr sidestake_ptr) : m_local_sidestake_ptr(sidestake_ptr) , m_mandatory_sidestake_ptr(nullptr) - , m_mandatory(false) + , m_type(Type::LOCAL) {} SideStake::SideStake(MandatorySideStake_ptr sidestake_ptr) : m_local_sidestake_ptr(nullptr) , m_mandatory_sidestake_ptr(sidestake_ptr) - , m_mandatory(true) + , m_type(Type::MANDATORY) {} bool SideStake::IsMandatory() const { - return m_mandatory; + return (m_type == Type::MANDATORY) ? true : false; } CTxDestination SideStake::GetDestination() const @@ -364,7 +364,7 @@ const std::vector SideStakeRegistry::SideStakeEntries() const return sidestakes; } -const std::vector SideStakeRegistry::ActiveSideStakeEntries(const bool& local_only, +const std::vector SideStakeRegistry::ActiveSideStakeEntries(const SideStake::FilterFlag& filter, const bool& include_zero_alloc) const { std::vector sidestakes; @@ -377,8 +377,7 @@ const std::vector SideStakeRegistry::ActiveSideStakeEntries(const LOCK(cs_lock); - // Do mandatory sidestakes first. - if (!local_only) { + if (filter & SideStake::FilterFlag::MANDATORY) { for (const auto& entry : m_mandatory_sidestake_entries) { if (entry.second->m_status == MandatorySideStake::MandatorySideStakeStatus::MANDATORY @@ -391,19 +390,21 @@ const std::vector SideStakeRegistry::ActiveSideStakeEntries(const } } - // Followed by local active sidestakes if sidestaking is enabled. Note that mandatory sidestaking cannot be disabled. - bool fEnableSideStaking = gArgs.GetBoolArg("-enablesidestaking"); + 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); + 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.0) { - if ((include_zero_alloc && entry.second->m_allocation == 0.0) || entry.second->m_allocation > 0.0) { - sidestakes.push_back(std::make_shared(entry.second)); - allocation_sum += entry.second->m_allocation; + for (const auto& entry : m_local_sidestake_entries) + { + if (entry.second->m_status == LocalSideStake::LocalSideStakeStatus::ACTIVE + && allocation_sum + entry.second->m_allocation <= 1.0) { + if ((include_zero_alloc && entry.second->m_allocation == 0.0) || entry.second->m_allocation > 0.0) { + sidestakes.push_back(std::make_shared(entry.second)); + allocation_sum += entry.second->m_allocation; + } } } } @@ -412,13 +413,13 @@ const std::vector SideStakeRegistry::ActiveSideStakeEntries(const return sidestakes; } -std::vector SideStakeRegistry::Try(const CTxDestination& key, const bool& local_only) const +std::vector SideStakeRegistry::Try(const CTxDestination& key, const SideStake::FilterFlag& filter) const { LOCK(cs_lock); std::vector result; - if (!local_only) { + if (filter & SideStake::FilterFlag::MANDATORY) { const auto mandatory_entry = m_mandatory_sidestake_entries.find(key); if (mandatory_entry != m_mandatory_sidestake_entries.end()) { @@ -426,22 +427,24 @@ std::vector SideStakeRegistry::Try(const CTxDestination& key, con } } - const auto local_entry = m_local_sidestake_entries.find(key); + 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)); + 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 bool& local_only) const +std::vector SideStakeRegistry::TryActive(const CTxDestination& key, const SideStake::FilterFlag& filter) const { LOCK(cs_lock); std::vector result; - for (const auto& iter : Try(key, local_only)) { + for (const auto& iter : Try(key, filter)) { if (iter->IsMandatory()) { if (std::get(iter->GetStatus()) == MandatorySideStake::MandatorySideStakeStatus::MANDATORY) { result.push_back(iter); @@ -927,13 +930,11 @@ bool SideStakeRegistry::SaveLocalSideStakesToConfig() double SideStakeRegistry::GetMandatoryAllocationsTotal() const { - std::vector sidestakes = ActiveSideStakeEntries(false, false); + std::vector sidestakes = ActiveSideStakeEntries(SideStake::FilterFlag::MANDATORY, false); double allocation_total = 0.0; for (const auto& entry : sidestakes) { - if (entry->IsMandatory()) { - allocation_total += entry->GetAllocation(); - } + allocation_total += entry->GetAllocation(); } return allocation_total; diff --git a/src/gridcoin/sidestake.h b/src/gridcoin/sidestake.h index 8c8a222097..b28cf123fa 100644 --- a/src/gridcoin/sidestake.h +++ b/src/gridcoin/sidestake.h @@ -317,6 +317,20 @@ typedef std::shared_ptr MandatorySideStake_ptr; 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. //! @@ -339,7 +353,7 @@ class SideStake private: LocalSideStake_ptr m_local_sidestake_ptr; MandatorySideStake_ptr m_mandatory_sidestake_ptr; - bool m_mandatory; + Type m_type; }; //! @@ -554,34 +568,34 @@ class SideStakeRegistry : public IContractHandler //! Mandatory sidestakes come before local ones, and the method ensures that the sidestakes //! returned do not total an allocation greater than 1.0. //! - //! \param bool true to return local sidestakes only + //! \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 bool& local_only, const bool& include_zero_alloc) const; + 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 local_only If true causes Try to only check the local sidestake map. Defaults to false. + //! \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, unless local only boolean is set true. //! - std::vector Try(const CTxDestination& key, const bool& local_only = false) const; + 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 local_only If true causes Try to only check the local sidestake map. Defaults to false. + //! \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, unless local only boolean //! is set true. //! - std::vector TryActive(const CTxDestination& key, const bool& local_only = false) const; + std::vector TryActive(const CTxDestination& key, const SideStake::FilterFlag& filter) const; //! //! \brief Destroy the contract handler state in case of an error in loading diff --git a/src/miner.cpp b/src/miner.cpp index b96e0d2699..4bb499b391 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -845,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 @@ -885,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 @@ -897,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 @@ -905,9 +905,37 @@ 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); + + // For mandatory sidestakes we need to check whether any of them will result in an output of less than 1 CENT, and in + // that case remove that entry from the vector. This is extremely important when validating this on a receiving node. + // In the case where there are more than GetMandatorySideStakeOutputLimit mandatory sidestakes, and a random shuffle + // selection is used, that shuffle CANNOT include any outputs that would be eliminated by the lambda below for output + // less than one cent, because on validation the receiving node will simply see this as a missing output and fail + // validation. Eliminating the outputs that would generate dust BEFORE the shuffle ensures that the set of outputs selected + // from the shuffle WILL be present as a result of the selection. 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. + for (auto iter = mandatory_sidestakes.begin(); iter != mandatory_sidestakes.end();) { + if (nReward * iter->get()->GetAllocation() < CENT) { + iter = mandatory_sidestakes.erase(iter); + } else { + ++iter; + } + } + + 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 @@ -929,13 +957,13 @@ void SplitCoinStakeOutput(CBlock &blocknew, int64_t &nReward, bool &fEnableStake 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) + // 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->get()->GetDestination()); double allocation = iterSideStake->get()->GetAllocation(); @@ -1001,13 +1029,16 @@ void SplitCoinStakeOutput(CBlock &blocknew, int64_t &nReward, bool &fEnableStake 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) + 1); + + // 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 @@ -1073,23 +1104,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; @@ -1334,14 +1369,10 @@ void StakeMiner(CWallet *pwallet) // nMinStakeSplitValue and dEfficiency are out parameters. bool fEnableStakeSplit = GetStakeSplitStatusAndParams(nMinStakeSplitValue, dEfficiency, nDesiredStakeOutputValue); - // Note that fEnableSideStaking is now processed internal to ActiveSideStakeEntries. The sidestaking flag only - // controls local sidestakes. If there exists mandatory sidestakes, they occur regardless of the flag. - vSideStakeAlloc = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(false, false); - // 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 = (!vSideStakeAlloc.empty()); + bool fEnableSideStaking = (!GRC::GetSideStakeRegistry().ActiveSideStakeEntries(GRC::SideStake::FilterFlag::ALL, false).empty()); // wait for next round if (!MilliSleep(nMinerSleep)) return; @@ -1429,7 +1460,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/qt/sidestaketablemodel.cpp b/src/qt/sidestaketablemodel.cpp index b02b311c4f..0f1286690f 100644 --- a/src/qt/sidestaketablemodel.cpp +++ b/src/qt/sidestaketablemodel.cpp @@ -81,7 +81,8 @@ class SideStakeTablePriv { m_cached_sidestakes.clear(); - std::vector core_sidestakes = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(false, true); + std::vector core_sidestakes + = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(GRC::SideStake::FilterFlag::ALL, true); m_cached_sidestakes.reserve(core_sidestakes.size()); @@ -211,7 +212,7 @@ bool SideStakeTableModel::setData(const QModelIndex &index, const QVariant &valu return false; } - std::vector sidestakes = registry.Try(address.Get(), true); + std::vector sidestakes = registry.Try(address.Get(), GRC::SideStake::FilterFlag::LOCAL); if (!sidestakes.empty()) { m_edit_status = DUPLICATE_ADDRESS; @@ -235,7 +236,7 @@ bool SideStakeTableModel::setData(const QModelIndex &index, const QVariant &valu std::string orig_description = rec->GetDescription(); GRC::SideStake::Status orig_status = rec->GetStatus(); - for (const auto& entry : registry.ActiveSideStakeEntries(false, true)) { + for (const auto& entry : registry.ActiveSideStakeEntries(GRC::SideStake::FilterFlag::ALL, true)) { CTxDestination destination = entry->GetDestination(); double allocation = entry->GetAllocation(); @@ -370,12 +371,12 @@ QString SideStakeTableModel::addRow(const QString &address, const QString &alloc // 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(), true); + std::vector core_local_sidestake = registry.Try(sidestake_address.Get(), GRC::SideStake::FilterFlag::LOCAL); double prior_total_allocation = 0.0; // Get total allocation of all active/mandatory sidestake entries - for (const auto& entry : registry.ActiveSideStakeEntries(false, true)) { + for (const auto& entry : registry.ActiveSideStakeEntries(GRC::SideStake::FilterFlag::ALL, true)) { prior_total_allocation += entry->GetAllocation() * 100.0; } diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index 1dee8aff77..2d923c69bd 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -109,7 +109,7 @@ UniValue getstakinginfo(const UniValue& params, bool fHelp) obj.pushKV("stake-splitting", stakesplitting); // This is what the miner sees... - vSideStakeAlloc = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(false, false); + vSideStakeAlloc = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(GRC::SideStake::FilterFlag::ALL, false); sidestaking.pushKV("local_side_staking_enabled", fEnableSideStaking); diff --git a/src/validation.cpp b/src/validation.cpp index 2c1b90dcfa..cc32414e86 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -612,6 +612,12 @@ unsigned int GetCoinstakeOutputLimit(const int& block_version) return output_limit; } +unsigned int GetMandatorySideStakeOutputLimit(const int& block_version) +{ + // For block version 13+ mandatory sidestake output limit is 4, otherwise 0. + return (block_version >= 13) ? 4 : 0; +} + Fraction FoundationSideStakeAllocation() { // Note that the 4/5 (80%) for mainnet was approved by a validated poll, @@ -838,45 +844,83 @@ 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) { - for (const auto& sidestake : GRC::GetSideStakeRegistry().ActiveSideStakeEntries(false, false)) { - if (sidestake->IsMandatory()) { - CTxDestination mandatory_sidestake_destination = sidestake->GetDestination(); - double mandatory_sidestake_allocation = sidestake->GetAllocation(); - // Check each non-MRC output for a match - bool matched = false; + // Get mandatory sidestakes + std::vector mandatory_sidestakes + = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(GRC::SideStake::FilterFlag::MANDATORY, false); - for (unsigned int i = 1; i < mrc_start_index; ++i) { - CTxDestination output_destination; + // This is exactly the same as the dust elimination in the SplitCoinStakeOutput function in the miner. + for (auto iter = mandatory_sidestakes.begin(); iter != mandatory_sidestakes.end();) { + if (total_owed_to_staker * iter->get()->GetAllocation() < CENT) { + iter = mandatory_sidestakes.erase(iter); + } else { + ++iter; + } + } - if (!ExtractDestination(coinstake.vout[i].scriptPubKey, output_destination)) { - return error("%s: FAILED: coinstake output has invalid destination."); - } + unsigned int validated_mandatory_sidestakes = 0; - double computed_output_alloc = (double) coinstake.vout[i].nValue / (double) total_owed_to_staker; + 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."); + } - // The output is deemed to match if the destination matches AND - // the output amount expressed as a double fraction of the awards owed to staker is within 1% - // of the required mandatory allocation. - if (output_destination == mandatory_sidestake_destination - && abs(computed_output_alloc - mandatory_sidestake_allocation) < 0.01) { + double computed_output_alloc = (double) coinstake.vout[i].nValue / (double) total_owed_to_staker; - matched = true; + std::vector mandatory_sidestake + = GRC::GetSideStakeRegistry().TryActive(output_destination, + GRC::SideStake::FilterFlag::MANDATORY);; - break; - } - } // iterate through non-MRC outputs + // The output is deemed to match if the destination matches AND + // the output amount expressed as a double fraction of the awards owed to staker is within 1% + // of the required mandatory allocation. We allow some leeway here because the sidestake allocations + // are double precision floating point fractions. + if (!mandatory_sidestake.empty() + && abs(computed_output_alloc - mandatory_sidestake[0]->GetAllocation()) < 0.01) { - if (!matched) { - return error("%s: FAILED: mandatory sidestake for address %s, allocation %f not found on coinstake.", - __func__, - CBitcoinAddress(mandatory_sidestake_destination).ToString(), - sidestake->GetAllocation() * 100.0); - } - } // IsMandatory sidestake? - } // active sidestakes - } // V13+ + ++validated_mandatory_sidestakes; + } + + // This should not happen, but include the check for thoroughness. + if (validated_mandatory_sidestakes > GetMandatorySideStakeOutputLimit(m_block.nVersion)) { + 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("%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())); + } + } // If the foundation mrc sidestake is present, we check the foundation sidestake specifically. The MRC // outputs were already checked by CheckMRCRewards. 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); From cf427941eebf65804b4e90771541680dd6fe3e46 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Fri, 29 Dec 2023 22:17:32 -0500 Subject: [PATCH 32/47] Correct addkey for missing parameter count guard for sidestakes --- src/rpc/blockchain.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 76e0f4cbc9..5adab057b5 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -2485,7 +2485,7 @@ UniValue addkey(const UniValue& params, bool fHelp) // 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 (!ParseDouble(params[3].get_str(), &allocation)) { + if (params.size() > 3 && !ParseDouble(params[3].get_str(), &allocation)) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid allocation specified."); } From 91a99108295ecfb6a898840933e4ea1714a7b3c5 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Sat, 30 Dec 2023 09:23:59 -0500 Subject: [PATCH 33/47] Corrections to miner and validation for mandatory sidestaking --- src/miner.cpp | 28 +++++++++------------------- src/validation.cpp | 33 ++++++++++++++++++++------------- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/src/miner.cpp b/src/miner.cpp index 4bb499b391..257a7a2931 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -910,24 +910,6 @@ void SplitCoinStakeOutput(CBlock &blocknew, int64_t &nReward, bool &fEnableStake SideStakeAlloc local_sidestakes = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(GRC::SideStake::FilterFlag::LOCAL, false); - // For mandatory sidestakes we need to check whether any of them will result in an output of less than 1 CENT, and in - // that case remove that entry from the vector. This is extremely important when validating this on a receiving node. - // In the case where there are more than GetMandatorySideStakeOutputLimit mandatory sidestakes, and a random shuffle - // selection is used, that shuffle CANNOT include any outputs that would be eliminated by the lambda below for output - // less than one cent, because on validation the receiving node will simply see this as a missing output and fail - // validation. Eliminating the outputs that would generate dust BEFORE the shuffle ensures that the set of outputs selected - // from the shuffle WILL be present as a result of the selection. 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. - for (auto iter = mandatory_sidestakes.begin(); iter != mandatory_sidestakes.end();) { - if (nReward * iter->get()->GetAllocation() < CENT) { - iter = mandatory_sidestakes.erase(iter); - } else { - ++iter; - } - } - if (mandatory_sidestakes.size() > GetMandatorySideStakeOutputLimit(blocknew.nVersion)) { Shuffle(mandatory_sidestakes.begin(), mandatory_sidestakes.end(), FastRandomContext()); } @@ -977,6 +959,14 @@ void SplitCoinStakeOutput(CBlock &blocknew, int64_t &nReward, bool &fEnableStake // 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. + // + // 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 (nReward * allocation < CENT) { LogPrintf("WARN: SplitCoinStakeOutput: distribution %f too small to address %s.", @@ -1034,7 +1024,7 @@ void SplitCoinStakeOutput(CBlock &blocknew, int64_t &nReward, bool &fEnableStake 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) + 1); + 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%. diff --git a/src/validation.cpp b/src/validation.cpp index cc32414e86..bf9ec042f4 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -844,14 +844,21 @@ 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 script public key for the base coinstake so we can reuse. + 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. + // 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 (total_owed_to_staker * iter->get()->GetAllocation() < CENT) { + if (total_owed_to_staker * iter->get()->GetAllocation() < CENT + || iter->get()->GetDestination() == coinstake_destination) { iter = mandatory_sidestakes.erase(iter); } else { ++iter; @@ -885,11 +892,11 @@ class ClaimValidator // This should not happen, but include the check for thoroughness. if (validated_mandatory_sidestakes > GetMandatorySideStakeOutputLimit(m_block.nVersion)) { - 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)); + 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)); } } @@ -914,13 +921,13 @@ class ClaimValidator // the minimum of GetMandatorySideStakeOutputLimit and mandatory_sidestakes. if (validated_mandatory_sidestakes < std::min(GetMandatorySideStakeOutputLimit(m_block.nVersion), mandatory_sidestakes.size())) { - 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())); + 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. From 059c20a6a50c694fde6dd9989f83f03f8cd9c796 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Sat, 30 Dec 2023 13:07:24 -0500 Subject: [PATCH 34/47] Adjust mandatory sidestake validation comments --- src/validation.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/validation.cpp b/src/validation.cpp index bf9ec042f4..26b4bd44e4 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -844,7 +844,7 @@ 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 script public key for the base coinstake so we can reuse. + // Record the base coinstake destination. CTxDestination coinstake_destination; ExtractDestination(m_block.vtx[1].vout[1].scriptPubKey, coinstake_destination); @@ -867,6 +867,7 @@ class ClaimValidator 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; From c984d22526604d1449d3abefa44d1bdf7f0c1c5e Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Mon, 1 Jan 2024 13:22:56 -0500 Subject: [PATCH 35/47] Minor sidestake comment cleanups --- src/gridcoin/sidestake.h | 88 ++++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 39 deletions(-) diff --git a/src/gridcoin/sidestake.h b/src/gridcoin/sidestake.h index b28cf123fa..f5ba1ba474 100644 --- a/src/gridcoin/sidestake.h +++ b/src/gridcoin/sidestake.h @@ -15,32 +15,6 @@ namespace GRC { -/* -//! -//! \brief The CBitcoinAddressForStorage class. This is a very small extension of the CBitcoinAddress class that -//! provides serialization/deserialization. -//! -class CBitcoinAddressForStorage : public CBitcoinAddress -{ -public: - CBitcoinAddressForStorage(); - - CBitcoinAddressForStorage(CBitcoinAddress address); - - ADD_SERIALIZE_METHODS; - - template - inline void SerializationOp(Stream& s, Operation ser_action) - { - // Note that (de)serializing the raw underlying vector char data for the address is safe here - // because this is only used in this module and validations were performed before serialization into - // storage. - READWRITE(nVersion); - READWRITE(vchData); - } -}; -*/ - //! //! \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 @@ -324,7 +298,7 @@ class SideStake OUT_OF_BOUND }; - enum FilterFlag : uint8_t { + enum FilterFlag : uint8_t { NONE = 0b00, LOCAL = 0b01, MANDATORY = 0b10, @@ -342,22 +316,59 @@ class SideStake 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 double between 0.0 and 1.0 inclusive representing the allocation fraction of the sidestake + //! double 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 mandatory sidestake +//! \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; @@ -509,7 +520,8 @@ class SideStakePayload : public IContractPayload }; // SideStakePayload //! -//! \brief Stores and manages sidestake entries. +//! \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 { @@ -527,9 +539,7 @@ class SideStakeRegistry : public IContractHandler }; //! - //! \brief The type that keys local 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. + //! \brief The type that keys local sidestake entries by their destinations. //! typedef std::map LocalSideStakeMap; @@ -565,8 +575,9 @@ class SideStakeRegistry : public IContractHandler //! \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 sidestakes - //! returned do not total an allocation greater than 1.0. + //! 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 //! @@ -581,7 +592,7 @@ class SideStakeRegistry : public IContractHandler //! \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, unless local only boolean is set true. + //! are returned, mandatory entry first, depending on the filter set. //! std::vector Try(const CTxDestination& key, const SideStake::FilterFlag& filter) const; @@ -592,8 +603,7 @@ class SideStakeRegistry : public IContractHandler //! \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, unless local only boolean - //! is set true. + //! 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; @@ -773,9 +783,9 @@ class SideStakeRegistry : public IContractHandler 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; + 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. + bool m_local_entry_already_saved_to_config = false; //!< Flag to prevent reload on signal if individual entry saved already. public: From a82e9cf8f33adbb3c13a35a8b474fb72662b76c0 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Mon, 1 Jan 2024 13:49:11 -0500 Subject: [PATCH 36/47] Add missing SideStake specializations for ContractToJson --- src/rpc/rawtransaction.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index c47e0debc7..16bd776f8d 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); + 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; From 6181ff5a3a4cbc146dea8a0aa80ebf5e1aad33b2 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Wed, 3 Jan 2024 21:21:15 -0500 Subject: [PATCH 37/47] Correct for segfault in variant access - use get_if non-throw --- src/qt/sidestaketablemodel.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/qt/sidestaketablemodel.cpp b/src/qt/sidestaketablemodel.cpp index 0f1286690f..884a6f6f29 100644 --- a/src/qt/sidestaketablemodel.cpp +++ b/src/qt/sidestaketablemodel.cpp @@ -334,8 +334,11 @@ Qt::ItemFlags SideStakeTableModel::flags(const QModelIndex &index) const Qt::ItemFlags retval = Qt::ItemIsSelectable | Qt::ItemIsEnabled; - if (!rec->IsMandatory() - && std::get(rec->GetStatus()) == GRC::LocalSideStake::LocalSideStakeStatus::ACTIVE + GRC::SideStake::Status status = rec->GetStatus(); + GRC::LocalSideStake::Status* local_status_ptr = std::get_if(&status); + + if (!rec->IsMandatory() && local_status_ptr + && *local_status_ptr == GRC::LocalSideStake::LocalSideStakeStatus::ACTIVE && (index.column() == Allocation || index.column() == Description)) { retval |= Qt::ItemIsEditable; } From c60aba5e642f6c1206b694175873d668db37668e Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Sat, 20 Jan 2024 16:45:12 -0500 Subject: [PATCH 38/47] Flesh out Fraction class This is to support fraction based arithmetic. Currently this is used in sidestake allocations as a replacement for the double type calculations to eliminate consensus problems in mandatory sidestakes due to floating point arithmetic. --- src/util.h | 430 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 420 insertions(+), 10 deletions(-) diff --git a/src/util.h b/src/util.h index 43480785ee..48c24c48ea 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,326 @@ 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; + } + + 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); + } + + // Otherwise do the full pattern of getting a common denominator and adding, then simplify... + return Fraction(overflow_add(overflow_mult(slhs.GetNumerator(), srhs.GetDenominator()), + overflow_mult(slhs.GetDenominator(), srhs.GetNumerator())), + overflow_mult(slhs.GetDenominator(), srhs.GetDenominator()), + 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); + + return Fraction(overflow_mult(slhs.GetNumerator(), srhs.GetNumerator()), + overflow_mult(slhs.GetDenominator(), srhs.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 + { + // Log2 is O(1) both time and space-wise and so is the best choice here. + return (static_cast(floor(log2(std::abs(n))))); + } + + 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) From 60a6a1a7c9d1b6bb5d2f9d0f0f53837322ca9276 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Sun, 21 Jan 2024 03:52:49 -0500 Subject: [PATCH 39/47] Add unit tests for Fraction class to util_tests --- src/test/util_tests.cpp | 452 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 452 insertions(+) diff --git a/src/test/util_tests.cpp b/src/test/util_tests.cpp index c7da69d74a..35b596667f 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. @@ -1024,4 +1025,455 @@ 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(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_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_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 << 31, 1); + Fraction rhs((int64_t) 1 << 32, 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_2) +{ + Fraction lhs((int64_t) 1 << 32, 1); + Fraction rhs((int64_t) 1 << 32, 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_SUITE_END() From ce9a37667871f37d8417b39b01bf37734d407b11 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Sat, 20 Jan 2024 22:50:11 -0500 Subject: [PATCH 40/47] Modify sidestake code to use new Allocation class instead of double Remove error banding in mandatory sidestake validation. --- src/chainparams.cpp | 4 +- src/consensus/params.h | 4 +- src/gridcoin/sidestake.cpp | 146 ++++++++++++++++++++------------- src/gridcoin/sidestake.h | 92 +++++++++++++++------ src/miner.cpp | 22 ++--- src/qt/sidestaketablemodel.cpp | 130 +++++++++++++---------------- src/rpc/mining.cpp | 2 +- src/rpc/rawtransaction.cpp | 2 +- src/validation.cpp | 18 ++-- 9 files changed, 241 insertions(+), 179 deletions(-) diff --git a/src/chainparams.cpp b/src/chainparams.cpp index f69ff806ce..fa9e268052 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -75,7 +75,7 @@ class CMainParams : public CChainParams { // 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 = 0.25; + 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; @@ -190,7 +190,7 @@ class CTestNetParams : public CChainParams { // 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 = 0.25; + 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 10f5ab230f..b4e5e1a82e 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -48,9 +48,9 @@ struct Params { */ int64_t MRCZeroPaymentInterval; /** - * @brief The maximum allocation (as a floating point) that can be used by all of the mandatory sidestakes + * @brief The maximum allocation (as a Fraction) that can be used by all of the mandatory sidestakes */ - double MaxMandatorySideStakeTotalAlloc; + Fraction MaxMandatorySideStakeTotalAlloc; int64_t StandardContractReplayLookback; diff --git a/src/gridcoin/sidestake.cpp b/src/gridcoin/sidestake.cpp index a9c285b02f..b922c3ffb7 100644 --- a/src/gridcoin/sidestake.cpp +++ b/src/gridcoin/sidestake.cpp @@ -33,18 +33,30 @@ SideStakeRegistry& GRC::GetSideStakeRegistry() return g_sidestake_entries; } -/* // ----------------------------------------------------------------------------- -// Class: CBitcoinAddressForStorage +// Class: Allocation // ----------------------------------------------------------------------------- -CBitcoinAddressForStorage::CBitcoinAddressForStorage() - : CBitcoinAddress() +Allocation::Allocation() + : Fraction() {} -CBitcoinAddressForStorage::CBitcoinAddressForStorage(CBitcoinAddress address) - : CBitcoinAddress(address) +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 @@ -56,7 +68,7 @@ LocalSideStake::LocalSideStake() , m_status(LocalSideStakeStatus::UNKNOWN) {} -LocalSideStake::LocalSideStake(CTxDestination destination, double allocation, std::string description) +LocalSideStake::LocalSideStake(CTxDestination destination, Allocation allocation, std::string description) : m_destination(destination) , m_allocation(allocation) , m_description(description) @@ -64,7 +76,7 @@ LocalSideStake::LocalSideStake(CTxDestination destination, double allocation, st {} LocalSideStake::LocalSideStake(CTxDestination destination, - double allocation, + Allocation allocation, std::string description, LocalSideStakeStatus status) : m_destination(destination) @@ -75,7 +87,7 @@ LocalSideStake::LocalSideStake(CTxDestination destination, bool LocalSideStake::WellFormed() const { - return CBitcoinAddress(m_destination).IsValid() && m_allocation >= 0.0 && m_allocation <= 1.0; + return CBitcoinAddress(m_destination).IsValid() && m_allocation >= 0 && m_allocation <= 1; } std::string LocalSideStake::StatusToString() const @@ -141,7 +153,7 @@ MandatorySideStake::MandatorySideStake() , m_status(MandatorySideStakeStatus::UNKNOWN) {} -MandatorySideStake::MandatorySideStake(CTxDestination destination, double allocation, std::string description) +MandatorySideStake::MandatorySideStake(CTxDestination destination, Allocation allocation, std::string description) : m_destination(destination) , m_allocation(allocation) , m_description(description) @@ -152,7 +164,7 @@ MandatorySideStake::MandatorySideStake(CTxDestination destination, double alloca {} MandatorySideStake::MandatorySideStake(CTxDestination destination, - double allocation, + Allocation allocation, std::string description, int64_t timestamp, uint256 hash, @@ -168,7 +180,7 @@ MandatorySideStake::MandatorySideStake(CTxDestination destination, bool MandatorySideStake::WellFormed() const { - return CBitcoinAddress(m_destination).IsValid() && m_allocation >= 0.0 && m_allocation <= 1.0; + return CBitcoinAddress(m_destination).IsValid() && m_allocation >= 0 && m_allocation <= 1; } CTxDestination MandatorySideStake::Key() const @@ -262,51 +274,67 @@ bool SideStake::IsMandatory() const CTxDestination SideStake::GetDestination() const { - if (IsMandatory()) { + if (m_type == Type::MANDATORY && m_mandatory_sidestake_ptr != nullptr) { return m_mandatory_sidestake_ptr->m_destination; - } else { + } else if (m_type == Type::LOCAL && m_local_sidestake_ptr != nullptr) { return m_local_sidestake_ptr->m_destination; } + + return CNoDestination(); } -double SideStake::GetAllocation() const +Allocation SideStake::GetAllocation() const { - if (IsMandatory()) { + if (m_type == Type::MANDATORY && m_mandatory_sidestake_ptr != nullptr) { return m_mandatory_sidestake_ptr->m_allocation; - } else { + } 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 (IsMandatory()) { + if (m_type == Type::MANDATORY && m_mandatory_sidestake_ptr != nullptr) { return m_mandatory_sidestake_ptr->m_description; - } else { + } 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 { - Status status; + // For trivial initializer case + if (m_mandatory_sidestake_ptr == nullptr && m_local_sidestake_ptr == nullptr) { + return {}; + } - if (IsMandatory()) { - status = m_mandatory_sidestake_ptr->m_status; - } else { - status = m_local_sidestake_ptr->m_status; + 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 status; + return {}; } std::string SideStake::StatusToString() const { - if (IsMandatory()) { + // 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 { + } else if (m_type == Type::LOCAL && m_local_sidestake_ptr != nullptr){ return m_local_sidestake_ptr->StatusToString(); } + + return std::string {}; } // ----------------------------------------------------------------------------- @@ -323,7 +351,7 @@ SideStakePayload::SideStakePayload(uint32_t version) SideStakePayload::SideStakePayload(const uint32_t version, CTxDestination destination, - double allocation, + Allocation allocation, std::string description, MandatorySideStake::MandatorySideStakeStatus status) : IContractPayload() @@ -368,7 +396,7 @@ const std::vector SideStakeRegistry::ActiveSideStakeEntries(const const bool& include_zero_alloc) const { std::vector sidestakes; - double allocation_sum = 0.0; + 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. @@ -382,7 +410,7 @@ const std::vector SideStakeRegistry::ActiveSideStakeEntries(const { 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.0) || entry.second->m_allocation > 0.0) { + 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; } @@ -400,8 +428,8 @@ const std::vector SideStakeRegistry::ActiveSideStakeEntries(const for (const auto& entry : m_local_sidestake_entries) { if (entry.second->m_status == LocalSideStake::LocalSideStakeStatus::ACTIVE - && allocation_sum + entry.second->m_allocation <= 1.0) { - if ((include_zero_alloc && entry.second->m_allocation == 0.0) || entry.second->m_allocation > 0.0) { + && 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; } @@ -516,7 +544,7 @@ void SideStakeRegistry::AddDelete(const ContractContext& ctx) ctx->m_version, payload.m_version, CBitcoinAddress(payload.m_entry.m_destination).ToString(), - payload.m_entry.m_allocation, + payload.m_entry.m_allocation.ToPercent(), payload.m_entry.m_timestamp, payload.m_entry.m_hash.ToString(), payload.m_entry.m_previous_hash.ToString(), @@ -532,7 +560,7 @@ void SideStakeRegistry::AddDelete(const ContractContext& ctx) "of the wallet to ensure multiple contracts in the same block get stored/replayed.", __func__, CBitcoinAddress(historical.m_destination).ToString(), - historical.m_allocation, + historical.m_allocation.ToPercent(), historical.m_hash.GetHex()); } @@ -556,7 +584,7 @@ void SideStakeRegistry::NonContractAdd(const LocalSideStake& sidestake, const bo { LOCK(cs_lock); - // Using this form of insert because we want the latest record with the same key to override any previous one. + // 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) { @@ -661,7 +689,7 @@ bool SideStakeRegistry::Validate(const Contract& contract, const CTransaction& t return false; } - double allocation = payload->m_entry.m_allocation; + 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 @@ -747,7 +775,7 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() std::vector vLocalSideStakes; std::vector> raw_vSideStakeAlloc; - double dSumAllocation = 0.0; + 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). @@ -808,17 +836,15 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() } // First, add the allocation already taken by mandatory sidestakes, because they must be allocated first. - dSumAllocation += GetMandatoryAllocationsTotal(); + sum_allocation += GetMandatoryAllocationsTotal(); LOCK(cs_lock); for (const auto& entry : raw_vSideStakeAlloc) { - std::string sAddress; - - double dAllocation = 0.0; - - sAddress = std::get<0>(entry); + 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()) @@ -827,15 +853,21 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() continue; } - if (!ParseDouble(std::get<1>(entry), &dAllocation)) + double read_allocation = 0.0; + if (!ParseDouble(sAllocation, &read_allocation)) { - LogPrintf("WARN: %s: Invalid allocation %s provided. Skipping allocation.", __func__, std::get<1>(entry)); + LogPrintf("WARN: %s: Invalid allocation %s provided. Skipping allocation.", __func__, sAllocation); continue; } - dAllocation /= 100.0; + 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 (dAllocation < 0) + if (allocation < 0) { LogPrintf("WARN: %s: Negative allocation provided. Skipping allocation.", __func__); continue; @@ -846,16 +878,16 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() // 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) + sum_allocation += allocation; + if (sum_allocation > 1) { LogPrintf("WARN: %s: allocation percentage over 100 percent, ending sidestake allocations.", __func__); break; } LocalSideStake sidestake(address.Get(), - dAllocation, - std::get<2>(entry), + allocation, + sDescription, LocalSideStake::LocalSideStakeStatus::ACTIVE); // This will add or update (replace) a non-contract entry in the registry for the local sidestake. @@ -866,7 +898,7 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() vLocalSideStakes.push_back(sidestake); LogPrint(BCLog::LogFlags::MINER, "INFO: %s: SideStakeAlloc Address %s, Allocation %f", - __func__, sAddress, dAllocation); + __func__, sAddress, allocation.ToPercent()); } for (auto& entry : m_local_sidestake_entries) @@ -885,7 +917,7 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() // 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) + if (!sum_allocation) LogPrintf("WARN: %s: enablesidestaking was set in config but nothing has been allocated for" " distribution!", __func__); } @@ -911,7 +943,7 @@ bool SideStakeRegistry::SaveLocalSideStakesToConfig() } addresses += separator + CBitcoinAddress(iter.second->m_destination).ToString(); - allocations += separator + ToString(iter.second->m_allocation * 100.0); + allocations += separator + ToString(iter.second->m_allocation.ToPercent()); descriptions += separator + iter.second->m_description; ++i; @@ -928,10 +960,10 @@ bool SideStakeRegistry::SaveLocalSideStakesToConfig() return status; } -double SideStakeRegistry::GetMandatoryAllocationsTotal() const +Allocation SideStakeRegistry::GetMandatoryAllocationsTotal() const { std::vector sidestakes = ActiveSideStakeEntries(SideStake::FilterFlag::MANDATORY, false); - double allocation_total = 0.0; + Allocation allocation_total; for (const auto& entry : sidestakes) { allocation_total += entry->GetAllocation(); diff --git a/src/gridcoin/sidestake.h b/src/gridcoin/sidestake.h index f5ba1ba474..d27c91e19d 100644 --- a/src/gridcoin/sidestake.h +++ b/src/gridcoin/sidestake.h @@ -15,6 +15,50 @@ 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 @@ -40,13 +84,13 @@ class LocalSideStake //! using Status = EnumByte; - CTxDestination m_destination; //!< The destination of the sidestake. + CTxDestination m_destination; //!< The destination of the sidestake. - double m_allocation; //!< The allocation is a double precision floating point between 0.0 and 1.0 inclusive + 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) + 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. + Status m_status; //!< The status of the sidestake. It is of type int instead of enum for serialization. //! @@ -62,7 +106,7 @@ class LocalSideStake //! \param allocation //! \param description (optional) //! - LocalSideStake(CTxDestination destination, double allocation, std::string description); + LocalSideStake(CTxDestination destination, Allocation allocation, std::string description); //! //! \brief Initialize a sidestake instance with the provided parameters. @@ -72,7 +116,7 @@ class LocalSideStake //! \param description (optional) //! \param status //! - LocalSideStake(CTxDestination destination, double allocation, std::string description, LocalSideStakeStatus status); + LocalSideStake(CTxDestination destination, Allocation allocation, std::string description, LocalSideStakeStatus status); //! //! \brief Determine whether a sidestake contains each of the required elements. @@ -156,19 +200,19 @@ class MandatorySideStake //! using Status = EnumByte; - CTxDestination m_destination; //!< The destination of the sidestake. + CTxDestination m_destination; //!< The destination of the sidestake. - double m_allocation; //!< The allocation is a double precision floating point between 0.0 and 1.0 inclusive + 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) + std::string m_description; //!< The description of the sidestake (optional) - int64_t m_timestamp; //!< Time of the sidestake contract transaction. + 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_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. + 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 int instead of enum for serialization. + 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. @@ -183,7 +227,7 @@ class MandatorySideStake //! \param allocation //! \param description (optional) //! - MandatorySideStake(CTxDestination destination, double allocation, std::string description); + MandatorySideStake(CTxDestination destination, Allocation allocation, std::string description); //! //! \brief Initialize a sidestake instance with the provided parameters. @@ -193,7 +237,7 @@ class MandatorySideStake //! \param description (optional) //! \param status //! - MandatorySideStake(CTxDestination destination, double allocation, std::string description, MandatorySideStakeStatus 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 @@ -206,7 +250,7 @@ class MandatorySideStake //! \param hash //! \param status //! - MandatorySideStake(CTxDestination destination, double allocation, std::string description, int64_t timestamp, + MandatorySideStake(CTxDestination destination, Allocation allocation, std::string description, int64_t timestamp, uint256 hash, MandatorySideStakeStatus status); //! @@ -329,9 +373,9 @@ class SideStake CTxDestination GetDestination() const; //! //! \brief Gets the allocation of the sidestake - //! \return A double between 0.0 and 1.0 inclusive representing the allocation fraction of the sidestake + //! \return A Fraction representing the allocation fraction of the sidestake. //! - double GetAllocation() const; + Allocation GetAllocation() const; //! //! \brief Gets the description of the sidestake //! \return The description string of the sidestake @@ -412,7 +456,7 @@ class SideStakePayload : public IContractPayload //! \param description. Description string for the sidstake entry //! \param status. Status of the sidestake entry //! - SideStakePayload(const uint32_t version, CTxDestination destination, double allocation, + SideStakePayload(const uint32_t version, CTxDestination destination, Allocation allocation, std::string description, MandatorySideStake::MandatorySideStakeStatus status); //! @@ -468,7 +512,7 @@ class SideStakePayload : public IContractPayload __func__, valid, CBitcoinAddress(m_entry.m_destination).ToString(), - m_entry.m_allocation, + m_entry.m_allocation.ToPercent(), m_entry.StatusToString() ); @@ -491,7 +535,7 @@ class SideStakePayload : public IContractPayload //! std::string LegacyValueString() const override { - return ToString(m_entry.m_allocation); + return ToString(m_entry.m_allocation.ToDouble()); } //! @@ -771,10 +815,10 @@ class SideStakeRegistry : public IContractHandler bool SaveLocalSideStakesToConfig(); //! - //! \brief Provides the total allocation for all active mandatory sidestakes as a floating point fraction. - //! \return total active mandatory sidestake allocation as a double. + //! \brief Provides the total allocation for all active mandatory sidestakes as a Fraction. + //! \return total active mandatory sidestake allocation as a Fraction. //! - double GetMandatoryAllocationsTotal() const; + Allocation GetMandatoryAllocationsTotal() const; void SubscribeToCoreSignals(); void UnsubscribeFromCoreSignals(); diff --git a/src/miner.cpp b/src/miner.cpp index 257a7a2931..1c45764078 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -205,7 +205,7 @@ bool CreateMRCRewards(CBlock &blocknew, std::mapget()->GetDestination()); - double allocation = iterSideStake->get()->GetAllocation(); + GRC::Allocation allocation = iterSideStake->get()->GetAllocation(); if (!address.IsValid()) { @@ -967,16 +967,16 @@ void SplitCoinStakeOutput(CBlock &blocknew, int64_t &nReward, bool &fEnableStake // 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 (nReward * allocation < CENT) + if (allocation * nReward < CENT) { LogPrintf("WARN: SplitCoinStakeOutput: distribution %f too small to address %s.", - CoinToDouble(nReward * allocation), + CoinToDouble(static_cast(allocation * nReward).ToCAmount()), address.ToString() ); continue; } - if (dSumAllocation + allocation > 1.0) + if (SumAllocation + allocation > 1) { LogPrintf("WARN: SplitCoinStakeOutput: allocation percentage over 100 percent, " "ending sidestake allocations."); @@ -999,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 + allocation < 1.0) - nSideStake = nReward * allocation; + 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 + allocation == 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; @@ -1012,10 +1012,10 @@ void SplitCoinStakeOutput(CBlock &blocknew, int64_t &nReward, bool &fEnableStake LogPrintf("SplitCoinStakeOutput: create sidestake UTXO %i value %f to address %s", nOutputsUsed, - CoinToDouble(nReward * allocation), + CoinToDouble(static_cast(allocation * nReward).ToCAmount()), address.ToString() ); - dSumAllocation += allocation; + SumAllocation += allocation; nRemainingStakeOutputValue -= nSideStake; nOutputsUsed++; } diff --git a/src/qt/sidestaketablemodel.cpp b/src/qt/sidestaketablemodel.cpp index 884a6f6f29..1b7af78559 100644 --- a/src/qt/sidestaketablemodel.cpp +++ b/src/qt/sidestaketablemodel.cpp @@ -151,18 +151,29 @@ QVariant SideStakeTableModel::data(const QModelIndex &index, int role) const GRC::SideStake* rec = static_cast(index.internalPointer()); const auto column = static_cast(index.column()); - if (role == Qt::DisplayRole || role == Qt::EditRole) { + if (role == Qt::DisplayRole) { switch (column) { case Address: return QString::fromStdString(CBitcoinAddress(rec->GetDestination()).ToString()); case Allocation: - return rec->GetAllocation() * 100.0; + 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: @@ -201,58 +212,35 @@ bool SideStakeTableModel::setData(const QModelIndex &index, const QVariant &valu { case Address: { - CBitcoinAddress address; - address.SetString(value.toString().toStdString()); - - if (CBitcoinAddress(rec->GetDestination()) == address) { - m_edit_status = NO_CHANGES; - return false; - } else if (!address.IsValid()) { - m_edit_status = INVALID_ADDRESS; - return false; - } - - std::vector sidestakes = registry.Try(address.Get(), GRC::SideStake::FilterFlag::LOCAL); - - if (!sidestakes.empty()) { - m_edit_status = DUPLICATE_ADDRESS; - return false; - } - - // There is no valid state change left for address. If you are editing the item, the address field is - // not editable, so will be NO_CHANGES. For a non-matching address, it will be covered by the dialog - // in New mode. - break; + // The address of a sidestake entry is not editable. + return false; } case Allocation: { - double prior_total_allocation = 0.0; + GRC::Allocation prior_total_allocation; // Save the original local sidestake (also in the core). GRC::SideStake orig_sidestake = *rec; - CTxDestination orig_destination = rec->GetDestination(); - double orig_allocation = rec->GetAllocation(); - std::string orig_description = rec->GetDescription(); - GRC::SideStake::Status orig_status = rec->GetStatus(); + 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(); - double allocation = entry->GetAllocation(); + GRC::Allocation allocation = entry->GetAllocation(); - if (destination == orig_destination) { + if (destination == orig_sidestake.GetDestination()) { continue; } - prior_total_allocation += allocation * 100.0; + prior_total_allocation += allocation; } - if (orig_allocation * 100.0 == value.toDouble()) { - m_edit_status = NO_CHANGES; - return false; - } + GRC::Allocation modified_allocation(value.toDouble() / 100.0); - if (value.toDouble() < 0.0 || prior_total_allocation + value.toDouble() > 100.0) { + if (modified_allocation < 0 || prior_total_allocation + modified_allocation > 1) { m_edit_status = INVALID_ALLOCATION; LogPrint(BCLog::LogFlags::VERBOSE, "INFO: %s: m_edit_status = %i", @@ -262,14 +250,12 @@ bool SideStakeTableModel::setData(const QModelIndex &index, const QVariant &valu return false; } - // Delete the original sidestake - registry.NonContractDelete(orig_destination, false); - - // Add back the sidestake with the modified allocation - registry.NonContractAdd(GRC::LocalSideStake(orig_destination, - value.toDouble() / 100.0, - orig_description, - std::get(orig_status).Value()), true); + // 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; } @@ -291,14 +277,12 @@ bool SideStakeTableModel::setData(const QModelIndex &index, const QVariant &valu // Save the original local sidestake (also in the core). GRC::SideStake orig_sidestake = *rec; - // Delete the original sidestake - registry.NonContractDelete(orig_sidestake.GetDestination(), false); - - // Add back the sidestake with the modified allocation + // 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); + std::get(orig_sidestake.GetStatus()).Value()), + true); break; } @@ -334,12 +318,7 @@ Qt::ItemFlags SideStakeTableModel::flags(const QModelIndex &index) const Qt::ItemFlags retval = Qt::ItemIsSelectable | Qt::ItemIsEnabled; - GRC::SideStake::Status status = rec->GetStatus(); - GRC::LocalSideStake::Status* local_status_ptr = std::get_if(&status); - - if (!rec->IsMandatory() && local_status_ptr - && *local_status_ptr == GRC::LocalSideStake::LocalSideStakeStatus::ACTIVE - && (index.column() == Allocation || index.column() == Description)) { + if (!rec->IsMandatory() && (index.column() == Allocation || index.column() == Description)) { retval |= Qt::ItemIsEditable; } @@ -363,8 +342,6 @@ QString SideStakeTableModel::addRow(const QString &address, const QString &alloc CBitcoinAddress sidestake_address; sidestake_address.SetString(address.toStdString()); - double sidestake_allocation = 0.0; - m_edit_status = OK; if (!sidestake_address.IsValid()) { @@ -376,27 +353,36 @@ QString SideStakeTableModel::addRow(const QString &address, const QString &alloc // UI model. std::vector core_local_sidestake = registry.Try(sidestake_address.Get(), GRC::SideStake::FilterFlag::LOCAL); - double prior_total_allocation = 0.0; - - // 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() * 100.0; - } - if (!core_local_sidestake.empty()) { m_edit_status = DUPLICATE_ADDRESS; return QString(); } - // 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 100. - if (!ParseDouble(allocation.toStdString(), &sidestake_allocation) - || sidestake_allocation < 0.0 || prior_total_allocation + sidestake_allocation > 100.0) { - m_edit_status = INVALID_ALLOCATION; - return QString(); + GRC::Allocation prior_total_allocation; + GRC::Allocation sidestake_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(); } - sidestake_allocation /= 100.0; + // 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 100%. + double read_allocation = 0.0; + + if (!ParseDouble(allocation.toStdString(), &read_allocation)) { + if (read_allocation < 0.0) { + m_edit_status = INVALID_ALLOCATION; + return QString(); + } + + sidestake_allocation += GRC::Allocation(read_allocation / 100.0); + + if (prior_total_allocation + sidestake_allocation > 1) { + m_edit_status = INVALID_ALLOCATION; + return QString(); + } + } std::string sidestake_description = description.toStdString(); std::string sanitized_description = SanitizeString(sidestake_description, SAFE_CHARS_CSV); diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index 2d923c69bd..69eb2eaae9 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -118,7 +118,7 @@ UniValue getstakinginfo(const UniValue& params, bool fHelp) for (const auto& alloc : vSideStakeAlloc) { sidestakingalloc.pushKV("address", CBitcoinAddress(alloc->GetDestination()).ToString()); - sidestakingalloc.pushKV("allocation_pct", alloc->GetAllocation() * 100); + sidestakingalloc.pushKV("allocation_pct", alloc->GetAllocation().ToPercent()); sidestakingalloc.pushKV("status", alloc->StatusToString()); vsidestakingalloc.push_back(sidestakingalloc); diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 16bd776f8d..d3688e459b 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -253,7 +253,7 @@ UniValue SideStakePayloadToJson (const GRC::ContractPayload& payload) UniValue out(UniValue::VOBJ); out.pushKV("address", CBitcoinAddress(sidestake.m_entry.m_destination).ToString()); - out.pushKV("allocation", sidestake.m_entry.m_allocation); + out.pushKV("allocation", sidestake.m_entry.m_allocation.ToPercent()); out.pushKV("description", sidestake.m_entry.m_description); out.pushKV("status", sidestake.m_entry.StatusToString()); diff --git a/src/validation.cpp b/src/validation.cpp index 26b4bd44e4..75abc76caa 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -657,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; } @@ -805,7 +805,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. @@ -857,7 +857,7 @@ class ClaimValidator // 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 (total_owed_to_staker * iter->get()->GetAllocation() < CENT + if (iter->get()->GetAllocation() * total_owed_to_staker < CENT || iter->get()->GetDestination() == coinstake_destination) { iter = mandatory_sidestakes.erase(iter); } else { @@ -875,18 +875,18 @@ class ClaimValidator return error("%s: FAILED: coinstake output has invalid destination."); } - double computed_output_alloc = (double) coinstake.vout[i].nValue / (double) total_owed_to_staker; + GRC::Allocation computed_output_alloc(Fraction(coinstake.vout[i].nValue, total_owed_to_staker, true)); 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 output amount expressed as a double fraction of the awards owed to staker is within 1% - // of the required mandatory allocation. We allow some leeway here because the sidestake allocations - // are double precision floating point fractions. + // 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() - && abs(computed_output_alloc - mandatory_sidestake[0]->GetAllocation()) < 0.01) { + && computed_output_alloc >= mandatory_sidestake[0]->GetAllocation()) { ++validated_mandatory_sidestakes; } From aeb62a367974e2cd621195539813cf95ac4c78d1 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Mon, 22 Jan 2024 11:25:34 -0500 Subject: [PATCH 41/47] Change data input to edit sidestake dialog to Qt::EditRole This suppresses the percent sign for editing and prevents a silent rejection due to inclusion of the percent sign in the saved field. --- src/qt/editsidestakedialog.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/qt/editsidestakedialog.cpp b/src/qt/editsidestakedialog.cpp index 2b8bc3f46f..01cb269f53 100644 --- a/src/qt/editsidestakedialog.cpp +++ b/src/qt/editsidestakedialog.cpp @@ -59,10 +59,10 @@ void EditSideStakeDialog::loadRow(int row) { m_row = row; - ui->addressLineEdit->setText(model->index(row, SideStakeTableModel::Address, QModelIndex()).data().toString()); - ui->allocationLineEdit->setText(model->index(row, SideStakeTableModel::Allocation, QModelIndex()).data().toString()); - ui->descriptionLineEdit->setText(model->index(row, SideStakeTableModel::Description, QModelIndex()).data().toString()); - ui->statusLineEdit->setText(model->index(row, SideStakeTableModel::Status, QModelIndex()).data().toString()); + 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() From c6b2f1df7ce2f91caf9fadbc6e6f199354d94e56 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Tue, 23 Jan 2024 16:11:45 -0500 Subject: [PATCH 42/47] Initial implementation of sidestake unit tests --- src/Makefile.test.include | 1 + src/test/CMakeLists.txt | 1 + src/test/gridcoin/sidestake_tests.cpp | 92 +++++++++++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 src/test/gridcoin/sidestake_tests.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/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..ac378a905c --- /dev/null +++ b/src/test/gridcoin/sidestake_tests.cpp @@ -0,0 +1,92 @@ +// 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) +{ + 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_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(Fraction(9999, 10000, true)); + + BOOST_CHECK_EQUAL(allocation.GetNumerator(), 9999); + BOOST_CHECK_EQUAL(allocation.GetDenominator(), 10000); + BOOST_CHECK_EQUAL(allocation.IsSimplified(), true); + + CAmount max_accrual = 16384 * COIN; + + CAmount output = static_cast(allocation * max_accrual).ToCAmount(); + + BOOST_CHECK_EQUAL(output, int64_t {1638236160000}); + + GRC::Allocation computed_output_alloc(Fraction(output, max_accrual, true)); + + // This is what the mandatory sidestake validation does at its core. The actual check is >= + // because it is allowed for the output to have an allocation to the destination greater than required. + // This test is for exactness, so it is checking whether it is equal. + BOOST_CHECK(computed_output_alloc == allocation); +} + +BOOST_AUTO_TEST_SUITE_END() From 55efa35c97e09157230f936bc90b008a5fd92f8d Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Wed, 24 Jan 2024 19:09:06 -0500 Subject: [PATCH 43/47] Changes to validator to deal with ToCAmount truncation of remainder --- src/util.h | 5 +++++ src/validation.cpp | 24 ++++++++++++++++++------ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/util.h b/src/util.h index 48c24c48ea..9d58039935 100644 --- a/src/util.h +++ b/src/util.h @@ -335,6 +335,11 @@ class Fraction { return *this; } + std::string ToString() const + { + return strprintf("%" PRId64 "/" "%" PRId64, m_numerator, m_denominator); + } + bool operator!() { return IsZero(); diff --git a/src/validation.cpp b/src/validation.cpp index 75abc76caa..828de51911 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -875,8 +875,6 @@ class ClaimValidator return error("%s: FAILED: coinstake output has invalid destination."); } - GRC::Allocation computed_output_alloc(Fraction(coinstake.vout[i].nValue, total_owed_to_staker, true)); - std::vector mandatory_sidestake = GRC::GetSideStakeRegistry().TryActive(output_destination, GRC::SideStake::FilterFlag::MANDATORY);; @@ -885,10 +883,24 @@ class ClaimValidator // 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() - && computed_output_alloc >= mandatory_sidestake[0]->GetAllocation()) { - - ++validated_mandatory_sidestakes; + 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("%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. From 6f868fa739045f360a00b29030d0e62f545d1c69 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Wed, 24 Jan 2024 21:10:49 -0500 Subject: [PATCH 44/47] Change equality to greater than or equal in validator comparison --- src/validation.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/validation.cpp b/src/validation.cpp index 828de51911..4c127f18d9 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -889,7 +889,7 @@ class ClaimValidator CAmount required_output = static_cast(mandatory_sidestake[0]->GetAllocation() * total_owed_to_staker).ToCAmount(); - if (actual_output == required_output) { + if (actual_output >= required_output) { ++validated_mandatory_sidestakes; } else { From 7badd96082f6c6ad4ba4234792335c49807ab073 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Wed, 24 Jan 2024 23:17:13 -0500 Subject: [PATCH 45/47] Tweaks to Fraction and Sidestake unit tests --- src/test/gridcoin/sidestake_tests.cpp | 77 +++++++++++++++++++++++---- src/test/util_tests.cpp | 8 +++ 2 files changed, 74 insertions(+), 11 deletions(-) diff --git a/src/test/gridcoin/sidestake_tests.cpp b/src/test/gridcoin/sidestake_tests.cpp index ac378a905c..590d2a43f9 100644 --- a/src/test/gridcoin/sidestake_tests.cpp +++ b/src/test/gridcoin/sidestake_tests.cpp @@ -22,6 +22,30 @@ BOOST_AUTO_TEST_CASE(sidestake_Allocation_Initialization_trivial) 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); @@ -35,6 +59,45 @@ BOOST_AUTO_TEST_CASE(sidestake_Allocation_Initialization_from_double) 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)); @@ -69,7 +132,7 @@ BOOST_AUTO_TEST_CASE(sidestake_Allocation_multiplication_and_derivation_of_alloc // 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(Fraction(9999, 10000, true)); + GRC::Allocation allocation(0.9999); BOOST_CHECK_EQUAL(allocation.GetNumerator(), 9999); BOOST_CHECK_EQUAL(allocation.GetDenominator(), 10000); @@ -77,16 +140,8 @@ BOOST_AUTO_TEST_CASE(sidestake_Allocation_multiplication_and_derivation_of_alloc CAmount max_accrual = 16384 * COIN; - CAmount output = static_cast(allocation * max_accrual).ToCAmount(); - - BOOST_CHECK_EQUAL(output, int64_t {1638236160000}); - - GRC::Allocation computed_output_alloc(Fraction(output, max_accrual, true)); - - // This is what the mandatory sidestake validation does at its core. The actual check is >= - // because it is allowed for the output to have an allocation to the destination greater than required. - // This test is for exactness, so it is checking whether it is equal. - BOOST_CHECK(computed_output_alloc == allocation); + 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/util_tests.cpp b/src/test/util_tests.cpp index 35b596667f..76b2ab1f7b 100755 --- a/src/test/util_tests.cpp +++ b/src/test/util_tests.cpp @@ -1476,4 +1476,12 @@ BOOST_AUTO_TEST_CASE(util_Fraction_logic_negation) 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() From 2c21474454e8f1dfea79a0ec462f1ef6e4304815 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Wed, 24 Jan 2024 23:52:19 -0500 Subject: [PATCH 46/47] Introduce error strings in validator to prevent misleading error messages Not all errors are where claimed aomount is greater than calculated. With MRC and mandatory sidestakes, number and destination of outputs are also checked. --- src/validation.cpp | 66 ++++++++++++++++++++++++++++++---------------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/src/validation.cpp b/src/validation.cpp index 4c127f18d9..53268c936a 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -772,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); @@ -780,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__, @@ -829,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__, @@ -893,6 +898,8 @@ class ClaimValidator ++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__, @@ -905,6 +912,8 @@ class ClaimValidator // 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__, @@ -934,6 +943,8 @@ class ClaimValidator // 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, @@ -947,6 +958,8 @@ class ClaimValidator 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__, @@ -961,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__); } @@ -1001,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... // @@ -1012,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", @@ -1040,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 @@ -1200,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(); @@ -1216,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__, @@ -1237,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__, @@ -1253,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", @@ -1273,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. From 83d3f9e1888e9a3576344873e6e26cd907588fd8 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Sat, 27 Jan 2024 22:25:13 -0500 Subject: [PATCH 47/47] Change out msb in Fraction class based on testing --- src/test/util_tests.cpp | 218 +++++++++++++++++++++++++++++++++++++++- src/util.h | 13 ++- 2 files changed, 225 insertions(+), 6 deletions(-) diff --git a/src/test/util_tests.cpp b/src/test/util_tests.cpp index 76b2ab1f7b..7c29dcf892 100755 --- a/src/test/util_tests.cpp +++ b/src/test/util_tests.cpp @@ -15,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) @@ -1025,6 +1077,144 @@ 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; @@ -1333,8 +1523,11 @@ BOOST_AUTO_TEST_CASE(util_Fraction_division_by_zero_int64_t) BOOST_AUTO_TEST_CASE(util_Fraction_multiplication_overflow_1) { - Fraction lhs((int64_t) 1 << 31, 1); - Fraction rhs((int64_t) 1 << 32, 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; @@ -1349,8 +1542,24 @@ BOOST_AUTO_TEST_CASE(util_Fraction_multiplication_overflow_1) BOOST_AUTO_TEST_CASE(util_Fraction_multiplication_overflow_2) { - Fraction lhs((int64_t) 1 << 32, 1); - Fraction rhs((int64_t) 1 << 32, 1); + 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; @@ -1363,6 +1572,7 @@ BOOST_AUTO_TEST_CASE(util_Fraction_multiplication_overflow_2) 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); diff --git a/src/util.h b/src/util.h index 9d58039935..8a300122f0 100644 --- a/src/util.h +++ b/src/util.h @@ -557,8 +557,17 @@ class Fraction { private: int msb(const int64_t& n) const { - // Log2 is O(1) both time and space-wise and so is the best choice here. - return (static_cast(floor(log2(std::abs(n))))); + 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