Line data Source code
1 : // Copyright (c) 2021 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 "test/test_pivx.h"
6 :
7 : #include "blockassembler.h"
8 : #include "consensus/merkle.h"
9 : #include "masternode-payments.h"
10 : #include "masternodeman.h"
11 : #include "spork.h"
12 : #include "tiertwo/tiertwo_sync_state.h"
13 : #include "primitives/transaction.h"
14 : #include "utilmoneystr.h"
15 : #include "util/blockstatecatcher.h"
16 : #include "validation.h"
17 :
18 : #include <boost/test/unit_test.hpp>
19 :
20 : BOOST_AUTO_TEST_SUITE(mnpayments_tests)
21 :
22 1 : void enableMnSyncAndMNPayments()
23 : {
24 : // force mnsync complete
25 1 : g_tiertwo_sync_state.SetCurrentSyncPhase(MASTERNODE_SYNC_FINISHED);
26 :
27 : // enable SPORK_13
28 1 : int64_t nTime = GetTime() - 10;
29 1 : CSporkMessage spork(SPORK_13_ENABLE_SUPERBLOCKS, nTime + 1, nTime);
30 1 : sporkManager.AddOrUpdateSporkMessage(spork);
31 2 : BOOST_CHECK(sporkManager.IsSporkActive(SPORK_13_ENABLE_SUPERBLOCKS));
32 :
33 1 : spork = CSporkMessage(SPORK_8_MASTERNODE_PAYMENT_ENFORCEMENT, nTime + 1, nTime);
34 1 : sporkManager.AddOrUpdateSporkMessage(spork);
35 2 : BOOST_CHECK(sporkManager.IsSporkActive(SPORK_8_MASTERNODE_PAYMENT_ENFORCEMENT));
36 1 : }
37 :
38 900 : static bool CreateMNWinnerPayment(const CTxIn& mnVinVoter, int paymentBlockHeight, const CScript& payeeScript,
39 : const CKey& signerKey, const CPubKey& signerPubKey, CValidationState& state)
40 : {
41 1800 : CMasternodePaymentWinner mnWinner(mnVinVoter, paymentBlockHeight);
42 900 : mnWinner.AddPayee(payeeScript);
43 1800 : BOOST_CHECK(mnWinner.Sign(signerKey, signerPubKey.GetID()));
44 1800 : return masternodePayments.ProcessMNWinner(mnWinner, nullptr, state);
45 : }
46 :
47 : class MNdata
48 : {
49 : public:
50 : COutPoint collateralOut;
51 : CKey mnPrivKey;
52 : CPubKey mnPubKey;
53 : CPubKey collateralPubKey;
54 : CScript mnPayeeScript;
55 :
56 40 : MNdata(const COutPoint& collateralOut, const CKey& mnPrivKey, const CPubKey& mnPubKey,
57 40 : const CPubKey& collateralPubKey, const CScript& mnPayeeScript) :
58 : collateralOut(collateralOut), mnPrivKey(mnPrivKey), mnPubKey(mnPubKey),
59 40 : collateralPubKey(collateralPubKey), mnPayeeScript(mnPayeeScript) {}
60 :
61 :
62 : };
63 :
64 40 : CMasternode buildMN(const MNdata& data, const uint256& tipHash, uint64_t tipTime)
65 : {
66 40 : CMasternode mn;
67 40 : mn.vin = CTxIn(data.collateralOut);
68 40 : mn.pubKeyCollateralAddress = data.mnPubKey;
69 40 : mn.pubKeyMasternode = data.collateralPubKey;
70 40 : mn.sigTime = GetTime() - 8000 - 1; // MN_WINNER_MINIMUM_AGE = 8000.
71 40 : mn.lastPing = CMasternodePing(mn.vin, tipHash, tipTime);
72 40 : return mn;
73 : }
74 :
75 999 : class FakeMasternode {
76 : public:
77 40 : explicit FakeMasternode(CMasternode& mn, const MNdata& data) : mn(mn), data(data) {}
78 : CMasternode mn;
79 : MNdata data;
80 : };
81 :
82 1 : std::vector<FakeMasternode> buildMNList(const uint256& tipHash, uint64_t tipTime, int size)
83 : {
84 1 : std::vector<FakeMasternode> ret;
85 41 : for (int i=0; i < size; i++) {
86 80 : CKey mnKey;
87 40 : mnKey.MakeNewKey(true);
88 40 : const CPubKey& mnPubKey = mnKey.GetPubKey();
89 80 : const CScript& mnPayeeScript = GetScriptForDestination(mnPubKey.GetID());
90 : // Fake collateral out and key for now
91 40 : COutPoint mnCollateral(GetRandHash(), 0);
92 40 : const CPubKey& collateralPubKey = mnPubKey;
93 :
94 : // Now add the MN
95 80 : MNdata mnData(mnCollateral, mnKey, mnPubKey, collateralPubKey, mnPayeeScript);
96 80 : CMasternode mn = buildMN(mnData, tipHash, tipTime);
97 80 : BOOST_CHECK(mnodeman.Add(mn));
98 40 : ret.emplace_back(mn, mnData);
99 : }
100 1 : return ret;
101 : }
102 :
103 896 : FakeMasternode findMNData(std::vector<FakeMasternode>& mnList, const MasternodeRef& ref)
104 : {
105 18595 : for (const auto& item : mnList) {
106 18595 : if (item.data.mnPubKey == ref->pubKeyMasternode) {
107 896 : return item;
108 : }
109 : }
110 0 : throw std::runtime_error("MN not found");
111 : }
112 :
113 7 : bool findStrError(CValidationState& state, const std::string& str)
114 : {
115 7 : return state.GetRejectReason().find(str) != std::string::npos;
116 : }
117 :
118 2 : BOOST_FIXTURE_TEST_CASE(mnwinner_test, TestChain100Setup)
119 : {
120 1 : CreateAndProcessBlock({}, coinbaseKey);
121 2 : CBlock tipBlock = CreateAndProcessBlock({}, coinbaseKey);
122 1 : enableMnSyncAndMNPayments();
123 1 : int nextBlockHeight = 103;
124 1 : UpdateNetworkUpgradeParameters(Consensus::UPGRADE_V5_3, nextBlockHeight - 1);
125 :
126 : // MN list.
127 2 : std::vector<FakeMasternode> mnList = buildMNList(tipBlock.GetHash(), tipBlock.GetBlockTime(), 40);
128 1 : std::vector<std::pair<int64_t, MasternodeRef>> mnRank = mnodeman.GetMasternodeRanks(nextBlockHeight - 100);
129 :
130 : // Test mnwinner failure for non-existent MN voter.
131 2 : CTxIn dummyVoter;
132 1 : CScript dummyPayeeScript;
133 2 : CKey dummyKey;
134 1 : dummyKey.MakeNewKey(true);
135 2 : CValidationState state0;
136 2 : BOOST_CHECK(!CreateMNWinnerPayment(dummyVoter, nextBlockHeight, dummyPayeeScript,
137 : dummyKey, dummyKey.GetPubKey(), state0));
138 5 : BOOST_CHECK_MESSAGE(findStrError(state0, "Non-existent mnwinner voter"), state0.GetRejectReason());
139 :
140 : // Take the first MN
141 2 : auto firstMN = findMNData(mnList, mnRank[0].second);
142 2 : CTxIn mnVinVoter(firstMN.mn.vin);
143 1 : int paymentBlockHeight = nextBlockHeight;
144 2 : CScript payeeScript = firstMN.data.mnPayeeScript;
145 1 : CMasternode* pFirstMN = mnodeman.Find(firstMN.mn.vin.prevout);
146 1 : pFirstMN->sigTime += 8000 + 1; // MN_WINNER_MINIMUM_AGE = 8000.
147 : // Voter MN1, fail because the sigTime - GetAdjustedTime() is not greater than MN_WINNER_MINIMUM_AGE.
148 2 : CValidationState state1;
149 2 : BOOST_CHECK(!CreateMNWinnerPayment(mnVinVoter, paymentBlockHeight, payeeScript,
150 : firstMN.data.mnPrivKey, firstMN.data.mnPubKey, state1));
151 : // future: add specific error cause
152 5 : BOOST_CHECK_MESSAGE(findStrError(state1, "Masternode not in the top"), state1.GetRejectReason());
153 :
154 : // Voter MN2, fail because MN2 doesn't match with the signing keys.
155 2 : auto secondMn = findMNData(mnList, mnRank[1].second);
156 1 : CMasternode* pSecondMN = mnodeman.Find(secondMn.mn.vin.prevout);
157 1 : mnVinVoter = CTxIn(pSecondMN->vin);
158 1 : payeeScript = secondMn.data.mnPayeeScript;
159 2 : CValidationState state2;
160 2 : BOOST_CHECK(!CreateMNWinnerPayment(mnVinVoter, paymentBlockHeight, payeeScript,
161 : firstMN.data.mnPrivKey, firstMN.data.mnPubKey, state2));
162 5 : BOOST_CHECK_MESSAGE(findStrError(state2, "invalid voter mnwinner signature"), state2.GetRejectReason());
163 :
164 : // Voter MN2, fail because mnwinner height is too far in the future.
165 1 : mnVinVoter = CTxIn(pSecondMN->vin);
166 2 : CValidationState state2_5;
167 2 : BOOST_CHECK(!CreateMNWinnerPayment(mnVinVoter, paymentBlockHeight + 20, payeeScript,
168 : secondMn.data.mnPrivKey, secondMn.data.mnPubKey, state2_5));
169 5 : BOOST_CHECK_MESSAGE(findStrError(state2_5, "block height out of range"), state2_5.GetRejectReason());
170 :
171 :
172 : // Voter MN2, fail because MN2 is not enabled
173 1 : pSecondMN->SetSpent();
174 2 : BOOST_CHECK(!pSecondMN->IsEnabled());
175 1 : CValidationState state3;
176 2 : BOOST_CHECK(!CreateMNWinnerPayment(mnVinVoter, paymentBlockHeight, payeeScript,
177 : secondMn.data.mnPrivKey, secondMn.data.mnPubKey, state3));
178 : // future: could add specific error cause.
179 5 : BOOST_CHECK_MESSAGE(findStrError(state3, "Masternode not in the top"), state3.GetRejectReason());
180 :
181 : // Voter MN3, fail because the payeeScript is not a P2PKH
182 2 : auto thirdMn = findMNData(mnList, mnRank[2].second);
183 1 : CMasternode* pThirdMN = mnodeman.Find(thirdMn.mn.vin.prevout);
184 1 : mnVinVoter = CTxIn(pThirdMN->vin);
185 2 : CScript scriptDummy = CScript() << OP_TRUE;
186 2 : CValidationState state4;
187 2 : BOOST_CHECK(!CreateMNWinnerPayment(mnVinVoter, paymentBlockHeight, scriptDummy,
188 : thirdMn.data.mnPrivKey, thirdMn.data.mnPubKey, state4));
189 5 : BOOST_CHECK_MESSAGE(findStrError(state4, "payee must be a P2PKH"), state4.GetRejectReason());
190 :
191 : // Voter MN15 pays to MN3, fail because the voter is not in the top ten.
192 2 : auto voterPos15 = findMNData(mnList, mnRank[14].second);
193 1 : CMasternode* p15dMN = mnodeman.Find(voterPos15.mn.vin.prevout);
194 1 : mnVinVoter = CTxIn(p15dMN->vin);
195 1 : payeeScript = thirdMn.data.mnPayeeScript;
196 2 : CValidationState state6;
197 2 : BOOST_CHECK(!CreateMNWinnerPayment(mnVinVoter, paymentBlockHeight, payeeScript,
198 : voterPos15.data.mnPrivKey, voterPos15.data.mnPubKey, state6));
199 5 : BOOST_CHECK_MESSAGE(findStrError(state6, "Masternode not in the top"), state6.GetRejectReason());
200 :
201 : // Voter MN3, passes
202 1 : mnVinVoter = CTxIn(pThirdMN->vin);
203 1 : CValidationState state7;
204 2 : BOOST_CHECK(CreateMNWinnerPayment(mnVinVoter, paymentBlockHeight, payeeScript,
205 : thirdMn.data.mnPrivKey, thirdMn.data.mnPubKey, state7));
206 3 : BOOST_CHECK_MESSAGE(state7.IsValid(), state7.GetRejectReason());
207 :
208 : // Create block and check that is being paid properly.
209 1 : tipBlock = CreateAndProcessBlock({}, coinbaseKey);
210 2 : BOOST_CHECK_MESSAGE(tipBlock.vtx[0]->vout.back().scriptPubKey == payeeScript, "error: block not paying to proper MN");
211 1 : nextBlockHeight++;
212 :
213 : // Now let's push two valid winner payments and make every MN in the top ten vote for them (having more votes in mnwinnerA than in mnwinnerB).
214 2 : mnRank = mnodeman.GetMasternodeRanks(nextBlockHeight - 100);
215 2 : CScript firstRankedPayee = GetScriptForDestination(mnRank[0].second->pubKeyCollateralAddress.GetID());
216 2 : CScript secondRankedPayee = GetScriptForDestination(mnRank[1].second->pubKeyCollateralAddress.GetID());
217 :
218 : // Let's vote with the first 6 nodes for MN ranked 1
219 : // And with the last 4 nodes for MN ranked 2
220 1 : payeeScript = firstRankedPayee;
221 11 : for (int i=0; i<10; i++) {
222 10 : if (i > 5) {
223 4 : payeeScript = secondRankedPayee;
224 : }
225 20 : auto voterMn = findMNData(mnList, mnRank[i].second);
226 10 : CMasternode* pVoterMN = mnodeman.Find(voterMn.mn.vin.prevout);
227 10 : mnVinVoter = CTxIn(pVoterMN->vin);
228 20 : CValidationState stateInternal;
229 20 : BOOST_CHECK(CreateMNWinnerPayment(mnVinVoter, nextBlockHeight, payeeScript,
230 : voterMn.data.mnPrivKey, voterMn.data.mnPubKey, stateInternal));
231 30 : BOOST_CHECK_MESSAGE(stateInternal.IsValid(), stateInternal.GetRejectReason());
232 : }
233 :
234 : // Check the votes count for each mnwinner.
235 2 : CMasternodeBlockPayees blockPayees = masternodePayments.mapMasternodeBlocks.at(nextBlockHeight);
236 2 : BOOST_CHECK_MESSAGE(blockPayees.HasPayeeWithVotes(firstRankedPayee, 6), "first ranked payee with no enough votes");
237 2 : BOOST_CHECK_MESSAGE(blockPayees.HasPayeeWithVotes(secondRankedPayee, 4), "second ranked payee with no enough votes");
238 :
239 : // let's try to create a bad block paying to the second most voted MN.
240 2 : CBlock badBlock = CreateBlock({}, coinbaseKey);
241 1 : CMutableTransaction coinbase(*badBlock.vtx[0]);
242 1 : coinbase.vout[coinbase.vout.size() - 1].scriptPubKey = secondRankedPayee;
243 2 : badBlock.vtx[0] = MakeTransactionRef(coinbase);
244 1 : badBlock.hashMerkleRoot = BlockMerkleRoot(badBlock);
245 1 : {
246 1 : auto pBadBlock = std::make_shared<CBlock>(badBlock);
247 1 : SolveBlock(pBadBlock, nextBlockHeight);
248 2 : BlockStateCatcherWrapper sc(pBadBlock->GetHash());
249 1 : sc.registerEvent();
250 1 : ProcessNewBlock(pBadBlock, nullptr);
251 2 : BOOST_CHECK(sc.get().found && !sc.get().state.IsValid());
252 2 : BOOST_CHECK_EQUAL(sc.get().state.GetRejectReason(), "bad-cb-payee");
253 : }
254 4 : BOOST_CHECK(WITH_LOCK(cs_main, return chainActive.Tip()->GetBlockHash();) != badBlock.GetHash());
255 :
256 :
257 : // And let's verify that the most voted one is the one being paid.
258 1 : tipBlock = CreateAndProcessBlock({}, coinbaseKey);
259 2 : BOOST_CHECK_MESSAGE(tipBlock.vtx[0]->vout.back().scriptPubKey == firstRankedPayee, "error: block not paying to first ranked MN");
260 1 : nextBlockHeight++;
261 :
262 : //
263 : // Generate 125 blocks paying to different MNs to load the payments cache.
264 126 : for (int i = 0; i < 125; i++) {
265 250 : mnRank = mnodeman.GetMasternodeRanks(nextBlockHeight - 100);
266 125 : payeeScript = GetScriptForDestination(mnRank[0].second->pubKeyCollateralAddress.GetID());
267 1000 : for (int j=0; j<7; j++) { // votes
268 1750 : auto voterMn = findMNData(mnList, mnRank[j].second);
269 875 : CMasternode* pVoterMN = mnodeman.Find(voterMn.mn.vin.prevout);
270 875 : mnVinVoter = CTxIn(pVoterMN->vin);
271 1750 : CValidationState stateInternal;
272 1750 : BOOST_CHECK(CreateMNWinnerPayment(mnVinVoter, nextBlockHeight, payeeScript,
273 : voterMn.data.mnPrivKey, voterMn.data.mnPubKey, stateInternal));
274 2625 : BOOST_CHECK_MESSAGE(stateInternal.IsValid(), stateInternal.GetRejectReason());
275 : }
276 : // Create block and check that is being paid properly.
277 125 : tipBlock = CreateAndProcessBlock({}, coinbaseKey);
278 250 : BOOST_CHECK_MESSAGE(tipBlock.vtx[0]->vout.back().scriptPubKey == payeeScript, "error: block not paying to proper MN");
279 125 : nextBlockHeight++;
280 : }
281 : // Check chain height.
282 2 : BOOST_CHECK_EQUAL(WITH_LOCK(cs_main, return chainActive.Height();), nextBlockHeight - 1);
283 :
284 : // Let's now verify what happen if a previously paid MN goes offline but still have scheduled a payment in the future.
285 : // The current system allows it (up to a certain point) as payments are scheduled ahead of time and a MN can go down in the
286 : // [proposedWinnerHeightTime < currentHeight < currentHeight + 20] window.
287 :
288 : // 1) Schedule payment and vote for it with the first 6 MNs.
289 2 : mnRank = mnodeman.GetMasternodeRanks(nextBlockHeight - 100);
290 2 : MasternodeRef mnToPay = mnRank[0].second;
291 1 : payeeScript = GetScriptForDestination(mnToPay->pubKeyCollateralAddress.GetID());
292 7 : for (int i=0; i<6; i++) {
293 12 : auto voterMn = findMNData(mnList, mnRank[i].second);
294 6 : CMasternode* pVoterMN = mnodeman.Find(voterMn.mn.vin.prevout);
295 6 : mnVinVoter = CTxIn(pVoterMN->vin);
296 12 : CValidationState stateInternal;
297 12 : BOOST_CHECK(CreateMNWinnerPayment(mnVinVoter, nextBlockHeight, payeeScript,
298 : voterMn.data.mnPrivKey, voterMn.data.mnPubKey, stateInternal));
299 18 : BOOST_CHECK_MESSAGE(stateInternal.IsValid(), stateInternal.GetRejectReason());
300 : }
301 :
302 : // 2) Remove payee MN from the MN list and try to emit a vote from MN7 to the same payee.
303 : // it should still be accepted because the MN was scheduled when it was online.
304 1 : mnodeman.Remove(mnToPay->vin.prevout);
305 2 : BOOST_CHECK_MESSAGE(!mnodeman.Find(mnToPay->vin.prevout), "error: removed MN is still available");
306 :
307 : // Now emit the vote from MN7
308 2 : auto voterMn = findMNData(mnList, mnRank[7].second);
309 1 : CMasternode* pVoterMN = mnodeman.Find(voterMn.mn.vin.prevout);
310 1 : mnVinVoter = CTxIn(pVoterMN->vin);
311 2 : CValidationState stateInternal;
312 2 : BOOST_CHECK(CreateMNWinnerPayment(mnVinVoter, nextBlockHeight, payeeScript,
313 : voterMn.data.mnPrivKey, voterMn.data.mnPubKey, stateInternal));
314 3 : BOOST_CHECK_MESSAGE(stateInternal.IsValid(), stateInternal.GetRejectReason());
315 1 : }
316 :
317 : BOOST_AUTO_TEST_SUITE_END()
|