Line data Source code
1 : // Copyright (c) 2014-2025 The Dash Core developers
2 : // Distributed under the MIT software license, see the accompanying
3 : // file COPYING or http://www.opensource.org/licenses/mit-license.php.
4 :
5 : #include <governance/signing.h>
6 :
7 : #include <active/masternode.h>
8 : #include <evo/deterministicmns.h>
9 : #include <governance/governance.h>
10 : #include <governance/superblock.h>
11 : #include <masternode/sync.h>
12 :
13 : #include <chainparams.h>
14 : #include <logging.h>
15 : #include <timedata.h>
16 : #include <util/time.h>
17 : #include <validation.h>
18 :
19 : #include <algorithm>
20 : #include <ranges>
21 :
22 : namespace {
23 : constexpr std::chrono::seconds GOVERNANCE_FUDGE_WINDOW{2h};
24 : } // anonymous namespace
25 :
26 0 : GovernanceSigner::GovernanceSigner(CConnman& connman, CDeterministicMNManager& dmnman, CGovernanceManager& govman,
27 : governance::SuperblockManager& superblocks, const CActiveMasternodeManager& mn_activeman,
28 : const ChainstateManager& chainman, const CMasternodeSync& mn_sync) :
29 0 : m_connman{connman},
30 0 : m_dmnman{dmnman},
31 0 : m_govman{govman},
32 0 : m_superblocks{superblocks},
33 0 : m_mn_activeman{mn_activeman},
34 0 : m_chainman{chainman},
35 0 : m_mn_sync{mn_sync}
36 0 : {
37 0 : }
38 :
39 0 : GovernanceSigner::~GovernanceSigner() = default;
40 :
41 0 : std::optional<const CSuperblock> GovernanceSigner::CreateSuperblockCandidate(int nHeight) const
42 : {
43 0 : if (!m_govman.IsValid()) return std::nullopt;
44 0 : if (!m_mn_sync.IsSynced()) return std::nullopt;
45 0 : if (nHeight % Params().GetConsensus().nSuperblockCycle <
46 0 : Params().GetConsensus().nSuperblockCycle - Params().GetConsensus().nSuperblockMaturityWindow)
47 0 : return std::nullopt;
48 0 : if (HasAlreadyVotedFundingTrigger()) return std::nullopt;
49 :
50 0 : const auto approvedProposals = m_govman.GetApprovedProposals(m_dmnman.GetListAtChainTip());
51 0 : if (approvedProposals.empty()) {
52 0 : LogPrint(BCLog::GOBJECT, "%s -- nHeight:%d empty approvedProposals\n", __func__, nHeight);
53 0 : return std::nullopt;
54 : }
55 :
56 0 : std::vector<CGovernancePayment> payments;
57 : int nLastSuperblock;
58 : int nNextSuperblock;
59 :
60 0 : CSuperblock::GetNearestSuperblocksHeights(nHeight, nLastSuperblock, nNextSuperblock);
61 0 : auto SBEpochTime = static_cast<int64_t>(GetTime<std::chrono::seconds>().count() +
62 0 : (nNextSuperblock - nHeight) * 2.62 * 60);
63 0 : auto governanceBudget = CSuperblock::GetPaymentsLimit(m_chainman.ActiveChain(), nNextSuperblock);
64 :
65 0 : CAmount budgetAllocated{};
66 0 : for (const auto& proposal : approvedProposals) {
67 : // Extract payment address and amount from proposal
68 0 : UniValue jproposal = proposal->GetJSONObject();
69 :
70 0 : CTxDestination dest = DecodeDestination(jproposal["payment_address"].getValStr());
71 0 : if (!IsValidDestination(dest)) continue;
72 :
73 0 : CAmount nAmount{};
74 : try {
75 0 : nAmount = ParsePaymentAmount(jproposal["payment_amount"].getValStr());
76 0 : } catch (const std::runtime_error& e) {
77 0 : LogPrint(BCLog::GOBJECT, "%s -- nHeight:%d Skipping payment exception:%s\n", __func__, nHeight, e.what());
78 : continue;
79 0 : }
80 :
81 : // Construct CGovernancePayment object and make sure it is valid
82 0 : CGovernancePayment payment(dest, nAmount, proposal->GetHash());
83 0 : if (!payment.IsValid()) continue;
84 :
85 : // Skip proposals that are too expensive
86 0 : if (budgetAllocated + payment.nAmount > governanceBudget) continue;
87 :
88 0 : int64_t windowStart = jproposal["start_epoch"].getInt<int64_t>() - count_seconds(GOVERNANCE_FUDGE_WINDOW);
89 0 : int64_t windowEnd = jproposal["end_epoch"].getInt<int64_t>() + count_seconds(GOVERNANCE_FUDGE_WINDOW);
90 :
91 : // Skip proposals if the SB isn't within the proposal time window
92 0 : if (SBEpochTime < windowStart) {
93 0 : LogPrint(BCLog::GOBJECT, "%s -- nHeight:%d SB:%d windowStart:%d\n", __func__, nHeight, SBEpochTime,
94 : windowStart);
95 0 : continue;
96 : }
97 0 : if (SBEpochTime > windowEnd) {
98 0 : LogPrint(BCLog::GOBJECT, "%s -- nHeight:%d SB:%d windowEnd:%d\n", __func__, nHeight, SBEpochTime, windowEnd);
99 0 : continue;
100 : }
101 :
102 : // Keep track of total budget allocation
103 0 : budgetAllocated += payment.nAmount;
104 :
105 : // Add the payment
106 0 : payments.push_back(payment);
107 0 : }
108 :
109 : // No proposals made the cut
110 0 : if (payments.empty()) {
111 0 : LogPrint(BCLog::GOBJECT, "%s -- CreateSuperblockCandidate nHeight:%d empty payments\n", __func__, nHeight);
112 0 : return std::nullopt;
113 : }
114 :
115 : // Sort by proposal hash descending
116 0 : std::sort(payments.begin(), payments.end(), [](const CGovernancePayment& a, const CGovernancePayment& b) {
117 0 : return UintToArith256(a.proposalHash) > UintToArith256(b.proposalHash);
118 : });
119 :
120 : // Create Superblock
121 0 : return CSuperblock(nNextSuperblock, std::move(payments));
122 0 : }
123 :
124 0 : std::optional<const CGovernanceObject> GovernanceSigner::CreateGovernanceTrigger(const std::optional<const CSuperblock>& sb_opt)
125 : {
126 : // no sb_opt, no trigger
127 0 : if (!sb_opt.has_value()) return std::nullopt;
128 :
129 : // TODO: Check if nHashParentIn, nRevision and nCollateralHashIn are correct
130 0 : LOCK(::cs_main);
131 :
132 : // Check if identical trigger (equal DataHash()) is already created (signed by other masternode)
133 0 : CGovernanceObject gov_sb(uint256(), 1, GetAdjustedTime(), uint256(), sb_opt.value().GetHexStrData());
134 0 : if (auto identical_sb = m_govman.FindGovernanceObjectByDataHash(gov_sb.GetDataHash())) {
135 : // Somebody submitted a trigger with the same data, support it instead of submitting a duplicate
136 0 : return std::make_optional<CGovernanceObject>(*identical_sb);
137 : }
138 :
139 : // Nobody submitted a trigger we'd like to see, so let's do it but only if we are the payee
140 0 : const CBlockIndex* tip = m_chainman.ActiveChain().Tip();
141 0 : const auto mnList = m_dmnman.GetListForBlock(tip);
142 0 : const auto mn_payees = mnList.GetProjectedMNPayees(tip);
143 :
144 0 : if (mn_payees.empty()) {
145 0 : LogPrint(BCLog::GOBJECT, "%s -- payee list is empty\n", __func__);
146 0 : return std::nullopt;
147 : }
148 :
149 0 : if (mn_payees.front()->proTxHash != m_mn_activeman.GetProTxHash()) {
150 0 : LogPrint(BCLog::GOBJECT, "%s -- we are not the payee, skipping\n", __func__);
151 0 : return std::nullopt;
152 : }
153 0 : gov_sb.SetMasternodeOutpoint(m_mn_activeman.GetOutPoint());
154 0 : gov_sb.SetSignature(m_mn_activeman.SignBasic(gov_sb.GetSignatureHash()));
155 :
156 0 : if (std::string strError; !gov_sb.IsValidLocally(m_dmnman.GetListAtChainTip(), m_chainman, strError, true)) {
157 0 : LogPrint(BCLog::GOBJECT, "%s -- Created trigger is invalid:%s\n", __func__, strError);
158 0 : return std::nullopt;
159 : }
160 :
161 0 : if (!m_govman.MasternodeRateCheck(gov_sb)) {
162 0 : LogPrint(BCLog::GOBJECT, "%s -- Trigger rejected because of rate check failure hash(%s)\n", __func__,
163 : gov_sb.GetHash().ToString());
164 0 : return std::nullopt;
165 : }
166 :
167 : // The trigger we just created looks good, submit it
168 0 : m_govman.AddGovernanceObject(gov_sb, "<local>");
169 0 : return std::make_optional<CGovernanceObject>(gov_sb);
170 0 : }
171 :
172 0 : void GovernanceSigner::VoteGovernanceTriggers(const std::optional<const CGovernanceObject>& trigger_opt)
173 : {
174 : // only active masternodes can vote on triggers
175 0 : if (m_mn_activeman.GetProTxHash().IsNull()) return;
176 :
177 0 : LOCK(::cs_main);
178 :
179 0 : if (trigger_opt.has_value()) {
180 : // We should never vote "yes" on another trigger or the same trigger twice
181 0 : assert(!votedFundingYesTriggerHash.has_value());
182 : // Vote YES-FUNDING for the trigger we like, unless we already did
183 0 : const uint256 gov_sb_hash = trigger_opt.value().GetHash();
184 0 : bool voted_already{false};
185 0 : if (vote_rec_t voteRecord; trigger_opt.value().GetCurrentMNVotes(m_mn_activeman.GetOutPoint(), voteRecord)) {
186 0 : const auto& strFunc = __func__;
187 : // Let's see if there is a VOTE_SIGNAL_FUNDING vote from us already
188 0 : voted_already = std::ranges::any_of(voteRecord.mapInstances, [&](const auto& voteInstancePair) {
189 0 : if (voteInstancePair.first == VOTE_SIGNAL_FUNDING) {
190 0 : if (voteInstancePair.second.eOutcome == VOTE_OUTCOME_YES) {
191 0 : votedFundingYesTriggerHash = gov_sb_hash;
192 0 : }
193 0 : LogPrint(BCLog::GOBJECT, /* Continued */
194 : "%s -- Not voting YES-FUNDING for trigger:%s, we voted %s for it already\n", strFunc,
195 : gov_sb_hash.ToString(),
196 : CGovernanceVoting::ConvertOutcomeToString(voteInstancePair.second.eOutcome));
197 0 : return true;
198 : }
199 0 : return false;
200 0 : });
201 0 : }
202 0 : if (!voted_already) {
203 : // No previous VOTE_SIGNAL_FUNDING was found, vote now
204 0 : if (VoteFundingTrigger(gov_sb_hash, VOTE_OUTCOME_YES)) {
205 0 : LogPrint(BCLog::GOBJECT, "%s -- Voting YES-FUNDING for new trigger:%s success\n", __func__,
206 : gov_sb_hash.ToString());
207 0 : votedFundingYesTriggerHash = gov_sb_hash;
208 0 : } else {
209 0 : LogPrint(BCLog::GOBJECT, "%s -- Voting YES-FUNDING for new trigger:%s failed\n", __func__,
210 : gov_sb_hash.ToString());
211 : // this should never happen, bail out
212 0 : return;
213 : }
214 0 : }
215 0 : }
216 :
217 : // Vote NO-FUNDING for the rest of the active triggers
218 0 : const auto activeTriggers = m_superblocks.GetActiveTriggers();
219 0 : for (const auto& trigger : activeTriggers) {
220 0 : auto govobj = m_govman.FindGovernanceObject(trigger->GetGovernanceObjHash());
221 0 : if (!govobj) {
222 0 : LogPrint(BCLog::GOBJECT, "%s -- Not voting NO-FUNDING for unknown trigger %s\n", __func__,
223 : trigger->GetGovernanceObjHash().ToString());
224 0 : continue;
225 : }
226 :
227 0 : const uint256 trigger_hash = govobj->GetHash();
228 0 : if (trigger->GetBlockHeight() <= m_govman.GetCachedBlockHeight()) {
229 : // ignore triggers from the past
230 0 : LogPrint(BCLog::GOBJECT, "%s -- Not voting NO-FUNDING for outdated trigger:%s\n", __func__,
231 : trigger_hash.ToString());
232 0 : continue;
233 : }
234 0 : if (trigger_hash == votedFundingYesTriggerHash) {
235 : // Skip actual trigger
236 0 : LogPrint(BCLog::GOBJECT, "%s -- Not voting NO-FUNDING for trigger:%s, we voted yes for it already\n",
237 : __func__, trigger_hash.ToString());
238 0 : continue;
239 : }
240 0 : if (vote_rec_t voteRecord; govobj->GetCurrentMNVotes(m_mn_activeman.GetOutPoint(), voteRecord)) {
241 0 : const auto& strFunc = __func__;
242 0 : if (std::ranges::any_of(voteRecord.mapInstances, [&](const auto& voteInstancePair) {
243 0 : if (voteInstancePair.first == VOTE_SIGNAL_FUNDING) {
244 0 : LogPrint(BCLog::GOBJECT, /* Continued */
245 : "%s -- Not voting NO-FUNDING for trigger:%s, we voted %s for it already\n", strFunc,
246 : trigger_hash.ToString(),
247 : CGovernanceVoting::ConvertOutcomeToString(voteInstancePair.second.eOutcome));
248 0 : return true;
249 : }
250 0 : return false;
251 0 : })) {
252 0 : continue;
253 : }
254 0 : }
255 0 : if (!VoteFundingTrigger(trigger_hash, VOTE_OUTCOME_NO)) {
256 0 : LogPrint(BCLog::GOBJECT, "%s -- Voting NO-FUNDING for trigger:%s failed\n", __func__, trigger_hash.ToString());
257 : // failing here is ok-ish
258 0 : continue;
259 : }
260 0 : LogPrint(BCLog::GOBJECT, "%s -- Voting NO-FUNDING for trigger:%s success\n", __func__, trigger_hash.ToString());
261 0 : }
262 0 : }
263 :
264 0 : bool GovernanceSigner::VoteFundingTrigger(const uint256& nHash, const vote_outcome_enum_t outcome)
265 : {
266 0 : CGovernanceVote vote(m_mn_activeman.GetOutPoint(), nHash, VOTE_SIGNAL_FUNDING, outcome);
267 0 : vote.SetTime(GetAdjustedTime());
268 0 : vote.SetSignature(m_mn_activeman.SignBasic(vote.GetSignatureHash()));
269 :
270 0 : CGovernanceException exception;
271 0 : if (!m_govman.ProcessVoteAndRelay(vote, exception, m_connman)) {
272 0 : LogPrint(BCLog::GOBJECT, "%s -- Vote FUNDING %d for trigger:%s failed:%s\n", __func__, outcome,
273 : nHash.ToString(), exception.what());
274 0 : return false;
275 : }
276 :
277 0 : return true;
278 0 : }
279 :
280 0 : bool GovernanceSigner::HasAlreadyVotedFundingTrigger() const
281 : {
282 0 : return votedFundingYesTriggerHash.has_value();
283 : }
284 :
285 0 : void GovernanceSigner::ResetVotedFundingTrigger()
286 : {
287 0 : votedFundingYesTriggerHash = std::nullopt;
288 0 : }
289 :
290 0 : void GovernanceSigner::UpdatedBlockTip(const CBlockIndex* pindex)
291 : {
292 0 : const auto sb_opt = CreateSuperblockCandidate(pindex->nHeight);
293 0 : const auto trigger_opt = CreateGovernanceTrigger(sb_opt);
294 0 : VoteGovernanceTriggers(trigger_opt);
295 0 : CSuperblock_sptr pSuperblock;
296 0 : if (m_superblocks.GetBestSuperblock(m_dmnman.GetListAtChainTip(), pSuperblock, pindex->nHeight)) {
297 0 : ResetVotedFundingTrigger();
298 0 : }
299 0 : }
|