PIVX Core  5.6.99
P2P Digital Currency
governancemodel.cpp
Go to the documentation of this file.
1 // Copyright (c) 2021-2022 The PIVX Core developers
2 // Distributed under the MIT software license, see the accompanying
3 // file COPYING or https://www.opensource.org/licenses/mit-license.php.
4 
5 #include "governancemodel.h"
6 
7 #include "budget/budgetmanager.h"
8 #include "budget/budgetutil.h"
9 #include "destination_io.h"
10 #include "guiconstants.h"
11 #include "mnmodel.h"
12 #include "qt/transactionrecord.h"
15 #include "utilmoneystr.h"
16 #include "utilstrencodings.h"
17 #include "walletmodel.h"
18 
19 #include <algorithm>
20 #include <QTimer>
21 
22 std::string ProposalInfo::statusToStr() const
23 {
24  switch(status) {
26  return _("Waiting");
27  case PASSING:
28  return _("Passing");
29  case PASSING_NOT_FUNDED:
30  return _("Passing not funded");
31  case NOT_PASSING:
32  return _("Not Passing");
33  case FINISHED:
34  return _("Finished");
35  }
36  return "";
37 }
38 
39 GovernanceModel::GovernanceModel(ClientModel* _clientModel, MNModel* _mnModel) : clientModel(_clientModel), mnModel(_mnModel) {}
41 
43 {
44  walletModel = _walletModel;
46 }
47 
48 ProposalInfo GovernanceModel::buildProposalInfo(const CBudgetProposal* prop, bool isPassing, bool isPending)
49 {
50  CTxDestination recipient;
51  ExtractDestination(prop->GetPayee(), recipient);
52 
53  // Calculate status
54  int votesYes = prop->GetYeas();
55  int votesNo = prop->GetNays();
56  int mnCount = clientModel->getMasternodesCount();
57  int remainingPayments = prop->GetRemainingPaymentCount(clientModel->getLastBlockProcessedHeight());
58  ProposalInfo::Status status;
59 
60  if (isPending) {
61  // Proposal waiting for confirmation to be broadcasted.
63  } else {
64  if (remainingPayments <= 0) {
65  status = ProposalInfo::FINISHED;
66  } else if (isPassing) {
67  status = ProposalInfo::PASSING;
68  } else if (allocatedAmount + prop->GetAmount() > getMaxAvailableBudgetAmount() && votesYes - votesNo > mnCount / 10) {
70  } else {
72  }
73  }
74 
75  return ProposalInfo(prop->GetHash(),
76  prop->GetName(),
77  prop->GetURL(),
78  votesYes,
79  votesNo,
80  Standard::EncodeDestination(recipient),
81  prop->GetAmount(),
82  prop->GetTotalPaymentCount(),
83  remainingPayments,
84  status,
85  prop->GetBlockStart(),
86  prop->GetBlockEnd());
87 }
88 
89 std::list<ProposalInfo> GovernanceModel::getProposals(const ProposalInfo::Status* filterByStatus, bool filterFinished)
90 {
91  if (!clientModel) return {};
92  std::list<ProposalInfo> ret;
93  std::vector<CBudgetProposal> budget = g_budgetman.GetBudget();
94  allocatedAmount = 0;
95  for (const auto& prop : g_budgetman.GetAllProposalsOrdered()) {
96  bool isPassing = std::find(budget.begin(), budget.end(), *prop) != budget.end();
97  ProposalInfo propInfo = buildProposalInfo(prop, isPassing, false);
98 
99  if (filterFinished && propInfo.isFinished()) continue;
100  if (!filterByStatus || propInfo.status == *filterByStatus) {
101  ret.emplace_back(propInfo);
102  }
103  if (isPassing) allocatedAmount += prop->GetAmount();
104  }
105 
106  // Add pending proposals
107  for (const auto& prop : waitingPropsForConfirmations) {
108  ProposalInfo propInfo = buildProposalInfo(&prop, false, true);
109  if (!filterByStatus || propInfo.status == *filterByStatus) {
110  ret.emplace_back(propInfo);
111  }
112  }
113  return ret;
114 }
115 
117 {
119 }
120 
122 {
124 }
125 
127 {
129 }
130 
132 {
133  return BUDGET_VOTE_UPDATE_MIN;
134 }
135 
137 {
139 }
140 
142 {
143  return PROPOSAL_FEE_TX;
144 }
145 
147 {
148  const int nBlocksPerCycle = getNumBlocksPerBudgetCycle();
149  const int chainHeight = clientModel->getNumBlocks();
150  return chainHeight - chainHeight % nBlocksPerCycle + nBlocksPerCycle;
151 }
152 
153 std::vector<VoteInfo> GovernanceModel::getLocalMNsVotesForProposal(const ProposalInfo& propInfo)
154 {
155  // First, get the local masternodes
156  std::vector<std::pair<COutPoint, std::string>> vecLocalMn;
157  for (int i = 0; i < mnModel->rowCount(); ++i) {
158  vecLocalMn.emplace_back(std::make_pair(
159  COutPoint(uint256S(mnModel->index(i, MNModel::COLLATERAL_ID, QModelIndex()).data().toString().toStdString()),
160  mnModel->index(i, MNModel::COLLATERAL_OUT_INDEX, QModelIndex()).data().toInt()),
161  mnModel->index(i, MNModel::ALIAS, QModelIndex()).data().toString().toStdString())
162  );
163  }
164 
165  std::vector<VoteInfo> localVotes;
166  {
167  LOCK(g_budgetman.cs_proposals); // future: encapsulate this mutex lock.
168  // Get the budget proposal, get the votes, then loop over it and return the ones that correspond to the local masternodes here.
169  CBudgetProposal* prop = g_budgetman.FindProposal(propInfo.id);
170  const auto& mapVotes = prop->GetVotes();
171  for (const auto& it : mapVotes) {
172  for (const auto& mn : vecLocalMn) {
173  if (it.first == mn.first && it.second.IsValid()) {
174  localVotes.emplace_back(mn.first, (VoteInfo::VoteDirection) it.second.GetDirection(), mn.second, it.second.GetTime());
175  break;
176  }
177  }
178  }
179  }
180  return localVotes;
181 }
182 
184 {
185  std::string strName = SanitizeString(name.toStdString());
186  if (strName != name.toStdString()) { // invalid characters
187  return {false, _("Invalid name, invalid characters")};
188  }
189  if (strName.size() > (int)PROP_NAME_MAX_SIZE) { // limit
190  return {false, strprintf(_("Invalid name, maximum size of %d exceeded"), PROP_NAME_MAX_SIZE)};
191  }
192  return {true};
193 }
194 
196 {
197  std::string strURL = SanitizeString(url.toStdString());
198  if (strURL != url.toStdString()) {
199  return {false, _("Invalid URL, invalid characters")};
200  }
201  std::string strError;
202  return {validateURL(strURL, strError, PROP_URL_MAX_SIZE), strError};
203 }
204 
206 {
207  if (amount < PROPOSAL_MIN_AMOUNT) { // Future: move constant to a budget interface.
208  return {false, strprintf(_("Amount below the minimum of %s PIV"), FormatMoney(PROPOSAL_MIN_AMOUNT))};
209  }
210 
211  if (amount > getMaxAvailableBudgetAmount()) {
212  return {false, strprintf(_("Amount exceeding the maximum available of %s PIV"), FormatMoney(getMaxAvailableBudgetAmount()))};
213  }
214  return {true};
215 }
216 
218 {
219  if (paymentCount < 1) return { false, _("Invalid payment count, must be greater than zero.")};
220  int nMaxPayments = getPropMaxPaymentsCount();
221  if (paymentCount > nMaxPayments) {
222  return { false, strprintf(_("Invalid payment count, cannot be greater than %d"), nMaxPayments)};
223  }
224  return {true};
225 }
226 
228 {
230 }
231 
232 OperationResult GovernanceModel::createProposal(const std::string& strProposalName,
233  const std::string& strURL,
234  int nPaymentCount,
235  CAmount nAmount,
236  const std::string& strPaymentAddr)
237 {
238  // First get the next superblock height
239  int nBlockStart = getNextSuperblockHeight();
240 
241  // Parse address
243  if (!dest) return {false, _("invalid recipient address for the proposal")};
244  CScript scriptPubKey = GetScriptForDestination(*dest);
245 
246  // Validate proposal
247  CBudgetProposal proposal(strProposalName, strURL, nPaymentCount, scriptPubKey, nAmount, nBlockStart, UINT256_ZERO);
248  if (!proposal.IsWellFormed(g_budgetman.GetTotalBudget(proposal.GetBlockStart()))) {
249  return {false, strprintf(_("Proposal is not valid %s"), proposal.IsInvalidReason())};
250  }
251 
252  // Craft and send transaction.
253  auto opRes = walletModel->createAndSendProposalFeeTx(proposal);
254  if (!opRes) return opRes;
255  scheduleBroadcast(proposal);
256 
257  return {true};
258 }
259 
261  bool isVotePositive,
262  const std::vector<std::string>& mnVotingAlias)
263 {
264  UniValue ret; // future: don't use UniValue here.
265  for (const auto& mnAlias : mnVotingAlias) {
266  bool fLegacyMN = true; // For now, only legacy MNs
267  ret = mnBudgetVoteInner(nullptr,
268  fLegacyMN,
269  prop.id,
270  false,
271  isVotePositive ? CBudgetVote::VoteDirection::VOTE_YES : CBudgetVote::VoteDirection::VOTE_NO,
272  mnAlias);
273  if (ret.exists("detail") && ret["detail"].isArray()) {
274  const UniValue& obj = ret["detail"].get_array()[0];
275  if (obj["result"].getValStr() != "success") {
276  return {false, obj["error"].getValStr()};
277  }
278  }
279  }
280  // add more information with ret["overall"]
281  return {true};
282 }
283 
285 {
286  // Cache the proposal to be sent as soon as it gets the minimum required confirmations
287  // without requiring user interaction
288  waitingPropsForConfirmations.emplace_back(proposal);
289 
290  // Launch timer if it's not already running
291  if (!pollTimer) pollTimer = new QTimer(this);
292  if (!pollTimer->isActive()) {
293  connect(pollTimer, &QTimer::timeout, this, &GovernanceModel::pollGovernanceChanged);
294  pollTimer->start(MODEL_UPDATE_DELAY * 60 * (walletModel->isTestNetwork() ? 0.5 : 3.5)); // Every 3.5 minutes
295  }
296 }
297 
299 {
300  if (!isTierTwoSync()) return;
301 
302  int chainHeight = clientModel->getNumBlocks();
303  // Try to broadcast any pending for confirmations proposal
304  auto it = waitingPropsForConfirmations.begin();
305  while (it != waitingPropsForConfirmations.end()) {
306  // Remove expired proposals
307  if (it->IsExpired(clientModel->getNumBlocks())) {
308  it = waitingPropsForConfirmations.erase(it);
309  continue;
310  }
311 
312  // Try to add it
313  if (!g_budgetman.AddProposal(*it)) {
314  LogPrint(BCLog::QT, "Cannot broadcast budget proposal - %s\n", it->IsInvalidReason());
315  // Remove proposals which due a reorg lost their fee tx
316  if (it->IsInvalidReason().find("Can't find collateral tx") != std::string::npos) {
317  // future: notify the user about it.
318  it = waitingPropsForConfirmations.erase(it);
319  continue;
320  }
321  // Check if the proposal didn't exceed the superblock start height
322  if (chainHeight >= it->GetBlockStart()) {
323  // Edge case, the proposal was never broadcasted before the next superblock, can be removed.
324  // future: notify the user about it.
325  it = waitingPropsForConfirmations.erase(it);
326  } else {
327  it++;
328  }
329  continue;
330  }
331  it->Relay();
332  it = waitingPropsForConfirmations.erase(it);
333  }
334 
335  // If there are no more waiting proposals, turn the timer off.
336  if (waitingPropsForConfirmations.empty()) {
337  pollTimer->stop();
338  }
339 }
340 
342 {
343  if (pollTimer && pollTimer->isActive()) {
344  pollTimer->stop();
345  }
346 }
347 
348 void GovernanceModel::txLoaded(const QString& id, const int txType, const int txStatus)
349 {
350  if (txType == TransactionRecord::SendToNobody) {
351  // If the tx is not longer available in the mainchain, drop it.
352  if (txStatus == TransactionStatus::Conflicted ||
353  txStatus == TransactionStatus::NotAccepted) {
354  return;
355  }
356  // If this is a proposal fee, parse it.
357  const auto& wtx = walletModel->getTx(uint256S(id.toStdString()));
358  assert(wtx);
359  const auto& it = wtx->mapValue.find("proposal");
360  if (it != wtx->mapValue.end()) {
361  const std::vector<unsigned char> vec = ParseHex(it->second);
362  if (vec.empty()) return;
363  CDataStream ss(vec, SER_DISK, CLIENT_VERSION);
364  CBudgetProposal proposal;
365  ss >> proposal;
366  proposal.SetFeeTxHash(wtx->GetHash());
367  if (!g_budgetman.HaveProposal(proposal.GetHash()) &&
368  !proposal.IsExpired(clientModel->getNumBlocks()) &&
369  proposal.GetBlockStart() > clientModel->getNumBlocks()) {
370  scheduleBroadcast(proposal);
371  }
372  }
373  }
374 }
int64_t CAmount
Amount in PIV (Can be negative)
Definition: amount.h:13
CBudgetManager g_budgetman
UniValue mnBudgetVoteInner(CWallet *const pwallet, bool fLegacyMN, const uint256 &budgetHash, bool fFinal, const CBudgetVote::VoteDirection &nVote, const Optional< std::string > &mnAliasFilter)
Definition: budgetutil.cpp:235
const CChainParams & Params()
Return the currently selected parameters.
bool AddProposal(CBudgetProposal &budgetProposal)
bool HaveProposal(const uint256 &propHash) const
Definition: budgetmanager.h:72
static CAmount GetTotalBudget(int nHeight)
bool HasAnyProposal() const
CBudgetProposal * FindProposal(const uint256 &nHash)
std::vector< CBudgetProposal > GetBudget()
RecursiveMutex cs_proposals
Definition: budgetmanager.h:61
std::vector< CBudgetProposal * > GetAllProposalsOrdered()
int GetYeas() const
int GetRemainingPaymentCount(int nCurrentHeight) const
bool IsExpired(int nCurrentHeight) const
int GetBlockStart() const
std::map< COutPoint, CBudgetVote > GetVotes() const
std::string GetURL() const
std::string IsInvalidReason() const
void SetFeeTxHash(const uint256 &txid)
std::string GetName() const
CScript GetPayee() const
int GetBlockEnd() const
CAmount GetAmount() const
bool IsWellFormed(const CAmount &nTotalBudget)
int GetNays() const
uint256 GetHash() const
int GetTotalPaymentCount() const
const Consensus::Params & GetConsensus() const
Definition: chainparams.h:72
An outpoint - a combination of a transaction hash and an index n into its vout.
Definition: transaction.h:72
Serialized script, used inside transaction inputs and outputs.
Definition: script.h:381
Model for PIVX network client.
Definition: clientmodel.h:50
int getMasternodesCount() const
Definition: clientmodel.h:111
int getNumBlocks()
int getLastBlockProcessedHeight() const
int getPropMaxPaymentsCount() const
~GovernanceModel() override
WalletModel * walletModel
int getNextSuperblockHeight() const
GovernanceModel(ClientModel *_clientModel, MNModel *_mnModel)
OperationResult validatePropPaymentCount(int paymentCount) const
int getProposalVoteUpdateMinTime() const
std::vector< CBudgetProposal > waitingPropsForConfirmations
OperationResult validatePropURL(const QString &url) const
OperationResult validatePropName(const QString &name) const
ProposalInfo buildProposalInfo(const CBudgetProposal *prop, bool isPassing, bool isPending)
void scheduleBroadcast(const CBudgetProposal &proposal)
ClientModel * clientModel
void txLoaded(const QString &hash, const int txType, const int txStatus)
OperationResult voteForProposal(const ProposalInfo &prop, bool isVotePositive, const std::vector< std::string > &mnVotingAlias)
CAmount getProposalFeeAmount() const
OperationResult validatePropAmount(CAmount amount) const
int getNumBlocksPerBudgetCycle() const
void setWalletModel(WalletModel *_walletModel)
OperationResult createProposal(const std::string &strProposalName, const std::string &strURL, int nPaymentCount, CAmount nAmount, const std::string &strPaymentAddr)
std::list< ProposalInfo > getProposals(const ProposalInfo::Status *filterByStatus=nullptr, bool filterFinished=true)
CAmount getMaxAvailableBudgetAmount() const
std::vector< VoteInfo > getLocalMNsVotesForProposal(const ProposalInfo &propInfo)
int rowCount(const QModelIndex &parent=QModelIndex()) const override
Definition: mnmodel.cpp:55
QModelIndex index(int row, int column, const QModelIndex &parent) const override
Definition: mnmodel.cpp:125
@ ALIAS
User specified MN alias.
Definition: mnmodel.h:28
@ COLLATERAL_ID
Definition: mnmodel.h:34
@ COLLATERAL_OUT_INDEX
Definition: mnmodel.h:35
bool IsBlockchainSynced() const
@ NotAccepted
Mined but not accepted.
@ Conflicted
Conflicts with other transaction or mempool.
void txLoaded(const QString &hash, const int txType, const int txStatus)
bool isArray() const
Definition: univalue.h:83
const std::string & getValStr() const
Definition: univalue.h:65
const UniValue & get_array() const
bool exists(const std::string &key) const
Definition: univalue.h:75
Interface to PIVX wallet from Qt view code.
Definition: walletmodel.h:109
bool isTestNetwork() const
Definition: walletmodel.cpp:76
OperationResult createAndSendProposalFeeTx(CBudgetProposal &prop)
const CWalletTx * getTx(uint256 id)
TransactionTableModel * getTransactionTableModel()
@ LOCK
Definition: lockunlock.h:16
#define LogPrint(category,...)
Definition: logging.h:163
@ QT
Definition: logging.h:56
const CTxDestination * GetTransparentDestination(const CWDestination &dest)
std::string EncodeDestination(const CWDestination &address, const CChainParams::Base58Type addrType)
CWDestination DecodeDestination(const std::string &strAddress)
const char * name
Definition: rest.cpp:37
const char * url
Definition: rpcconsole.cpp:51
@ SER_DISK
Definition: serialize.h:175
CScript GetScriptForDestination(const CTxDestination &dest)
Generate a PIVX scriptPubKey for the given CTxDestination.
Definition: standard.cpp:278
bool ExtractDestination(const CScript &scriptPubKey, CTxDestination &addressRet, bool fColdStake)
Parse a standard scriptPubKey for the destination address.
Definition: standard.cpp:162
boost::variant< CNoDestination, CKeyID, CScriptID, CExchangeKeyID > CTxDestination
A txout script template with a specific destination.
Definition: standard.h:72
int nMaxProposalPayments
Definition: params.h:195
int nBudgetCycleBlocks
Definition: params.h:178
bool isFinished() const
uint256 id
Proposal hash.
std::string statusToStr() const
Status status
Proposal state.
std::string _(const char *psz)
Translation function: Call Translate signal on UI interface, which returns a Optional result.
Definition: system.h:65
TierTwoSyncState g_tiertwo_sync_state
#define strprintf
Definition: tinyformat.h:1056
const uint256 UINT256_ZERO
constant uint256 instances
Definition: uint256.h:175
uint256 uint256S(const char *str)
Definition: uint256.h:157
std::string FormatMoney(const CAmount &n, bool fPlus)
Money parsing/formatting utilities.
std::vector< unsigned char > ParseHex(const char *psz)
bool validateURL(const std::string &strURL)
std::string SanitizeString(const std::string &str, int rule)
Remove unsafe chars.