diff --git a/src/chainparams.cpp b/src/chainparams.cpp index 06bf58b2a2..ce18316528 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -66,6 +66,7 @@ class CMainParams : public CChainParams { consensus.BlockV10Height = 1420000; consensus.BlockV11Height = 2053000; consensus.BlockV12Height = std::numeric_limits::max(); + consensus.PollV3Height = std::numeric_limits::max(); // Immediately post zero payment interval fees 40% for mainnet consensus.InitialMRCFeeFractionPostZeroInterval = Fraction(2, 5); // Zero day interval is 14 days on mainnet @@ -160,6 +161,7 @@ class CTestNetParams : public CChainParams { consensus.BlockV10Height = 629409; consensus.BlockV11Height = 1301500; consensus.BlockV12Height = 1871830; + consensus.PollV3Height = 1944820; // Immediately post zero payment interval fees 40% for testnet, the same as mainnet consensus.InitialMRCFeeFractionPostZeroInterval = Fraction(2, 5); // Zero day interval is 10 minutes on testnet. The very short interval facilitates testing. diff --git a/src/chainparams.h b/src/chainparams.h index 8c150b39e5..41785f43b8 100644 --- a/src/chainparams.h +++ b/src/chainparams.h @@ -150,6 +150,14 @@ inline bool IsV12Enabled(int nHeight) return nHeight >= BlockV12Height; } +inline bool IsPollV3Enabled(int nHeight) +{ + // Temporary override for testing. Cf. Corresponding code in init.cpp + int PollV3Height = gArgs.GetArg("-pollv3height", Params().GetConsensus().PollV3Height); + + return nHeight >= PollV3Height; +} + inline int GetSuperblockAgeSpacing(int nHeight) { return (fTestNet ? 86400 : (nHeight > 364500) ? 86400 : 43200); diff --git a/src/consensus/params.h b/src/consensus/params.h index 3a04f820ec..9ce038db2b 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -33,6 +33,8 @@ struct Params { int BlockV11Height; /** Block height at which v12 blocks are created */ int BlockV12Height; + /** Block height at which poll v3 contract payloads are valid */ + int PollV3Height; /** The fraction of rewards taken as fees in an MRC after the zero payment interval. Only consesnus critical * at BlockV12Height or above. */ diff --git a/src/gridcoin/beacon.cpp b/src/gridcoin/beacon.cpp index 9ed496aecc..43a333f3cc 100644 --- a/src/gridcoin/beacon.cpp +++ b/src/gridcoin/beacon.cpp @@ -817,6 +817,11 @@ bool BeaconRegistry::Validate(const Contract& contract, const CTransaction& tx, return true; } +bool BeaconRegistry::BlockValidate(const ContractContext& ctx, int& DoS) const +{ + return Validate(ctx.m_contract, ctx.m_tx, DoS); +} + void BeaconRegistry::ActivatePending( const std::vector& beacon_ids, const int64_t superblock_time, const uint256& block_hash, const int& height) diff --git a/src/gridcoin/beacon.h b/src/gridcoin/beacon.h index dceca0f0b9..17c84f65bc 100644 --- a/src/gridcoin/beacon.h +++ b/src/gridcoin/beacon.h @@ -619,6 +619,17 @@ class BeaconRegistry : public IContractHandler //! bool Validate(const Contract& contract, const CTransaction& tx, int &DoS) const override; + //! + //! \brief Determine whether a beacon contract is valid including block context. This is used + //! in ConnectBlock. + //! + //! \param ctx ContractContext containing the beacon 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 Register a beacon from contract data. //! diff --git a/src/gridcoin/contract/contract.cpp b/src/gridcoin/contract/contract.cpp index 8fbc32e347..108c768d0c 100644 --- a/src/gridcoin/contract/contract.cpp +++ b/src/gridcoin/contract/contract.cpp @@ -163,6 +163,11 @@ class AppCacheContractHandler : public IContractHandler return true; // No contextual validation needed yet } + bool BlockValidate(const ContractContext& ctx, int& DoS) const override + { + return true; // No contextual validation needed yet + } + void Add(const ContractContext& ctx) override { const auto payload = ctx->SharePayloadAs(); @@ -200,6 +205,11 @@ class UnknownContractHandler : public IContractHandler return true; // No contextual validation needed yet } + bool BlockValidate(const ContractContext& ctx, int& DoS) const override + { + return true; // No contextual validation needed yet + } + //! //! \brief Handle a contract addition. //! @@ -288,6 +298,28 @@ class Dispatcher return GetHandler(contract.m_type.Value()).Validate(contract, tx, DoS); } + //! + //! \brief Perform contextual validation for the provided contract including block context. This is used + //! in ConnectBlock. + //! + //! \param ctx ContractContext to validate. + //! \param DoS Misbehavior score out. + //! + //! \return \c false If the contract fails validation. + //! + bool BlockValidate(const ContractContext& ctx, int& DoS) + { + if (!GetHandler(ctx.m_contract.m_type.Value()).BlockValidate(ctx, DoS)) { + error("%s: Contract of type %s failed validation.", + __func__, + ctx.m_contract.m_type.ToString()); + + return false; + } + + return true; + } + //! //! \brief Revert a previously-applied contract from a transaction message //! by passing it to the appropriate contract handler. @@ -604,6 +636,17 @@ bool GRC::ValidateContracts(const CTransaction& tx, int& DoS) return true; } +bool GRC::BlockValidateContracts(const CBlockIndex* const pindex, const CTransaction& tx, int& DoS) +{ + for (const auto& contract: tx.GetContracts()) { + if (!g_dispatcher.BlockValidate({ contract, tx, pindex }, DoS)) { + return false; + } + } + + return true; +} + void GRC::RevertContracts(const CTransaction& tx, const CBlockIndex* const pindex) { // Reverse the contracts. Reorganize will load any previous versions: @@ -896,7 +939,11 @@ void Contract::Body::ResetType(const ContractType type) m_payload.Reset(new TxMessage()); break; case ContractType::POLL: - m_payload.Reset(new PollPayload()); + // Note that the contract code expects cs_main to already be taken which + // means that the access to nBestHeight is safe. + // TODO: This ternary should be removed at the next mandatory after + // Kermit's Mom. + m_payload.Reset(new PollPayload(IsPollV3Enabled(nBestHeight) ? 3 : 2)); break; case ContractType::PROJECT: m_payload.Reset(new Project()); diff --git a/src/gridcoin/contract/contract.h b/src/gridcoin/contract/contract.h index 2a9c38e519..8005ea73e2 100644 --- a/src/gridcoin/contract/contract.h +++ b/src/gridcoin/contract/contract.h @@ -6,6 +6,7 @@ #define GRIDCOIN_CONTRACT_CONTRACT_H #include "amount.h" +#include "gridcoin/contract/handler.h" #include "gridcoin/contract/payload.h" #include "gridcoin/support/enumbytes.h" #include "serialize.h" @@ -520,6 +521,18 @@ void ApplyContracts( //! bool ValidateContracts(const CTransaction& tx, int& DoS); +//! +//! \brief Perform contextual validation for the contracts in a transaction including block context. This is used +//! in ConnectBlock. +//! +//! \param pindex The CBlockIndex of the block context of the transaction. +//! \param tx The transaction to validate contracts for. +//! \param DoS Misbehavior score out. +//! +//! \return \c false If the contract fails validation. +//! +bool BlockValidateContracts(const CBlockIndex* const pindex, const CTransaction& tx, int& DoS); + //! //! \brief Revert previously-applied contracts from a transaction by passing //! them to the appropriate contract handlers. diff --git a/src/gridcoin/contract/handler.h b/src/gridcoin/contract/handler.h index 01763866b4..7d5e93f4bf 100644 --- a/src/gridcoin/contract/handler.h +++ b/src/gridcoin/contract/handler.h @@ -83,6 +83,17 @@ struct IContractHandler //! virtual bool Validate(const Contract& contract, const CTransaction& tx, int& DoS) const = 0; + //! + //! \brief Perform contextual validation for the provided contract including block context. This is used + //! in ConnectBlock. + //! + //! \param ctx ContractContext to validate. + //! \param DoS Misbehavior score out. + //! + //! \return \c false If the contract fails validation. + //! + virtual bool BlockValidate(const ContractContext& ctx, int& DoS) const = 0; + //! //! \brief Destroy the contract handler state to prepare for historical //! contract replay. diff --git a/src/gridcoin/mrc.cpp b/src/gridcoin/mrc.cpp index d611cccb5a..e93614d80e 100644 --- a/src/gridcoin/mrc.cpp +++ b/src/gridcoin/mrc.cpp @@ -207,6 +207,11 @@ bool GRC::MRCContractHandler::Validate(const Contract& contract, const CTransact return ValidateMRC(contract, tx, DoS); } +bool GRC::MRCContractHandler::BlockValidate(const ContractContext& ctx, int& DoS) const +{ + return Validate(ctx.m_contract, ctx.m_tx, DoS); +} + namespace { //! //! \brief Sign the mrc. diff --git a/src/gridcoin/mrc.h b/src/gridcoin/mrc.h index 3c4ff9015d..b05d528a2d 100644 --- a/src/gridcoin/mrc.h +++ b/src/gridcoin/mrc.h @@ -322,8 +322,28 @@ class MRCContractHandler : public IContractHandler // Reset is a noop for MRC's here. void Reset() override {} + //! + //! \brief Perform contextual validation for the provided contract. + //! + //! \param contract Contract to validate. + //! \param tx Transaction that contains the contract. + //! \param DoS Misbehavior score out. + //! + //! \return \c false If the contract fails validation. + //! bool Validate(const Contract& contract, const CTransaction& tx, int& DoS) const override; + //! + //! \brief Perform contextual validation for the provided contract including block context. This is used + //! in ConnectBlock. + //! + //! \param ctx ContractContext to validate. + //! \param DoS Misbehavior score out. + //! + //! \return \c false If the contract fails validation. + //! + bool BlockValidate(const ContractContext& ctx, int& DoS) const override; + // Add is a noop here, because this is handled at the block level by the staker (in the miner) as with the claim. void Add(const ContractContext& ctx) override {} diff --git a/src/gridcoin/project.h b/src/gridcoin/project.h index 85cf7e1d8a..075303c572 100644 --- a/src/gridcoin/project.h +++ b/src/gridcoin/project.h @@ -306,6 +306,20 @@ class Whitelist : public IContractHandler return true; // No contextual validation needed yet } + //! + //! \brief Perform contextual validation for the provided contract including block context. This is used + //! in ConnectBlock. + //! + //! \param ctx ContractContext to validate. + //! \param DoS Misbehavior score out. + //! + //! \return \c false If the contract fails validation. + //! + bool BlockValidate(const ContractContext& ctx, int& DoS) const override + { + return true; // No contextual validation needed yet + } + //! //! \brief Add a project to the whitelist from contract data. //! diff --git a/src/gridcoin/support/block_finder.cpp b/src/gridcoin/support/block_finder.cpp index 8f69ba38ad..c31d5b06e1 100644 --- a/src/gridcoin/support/block_finder.cpp +++ b/src/gridcoin/support/block_finder.cpp @@ -18,12 +18,14 @@ CBlockIndex* BlockFinder::FindByHeight(int height) if(index != nullptr) { // Traverse towards the tail. - while (index && index->pprev && index->nHeight > height) + while (index && index->pprev && index->nHeight > height) { index = index->pprev; + } // Traverse towards the head. - while (index && index->pnext && index->nHeight < height) + while (index && index->pnext && index->nHeight < height) { index = index->pnext; + } } return index; @@ -38,15 +40,33 @@ CBlockIndex* BlockFinder::FindByMinTime(int64_t time) ? pindexBest : pindexGenesisBlock; - if(index != nullptr) + if (index != nullptr) { // Move back until the previous block is no longer younger than "time". - while(index && index->pprev && index->pprev->nTime > time) + while (index && index->pprev && index->pprev->nTime > time) { index = index->pprev; + } // Move forward until the current block is younger than "time". - while(index && index->pnext && index->nTime < time) + while (index && index->pnext && index->nTime < time) { index = index->pnext; + } + } + + return index; +} + +// The arguments are passed by value on purpose. +CBlockIndex* BlockFinder::FindByMinTimeFromGivenIndex(int64_t time, CBlockIndex* index) +{ + // If no starting index is provided (i.e. second parameter is omitted or nullptr is passed in, + // then start at the Genesis Block. This is in general expensive and should be avoided. + if (!index) { + index = pindexGenesisBlock; + } + + while (index && index->pnext && index->nTime < time) { + index = index->pnext; } return index; diff --git a/src/gridcoin/support/block_finder.h b/src/gridcoin/support/block_finder.h index 5b56805707..9d7ea552ef 100644 --- a/src/gridcoin/support/block_finder.h +++ b/src/gridcoin/support/block_finder.h @@ -5,6 +5,8 @@ #ifndef GRIDCOIN_SUPPORT_BLOCK_FINDER_H #define GRIDCOIN_SUPPORT_BLOCK_FINDER_H +#include + class CBlockIndex; namespace GRC { @@ -38,6 +40,15 @@ class BlockFinder //! head of the chain if it is older than \p time. //! static CBlockIndex* FindByMinTime(int64_t time); + + //! + //! \brief Find block by time going forward from given index. + //! \param time + //! \param CBlockIndex from where to start + //! \return CBlockIndex pointing to the youngest block which is not older than \p time, or + //! the head of the chain if it is older than \p time. + //! + static CBlockIndex* FindByMinTimeFromGivenIndex(int64_t time, CBlockIndex* index = nullptr); }; } // namespace GRC diff --git a/src/gridcoin/voting/builders.cpp b/src/gridcoin/voting/builders.cpp index 25c75a2b6a..bbba0bf1d6 100644 --- a/src/gridcoin/voting/builders.cpp +++ b/src/gridcoin/voting/builders.cpp @@ -307,6 +307,12 @@ class AddressClaimBuilder return std::nullopt; } + LogPrint(BCLog::LogFlags::VOTE, "INFO: %s: building address claim for address_outputs.m_key_id %s, " + "claim.m_public_key address %s.", + __func__, + DestinationToAddressString(address_outputs.m_key_id), + DestinationToAddressString(claim.m_public_key.GetID())); + // An address claim must submit outputs in ascending order. This // improves the performance of duplicate output validation: // @@ -412,6 +418,11 @@ class BalanceClaimBuilder const AddressClaimBuilder builder(m_wallet); for (auto& address_claim : claim.m_address_claims) { + LogPrint(BCLog::LogFlags::VOTE, "INFO: %s: signing claim address %s", + __func__, + DestinationToAddressString(address_claim.m_public_key.GetID()) + ); + if (!builder.SignClaim(address_claim, message)) { return false; } @@ -886,6 +897,20 @@ PollBuilder::PollBuilder(PollBuilder&& builder) = default; PollBuilder::~PollBuilder() = default; PollBuilder& PollBuilder::operator=(PollBuilder&& builder) = default; +PollBuilder PollBuilder::SetPayloadVersion(uint32_t version) +{ + bool v3_enabled = IsPollV3Enabled(nBestHeight); + + if ((v3_enabled && version < 3) + || (!v3_enabled && version >= 3)) { + throw VotingError(_("Wrong Payload version specified for current block height.")); + } + + m_poll_payload_version = version; + + return std::move(*this); +} + PollBuilder PollBuilder::SetType(const PollType type) { if (type <= PollType::UNKNOWN || type >= PollType::OUT_OF_BOUND) { @@ -949,10 +974,13 @@ PollBuilder PollBuilder::SetResponseType(const int64_t type) PollBuilder PollBuilder::SetDuration(const uint32_t days) { - if (days < Poll::MIN_DURATION_DAYS) { + uint32_t min_duration_days = std::max(Poll::MIN_DURATION_DAYS, + Poll::POLL_TYPE_RULES[m_poll->m_type.Raw()].m_mininum_duration); + + if (days < min_duration_days) { throw VotingError(strprintf( _("Poll duration must be at least %s days."), - ToString(Poll::MIN_DURATION_DAYS))); + ToString(min_duration_days))); } // The protocol allows poll durations up to 180 days. To limit unhelpful @@ -1067,6 +1095,87 @@ PollBuilder PollBuilder::AddChoice(std::string label) return std::move(*this); } +PollBuilder PollBuilder::SetAdditionalFields(std::vector fields) +{ + m_poll->m_additional_fields = Poll::AdditionalFieldList(); + + return AddAdditionalFields(std::move(fields)); +} + +PollBuilder PollBuilder::SetAdditionalFields(Poll::AdditionalFieldList fields) +{ + m_poll->m_additional_fields = Poll::AdditionalFieldList(); + + return AddAdditionalFields(std::move(fields)); +} + +PollBuilder PollBuilder::AddAdditionalFields(std::vector fields) +{ + for (auto& field : fields) { + *this = AddAdditionalField(std::move(field)); + } + + if (!m_poll->m_additional_fields.WellFormed(m_poll->m_type.Value())) { + throw VotingError(_("The field list is not well-formed.")); + } + + return std::move(*this); +} + +PollBuilder PollBuilder::AddAdditionalFields(Poll::AdditionalFieldList fields) +{ + for (auto& field : fields) { + *this = AddAdditionalField(std::move(field)); + } + + if (!m_poll->m_additional_fields.WellFormed(m_poll->m_type.Value())) { + throw VotingError(_("The field list is not well-formed.")); + } + + return std::move(*this); +} + +PollBuilder PollBuilder::AddAdditionalField(Poll::AdditionalField field) +{ + // Make sure there are no leading and trailing spaces. + field.m_name = TrimString(field.m_name); + field.m_value = TrimString(field.m_value); + field.m_required = field.m_required; + + if (!field.WellFormed()) { + throw VotingError(_("The field is not well-formed.")); + } + + if (m_poll->m_additional_fields.size() + 1 > POLL_MAX_ADDITIONAL_FIELDS_SIZE) { + throw VotingError(strprintf( + _("Poll cannot contain more than %s additional fields"), + ToString(POLL_MAX_ADDITIONAL_FIELDS_SIZE))); + } + + if (field.m_name.size() > Poll::AdditionalField::MAX_N_OR_V_SIZE) { + throw VotingError(strprintf( + _("Poll additional field name \"%s\" exceeds %s characters."), + field.m_name, + ToString(Poll::AdditionalField::MAX_N_OR_V_SIZE))); + } + + if (field.m_value.size() > Poll::AdditionalField::MAX_N_OR_V_SIZE) { + throw VotingError(strprintf( + _("Poll additional field value \"%s\" for field name \"%s\" exceeds %s characters."), + field.m_value, + field.m_name, + ToString(Poll::AdditionalField::MAX_N_OR_V_SIZE))); + } + + if (m_poll->m_additional_fields.FieldExists(field.m_name)) { + throw VotingError(strprintf(_("Duplicate poll additional field: %s"), field.m_name)); + } + + m_poll->m_additional_fields.Add(std::move(field)); + + return std::move(*this); +} + CWalletTx PollBuilder::BuildContractTx(CWallet* const pwallet) { if (!pwallet) { @@ -1099,13 +1208,20 @@ CWalletTx PollBuilder::BuildContractTx(CWallet* const pwallet) PollEligibilityClaim claim = claim_builder.BuildClaim(*m_poll); tx.vContracts.emplace_back(MakeContract( - ContractAction::ADD, - std::move(*m_poll), - std::move(claim))); + ContractAction::ADD, + std::move(m_poll_payload_version), + std::move(*m_poll), + std::move(claim))); SelectFinalInputs(*pwallet, tx); PollPayload& poll_payload = tx.vContracts.back().SharePayload().As(); + LogPrint(BCLog::LogFlags::VOTE, "INFO: %s: tx contract payload claim address %s, poll title %s.", + __func__, + DestinationToAddressString(poll_payload.m_claim.m_address_claim.m_public_key.GetID()), + poll_payload.m_poll.m_title + ); + if (!claim_builder.SignClaim(poll_payload, tx)) { throw VotingError(_("Poll signature failed. See debug.log.")); } @@ -1117,6 +1233,12 @@ CWalletTx PollBuilder::BuildContractTx(CWallet* const pwallet) throw VotingError("Poll incomplete. This is probably a bug."); } + // Validate poll + int DoS = 0; // unused here + if (!GetPollRegistry().Validate(tx.vContracts.back(), tx, DoS)) { + throw VotingError("Poll invalid."); + } + return tx; } diff --git a/src/gridcoin/voting/builders.h b/src/gridcoin/voting/builders.h index 769fed0f25..ae5b7cf24f 100644 --- a/src/gridcoin/voting/builders.h +++ b/src/gridcoin/voting/builders.h @@ -5,9 +5,10 @@ #ifndef GRIDCOIN_VOTING_BUILDERS_H #define GRIDCOIN_VOTING_BUILDERS_H -#include "gridcoin/voting/fwd.h" +#include "gridcoin/voting/poll.h" #include +#include class CWallet; class CWalletTx; @@ -43,6 +44,13 @@ class PollBuilder //! PollBuilder& operator=(PollBuilder&& builder); + //! + //! \brief SetPayloadVersion + //! + //! \throws VotingError If the version is not valid for the current wallet height. + //! + PollBuilder SetPayloadVersion(uint32_t version); + //! //! \brief Set the type of the poll. //! @@ -158,6 +166,56 @@ class PollBuilder //! PollBuilder AddChoice(std::string label); + //! + //! \brief Set the set of additional fields for the poll. SetType() should be called beforehand. + //! + //! \param field A set of AdditionalFields to set. + //! + //! \throws VotingError If any of the fields are malformed, if the set of fields + //! contains a duplicate name, or the required boolean(s) are improperly set. + //! + PollBuilder SetAdditionalFields(std::vector fields); + + //! + //! \brief Set the set of additional fields for the poll. SetType() should be called beforehand. + //! + //! \param fields A set of AdditionalFields to set. + //! + //! \throws VotingError If any of the fields are malformed, or if the set of fields + //! contains a duplicate name, or the required boolean(s) are improperly set. + //! + PollBuilder SetAdditionalFields(Poll::AdditionalFieldList fields); + + //! + //! \brief Add a set of additional fields for the poll. SetType() should be called beforehand. + //! + //! \param fields A set of AdditionalFields to add. + //! + //! \throws VotingError If any of the fields are malformed, or if the set of fields + //! contains a duplicate name, or the required boolean(s) are improperly set. + //! + PollBuilder AddAdditionalFields(std::vector fields); + + //! + //! \brief Add a set of additional fields for the poll. SetType() should be called beforehand. + //! + //! \param fields A set of AdditionalFields to add. + //! + //! \throws VotingError If any of the fields are malformed, or if the set of fields + //! contains a duplicate name, or the required boolean(s) are improperly set. + //! + PollBuilder AddAdditionalFields(Poll::AdditionalFieldList fields); + + //! + //! \brief Add an additional field for the poll. + //! + //! \param field The additional field name-value to add. + //! + //! \throws VotingError If the field is malformed, or if the set of fields + //! contains a duplicate name. + //! + PollBuilder AddAdditionalField(Poll::AdditionalField field); + //! //! \brief Generate a poll contract transaction with the constructed poll. //! @@ -170,7 +228,8 @@ class PollBuilder CWalletTx BuildContractTx(CWallet* const pwallet); private: - std::unique_ptr m_poll; //!< The poll under construction. + std::unique_ptr m_poll; //!< The poll under construction. + uint32_t m_poll_payload_version; //!< The poll payload version appropriate for the current block height }; // PollBuilder //! diff --git a/src/gridcoin/voting/claims.h b/src/gridcoin/voting/claims.h index 8213fda924..44fb1a1064 100644 --- a/src/gridcoin/voting/claims.h +++ b/src/gridcoin/voting/claims.h @@ -10,10 +10,10 @@ #include "gridcoin/cpid.h" #include "gridcoin/magnitude.h" #include "serialize.h" +#include "primitives/transaction.h" #include -class COutPoint; class CTransaction; namespace GRC { diff --git a/src/gridcoin/voting/fwd.h b/src/gridcoin/voting/fwd.h index 5f8020a163..f317dc6ae6 100644 --- a/src/gridcoin/voting/fwd.h +++ b/src/gridcoin/voting/fwd.h @@ -6,7 +6,8 @@ #define GRIDCOIN_VOTING_FWD_H #include "amount.h" - +#include +#include namespace GRC { @@ -28,19 +29,27 @@ constexpr CAmount POLL_REQUIRED_BALANCE = 100000 * COIN; //! constexpr size_t POLL_MAX_CHOICES_SIZE = 20; +//! +//! \brief The maximum number of additional fields that a poll can contain. +//! +constexpr size_t POLL_MAX_ADDITIONAL_FIELDS_SIZE = 16; + //! //! \brief Describes the poll types. //! //! CONSENSUS: Do not remove or reorder items in this enumeration except for //! OUT_OF_BOUND which must remain at the end. //! -//! TODO: we may add additional poll types with specialized requirements, data, -//! or behavior such as project whitelist polls. -//! enum class PollType { UNKNOWN, //!< An invalid, non-standard, or empty poll type. SURVEY, //!< For casual, opinion, and legacy polls. + PROJECT, //!< Propose additions or removals of projects for research rewards eligibility. + DEVELOPMENT, //!< Propose a change to Gridcoin at the protocol level. + GOVERNANCE, //!< Proposals related to Gridcoin management like poll requirements or funding. + MARKETING, //!< Propose marketing initiatives like ad campaigns. + OUTREACH, //!< For polls about community representation, public relations, and communications. + COMMUNITY, //!< For other initiatives related to the Gridcoin community not included in the above. OUT_OF_BOUND, //!< Marker value for the end of the valid range. }; diff --git a/src/gridcoin/voting/payloads.h b/src/gridcoin/voting/payloads.h index 850a90188e..5c012dc974 100644 --- a/src/gridcoin/voting/payloads.h +++ b/src/gridcoin/voting/payloads.h @@ -5,6 +5,7 @@ #ifndef GRIDCOIN_VOTING_PAYLOADS_H #define GRIDCOIN_VOTING_PAYLOADS_H +#include "chainparams.h" #include "gridcoin/contract/payload.h" #include "gridcoin/voting/claims.h" #include "gridcoin/voting/poll.h" @@ -25,7 +26,7 @@ class PollPayload : public IContractPayload //! ensure that the serialization/deserialization routines also handle all //! of the previous versions. //! - static constexpr uint32_t CURRENT_VERSION = 2; + static constexpr uint32_t CURRENT_VERSION = 3; //! //! \brief Version number of the serialized poll format. @@ -45,18 +46,15 @@ class PollPayload : public IContractPayload //! \brief Initialize an empty, invalid poll payload. //! PollPayload() - : m_version(CURRENT_VERSION) { + m_version = CURRENT_VERSION; } //! - //! \brief Initialize a poll payload for submission in a transaction. + //! \brief Initialize an empty, invalid poll payload with the provided version + //! \param version //! - //! \param poll The body of the poll. - //! \param claim Testifies that the poll author owns the required balance. - //! - PollPayload(Poll poll, PollEligibilityClaim claim) - : PollPayload(CURRENT_VERSION, std::move(poll), std::move(claim)) + PollPayload(uint32_t version) : m_version(version) { } @@ -71,7 +69,7 @@ class PollPayload : public IContractPayload } //! - //! \brief Initialize a poll from data in a contract. + //! \brief Initialize a poll from data in a contract or for submission in a transaction //! //! \param version Version number of the serialized poll format. //! \param poll The body of the poll. @@ -141,6 +139,26 @@ class PollPayload : public IContractPayload return (50 * COIN) + m_claim.RequiredBurnAmount(); } + //! + //! \brief This returns the poll type(s) that are valid for the provided poll (payload) version. + //! + static std::vector GetValidPollTypes(const uint32_t& version) + { + std::vector poll_type; + + if (version < 3) { + poll_type.push_back(PollType::SURVEY); + } else { + for (const auto& type : Poll::POLL_TYPES) { + if (type == PollType::UNKNOWN || type == PollType::OUT_OF_BOUND) continue; + + poll_type.push_back(type); + } + } + + return poll_type; + } + ADD_CONTRACT_PAYLOAD_SERIALIZE_METHODS; template diff --git a/src/gridcoin/voting/poll.cpp b/src/gridcoin/voting/poll.cpp index 7347cefecb..137dfc6862 100644 --- a/src/gridcoin/voting/poll.cpp +++ b/src/gridcoin/voting/poll.cpp @@ -168,6 +168,10 @@ bool Poll::WellFormed(const uint32_t version) const } } + if (version >= 3 && !m_additional_fields.WellFormed(m_type.Value())) { + return false; + } + return true; } @@ -206,16 +210,88 @@ const Poll::ChoiceList& Poll::Choices() const return m_choices; } +const Poll::AdditionalFieldList& Poll::AdditionalFields() const +{ + return m_additional_fields; +} + +std::string Poll::PollTypeToString() const +{ + return PollTypeToString(m_type.Value()); +} + +std::string Poll::PollTypeToString(const PollType& type, const bool& translated) +{ + if (translated) { + switch(type) { + case PollType::UNKNOWN: return _("Unknown"); + case PollType::SURVEY: return _("Survey"); + case PollType::PROJECT: return _("Project Listing"); + case PollType::DEVELOPMENT: return _("Protocol Development"); + case PollType::GOVERNANCE: return _("Governance"); + case PollType::MARKETING: return _("Marketing"); + case PollType::OUTREACH: return _("Outreach"); + case PollType::COMMUNITY: return _("Community"); + case PollType::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(type) { + case PollType::UNKNOWN: return "unknown"; + case PollType::SURVEY: return "survey"; + case PollType::PROJECT: return "project"; + case PollType::DEVELOPMENT: return "development"; + case PollType::GOVERNANCE: return "governance"; + case PollType::MARKETING: return "marketing"; + case PollType::OUTREACH: return "outreach"; + case PollType::COMMUNITY: return "community"; + case PollType::OUT_OF_BOUND: break; + } + + assert(false); // Suppress warning + } + + +} + +std::string Poll::PollTypeToDescString() const +{ + return PollTypeToDescString(m_type.Value()); +} + + +std::string Poll::PollTypeToDescString(const PollType& type) +{ + switch(type) { + case PollType::UNKNOWN: return _("Unknown poll type. This should never happen."); + case PollType::SURVEY: return _("For opinion or casual polls without any particular requirements."); + case PollType::PROJECT: return _("Propose additions or removals of computing projects for research reward " + "eligibility."); + case PollType::DEVELOPMENT: return _("Propose a change to Gridcoin at the protocol level."); + case PollType::GOVERNANCE: return _("Proposals related to Gridcoin management like poll requirements and funding."); + case PollType::MARKETING: return _("Propose marketing initiatives like ad campaigns."); + case PollType::OUTREACH: return _("For polls about community representation, public relations, and " + "communications."); + case PollType::COMMUNITY: return _("For initiatives related to the Gridcoin community not covered by other " + "poll types."); + case PollType::OUT_OF_BOUND: break; + } + + assert(false); // Suppress warning +} + std::string Poll::WeightTypeToString() const { switch (m_weight_type.Value()) { - case PollWeightType::UNKNOWN: - case PollWeightType::OUT_OF_BOUND: return _("Unknown"); - case PollWeightType::MAGNITUDE: return _("Magnitude"); - case PollWeightType::BALANCE: return _("Balance"); - case PollWeightType::BALANCE_AND_MAGNITUDE: return _("Magnitude+Balance"); - case PollWeightType::CPID_COUNT: return _("CPID Count"); - case PollWeightType::PARTICIPANT_COUNT: return _("Participant Count"); + case PollWeightType::UNKNOWN: return std::string{}; + case PollWeightType::MAGNITUDE: return _("Magnitude"); + case PollWeightType::BALANCE: return _("Balance"); + case PollWeightType::BALANCE_AND_MAGNITUDE: return _("Magnitude+Balance"); + case PollWeightType::CPID_COUNT: return _("CPID Count"); + case PollWeightType::PARTICIPANT_COUNT: return _("Participant Count"); + case PollWeightType::OUT_OF_BOUND: break; } assert(false); // Suppress warning @@ -224,16 +300,153 @@ std::string Poll::WeightTypeToString() const std::string Poll::ResponseTypeToString() const { switch (m_response_type.Value()) { - case PollResponseType::UNKNOWN: - case PollResponseType::OUT_OF_BOUND: return _("Unknown"); - case PollResponseType::YES_NO_ABSTAIN: return _("Yes/No/Abstain"); - case PollResponseType::SINGLE_CHOICE: return _("Single Choice"); - case PollResponseType::MULTIPLE_CHOICE: return _("Multiple Choice"); + case PollResponseType::UNKNOWN: return std::string{}; + case PollResponseType::YES_NO_ABSTAIN: return _("Yes/No/Abstain"); + case PollResponseType::SINGLE_CHOICE: return _("Single Choice"); + case PollResponseType::MULTIPLE_CHOICE: return _("Multiple Choice"); + case PollResponseType::OUT_OF_BOUND: break; } assert(false); // Suppress warning } +const std::vector Poll::POLL_TYPE_RULES = { + // These must be kept in the order that corresponds to the PollType enum. + // { min duration, min vote percent AVW, { vector of required additional fieldnames } } + { 0, 0, {} }, // PollType::UNKNOWN + // Note there is NO payload version protection on the vector of required additional fieldnames + // and all payloads less than v3 only allowed PollType::SURVEY. Furthermore, the serialization + // of the poll class additional fields depends only on whether the poll type is SURVEY. The net + // of this is that the required additional fieldnames need to remain an empty vector for SURVEY. + // + // If a new SURVEY type is needed in the future with additional fields, a new enum entry should + // be created for it. + // + // In addition note that any poll type that has a min vote percent AVW requirement must + // also require the weight type of BALANCE_AND_MAGNITUDE, so therefore the + // only poll type that can actually use BALANCE is SURVEY. All other WeightTypes are deprecated. + { 7, 0, {} }, // PollType::SURVEY + { 21, 40, { "project_name", "project_url" } }, // PollType::PROJECT + { 42, 50, {} }, // PollType::DEVELOPMENT + { 21, 20, {} }, // PollType::GOVERNANCE + { 21, 40, {} }, // PollType::MARKETING + { 21, 40, {} }, // PollType::OUTREACH + { 21, 10, {} } // PollType::COMMUNITY +}; + +// ----------------------------------------------------------------------------- +// Class: Poll::AdditionalFieldList +// ----------------------------------------------------------------------------- +using AdditionalFieldList = Poll::AdditionalFieldList; +using AdditionalField = Poll::AdditionalField; + +AdditionalFieldList::AdditionalFieldList(std::vector additional_fields) + : m_additional_fields(std::move(additional_fields)) +{ +} + +AdditionalFieldList::const_iterator AdditionalFieldList::begin() const +{ + return m_additional_fields.begin(); +} + +AdditionalFieldList::const_iterator AdditionalFieldList::end() const +{ + return m_additional_fields.end(); +} + +size_t AdditionalFieldList::size() const +{ + return m_additional_fields.size(); +} + +bool AdditionalFieldList::empty() const +{ + return m_additional_fields.empty(); +} + +bool AdditionalFieldList::WellFormed(const PollType poll_type) const +{ + if (m_additional_fields.size() > POLL_MAX_ADDITIONAL_FIELDS_SIZE) { + return false; + } + + const std::vector required_field_names = Poll::POLL_TYPE_RULES[(int) poll_type].m_required_fields; + + for (const auto& iter : required_field_names) { + std::optional offset = OffsetOf(iter); + // If the field name (entry) does not exist, return false. + if (!offset) return false; + + // If the field name (entry) m_required flag is not set properly then return false. + if (At(*offset)->m_required != true) return false; + + // If the field value is empty, return false. A required field cannot have an empty value. + if (At(*offset)->m_value.empty()) return false; + } + + // We check to ensure that each field is well formed. We also need to check whether fields that are NOT required are + // marked accordingly. This requires us to iterate through the m_additional_fields. If a field entry is not well formed + // or if not found in the required fields list and the field entry is marked required, then return false. + for (const auto& iter : m_additional_fields) { + if (!iter.WellFormed()) { + return false; + } + + if (std::find(required_field_names.begin(), required_field_names.end(), iter.m_name) == required_field_names.end() + && iter.m_required) { + return false; + } + } + + return true; +} + +bool AdditionalFieldList::OffsetInRange(const size_t offset) const +{ + return offset < m_additional_fields.size(); +} + +bool AdditionalFieldList::FieldExists(const std::string& name) const +{ + return OffsetOf(name).operator bool(); +} + +std::optional AdditionalFieldList::OffsetOf(const std::string& name) const +{ + const auto iter = std::find_if( + m_additional_fields.begin(), + m_additional_fields.end(), + [&](const AdditionalField& additional_field) { return additional_field.m_name == name; }); + + if (iter == m_additional_fields.end()) { + return std::nullopt; + } + + return std::distance(m_additional_fields.begin(), iter); +} + +const AdditionalField* AdditionalFieldList::At(const size_t offset) const +{ + if (offset >= m_additional_fields.size()) { + return nullptr; + } + + return &m_additional_fields[offset]; +} + +void AdditionalFieldList::Add(std::string name, std::string value, bool required) +{ + AdditionalField additional_field { name, value, required }; + + m_additional_fields.emplace_back(std::move(additional_field)); +} + +void AdditionalFieldList::Add(AdditionalField field) +{ + m_additional_fields.emplace_back(std::move(field)); +} + // ----------------------------------------------------------------------------- // Class: Poll::ChoiceList // ----------------------------------------------------------------------------- diff --git a/src/gridcoin/voting/poll.h b/src/gridcoin/voting/poll.h index 6047cbf671..01b27b82b4 100644 --- a/src/gridcoin/voting/poll.h +++ b/src/gridcoin/voting/poll.h @@ -26,27 +26,27 @@ class Poll using ResponseType = EnumByte; //! - //! \brief Minimum duration that a poll must remain active for. + //! \brief Minimum duration that a poll must remain active for. This is a global rule. //! static constexpr uint32_t MIN_DURATION_DAYS = 7; //! - //! \brief Maximum duration that a poll cannot remain active after. + //! \brief Maximum duration that a poll cannot remain active after. This is a global rule. //! static constexpr uint32_t MAX_DURATION_DAYS = 180; //! - //! \brief Maximum allowed length of a poll title. + //! \brief Maximum allowed length of a poll title. This is a global rule. //! static constexpr size_t MAX_TITLE_SIZE = 80; //! - //! \brief Maximum allowed length of a poll URL. + //! \brief Maximum allowed length of a poll URL. This is a global rule. //! static constexpr size_t MAX_URL_SIZE = 100; //! - //! \brief Maximum allowed length of a poll question. + //! \brief Maximum allowed length of a poll question. This is a global rule. //! static constexpr size_t MAX_QUESTION_SIZE = 100; @@ -199,14 +199,197 @@ class Poll std::vector m_choices; //!< The set of choices for the poll. }; // ChoiceList - Type m_type; //!< Type of the poll. - WeightType m_weight_type; //!< Method used to weigh votes. - ResponseType m_response_type; //!< Method for choosing poll answers. - uint32_t m_duration_days; //!< Number of days the poll remains active. - std::string m_title; //!< UTF-8 title of the poll. - std::string m_url; //!< UTF-8 URL of the poll discussion webpage. - std::string m_question; //!< UTF-8 prompt that voters shall answer. - ChoiceList m_choices; //!< The set of possible answers to the poll. + class AdditionalField + { + public: + //! + //! \brief The maximum length for a poll additional field name or value. + //! + static constexpr size_t MAX_N_OR_V_SIZE = 100; + + std::string m_name; + std::string m_value; + bool m_required; + + //! + //! \brief Initialize an empty, invalid additional field. + //! + AdditionalField() + { + } + + //! + //! \brief Initialize an additional field for the poll with the specified parameters + //! \param name UTF-8 name of the field + //! \param value UTF-8 value of the field + //! \param required bool whether the field is required + //! + AdditionalField(std::string name, std::string value, bool required) + : m_name(std::move(name)) + , m_value(std::move(value)) + , m_required(std::move(required)) + { + } + + //! + //! \brief Determine whether a poll additional field name value pair is complete. + //! + bool WellFormed() const + { + return !m_name.empty() && (!m_required || !m_value.empty()); + } + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) + { + READWRITE(LIMITED_STRING(m_name, MAX_N_OR_V_SIZE)); + READWRITE(LIMITED_STRING(m_value, MAX_N_OR_V_SIZE)); + READWRITE(m_required); + } + }; // AdditionalField + + class AdditionalFieldList + { + public: + using const_iterator = std::vector::const_iterator; + + //! + //! \brief Initialize an empty additional field list. + //! + AdditionalFieldList() + { + } + + //! + //! \brief Initialize an additional field list with the supplied additional fields. + //! + AdditionalFieldList(std::vector additional_fields); + + //! + //! \brief Returns an iterator to the beginning. + //! + const_iterator begin() const; + + //! + //! \brief Returns an iterator to the end. + //! + const_iterator end() const; + + //! + //! \brief Get the number of additional fields in the poll. + //! + size_t size() const; + + //! + //! \brief Determine whether the poll contains no additional fields. + //! + bool empty() const; + + //! + //! \brief Determine whether the additional fields are sufficient for a poll. + //! + //! \return \c false if the set of additional fields contains invalid entries or + //! if the required additional fields are missing for a poll. + //! + bool WellFormed(const PollType poll_type) const; + + //! + //! \brief Determine whether the specified offset matches an offset in + //! the range of poll additional fields. + //! + //! \return \c true if the offset does not exceed the bounds of the + //! container. + //! + bool OffsetInRange(const size_t offset) const; + + //! + //! \brief Determine whether the specified additional field name-value pair already exists. + //! + //! \return \c true if another field name-value pair is already in the vector that contains a matching name. + //! + bool FieldExists(const std::string& name) const; + + //! + //! \brief Get the offset of the specified field name. + //! + //! \param label The additional field name to find the offset of. + //! + //! \return An object that either contains the offset of the field name or + //! does not when no additional field contains a matching name. + //! + std::optional OffsetOf(const std::string& name) const; + + //! + //! \brief Get the poll additional field at the specified offset. + //! + //! \return The additional field at the specified offset or a null pointer if + //! the offset exceeds the range of the additional fields. + //! + const AdditionalField* At(const size_t offset) const; + + //! + //! \brief Add an additional field to the poll. + //! + void Add(std::string name, std::string value, bool required); + + //! + //! \brief Add an additional field to the poll. + //! + void Add(AdditionalField field); + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) + { + READWRITE(m_additional_fields); + } + + private: + std::vector m_additional_fields; //!< The set of additional fields for the poll. + }; + + //! + //! \brief Used for poll (payload) version 3+ + //! + struct PollTypeRules + { + uint32_t m_mininum_duration; + uint32_t m_min_vote_percent_AVW; + std::vector m_required_fields; + }; + + //! + //! \brief Allows use of the PollType enum in range based for loops. + //! + static constexpr GRC::PollType POLL_TYPES[] = { + PollType::UNKNOWN, + PollType::SURVEY, + PollType::PROJECT, + PollType::DEVELOPMENT, + PollType::GOVERNANCE, + PollType::MARKETING, + PollType::OUTREACH, + PollType::COMMUNITY, + PollType::OUT_OF_BOUND + }; + + //! + //! \brief Poll rules that are specific to poll type. Enforced for poll payload version 3+. + //! + static const std::vector POLL_TYPE_RULES; + + Type m_type; //!< Type of the poll. + WeightType m_weight_type; //!< Method used to weigh votes. + ResponseType m_response_type; //!< Method for choosing poll answers. + uint32_t m_duration_days; //!< Number of days the poll remains active. + std::string m_title; //!< UTF-8 title of the poll. + std::string m_url; //!< UTF-8 URL of the poll discussion webpage. + std::string m_question; //!< UTF-8 prompt that voters shall answer. + ChoiceList m_choices; //!< The set of possible answers to the poll. + AdditionalFieldList m_additional_fields; //!< The set of additional fields for the poll. // Memory only: int64_t m_timestamp; //!< Time of the poll's containing transaction. @@ -314,6 +497,33 @@ class Poll //! const ChoiceList& Choices() const; + //! + //! \brief Get the set of additional fields in the poll. + //! + const AdditionalFieldList& AdditionalFields() const; + + //! + //! \brief Get the string representation of the poll type for the poll object. + //! + std::string PollTypeToString() const; + + //! + //! \brief Get the string representation of the poll type for the provided poll type. + //! \param type + //! + static std::string PollTypeToString(const PollType& type, const bool& translated = true); + + //! + //! \brief Get the poll type description string for the poll object. + //! + std::string PollTypeToDescString() const; + + //! + //! \brief Get the poll type description string for the provided poll type. + //! \param type + //! + static std::string PollTypeToDescString(const PollType& type); + //! //! \brief Get the string representation of the poll's weight type. //! @@ -340,8 +550,22 @@ class Poll if (m_response_type != PollResponseType::YES_NO_ABSTAIN) { READWRITE(m_choices); } + + // Note: this is a little dirty but works, because all polls prior to v3 are SURVEY, and the + // additional fields for survey is an empty vector. Therefore this serialization will only + // be operative if a poll type other than survey is used, and this cannot occur until v3+. + // Refer to the comments in POLL_TYPE_RULES. This is necessary because the only other solution would be + // to pass the poll payload version into the poll object, which would be problematic. + // + // TODO: Remove COMMUNITY after finishing isolated fork testing. (Community was used to test v3 polls + // before the introduction of additional fields, and therefore the community polls on the isolated + // testing fork do not have the m_additional_fields serialization and removal of the COMMUNITY below + // will result in an serialization I/O error. + if (m_type != PollType::SURVEY && m_type != PollType::COMMUNITY) { + READWRITE(m_additional_fields); + } } }; // Poll -} +} // namespace GRC #endif // GRIDCOIN_VOTING_POLL_H diff --git a/src/gridcoin/voting/registry.cpp b/src/gridcoin/voting/registry.cpp index 765618f637..b5e70f66d7 100644 --- a/src/gridcoin/voting/registry.cpp +++ b/src/gridcoin/voting/registry.cpp @@ -53,6 +53,92 @@ class InvalidPollError : public std::exception } }; +class PollValidator +{ +public: + explicit PollValidator(const PollPayload& payload, const CTransaction& tx) : m_payload(payload), m_tx(tx) + { + } + + bool Validate(int& DoS) + { + if (m_payload.m_version < 2) { + DoS = 25; + LogPrint(LogFlags::CONTRACT, "%s: rejected legacy poll", __func__); + return false; + } + + // WellFormed() checks: + // m_type is not UNKNOWN or OUT_OF_BOUND + // m_weight_type is not UNKNOWN or OUT_OF_BOUND + // m_response_type is not UNKNOWN or OUT_OF_BOUND + // m_duration_days is inclusive between MIN_DURATION_DAYS and MAX_DURATION_DAYS + // m_title is not empty + // m_url is not empty + // m_choices are well formed based on the response type + // For version 2+ + // m_weight type is only BALANCE and BALANCE_AND_MAGNITUDE + if (!m_payload.m_poll.WellFormed(m_payload.m_version)) { + DoS = 25; + LogPrint(LogFlags::CONTRACT, "%s: rejected poll that is not well formed", __func__); + return false; + } + + // Make sure poll type is valid for the version of the poll payload. + // The only valid type for v2 polls is SURVEY. + // The valid types for v3 polls are SURVEY, PROJECT, DEVELOPMENT, GOVERNANCE, MARKETING, OUTREACH, + // and COMMUNITY. + std::vector valid_poll_types = GRC::PollPayload::GetValidPollTypes(m_payload.m_version); + + if (std::find(valid_poll_types.begin(), valid_poll_types.end(), m_payload.m_poll.m_type.Value()) + == valid_poll_types.end()) { + DoS = 25; + LogPrint(LogFlags::CONTRACT, "%s: rejected poll payload with improper type", __func__); + return false; + } + + // Poll payload v3+ validations which depend on poll type + if (m_payload.m_version >= 3) { + // Select the rules for the poll type in the payload. + Poll::PollTypeRules poll_type_rules = Poll::POLL_TYPE_RULES[m_payload.m_poll.m_type.Raw()]; + + // v3 polls must meet the by type minimum duration requirements as well as the global requirements above. + if (m_payload.m_poll.m_duration_days < poll_type_rules.m_mininum_duration) { + DoS = 25; + LogPrint(LogFlags::CONTRACT, "%s: rejected v3 poll payload with duration %i, less than the required " + "minimum %i", + __func__, + m_payload.m_poll.m_duration_days, + poll_type_rules.m_mininum_duration); + return false; + } + + // v3 polls must be balance + magnitude for the weight type if the by poll type rule specifies a minimum + // vote weight % of AVW greater than zero. + if (poll_type_rules.m_min_vote_percent_AVW + && m_payload.m_poll.m_weight_type != PollWeightType::BALANCE_AND_MAGNITUDE) { + DoS = 25; + LogPrint(LogFlags::CONTRACT, "%s: rejected v3 poll payload with wrong weight type %s for given poll type %s " + "requiring %u vote weight percent of active vote weight for validation.", + __func__, + m_payload.m_poll.WeightTypeToString(), + m_payload.m_poll.PollTypeToString(), + poll_type_rules.m_min_vote_percent_AVW); + return false; + } + } + + return true; + } + +private: + const PollPayload& m_payload; + CTransaction m_tx; +}; + + + + //! //! \brief Verifies a participant's eligibility to create a poll. //! @@ -240,6 +326,8 @@ ClaimMessage GRC::PackPollMessage(const Poll& poll, const CTransaction& tx) PollReference::PollReference() : m_ptxid(nullptr) + , m_payload_version(0) + , m_type(PollType::UNKNOWN) , m_ptitle(nullptr) , m_timestamp(0) , m_duration_days(0) @@ -285,6 +373,16 @@ uint256 PollReference::Txid() const return *m_ptxid; } +uint32_t PollReference::GetPollPayloadVersion() const +{ + return m_payload_version; +} + +PollType PollReference::GetPollType() const +{ + return m_type; +} + const std::string& PollReference::Title() const { if (!m_ptitle) { @@ -331,15 +429,30 @@ CBlockIndex* PollReference::GetStartingBlockIndexPtr() const GetTransaction(*m_ptxid, tx, block_hash); - return mapBlockIndex[block_hash]; + auto iter = mapBlockIndex.find(block_hash); + + if (iter == mapBlockIndex.end()) { + return nullptr; + } + + return iter->second; } -CBlockIndex* PollReference::GetEndingBlockIndexPtr() const +CBlockIndex* PollReference::GetEndingBlockIndexPtr(CBlockIndex* pindex_start) const { + if (!pindex_start) { + pindex_start = GetStartingBlockIndexPtr(); + + // If there is still no pindex_start, there cannot be an end either. + if (!pindex_start) { + return nullptr; + } + } + // Has poll ended? if (Expired(GetAdjustedTime())) { // Find and return the last block that contains valid votes for the poll. - return GRC::BlockFinder::FindByMinTime(Expiration()); + return GRC::BlockFinder::FindByMinTimeFromGivenIndex(Expiration(), pindex_start); } return nullptr; @@ -374,11 +487,13 @@ std::optional PollReference::GetActiveVoteWeight(const PollResultOption // Get the start and end of the poll. CBlockIndex* const pindex_start = GetStartingBlockIndexPtr(); - const CBlockIndex* pindex_end = GetEndingBlockIndexPtr(); // If pindex_start is a nullptr, this is a degenerate poll reference. Return std::nullopt. if (pindex_start == nullptr) return std::nullopt; + const CBlockIndex* pindex_end = GetEndingBlockIndexPtr(pindex_start); + + LogPrint(BCLog::LogFlags::VOTE, "INFO: %s: Poll start height = %i.", __func__, pindex_start->nHeight); @@ -788,13 +903,10 @@ bool PollRegistry::Validate(const Contract& contract, const CTransaction& tx, in return true; } + // V2+ validations const auto payload = contract.SharePayloadAs(); - if (payload->m_version < 2) { - DoS = 25; - LogPrint(LogFlags::CONTRACT, "%s: rejected legacy poll", __func__); - return false; - } + if (!PollValidator(*payload, tx).Validate(DoS)) return false; CTxDB txdb("r"); @@ -806,6 +918,27 @@ bool PollRegistry::Validate(const Contract& contract, const CTransaction& tx, in return true; } +bool PollRegistry::BlockValidate(const ContractContext& ctx, int& DoS) const +{ + // Vote contract claims do not affect consensus. Vote claim validation + // occurs on-demand while computing the results of the poll: + // + if (ctx.m_contract.m_type == ContractType::VOTE) { + return true; + } + + const auto payload = ctx->SharePayloadAs(); + + // This is why we had to introduce BlockValidate, and this is critical + // to ensure that v2 poll payloads are not allowed in blocks for the v3 + // height and beyond. + if (IsPollV3Enabled(ctx.m_pindex->nHeight) && payload->m_version < 3) { + return false; + } + + return Validate(ctx.m_contract, ctx.m_tx, DoS); +} + void PollRegistry::Add(const ContractContext& ctx) { if (ctx->m_type == ContractType::VOTE) { @@ -841,6 +974,8 @@ void PollRegistry::AddPoll(const ContractContext& ctx) PollReference& poll_ref = result_pair.first->second; poll_ref.m_ptitle = &title; + poll_ref.m_payload_version = payload->m_version; + poll_ref.m_type = payload->m_poll.m_type.Value(); poll_ref.m_timestamp = ctx.m_tx.nTime; poll_ref.m_duration_days = payload->m_poll.m_duration_days; diff --git a/src/gridcoin/voting/registry.h b/src/gridcoin/voting/registry.h index b4d52474e3..ff84394d1d 100644 --- a/src/gridcoin/voting/registry.h +++ b/src/gridcoin/voting/registry.h @@ -8,6 +8,8 @@ #include "gridcoin/contract/handler.h" #include "gridcoin/voting/filter.h" #include "gridcoin/voting/fwd.h" +#include "uint256.h" +#include class CTxDB; @@ -55,6 +57,16 @@ class PollReference //! uint256 Txid() const; + //! + //! \brief Get the poll (payload) version + //! + uint32_t GetPollPayloadVersion() const; + + //! + //! \brief Get the poll type + //! + PollType GetPollType() const; + //! //! \brief Get the title of the associated poll. //! @@ -111,7 +123,7 @@ class PollReference //! //! \return pointer to block index object. //! - CBlockIndex* GetEndingBlockIndexPtr() const; + CBlockIndex* GetEndingBlockIndexPtr(CBlockIndex* pindex_start = nullptr) const; //! //! \brief Get the starting block height for the poll. @@ -127,6 +139,11 @@ class PollReference //! std::optional GetEndingHeight() const; + //! + //! \brief Computes the Active Vote Weight for the poll, which is used to determine whether the poll is validated. + //! \param result: The actual tabulated votes (poll result) + //! \return ActiveVoteWeight + //! std::optional GetActiveVoteWeight(const PollResultOption &result) const; //! @@ -146,6 +163,8 @@ class PollReference private: const uint256* m_ptxid; //!< Hash of the poll transaction. + uint32_t m_payload_version; //!< Version of the poll (payload). + PollType m_type; //!< Type of the poll. const std::string* m_ptitle; //!< Title of the poll. int64_t m_timestamp; //!< Timestamp of the poll transaction. uint32_t m_duration_days; //!< Number of days the poll remains active. @@ -363,6 +382,17 @@ class PollRegistry : public IContractHandler //! bool Validate(const Contract& contract, const CTransaction& tx, int& DoS) const override; + //! + //! \brief Perform contextual validation for the provided contract including block context. This is used + //! in ConnectBlock. + //! + //! \param ctx ContractContext 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 Register a poll or vote from contract data. //! diff --git a/src/gridcoin/voting/result.cpp b/src/gridcoin/voting/result.cpp index 94aeb8e49c..0f26b83c0f 100644 --- a/src/gridcoin/voting/result.cpp +++ b/src/gridcoin/voting/result.cpp @@ -1108,6 +1108,9 @@ PollResult::PollResult(Poll poll) , m_total_weight(0) , m_invalid_votes(0) , m_pools_voted({}) + , m_active_vote_weight() + , m_vote_percent_avw() + , m_poll_results_validated() , m_finished(m_poll.Expired(GetAdjustedTime())) { m_responses.resize(m_poll.Choices().size()); @@ -1128,6 +1131,25 @@ PollResultOption PollResult::BuildFor(const PollReference& poll_ref) counter.CountVotes(result, poll_ref.Votes()); + if (auto active_vote_weight = poll_ref.GetActiveVoteWeight(result)) { + result.m_active_vote_weight = active_vote_weight; + + result.m_vote_percent_avw = (double) result.m_total_weight / (double) *result.m_active_vote_weight * 100.0; + + // For purposes of validation, integer arithmetic is used. + uint32_t vote_percent_avw_for_validation = (uint32_t)((int64_t) result.m_total_weight * (int64_t) 100 + / (int64_t) *result.m_active_vote_weight); + + // For v1 and v2 polls, there is only one type, SURVEY, that was used on the blockchain for all polls, so this + // can only be done for v3+. + if (poll_ref.GetPollPayloadVersion() > 2) { + uint32_t min_vote_percent_avw_for_validation + = Poll::POLL_TYPE_RULES[(int) poll_ref.GetPollType()].m_min_vote_percent_AVW; + + result.m_poll_results_validated = (vote_percent_avw_for_validation >= min_vote_percent_avw_for_validation); + } + } + return result; } diff --git a/src/gridcoin/voting/result.h b/src/gridcoin/voting/result.h index f65e7de362..16705b10b5 100644 --- a/src/gridcoin/voting/result.h +++ b/src/gridcoin/voting/result.h @@ -77,11 +77,14 @@ class PollResult bool Empty() const; }; - const Poll m_poll; //!< The poll associated with the result. - Weight m_total_weight; //!< Aggregate weight of all the votes submitted. - size_t m_invalid_votes; //!< Number of votes that failed validation. - std::vector m_pools_voted; //!< Cpids of pools that actually voted - bool m_finished; //!< Whether the poll finished as of this result. + const Poll m_poll; //!< The poll associated with the result. + Weight m_total_weight; //!< Aggregate weight of all the votes submitted. + size_t m_invalid_votes; //!< Number of votes that failed validation. + std::vector m_pools_voted; //!< Cpids of pools that actually voted. + std::optional m_active_vote_weight; //!< Active vote weight of poll. + std::optional m_vote_percent_avw; //!< Vote weight percent of AVW. + std::optional m_poll_results_validated; //!< Whether the poll's AVW is >= the minimum AVW for the poll. + bool m_finished; //!< Whether the poll finished as of this result. //! //! \brief The aggregated voting weight tallied for each poll choice. diff --git a/src/init.cpp b/src/init.cpp index a1f35d740d..b5eaa39220 100755 --- a/src/init.cpp +++ b/src/init.cpp @@ -571,6 +571,9 @@ void SetupServerArgs() hidden_args.emplace_back("-foundationaddress"); hidden_args.emplace_back("-foundationsidestakeallocation"); + // Temporary for poll v3 testing + hidden_args.emplace_back("-pollv3height"); + // -boinckey should now be removed entirely. It is put here to prevent the executable erroring out on // an invalid parameter for old clients that may have left the argument in. hidden_args.emplace_back("-boinckey"); diff --git a/src/main.cpp b/src/main.cpp index dd9dcb92ab..af5db598e8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1957,7 +1957,7 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex, bool fJustCheck) } int DoS = 0; - if (nVersion >= 11 && !GRC::ValidateContracts(tx, DoS)) { + if (nVersion >= 11 && !GRC::BlockValidateContracts(pindex, tx, DoS)) { return tx.DoS(DoS, error("%s: invalid contract in tx %s, assigning DoS misbehavior of %i", __func__, tx.GetHash().ToString(), diff --git a/src/miner.cpp b/src/miner.cpp index 2b59f33000..7e3b9412b4 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -311,6 +311,10 @@ bool CreateRestOfTheBlock(CBlock &block, CBlockIndex* pindexPrev, int nHeight = pindexPrev->nHeight + 1; + // This is specifically for BlockValidateContracts, and only the nHeight is filled in. + CBlockIndex* pindex_contract_validate = new CBlockIndex(); + pindex_contract_validate->nHeight = nHeight; + // Create coinbase tx CTransaction &CoinBase = block.vtx[0]; CoinBase.nTime = block.nTime; @@ -372,10 +376,14 @@ bool CreateRestOfTheBlock(CBlock &block, CBlockIndex* pindexPrev, // Double-check that contracts pass contextual validation again so // that we don't include a transaction that disrupts validation of - // the block: + // the block. Note that this is especially important now that there + // are block level rules that cannot be checked for transactions + // that are just in the mempool. Note that the only block level rules + // currently implemented depend on block height only, so the + // pindex_contract_validate only has the block height filled out. // int DoS = 0; // Unused here. - if (!tx.GetContracts().empty() && !GRC::ValidateContracts(tx, DoS)) { + if (!tx.GetContracts().empty() && !GRC::BlockValidateContracts(pindex_contract_validate, tx, DoS)) { LogPrint(BCLog::LogFlags::MINER, "%s: contract failed contextual validation. Skipped tx %s", __func__, diff --git a/src/qt/forms/voting/pollcard.ui b/src/qt/forms/voting/pollcard.ui index 1010419c62..02a7b92063 100644 --- a/src/qt/forms/voting/pollcard.ui +++ b/src/qt/forms/voting/pollcard.ui @@ -59,18 +59,42 @@ 0 - - - - Title - - - Qt::PlainText - - - true - - + + + + + + Title + + + Qt::PlainText + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Poll Type + + + + @@ -206,19 +230,46 @@ 0 - + Balance - + Magnitude + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Validated + + + + + + + Invalid + + + diff --git a/src/qt/res/stylesheets/dark_stylesheet.qss b/src/qt/res/stylesheets/dark_stylesheet.qss index 76a7678727..4290e39ed7 100644 --- a/src/qt/res/stylesheets/dark_stylesheet.qss +++ b/src/qt/res/stylesheets/dark_stylesheet.qss @@ -972,13 +972,28 @@ VoteWizard #pageTitleLabel { } PollCard #balanceLabel, -PollCard #magnitudeLabel { +PollCard #magnitudeLabel, +PollCard #typeLabel { border: 0.065em solid rgb(115, 131, 161); border-radius: 0.65em; padding: 0.1em 0.3em; color: rgb(115, 131, 161); } +PollCard #validatedLabel { + border: 0.065em solid rgb(0, 131, 0); + border-radius: 0.65em; + padding: 0.1em 0.3em; + color: rgb(0, 131, 0); +} + +PollCard #invalidLabel { + border: 0.065em solid rgb(150, 0, 0); + border-radius: 0.65em; + padding: 0.1em 0.3em; + color: rgb(150, 0, 0); +} + PollCard #remainingLabel, PollResultChoiceItem #percentageLabel, PollResultChoiceItem #weightLabel, diff --git a/src/qt/res/stylesheets/light_stylesheet.qss b/src/qt/res/stylesheets/light_stylesheet.qss index 3cfe1845a5..9d2a6e32b0 100644 --- a/src/qt/res/stylesheets/light_stylesheet.qss +++ b/src/qt/res/stylesheets/light_stylesheet.qss @@ -947,13 +947,28 @@ PollWizardTypePage #typeTextLabel { } PollCard #balanceLabel, -PollCard #magnitudeLabel { +PollCard #magnitudeLabel, +PollCard #typeLabel { border: 0.065em solid rgb(115, 131, 161); border-radius: 0.65em; padding: 0.1em 0.3em; color: rgb(115, 131, 161); } +PollCard #validatedLabel { + border: 0.065em solid rgb(0, 131, 0); + border-radius: 0.65em; + padding: 0.1em 0.3em; + color: rgb(0, 131, 0); +} + +PollCard #invalidLabel { + border: 0.065em solid rgb(150, 0, 0); + border-radius: 0.65em; + padding: 0.1em 0.3em; + color: rgb(150, 0, 0); +} + PollCard #remainingLabel, PollResultChoiceItem #percentageLabel, PollResultChoiceItem #weightLabel, diff --git a/src/qt/voting/poll_types.cpp b/src/qt/voting/poll_types.cpp index faeeaab8b5..d39035b3ad 100644 --- a/src/qt/voting/poll_types.cpp +++ b/src/qt/voting/poll_types.cpp @@ -2,6 +2,7 @@ // Distributed under the MIT/X11 software license, see the accompanying // file COPYING or https://opensource.org/licenses/mit-license.php. +#include "gridcoin/voting/poll.h" #include "qt/voting/poll_types.h" #include @@ -13,45 +14,6 @@ struct PollTypeDefinition const char* m_description; int m_min_duration_days; }; - -const PollTypeDefinition g_poll_types[] = { - { "Unknown", "Unknown", 0 }, - { - QT_TRANSLATE_NOOP("PollTypes", "Project Listing"), - QT_TRANSLATE_NOOP("PollTypes", "Propose additions or removals of computing projects for research reward eligibility."), - 21, // min duration days - }, - { - QT_TRANSLATE_NOOP("PollTypes", "Protocol Development"), - QT_TRANSLATE_NOOP("PollTypes", "Propose a change to Gridcoin at the protocol level."), - 42, // min duration days - }, - { - QT_TRANSLATE_NOOP("PollTypes", "Governance"), - QT_TRANSLATE_NOOP("PollTypes", "Proposals related to Gridcoin management like poll requirements and funding."), - 21, // min duration days - }, - { - QT_TRANSLATE_NOOP("PollTypes", "Marketing"), - QT_TRANSLATE_NOOP("PollTypes", "Propose marketing initiatives like ad campaigns."), - 21, // min duration days - }, - { - QT_TRANSLATE_NOOP("PollTypes", "Outreach"), - QT_TRANSLATE_NOOP("PollTypes", "For polls about community representation, public relations, and communications."), - 21, // min duration days - }, - { - QT_TRANSLATE_NOOP("PollTypes", "Community"), - QT_TRANSLATE_NOOP("PollTypes", "For other initiatives related to the Gridcoin community."), - 21, // min duration days - }, - { - QT_TRANSLATE_NOOP("PollTypes", "Survey"), - QT_TRANSLATE_NOOP("PollTypes", "For opinion or casual polls without any particular requirements."), - 7, // min duration days - }, -}; } // Anonymous namespace // ----------------------------------------------------------------------------- @@ -60,10 +22,19 @@ const PollTypeDefinition g_poll_types[] = { PollTypes::PollTypes() { - for (const auto& type : g_poll_types) { + // Load from core Poll class. Note we do not use PollPayload::GetValidPollTypes, because with poll v2 the UI supported + // the other poll types from a user perspective, but in fact only submitted the polls as "SURVEY" in the core. + // GetValidPollTypes will only return SURVEY for v2, so it is not appropriate to use here. + // + // I am not particularly fond of how this is crosswired into the GUI code here, but it will suffice for now. + // TODO: refactor poll types between core and UI. + for (const auto& type : GRC::Poll::POLL_TYPES) { + if (type == GRC::PollType::OUT_OF_BOUND) continue; + emplace_back(); - back().m_name = QCoreApplication::translate("PollTypes", type.m_name); - back().m_description = QCoreApplication::translate("PollTypes", type.m_description); - back().m_min_duration_days = type.m_min_duration_days; + // Note that these use the default value for the translated boolean, which is true. + back().m_name = QString::fromStdString(GRC::Poll::PollTypeToString(type)); + back().m_description = QString::fromStdString(GRC::Poll::PollTypeToDescString(type)); + back().m_min_duration_days = GRC::Poll::POLL_TYPE_RULES[(int) type].m_mininum_duration; } } diff --git a/src/qt/voting/poll_types.h b/src/qt/voting/poll_types.h index b4420d76cb..d93ebd474c 100644 --- a/src/qt/voting/poll_types.h +++ b/src/qt/voting/poll_types.h @@ -19,18 +19,6 @@ class PollTypeItem class PollTypes : public std::vector { public: - enum PollType - { - PollTypeUnknown, - PollTypeProject, - PollTypeDevelopment, - PollTypeGovernance, - PollTypeMarketing, - PollTypeOutreach, - PollTypeCommunity, - PollTypeSurvey, - }; - PollTypes(); }; diff --git a/src/qt/voting/pollcard.cpp b/src/qt/voting/pollcard.cpp index 57c0a057ba..a80608d2c1 100644 --- a/src/qt/voting/pollcard.cpp +++ b/src/qt/voting/pollcard.cpp @@ -19,11 +19,44 @@ PollCard::PollCard(const PollItem& poll_item, QWidget* parent) ui->setupUi(this); ui->titleLabel->setText(poll_item.m_title); + + ui->typeLabel->setText(poll_item.m_type_str); + if (poll_item.m_version >= 3) { + ui->typeLabel->show(); + } else { + ui->typeLabel->hide(); + } + ui->expirationLabel->setText(GUIUtil::dateTimeStr(poll_item.m_expiration)); ui->voteCountLabel->setText(QString::number(poll_item.m_total_votes)); ui->totalWeightLabel->setText(QString::number(poll_item.m_total_weight)); ui->activeVoteWeightLabel->setText(QString::number(poll_item.m_active_weight)); ui->votePercentAVWLabel->setText(QString::number(poll_item.m_vote_percent_AVW, 'f', 4) + '\%'); + + if (!(poll_item.m_weight_type == (int)GRC::PollWeightType::BALANCE || + poll_item.m_weight_type == (int)GRC::PollWeightType::BALANCE_AND_MAGNITUDE)) { + ui->balanceLabel->hide(); + } + + if (!(poll_item.m_weight_type == (int)GRC::PollWeightType::MAGNITUDE || + poll_item.m_weight_type == (int)GRC::PollWeightType::BALANCE_AND_MAGNITUDE)) { + ui->magnitudeLabel->hide(); + } + + if (poll_item.m_validated.toString() == QString{} || (!poll_item.m_validated.toBool() && !poll_item.m_finished)) { + // Hide both validated and invalid tags if less than v3 poll, or, not valid and not finished + ui->validatedLabel->hide(); + ui->invalidLabel->hide(); + } else if (poll_item.m_validated.toBool()) { + // Show validated if v3 poll and valid by vote weight % of AVW, even if not finished + ui->validatedLabel->show(); + ui->invalidLabel->hide(); + } else if (!poll_item.m_validated.toBool() && poll_item.m_finished) { + // Show invalid if v3 poll and invalid by vote weight % of AVW and finished + ui->validatedLabel->hide(); + ui->invalidLabel->show(); + } + ui->topAnswerLabel->setText(poll_item.m_top_answer); if (!poll_item.m_finished) { diff --git a/src/qt/voting/polltablemodel.cpp b/src/qt/voting/polltablemodel.cpp index 487eaf1685..1bf155df4d 100644 --- a/src/qt/voting/polltablemodel.cpp +++ b/src/qt/voting/polltablemodel.cpp @@ -23,11 +23,13 @@ class PollTableDataModel : public QAbstractTableModel m_columns << tr("Title") + << tr("Poll Type") << tr("Expiration") << tr("Weight Type") << tr("Votes") << tr("Total Weight") << tr("% of Active Vote Weight") + << tr("Validated") << tr("Top Answer"); } @@ -60,16 +62,24 @@ class PollTableDataModel : public QAbstractTableModel switch (index.column()) { case PollTableModel::Title: return row->m_title; + case PollTableModel::PollType: + if (row->m_version >= 3) { + return row->m_type_str; + } else { + return QString{}; + } case PollTableModel::Expiration: return GUIUtil::dateTimeStr(row->m_expiration); case PollTableModel::WeightType: - return row->m_weight_type; + return row->m_weight_type_str; case PollTableModel::TotalVotes: return row->m_total_votes; case PollTableModel::TotalWeight: return QString::number(row->m_total_weight); case PollTableModel::VotePercentAVW: return QString::number(row->m_vote_percent_AVW, 'f', 4); + case PollTableModel::Validated: + return row->m_validated; case PollTableModel::TopAnswer: return row->m_top_answer; } // no default case, so the compiler can warn about missing cases @@ -82,6 +92,8 @@ class PollTableDataModel : public QAbstractTableModel case PollTableModel::TotalWeight: // Pass-through case case PollTableModel::VotePercentAVW: + // Pass-through case + case PollTableModel::Validated: return QVariant(Qt::AlignRight | Qt::AlignVCenter); } break; @@ -90,16 +102,20 @@ class PollTableDataModel : public QAbstractTableModel switch (index.column()) { case PollTableModel::Title: return row->m_title; + case PollTableModel::PollType: + return row->m_type_str; case PollTableModel::Expiration: return row->m_expiration; case PollTableModel::WeightType: - return row->m_weight_type; + return row->m_weight_type_str; case PollTableModel::TotalVotes: return row->m_total_votes; case PollTableModel::TotalWeight: return QVariant::fromValue(row->m_total_weight); case PollTableModel::VotePercentAVW: return QVariant::fromValue(row->m_vote_percent_AVW); + case PollTableModel::Validated: + return row->m_validated; case PollTableModel::TopAnswer: return row->m_top_answer; } // no default case, so the compiler can warn about missing cases diff --git a/src/qt/voting/polltablemodel.h b/src/qt/voting/polltablemodel.h index 6ec6c01c54..c6c0d3257a 100644 --- a/src/qt/voting/polltablemodel.h +++ b/src/qt/voting/polltablemodel.h @@ -22,11 +22,13 @@ class PollTableModel : public QSortFilterProxyModel enum ColumnIndex { Title, + PollType, Expiration, WeightType, TotalVotes, TotalWeight, VotePercentAVW, + Validated, TopAnswer, }; diff --git a/src/qt/voting/pollwizarddetailspage.cpp b/src/qt/voting/pollwizarddetailspage.cpp index cad4399096..b83c447a1a 100644 --- a/src/qt/voting/pollwizarddetailspage.cpp +++ b/src/qt/voting/pollwizarddetailspage.cpp @@ -229,7 +229,7 @@ void PollWizardDetailsPage::initializePage() ui->durationField->setMinimum(poll_type.m_min_duration_days); ui->durationField->setValue(poll_type.m_min_duration_days); - if (type_id != PollTypes::PollTypeSurvey) { + if (type_id != (int) GRC::PollType::SURVEY) { ui->pollTypeAlert->show(); ui->weightTypeList->setCurrentIndex(1); // Magnitude+Balance ui->weightTypeList->setDisabled(true); @@ -238,7 +238,7 @@ void PollWizardDetailsPage::initializePage() ui->weightTypeList->setEnabled(true); } - if (type_id == PollTypes::PollTypeProject) { + if (type_id == (int) GRC::PollType::PROJECT) { ui->titleField->setText(QStringLiteral("[%1] %2") .arg(poll_type.m_name) .arg(field("projectPollTitle").toString())); @@ -267,7 +267,11 @@ bool PollWizardDetailsPage::validatePage() return false; } + const int type_id = field("pollType").toInt(); + const GRC::PollType& core_poll_type = GRC::Poll::POLL_TYPES[type_id]; + const VotingResult result = m_voting_model->sendPoll( + core_poll_type, field("title").toString(), field("durationDays").toInt(), field("question").toString(), diff --git a/src/qt/voting/pollwizardtypepage.cpp b/src/qt/voting/pollwizardtypepage.cpp index d4b2141c5c..0a9e3f5ee9 100644 --- a/src/qt/voting/pollwizardtypepage.cpp +++ b/src/qt/voting/pollwizardtypepage.cpp @@ -32,7 +32,7 @@ PollWizardTypePage::PollWizardTypePage(QWidget* parent) type_proxy->setVisible(false); registerField("pollType*", type_proxy); - setField("pollType", PollTypes::PollTypeUnknown); + setField("pollType", (int) GRC::PollType::UNKNOWN); #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) connect( @@ -76,7 +76,7 @@ void PollWizardTypePage::setPollTypes(const PollTypes* const poll_types) int PollWizardTypePage::nextId() const { switch (field("pollType").toInt()) { - case PollTypes::PollTypeProject: + case (int) GRC:: PollType::PROJECT: return PollWizard::PageProject; } diff --git a/src/qt/voting/votingmodel.cpp b/src/qt/voting/votingmodel.cpp index e4af144a03..165f2a1824 100644 --- a/src/qt/voting/votingmodel.cpp +++ b/src/qt/voting/votingmodel.cpp @@ -5,12 +5,14 @@ #include #include "hash.h" +#include "chainparams.h" #include "gridcoin/contract/contract.h" #include "gridcoin/project.h" #include "gridcoin/voting/builders.h" #include "gridcoin/voting/poll.h" #include "gridcoin/voting/registry.h" #include "gridcoin/voting/result.h" +#include "gridcoin/voting/payloads.h" #include "logging.h" #include "qt/clientmodel.h" #include "qt/voting/votingmodel.h" @@ -25,6 +27,7 @@ using namespace GRC; using LogFlags = BCLog::LogFlags; extern CCriticalSection cs_main; +extern int nBestHeight; namespace { //! @@ -51,25 +54,32 @@ std::optional BuildPollItem(const PollRegistry::Sequence::Iterator& it PollItem item; item.m_id = QString::fromStdString(iter->Ref().Txid().ToString()); + item.m_version = ref.GetPollPayloadVersion(); item.m_title = QString::fromStdString(poll.m_title).replace("_", " "); + item.m_type_str = QString::fromStdString(poll.PollTypeToString()); item.m_question = QString::fromStdString(poll.m_question).replace("_", " "); item.m_url = QString::fromStdString(poll.m_url).trimmed(); item.m_start_time = QDateTime::fromMSecsSinceEpoch(poll.m_timestamp * 1000); item.m_expiration = QDateTime::fromMSecsSinceEpoch(poll.Expiration() * 1000); - item.m_weight_type = QString::fromStdString(poll.WeightTypeToString()); + item.m_weight_type = poll.m_weight_type.Raw(); + item.m_weight_type_str = QString::fromStdString(poll.WeightTypeToString()); item.m_response_type = QString::fromStdString(poll.ResponseTypeToString()); item.m_total_votes = result->m_votes.size(); item.m_total_weight = result->m_total_weight / COIN; - if (auto active_vote_weight = ref.GetActiveVoteWeight(result)) { - item.m_active_weight = *active_vote_weight / COIN; - } else { - item.m_active_weight = 0; + item.m_active_weight = 0; + if (result->m_active_vote_weight) { + item.m_active_weight = *result->m_active_vote_weight / COIN; } item.m_vote_percent_AVW = 0; - if (item.m_active_weight > 0) { - item.m_vote_percent_AVW = (double) item.m_total_weight / (double) item.m_active_weight * 100.0; + if (result->m_vote_percent_avw) { + item.m_vote_percent_AVW = *result->m_vote_percent_avw; + } + + item.m_validated = QString{}; + if (result->m_poll_results_validated) { + item.m_validated = *result->m_poll_results_validated; } item.m_finished = result->m_finished; @@ -222,6 +232,7 @@ CAmount VotingModel::estimatePollFee() const } VotingResult VotingModel::sendPoll( + const PollType& type, const QString& title, const int duration_days, const QString& question, @@ -230,11 +241,30 @@ VotingResult VotingModel::sendPoll( const int response_type, const QStringList& choices) const { + // The poll types must be constrained based on the poll payload version, since < v3 only the SURVEY type is + // actually used, regardless of what is selected in the GUI. In v3+, all of the types are valid. This code + // can be removed at the next mandatory after Kermit's Mom, when PollV3Height is passed. + uint32_t payload_version = 0; + PollType type_by_poll_payload_version; + + { + LOCK(cs_main); + + bool v3_enabled = IsPollV3Enabled(nBestHeight); + + payload_version = v3_enabled ? 3 : 2; + + // This is slightly different than what is in the rpc addpoll, because the types have already been constrained + // by the GUI code. + type_by_poll_payload_version = v3_enabled ? type : PollType::SURVEY; + } + PollBuilder builder = PollBuilder(); try { builder = builder - .SetType(PollType::SURVEY) + .SetPayloadVersion(payload_version) + .SetType(type_by_poll_payload_version) .SetTitle(title.toStdString()) .SetDuration(duration_days) .SetQuestion(question.toStdString()) diff --git a/src/qt/voting/votingmodel.h b/src/qt/voting/votingmodel.h index 3b86d96e07..d3527b9217 100644 --- a/src/qt/voting/votingmodel.h +++ b/src/qt/voting/votingmodel.h @@ -8,13 +8,14 @@ #include "amount.h" #include "gridcoin/voting/filter.h" #include "qt/voting/poll_types.h" +#include "gridcoin/voting/poll.h" #include #include #include +#include namespace GRC { -class Poll; class PollRegistry; } @@ -48,18 +49,22 @@ class PollItem { public: QString m_id; + uint32_t m_version; + QString m_type_str; QString m_title; QString m_question; QString m_url; QDateTime m_start_time; QDateTime m_expiration; - QString m_weight_type; + int m_weight_type; + QString m_weight_type_str; QString m_response_type; QString m_top_answer; uint32_t m_total_votes; uint64_t m_total_weight; uint64_t m_active_weight; double m_vote_percent_AVW; + QVariant m_validated; bool m_finished; bool m_multiple_choice; std::vector m_choices; @@ -113,6 +118,7 @@ class VotingModel : public QObject CAmount estimatePollFee() const; VotingResult sendPoll( + const GRC::PollType& type, const QString& title, const int duration_days, const QString& question, @@ -120,12 +126,13 @@ class VotingModel : public QObject const int weight_type, const int response_type, const QStringList& choices) const; + VotingResult sendVote( const QString& poll_id, const std::vector& choice_offsets) const; signals: - void newPollReceived() const; + void newPollReceived(); private: GRC::PollRegistry& m_registry; diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 00a4c9cda7..56b522906a 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -233,9 +233,9 @@ static const CRPCConvertParam vRPCConvertParams[] = { "showblock" , 0 }, // Voting - { "addpoll" , 1 }, - { "addpoll" , 4 }, + { "addpoll" , 2 }, { "addpoll" , 5 }, + { "addpoll" , 6 }, { "listpolls" , 0 }, { "votebyid" , 1 }, { "votebyid" , 2 }, diff --git a/src/rpc/voting.cpp b/src/rpc/voting.cpp index d0c1e46104..1bcecf4865 100644 --- a/src/rpc/voting.cpp +++ b/src/rpc/voting.cpp @@ -51,6 +51,23 @@ UniValue PollChoicesToJson(const Poll::ChoiceList& choices) return json; } +UniValue PollAdditionalFieldsToJson(const Poll::AdditionalFieldList fields) +{ + UniValue json(UniValue::VARR); + + for (size_t i = 0; i < fields.size(); ++i) { + UniValue field(UniValue::VOBJ); + + field.pushKV("name", fields.At(i)->m_name); + field.pushKV("value", fields.At(i)->m_value); + field.pushKV("required", fields.At(i)->m_required); + + json.push_back(field); + } + + return json; +} + UniValue PollToJson(const Poll& poll, const uint256 txid) { UniValue json(UniValue::VOBJ); @@ -59,9 +76,13 @@ UniValue PollToJson(const Poll& poll, const uint256 txid) json.pushKV("id", txid.ToString()); json.pushKV("question", poll.m_question); json.pushKV("url", poll.m_url); - json.pushKV("sharetype", poll.WeightTypeToString()); - json.pushKV("weight_type", (int)poll.m_weight_type.Raw()); - json.pushKV("response_type", (int)poll.m_response_type.Raw()); + json.pushKV("additional_fields", PollAdditionalFieldsToJson(poll.AdditionalFields())); + json.pushKV("poll_type", poll.PollTypeToString()); + json.pushKV("poll_type_id", (int)poll.m_type.Raw()); + json.pushKV("weight_type", poll.WeightTypeToString()); + json.pushKV("weight_type_id", (int)poll.m_weight_type.Raw()); + json.pushKV("response_type", poll.ResponseTypeToString()); + json.pushKV("response_type_id", (int)poll.m_response_type.Raw()); json.pushKV("duration_days", (int)poll.m_duration_days); json.pushKV("expiration", TimestampToHRDate(poll.Expiration())); json.pushKV("timestamp", TimestampToHRDate(poll.m_timestamp)); @@ -98,9 +119,16 @@ UniValue PollResultToJson(const PollResult& result, const PollReference& poll_re json.pushKV("invalid_votes", (uint64_t)result.m_invalid_votes); json.pushKV("total_weight", ValueFromAmount(result.m_total_weight)); - if (auto active_vote_weight = poll_ref.GetActiveVoteWeight(result)) { - json.pushKV("active_vote_weight", ValueFromAmount(*active_vote_weight)); - json.pushKV("vote_percent_avw", (double) result.m_total_weight / (double) *active_vote_weight * 100.0); + if (result.m_active_vote_weight) { + json.pushKV("active_vote_weight", ValueFromAmount(*result.m_active_vote_weight)); + } + + if (result.m_vote_percent_avw) { + json.pushKV("vote_percent_avw", *result.m_vote_percent_avw); + } + + if (result.m_poll_results_validated) { + json.pushKV("poll_results_validated", *result.m_poll_results_validated); } if (!result.m_votes.empty()) { @@ -283,35 +311,170 @@ UniValue SubmitVote(const Poll& poll, VoteBuilder builder) UniValue addpoll(const UniValue& params, bool fHelp) { - if (fHelp || params.size() != 7) - throw std::runtime_error( - "addpoll <days> <question> <answer1;answer2...> <weighttype> <responsetype> <url>\n" - "\n" - "<title> --------> Title for the poll\n" - "<days> ---------> Number of days that the poll will run\n" - "<question> -----> Prompt that voters shall answer\n" - "<answers> ------> Answers for voters to choose from. Separate answers with semicolons (;)\n" - "<weighttype> ---> Weighing method for the poll: 1 = Balance, 2 = Magnitude + Balance\n" - "<responsetype> -> 1 = yes/no/abstain, 2 = single-choice, 3 = multiple-choice\n" - "<url> ----------> Discussion web page URL for the poll\n" - "\n" - "Add a poll to the network.\n" - "Requires 100K GRC balance. Costs 50 GRC.\n" - "Provide an empty string for <answers> when choosing \"yes/no/abstain\" for <responsetype>.\n"); + uint32_t payload_version = 0; + std::vector<PollType> valid_poll_types; + + { + if (OutOfSyncByAge()) { + throw JSONRPCError(RPC_MISC_ERROR, "Cannot add a poll with a wallet that is not in sync."); + } + + LOCK(cs_main); + + payload_version = IsPollV3Enabled(nBestHeight) ? 3 : 2; + + valid_poll_types = GRC::PollPayload::GetValidPollTypes(payload_version); + } + + std::stringstream types_ss; + + for (const auto& type : valid_poll_types) { + if (types_ss.str() != std::string{}) { + types_ss << ", "; + } + + types_ss << ToLower(Poll::PollTypeToString(type, false)); + } + + if (params.size() == 0) { + std::string e = strprintf( + "addpoll <type> <title> <days> <question> <answer1;answer2...> <weighttype> <responsetype> <url> " + "<required_field_name1=value1;required_field_name2=value2...>\n" + "\n" + "<type> -----------> Type of poll. Valid types are: %s.\n" + "<title> ----------> Title for the poll\n" + "<days> -----------> Number of days that the poll will run\n" + "<question> -------> Prompt that voters shall answer\n" + "<answers> --------> Answers for voters to choose from. Separate answers with semicolons (;)\n" + "<weighttype> -----> Weighing method for the poll: 1 = Balance, 2 = Magnitude + Balance\n" + "<responsetype> ---> 1 = yes/no/abstain, 2 = single-choice, 3 = multiple-choice\n" + "<url> ------------> Discussion web page URL for the poll\n" + "<required fields>-> Required additional field(s) if any (see below)\n" + "\n" + "Add a poll to the network.\n" + "Requires 100K GRC balance. Costs 50 GRC.\n" + "Provide an empty string for <answers> when choosing \"yes/no/abstain\" for <responsetype>.\n" + "Certain poll types may require additional fields. You can see these with addpoll <type> \n" + "with no other parameters.", + types_ss.str()); + + throw std::runtime_error(e); + } + + std::string type_string = ToLower(params[0].get_str()); + + PollType poll_type; + + bool valid_type_parameter = false; + + for (const auto& type : valid_poll_types) { + if (ToLower(Poll::PollTypeToString(type, false)) == type_string) { + poll_type = type; + valid_type_parameter = true; + break; + } + } + + if (!valid_type_parameter) { + std::string e = strprintf("Invalid poll type specified. Valid types are %s.", types_ss.str()); + + throw JSONRPCError(RPC_INVALID_PARAMETER, e); + } + + const std::vector<std::string>& required_fields = Poll::POLL_TYPE_RULES[(int) poll_type].m_required_fields; + std::stringstream required_fields_ss; + + for (const auto& required_field : required_fields) { + if (required_fields_ss.str() != std::string{}) { + required_fields_ss << ", "; + } + + required_fields_ss << required_field; + } + + if (params.size() == 1) { + std::string e = strprintf( + "For addpoll %s, the required fields are the following: %s.\n", + ToLower(params[0].get_str()), + required_fields.empty() ? "none" : required_fields_ss.str()); + + throw std::runtime_error(e); + } + + size_t required_number_of_params = required_fields.empty() ? 8 : 9; + + if (fHelp || params.size() < required_number_of_params) { + std::string e = strprintf( + "addpoll <type> <title> <days> <question> <answer1;answer2...> <weighttype> <responsetype> <url> " + "<required_field_name1=value1;required_field_name2=value2...>\n" + "\n" + "<type> -----------> Type of poll. Valid types are: %s.\n" + "<title> ----------> Title for the poll\n" + "<days> -----------> Number of days that the poll will run\n" + "<question> -------> Prompt that voters shall answer\n" + "<answers> --------> Answers for voters to choose from. Separate answers with semicolons (;)\n" + "<weighttype> -----> Weighing method for the poll: 1 = Balance, 2 = Magnitude + Balance\n" + "<responsetype> ---> 1 = yes/no/abstain, 2 = single-choice, 3 = multiple-choice\n" + "<url> ------------> Discussion web page URL for the poll\n" + "<required fields>-> Required additional field(s) if any (see below)\n" + "\n" + "Add a poll to the network.\n" + "Requires 100K GRC balance. Costs 50 GRC.\n" + "Provide an empty string for <answers> when choosing \"yes/no/abstain\" for <responsetype>.\n" + "Certain poll types may require additional fields. You can see these with addpoll <type> \n" + "with no other parameters.", + types_ss.str()); + + throw std::runtime_error(e); + } EnsureWalletIsUnlocked(); PollBuilder builder = PollBuilder() - .SetType(PollType::SURVEY) - .SetTitle(params[0].get_str()) - .SetDuration(params[1].get_int()) - .SetQuestion(params[2].get_str()) - .SetWeightType(params[4].get_int() + 1) - .SetResponseType(params[5].get_int()) - .SetUrl(params[6].get_str()); - - if (!params[3].isNull() && !params[3].get_str().empty()) { - builder = builder.SetChoices(split(params[3].get_str(), ";")); + .SetPayloadVersion(payload_version) + .SetType(poll_type) + .SetTitle(params[1].get_str()) + .SetDuration(params[2].get_int()) + .SetQuestion(params[3].get_str()) + .SetWeightType(params[5].get_int() + 1) + .SetResponseType(params[6].get_int()) + .SetUrl(params[7].get_str()); + + if (!params[4].isNull() && !params[4].get_str().empty()) { + builder = builder.SetChoices(split(params[4].get_str(), ";")); + } + + if (params.size() == 9 && !params[8].isNull() && !params[8].get_str().empty()) { + std::vector<std::string> name_value_pairs = split(params[8].get_str(), ";"); + Poll::AdditionalFieldList fields; + + for (const auto& name_value_pair : name_value_pairs) { + std::vector v_field = split(name_value_pair, "="); + bool required = true; + + if (v_field.size() != 2) { + throw std::runtime_error("Required fields parameter for poll is malformed."); + } + + std::string field_name = TrimString(v_field[0]); + std::string field_value = TrimString(v_field[1]); + + if (std::find(required_fields.begin(), required_fields.end(), field_name) == required_fields.end()) { + required = false; + } + + Poll::AdditionalField field(field_name, field_value, required); + + fields.Add(field); + } + + // TODO: Extend Wellformed to do a duplicate check on the field name? This is done in the builder anyway. This + // makes sure that at least the required fields have been provided and that they are well formed. + if (!fields.WellFormed(poll_type)) { + throw std::runtime_error("Required field list is malformed."); + } + + builder = builder.AddAdditionalFields(fields); } std::pair<CWalletTx, std::string> result_pair;