Line data Source code
1 : // Copyright (c) 2023-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 <test/util/setup_common.h>
6 :
7 : #include <consensus/amount.h>
8 : #include <consensus/tx_check.h>
9 : #include <consensus/validation.h>
10 : #include <evo/assetlocktx.h>
11 : #include <evo/specialtx.h>
12 : #include <llmq/context.h>
13 : #include <policy/settings.h>
14 : #include <script/script.h>
15 : #include <script/signingprovider.h>
16 : #include <util/ranges_set.h>
17 : #include <validation.h>
18 :
19 : #include <boost/test/unit_test.hpp>
20 :
21 :
22 : //
23 : // Helper: create two dummy transactions, each with
24 : // two outputs. The first has 11 and 50 CENT outputs
25 : // paid to a TX_PUBKEY, the second 21 and 22 CENT outputs
26 : // paid to a TX_PUBKEYHASH.
27 : //
28 : static std::vector<CMutableTransaction>
29 2 : SetupDummyInputs(FillableSigningProvider& keystoreRet, CCoinsViewCache& coinsRet)
30 : {
31 2 : std::vector<CMutableTransaction> dummyTransactions;
32 2 : dummyTransactions.resize(2);
33 :
34 : // Add some keys to the keystore:
35 2 : std::array<CKey, 4> key;
36 : {
37 2 : bool flip = true;
38 10 : for (auto& k : key) {
39 8 : k.MakeNewKey(flip);
40 8 : keystoreRet.AddKey(k);
41 8 : flip = !flip;
42 : }
43 : }
44 :
45 : // Create some dummy input transactions
46 2 : dummyTransactions[0].vout.resize(2);
47 2 : dummyTransactions[0].vout[0].nValue = 11*CENT;
48 2 : dummyTransactions[0].vout[0].scriptPubKey = GetScriptForRawPubKey(key[0].GetPubKey());
49 2 : dummyTransactions[0].vout[1].nValue = 50*CENT;
50 2 : dummyTransactions[0].vout[1].scriptPubKey = GetScriptForRawPubKey(key[1].GetPubKey());
51 2 : AddCoins(coinsRet, CTransaction(dummyTransactions[0]), 0);
52 :
53 2 : dummyTransactions[1].vout.resize(2);
54 2 : dummyTransactions[1].vout[0].nValue = 21*CENT;
55 2 : dummyTransactions[1].vout[0].scriptPubKey = GetScriptForDestination(PKHash(key[2].GetPubKey()));
56 2 : dummyTransactions[1].vout[1].nValue = 22*CENT;
57 2 : dummyTransactions[1].vout[1].scriptPubKey = GetScriptForDestination(PKHash(key[3].GetPubKey()));
58 2 : AddCoins(coinsRet, CTransaction(dummyTransactions[1]), 0);
59 :
60 2 : return dummyTransactions;
61 2 : }
62 :
63 1 : static CMutableTransaction CreateAssetLockTx(FillableSigningProvider& keystore, CCoinsViewCache& coins, CKey& key)
64 : {
65 1 : std::vector<CMutableTransaction> dummyTransactions = SetupDummyInputs(keystore, coins);
66 :
67 1 : std::vector<CTxOut> creditOutputs(2);
68 1 : creditOutputs[0].nValue = 17 * CENT;
69 1 : creditOutputs[0].scriptPubKey = GetScriptForDestination(PKHash(key.GetPubKey()));
70 1 : creditOutputs[1].nValue = 13 * CENT;
71 1 : creditOutputs[1].scriptPubKey = GetScriptForDestination(PKHash(key.GetPubKey()));
72 :
73 1 : CAssetLockPayload assetLockTx(creditOutputs);
74 :
75 1 : CMutableTransaction tx;
76 1 : tx.nVersion = 3;
77 1 : tx.nType = TRANSACTION_ASSET_LOCK;
78 1 : SetTxPayload(tx, assetLockTx);
79 :
80 1 : tx.vin.resize(1);
81 1 : tx.vin[0].prevout.hash = dummyTransactions[0].GetHash();
82 1 : tx.vin[0].prevout.n = 1;
83 1 : tx.vin[0].scriptSig << std::vector<unsigned char>(65, 0);
84 :
85 1 : tx.vout.resize(2);
86 1 : tx.vout[0].nValue = 30 * CENT;
87 1 : tx.vout[0].scriptPubKey = CScript() << OP_RETURN << ParseHex("");
88 :
89 1 : tx.vout[1].nValue = 20 * CENT;
90 1 : tx.vout[1].scriptPubKey = GetScriptForDestination(PKHash(key.GetPubKey()));
91 :
92 1 : return tx;
93 1 : }
94 :
95 1 : static CMutableTransaction CreateAssetUnlockTx(FillableSigningProvider& keystore, CKey& key)
96 : {
97 1 : int nVersion = 1;
98 : // just a big number bigger than uint32_t
99 1 : uint64_t index = 0x001122334455667788L;
100 : // big enough to overflow int32_t
101 1 : uint32_t fee = 2000'000'000u;
102 : // just big enough to overflow uint16_t
103 1 : uint32_t requestedHeight = 1000'000;
104 1 : uint256 quorumHash;
105 1 : CBLSSignature quorumSig;
106 1 : CAssetUnlockPayload assetUnlockTx(nVersion, index, fee, requestedHeight, quorumHash, quorumSig);
107 :
108 1 : CMutableTransaction tx;
109 1 : tx.nVersion = 3;
110 1 : tx.nType = TRANSACTION_ASSET_UNLOCK;
111 1 : SetTxPayload(tx, assetUnlockTx);
112 :
113 1 : tx.vin.resize(0);
114 :
115 1 : tx.vout.resize(2);
116 1 : tx.vout[0].nValue = 10 * CENT;
117 1 : tx.vout[0].scriptPubKey = GetScriptForDestination(PKHash(key.GetPubKey()));
118 :
119 1 : tx.vout[1].nValue = 20 * CENT;
120 1 : tx.vout[1].scriptPubKey = GetScriptForDestination(PKHash(key.GetPubKey()));
121 :
122 1 : return tx;
123 1 : }
124 :
125 146 : BOOST_FIXTURE_TEST_SUITE(evo_assetlocks_tests, TestChain100Setup)
126 :
127 149 : BOOST_FIXTURE_TEST_CASE(evo_assetlock, TestChain100Setup)
128 : {
129 :
130 1 : LOCK(cs_main);
131 1 : FillableSigningProvider keystore;
132 1 : CCoinsView coinsDummy;
133 1 : CCoinsViewCache coins(&coinsDummy);
134 :
135 1 : CKey key;
136 1 : key.MakeNewKey(true);
137 :
138 1 : const CTransaction tx{CreateAssetLockTx(keystore, coins, key)};
139 1 : std::string reason;
140 1 : BOOST_CHECK(IsStandardTx(CTransaction(tx), reason));
141 :
142 1 : TxValidationState tx_state;
143 1 : std::string strTest;
144 1 : BOOST_CHECK_MESSAGE(CheckTransaction(CTransaction(tx), tx_state), strTest);
145 1 : BOOST_CHECK(tx_state.IsValid());
146 :
147 1 : BOOST_CHECK(CheckAssetLockTx(CTransaction(tx), tx_state));
148 :
149 1 : BOOST_CHECK(AreInputsStandard(CTransaction(tx), coins));
150 :
151 : // Check version
152 : {
153 1 : BOOST_CHECK(tx.IsSpecialTxVersion());
154 :
155 1 : const auto opt_payload = GetTxPayload<CAssetLockPayload>(tx);
156 :
157 1 : BOOST_CHECK(opt_payload.has_value());
158 1 : BOOST_CHECK(opt_payload->getVersion() == 1);
159 1 : }
160 :
161 : {
162 : // Wrong type "Asset Unlock TX" instead "Asset Lock TX"
163 1 : CMutableTransaction txWrongType(tx);
164 1 : txWrongType.nType = TRANSACTION_ASSET_UNLOCK;
165 1 : BOOST_CHECK(!CheckAssetLockTx(CTransaction(txWrongType), tx_state));
166 1 : BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-type");
167 1 : }
168 :
169 : {
170 1 : CAmount inSum = 0;
171 2 : for (const auto& vin : tx.vin) {
172 1 : inSum += coins.AccessCoin(vin.prevout).out.nValue;
173 : }
174 :
175 1 : auto outSum = CTransaction(tx).GetValueOut();
176 1 : BOOST_CHECK(inSum == outSum);
177 :
178 : // Outputs should not be bigger than inputs
179 1 : CMutableTransaction txBigOutput(tx);
180 1 : txBigOutput.vout[0].nValue += 1;
181 1 : BOOST_CHECK(!CheckAssetLockTx(CTransaction(txBigOutput), tx_state));
182 1 : BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-creditamount");
183 :
184 : // Smaller outputs are allown
185 1 : CMutableTransaction txSmallOutput(tx);
186 1 : txSmallOutput.vout[1].nValue -= 1;
187 1 : BOOST_CHECK(CheckAssetLockTx(CTransaction(txSmallOutput), tx_state));
188 1 : }
189 :
190 1 : const auto assetLockPayload = GetTxPayload<CAssetLockPayload>(tx);
191 1 : const std::vector<CTxOut> creditOutputs = assetLockPayload->getCreditOutputs();
192 :
193 : {
194 : // Sum of credit output greater than OP_RETURN
195 1 : std::vector<CTxOut> wrongOutput = creditOutputs;
196 1 : wrongOutput[0].nValue += CENT;
197 1 : CAssetLockPayload greaterCreditsPayload(wrongOutput);
198 :
199 1 : CMutableTransaction txGreaterCredits(tx);
200 1 : SetTxPayload(txGreaterCredits, greaterCreditsPayload);
201 :
202 1 : BOOST_CHECK(!CheckAssetLockTx(CTransaction(txGreaterCredits), tx_state));
203 1 : BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-creditamount");
204 :
205 : // Sum of credit output less than OP_RETURN
206 1 : wrongOutput[1].nValue -= 2 * CENT;
207 1 : CAssetLockPayload lessCreditsPayload(wrongOutput);
208 :
209 1 : CMutableTransaction txLessCredits(tx);
210 1 : SetTxPayload(txLessCredits, lessCreditsPayload);
211 :
212 1 : BOOST_CHECK(!CheckAssetLockTx(CTransaction(txLessCredits), tx_state));
213 1 : BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-creditamount");
214 1 : }
215 :
216 : {
217 : // Credit output is out-of-range
218 1 : std::vector<CTxOut> creditOutputsOutOfRange = creditOutputs;
219 1 : creditOutputsOutOfRange[0].nValue = 0;
220 1 : CAssetLockPayload invalidOutputsPayload(creditOutputsOutOfRange);
221 :
222 1 : CMutableTransaction txInvalidOutputs(tx);
223 1 : SetTxPayload(txInvalidOutputs, invalidOutputsPayload);
224 :
225 1 : BOOST_CHECK(!CheckAssetLockTx(CTransaction(txInvalidOutputs), tx_state));
226 1 : BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-credit-outofrange");
227 :
228 : // one of output is out of range
229 1 : creditOutputsOutOfRange[0].nValue = MAX_MONEY + 1;
230 1 : SetTxPayload(txInvalidOutputs, CAssetLockPayload{creditOutputsOutOfRange});
231 1 : BOOST_CHECK(!CheckAssetLockTx(CTransaction(txInvalidOutputs), tx_state));
232 1 : BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-credit-outofrange");
233 :
234 : // sum of some of output is out of range
235 1 : creditOutputsOutOfRange[0].nValue = MAX_MONEY + 1 - creditOutputsOutOfRange[1].nValue;
236 1 : SetTxPayload(txInvalidOutputs, CAssetLockPayload{creditOutputsOutOfRange});
237 1 : BOOST_CHECK(!CheckAssetLockTx(CTransaction(txInvalidOutputs), tx_state));
238 1 : BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-credit-outofrange");
239 :
240 1 : }
241 : {
242 : // One credit output keys is not pub key
243 1 : std::vector<CTxOut> creditOutputsNotPubkey = creditOutputs;
244 1 : creditOutputsNotPubkey[0].scriptPubKey = CScript() << OP_1;
245 1 : CAssetLockPayload notPubkeyPayload(creditOutputsNotPubkey);
246 :
247 1 : CMutableTransaction txNotPubkey(tx);
248 1 : SetTxPayload(txNotPubkey, notPubkeyPayload);
249 :
250 1 : BOOST_CHECK(!CheckAssetLockTx(CTransaction(txNotPubkey), tx_state));
251 1 : BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-pubKeyHash");
252 :
253 1 : }
254 :
255 : {
256 : // OP_RETURN must be only one, not more
257 1 : CMutableTransaction txMultipleReturn(tx);
258 1 : txMultipleReturn.vout[1].scriptPubKey = CScript() << OP_RETURN << ParseHex("");
259 :
260 1 : BOOST_CHECK(!CheckAssetLockTx(CTransaction(txMultipleReturn), tx_state));
261 1 : BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-multiple-return");
262 :
263 1 : }
264 :
265 : {
266 : // zero/negative OP_RETURN
267 1 : CMutableTransaction txReturnOutOfRange(tx);
268 1 : txReturnOutOfRange.vout[0].nValue = 0;
269 :
270 1 : BOOST_CHECK(!CheckAssetLockTx(CTransaction(txReturnOutOfRange), tx_state));
271 1 : BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-opreturn-outofrange");
272 :
273 1 : txReturnOutOfRange.vout[0].nValue = MAX_MONEY + 1;
274 :
275 1 : BOOST_CHECK(!CheckAssetLockTx(CTransaction(txReturnOutOfRange), tx_state));
276 1 : BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-opreturn-outofrange");
277 1 : }
278 :
279 :
280 : {
281 : // OP_RETURN is missing
282 1 : CMutableTransaction txNoReturn(tx);
283 1 : txNoReturn.vout[0].scriptPubKey = GetScriptForDestination(PKHash(key.GetPubKey()));
284 :
285 1 : BOOST_CHECK(!CheckAssetLockTx(CTransaction(txNoReturn), tx_state));
286 1 : BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-no-return");
287 1 : }
288 :
289 : {
290 : // OP_RETURN should not have any data
291 1 : CMutableTransaction txReturnData(tx);
292 1 : txReturnData.vout[0].scriptPubKey = CScript() << OP_RETURN << ParseHex("abcd");
293 :
294 1 : BOOST_CHECK(!CheckAssetLockTx(CTransaction(txReturnData), tx_state));
295 1 : BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-non-empty-return");
296 1 : }
297 1 : }
298 :
299 149 : BOOST_FIXTURE_TEST_CASE(evo_assetunlock, TestChain100Setup)
300 : {
301 1 : LOCK(cs_main);
302 1 : FillableSigningProvider keystore;
303 :
304 1 : CKey key;
305 1 : key.MakeNewKey(true);
306 :
307 1 : const CTransaction tx{CreateAssetUnlockTx(keystore, key)};
308 1 : std::string reason;
309 1 : BOOST_CHECK(IsStandardTx(CTransaction(tx), reason));
310 :
311 1 : TxValidationState tx_state;
312 1 : std::string strTest;
313 1 : BOOST_CHECK_MESSAGE(CheckTransaction(CTransaction(tx), tx_state), strTest);
314 1 : BOOST_CHECK(tx_state.IsValid());
315 :
316 1 : auto& blockman = Assert(m_node.chainman)->m_blockman;
317 1 : auto& qman = *Assert(m_node.llmq_ctx)->qman;
318 :
319 1 : const CBlockIndex *block_index = m_node.chainman->ActiveChain().Tip();
320 1 : BOOST_CHECK(!CheckAssetUnlockTx(blockman, qman, CTransaction(tx), block_index, std::nullopt, tx_state));
321 1 : BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetunlock-quorum-hash");
322 :
323 : {
324 : // Any input should be a reason to fail CheckAssetUnlockTx()
325 1 : CCoinsView coinsDummy;
326 1 : CCoinsViewCache coins(&coinsDummy);
327 1 : std::vector<CMutableTransaction> dummyTransactions = SetupDummyInputs(keystore, coins);
328 :
329 1 : CMutableTransaction txNonemptyInput(tx);
330 1 : txNonemptyInput.vin.resize(1);
331 1 : txNonemptyInput.vin[0].prevout.hash = dummyTransactions[0].GetHash();
332 1 : txNonemptyInput.vin[0].prevout.n = 1;
333 1 : txNonemptyInput.vin[0].scriptSig << std::vector<unsigned char>(65, 0);
334 :
335 1 : std::string reason;
336 1 : BOOST_CHECK(IsStandardTx(CTransaction(tx), reason));
337 :
338 1 : BOOST_CHECK(!CheckAssetUnlockTx(blockman, qman, CTransaction(txNonemptyInput), block_index, std::nullopt, tx_state));
339 1 : BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetunlocktx-have-input");
340 1 : }
341 :
342 : {
343 1 : const auto unlockPayload = GetTxPayload<CAssetUnlockPayload>(tx);
344 1 : BOOST_CHECK(unlockPayload.has_value());
345 1 : BOOST_CHECK(unlockPayload->getVersion() == 1);
346 1 : BOOST_CHECK(unlockPayload->getRequestedHeight() == 1000'000);
347 1 : BOOST_CHECK(unlockPayload->getFee() == 2000'000'000u);
348 1 : BOOST_CHECK(unlockPayload->getIndex() == 0x001122334455667788L);
349 :
350 : // Wrong type "Asset Lock TX" instead "Asset Unlock TX"
351 1 : CMutableTransaction txWrongType(tx);
352 1 : txWrongType.nType = TRANSACTION_ASSET_LOCK;
353 1 : BOOST_CHECK(!CheckAssetUnlockTx(blockman, qman, CTransaction(txWrongType), block_index, std::nullopt, tx_state));
354 1 : BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetunlocktx-type");
355 :
356 : // Check version of tx and payload
357 1 : BOOST_CHECK(tx.IsSpecialTxVersion());
358 5 : for (uint8_t payload_version : {0, 1, 2, 255}) {
359 8 : CAssetUnlockPayload unlockPayload_tmp{payload_version,
360 4 : unlockPayload->getIndex(),
361 4 : unlockPayload->getFee(),
362 4 : unlockPayload->getRequestedHeight(),
363 4 : unlockPayload->getQuorumHash(),
364 4 : unlockPayload->getQuorumSig()};
365 4 : CMutableTransaction txWrongVersion(tx);
366 4 : SetTxPayload(txWrongVersion, unlockPayload_tmp);
367 4 : if (payload_version != 1) {
368 3 : BOOST_CHECK(!CheckAssetUnlockTx(blockman, qman, CTransaction(txWrongVersion), block_index, std::nullopt, tx_state));
369 3 : BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetunlocktx-version");
370 3 : } else {
371 1 : BOOST_CHECK(!CheckAssetUnlockTx(blockman, qman, CTransaction(txWrongVersion), block_index, std::nullopt, tx_state));
372 1 : BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetunlock-quorum-hash");
373 : }
374 4 : }
375 1 : }
376 :
377 : {
378 : // Exactly 32 withdrawal is fine
379 1 : CMutableTransaction txManyOutputs(tx);
380 1 : int outputsLimit = 32;
381 1 : txManyOutputs.vout.resize(outputsLimit);
382 33 : for (auto& out : txManyOutputs.vout) {
383 32 : out.nValue = CENT;
384 32 : out.scriptPubKey = GetScriptForDestination(PKHash(key.GetPubKey()));
385 : }
386 :
387 1 : BOOST_CHECK(!CheckAssetUnlockTx(blockman, qman, CTransaction(txManyOutputs), block_index, std::nullopt, tx_state));
388 1 : BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetunlock-quorum-hash");
389 :
390 : // Basic checks for CRangesSet
391 1 : CRangesSet indexes;
392 1 : BOOST_CHECK(!CheckAssetUnlockTx(blockman, qman, CTransaction(txManyOutputs), block_index, indexes, tx_state));
393 1 : BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetunlock-quorum-hash");
394 1 : BOOST_CHECK(indexes.Add(0x001122334455667788L));
395 1 : BOOST_CHECK(!CheckAssetUnlockTx(blockman, qman, CTransaction(txManyOutputs), block_index, indexes, tx_state));
396 1 : BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetunlock-duplicated-index");
397 :
398 :
399 : // Should not be more than 32 withdrawal in one transaction
400 1 : txManyOutputs.vout.resize(outputsLimit + 1);
401 1 : txManyOutputs.vout.back().nValue = CENT;
402 1 : txManyOutputs.vout.back().scriptPubKey = GetScriptForDestination(PKHash(key.GetPubKey()));
403 1 : BOOST_CHECK(!CheckAssetUnlockTx(blockman, qman, CTransaction(txManyOutputs), block_index, std::nullopt, tx_state));
404 1 : BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetunlocktx-too-many-outs");
405 1 : }
406 :
407 1 : }
408 :
409 146 : BOOST_AUTO_TEST_SUITE_END()
|