/* * This file is part of the Flowee project * Copyright (C) 2011-2015 The Bitcoin Core developers * Copyright (C) 2016-2026 Tom Zander * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "transaction_tests.h" #include #include "TransactionBuilder.h" #include "data/tx_invalid.json.h" #include "data/tx_valid.json.h" #include "core_io.h" #include "main.h" #include "validation/Engine.h" // For checkTransaction #include "validation/BlockValidation_p.h" #include "validation/ValidationException.h" #include "policy/policy.h" #include "primitives/Block.h" #include "primitives/ScriptDefines.h" #include "primitives/script.h" #include "transaction_utils.h" #include "chainparams.h" #include #include #include #include #include #include #include #include #include #include #include // In script_tests.cpp extern UniValue read_json(const std::string& jsondata); static std::map mapFlagNames = boost::assign::map_list_of (std::string("NONE"), (unsigned int)SCRIPT_VERIFY_NONE) (std::string("P2SH"), (unsigned int)SCRIPT_VERIFY_P2SH) (std::string("STRICTENC"), (unsigned int)SCRIPT_VERIFY_STRICTENC) (std::string("DERSIG"), (unsigned int)SCRIPT_VERIFY_DERSIG) (std::string("LOW_S"), (unsigned int)SCRIPT_VERIFY_LOW_S) (std::string("SIGPUSHONLY"), (unsigned int)SCRIPT_VERIFY_SIGPUSHONLY) (std::string("MINIMALDATA"), (unsigned int)SCRIPT_VERIFY_MINIMALDATA) (std::string("NULLDUMMY"), (unsigned int)SCRIPT_VERIFY_NULLDUMMY) (std::string("DISCOURAGE_UPGRADABLE_NOPS"), (unsigned int)SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_NOPS) (std::string("CLEANSTACK"), (unsigned int)SCRIPT_VERIFY_CLEANSTACK) (std::string("NULLFAIL"), (unsigned int)SCRIPT_VERIFY_NULLFAIL) (std::string("CHECKLOCKTIMEVERIFY"), (unsigned int)SCRIPT_VERIFY_CHECKLOCKTIMEVERIFY) (std::string("CHECKSEQUENCEVERIFY"), (unsigned int)SCRIPT_VERIFY_CHECKSEQUENCEVERIFY) (std::string("SIGHASH_FORKID"), (unsigned int)SCRIPT_ENABLE_SIGHASH_FORKID); unsigned int TransactionTests::parseScriptFlags(const std::string &strFlags) { if (strFlags.empty()) { return 0; } unsigned int flags = 0; std::vector words; boost::algorithm::split(words, strFlags, boost::algorithm::is_any_of(",")); for (const std::string &word : words) { Q_ASSERT(mapFlagNames.count(word)); // if fail; "unknown verification flag"); flags |= mapFlagNames[word]; } return flags; } std::string FormatScriptFlags(unsigned int flags) { if (flags == 0) return std::string(); std::string ret; std::map::const_iterator it = mapFlagNames.begin(); while (it != mapFlagNames.end()) { if (flags & it->second) { ret += it->first + ","; } it++; } return ret.substr(0, ret.size() - 1); } void TransactionTests::tx_valid() { // Read tests from test/data/tx_valid.json // Format is an array of arrays // Inner arrays are either [ "comment" ] // or [[[prevout hash, prevout index, prevout scriptPubKey], [input 2], ...],"], serializedTransaction, verifyFlags // ... where all scripts are stringified scripts. // // verifyFlags is a comma separated list of script verification flags to apply, or "NONE" UniValue tests = read_json(std::string(json_tests::tx_valid, json_tests::tx_valid + sizeof(json_tests::tx_valid))); for (unsigned int idx = 0; idx < tests.size(); idx++) { UniValue test = tests[idx]; std::string strTest = test.write(); if (test[0].isArray()) { if (test.size() != 3 || !test[1].isStr() || !test[2].isStr() || test[1].get_str().empty() || test[2].get_str().empty()) { logCritical() << strTest; QFAIL("Bad test"); continue; } std::map mapprevOutScriptPubKeys; UniValue inputs = test[0].get_array(); bool fValid = true; for (unsigned int inpIdx = 0; inpIdx < inputs.size(); inpIdx++) { const UniValue& input = inputs[inpIdx]; if (!input.isArray()) { fValid = false; break; } UniValue vinput = input.get_array(); if (vinput.size() != 3) { fValid = false; break; } mapprevOutScriptPubKeys[COutPoint(uint256S(vinput[0].get_str()), vinput[1].get_int())] = ParseScript(vinput[2].get_str()); } Q_ASSERT(fValid); std::string transaction = test[1].get_str(); CDataStream stream(ParseHex(transaction), SER_NETWORK, PROTOCOL_VERSION); CTransaction tx; stream >> tx; auto newTx = Tx::fromOldTransaction(tx); try { Validation::checkTransaction(newTx); } catch (...) { QFAIL("validation of tx failed"); } for (unsigned int i = 0; i < tx.vin.size(); i++) { if (!mapprevOutScriptPubKeys.count(tx.vin[i].prevout)) QFAIL("Bad test"); int64_t amount = 0; unsigned int verify_flags = parseScriptFlags(test[2].get_str()); Script::State state(verify_flags); const bool ok = Script::verify(tx.vin[i].scriptSig, mapprevOutScriptPubKeys[tx.vin[i].prevout], TransactionSignatureChecker(&tx, newTx, i, amount), state); if (!ok) logDebug() << strTest; QCOMPARE(state.errorString(), "No error"); QVERIFY(ok); } } } } void TransactionTests::tx_invalid() { // Read tests from test/data/tx_invalid.json // Format is an array of arrays // Inner arrays are either [ "comment" ] // or [[[prevout hash, prevout index, prevout scriptPubKey], [input 2], ...],"], serializedTransaction, verifyFlags // ... where all scripts are stringified scripts. // // verifyFlags is a comma separated list of script verification flags to apply, or "NONE" UniValue tests = read_json(std::string(json_tests::tx_invalid, json_tests::tx_invalid + sizeof(json_tests::tx_invalid))); for (unsigned int idx = 0; idx < tests.size(); idx++) { UniValue test = tests[idx]; std::string strTest = test.write(); if (test[0].isArray()) { if (test.size() != 3 || !test[1].isStr() || !test[2].isStr()) QFAIL("Bad test"); std::map mapprevOutScriptPubKeys; UniValue inputs = test[0].get_array(); bool fValid = true; for (unsigned int inpIdx = 0; inpIdx < inputs.size(); inpIdx++) { const UniValue& input = inputs[inpIdx]; if (!input.isArray()) { fValid = false; break; } UniValue vinput = input.get_array(); if (vinput.size() != 3) { fValid = false; break; } mapprevOutScriptPubKeys[COutPoint(uint256S(vinput[0].get_str()), vinput[1].get_int())] = ParseScript(vinput[2].get_str()); } Q_ASSERT(fValid); std::string transaction = test[1].get_str(); CDataStream stream(ParseHex(transaction), SER_NETWORK, PROTOCOL_VERSION); CTransaction tx; stream >> tx; auto newTx = Tx::fromOldTransaction(tx); try { Validation::checkTransaction(newTx); } catch (...) { fValid = false; } Script::State scriptState; for (unsigned int i = 0; i < tx.vin.size() && fValid; i++) { if (!mapprevOutScriptPubKeys.count(tx.vin[i].prevout)) QFAIL("Bad test"); int64_t amount = 0; scriptState.flags = parseScriptFlags(test[2].get_str()); fValid = Script::verify(tx.vin[i].scriptSig, mapprevOutScriptPubKeys[tx.vin[i].prevout], TransactionSignatureChecker(&tx, newTx, i, amount), scriptState); if (fValid) QVERIFY(scriptState.error == SCRIPT_ERR_OK); else QVERIFY(scriptState.error != SCRIPT_ERR_OK); } QVERIFY(!fValid); } } } static std::vector getTestTx() { // Random real transaction (e2769b09e784f32f62ef849763d4f45b98e07ba658647343b915ff832b110436) unsigned char ch[] = {0x01, 0x00, 0x00, 0x00, 0x01, 0x6b, 0xff, 0x7f, 0xcd, 0x4f, 0x85, 0x65, 0xef, 0x40, 0x6d, 0xd5, 0xd6, 0x3d, 0x4f, 0xf9, 0x4f, 0x31, 0x8f, 0xe8, 0x20, 0x27, 0xfd, 0x4d, 0xc4, 0x51, 0xb0, 0x44, 0x74, 0x01, 0x9f, 0x74, 0xb4, 0x00, 0x00, 0x00, 0x00, 0x8c, 0x49, 0x30, 0x46, 0x02, 0x21, 0x00, 0xda, 0x0d, 0xc6, 0xae, 0xce, 0xfe, 0x1e, 0x06, 0xef, 0xdf, 0x05, 0x77, 0x37, 0x57, 0xde, 0xb1, 0x68, 0x82, 0x09, 0x30, 0xe3, 0xb0, 0xd0, 0x3f, 0x46, 0xf5, 0xfc, 0xf1, 0x50, 0xbf, 0x99, 0x0c, 0x02, 0x21, 0x00, 0xd2, 0x5b, 0x5c, 0x87, 0x04, 0x00, 0x76, 0xe4, 0xf2, 0x53, 0xf8, 0x26, 0x2e, 0x76, 0x3e, 0x2d, 0xd5, 0x1e, 0x7f, 0xf0, 0xbe, 0x15, 0x77, 0x27, 0xc4, 0xbc, 0x42, 0x80, 0x7f, 0x17, 0xbd, 0x39, 0x01, 0x41, 0x04, 0xe6, 0xc2, 0x6e, 0xf6, 0x7d, 0xc6, 0x10, 0xd2, 0xcd, 0x19, 0x24, 0x84, 0x78, 0x9a, 0x6c, 0xf9, 0xae, 0xa9, 0x93, 0x0b, 0x94, 0x4b, 0x7e, 0x2d, 0xb5, 0x34, 0x2b, 0x9d, 0x9e, 0x5b, 0x9f, 0xf7, 0x9a, 0xff, 0x9a, 0x2e, 0xe1, 0x97, 0x8d, 0xd7, 0xfd, 0x01, 0xdf, 0xc5, 0x22, 0xee, 0x02, 0x28, 0x3d, 0x3b, 0x06, 0xa9, 0xd0, 0x3a, 0xcf, 0x80, 0x96, 0x96, 0x8d, 0x7d, 0xbb, 0x0f, 0x91, 0x78, 0xff, 0xff, 0xff, 0xff, 0x02, 0x8b, 0xa7, 0x94, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x19, 0x76, 0xa9, 0x14, 0xba, 0xde, 0xec, 0xfd, 0xef, 0x05, 0x07, 0x24, 0x7f, 0xc8, 0xf7, 0x42, 0x41, 0xd7, 0x3b, 0xc0, 0x39, 0x97, 0x2d, 0x7b, 0x88, 0xac, 0x40, 0x94, 0xa8, 0x02, 0x00, 0x00, 0x00, 0x00, 0x19, 0x76, 0xa9, 0x14, 0xc1, 0x09, 0x32, 0x48, 0x3f, 0xec, 0x93, 0xed, 0x51, 0xf5, 0xfe, 0x95, 0xe7, 0x25, 0x59, 0xf2, 0xcc, 0x70, 0x43, 0xf9, 0x88, 0xac, 0x00, 0x00, 0x00, 0x00, 0x00}; std::vector vch(ch, ch + sizeof(ch) -1); return vch; } void TransactionTests::basic_transaction_tests() { auto vch = getTestTx(); CDataStream stream(vch, SER_DISK, CLIENT_VERSION); CMutableTransaction tx; stream >> tx; try { // Simple deserialized transaction should be valid. Validation::checkTransaction(Tx::fromOldTransaction(tx)); } catch (...) { QFAIL("failed validation 1"); } // Check that duplicate txins fail tx.vin.push_back(tx.vin[0]); try { // Transaction with duplicate txins should be invalid. Validation::checkTransaction(Tx::fromOldTransaction(tx)); QFAIL("Failed to fail validation checks"); } catch (...) { } } // // Helper: create two dummy transactions, each with // two outputs. The first has 11 and 50 CENT outputs // paid to a TX_PUBKEY, the second 21 and 22 CENT outputs // paid to a TX_PUBKEYHASH. // static std::vector SetupDummyInputs(std::vector &keys) { std::vector dummyTransactions; dummyTransactions.resize(2); keys.resize(4); // Add some keys to the keystore: for (int i = 0; i < 4; i++) { keys[i].makeNewKey(i % 2); } // Create some dummy input transactions dummyTransactions[0].vout.resize(2); dummyTransactions[0].vout[0].nValue = 11*CENT; dummyTransactions[0].vout[0].scriptPubKey << ToByteVector(keys[0].getPubKey()) << OP_CHECKSIG; dummyTransactions[0].vout[1].nValue = 50*CENT; dummyTransactions[0].vout[1].scriptPubKey << ToByteVector(keys[1].getPubKey()) << OP_CHECKSIG; dummyTransactions[1].vout.resize(2); dummyTransactions[1].vout[0].nValue = 21*CENT; dummyTransactions[1].vout[0].scriptPubKey = GetScriptForDestination(keys[2].getPubKey().getKeyId()); dummyTransactions[1].vout[1].nValue = 22*CENT; dummyTransactions[1].vout[1].scriptPubKey = GetScriptForDestination(keys[3].getPubKey().getKeyId()); return dummyTransactions; } void TransactionTests::test_IsStandard() { LOCK(cs_main); std::vector keystore; std::vector dummyTransactions = SetupDummyInputs(keystore); CMutableTransaction t; t.vin.resize(1); t.vin[0].prevout.hash = dummyTransactions[0].GetHash(); t.vin[0].prevout.n = 1; t.vin[0].scriptSig << std::vector(65, 0); t.vout.resize(1); t.vout[0].nValue = 90*CENT; PrivateKey key; key.makeNewKey(true); t.vout[0].scriptPubKey = GetScriptForDestination(key.getPubKey().getKeyId()); std::string reason; QVERIFY(IsStandardTx(t, reason)); // Check dust with default relay fee: int64_t nDustThreshold = 182 * minRelayTxFee.GetFeePerK()/1000 * 3; QCOMPARE(nDustThreshold, (int64_t) 546); // dust: t.vout[0].nValue = nDustThreshold - 1; QVERIFY(!IsStandardTx(t, reason)); // not dust: t.vout[0].nValue = nDustThreshold; QVERIFY(IsStandardTx(t, reason)); // Check dust with odd relay fee to verify rounding: // nDustThreshold = 182 * 1234 / 1000 * 3 minRelayTxFee = CFeeRate(1234); // dust: t.vout[0].nValue = 672 - 1; QVERIFY(!IsStandardTx(t, reason)); // not dust: t.vout[0].nValue = 672; QVERIFY(IsStandardTx(t, reason)); minRelayTxFee = CFeeRate(Settings::DefaultMinRelayTxFee); t.vout[0].scriptPubKey = CScript() << OP_1; QVERIFY(IsStandardTx(t, reason)); Script::TxnOutType type; int dataUsed = 0; QVERIFY(IsStandard(t.vout[0].scriptPubKey, type, dataUsed)); QCOMPARE(type, Script::TX_SCRIPT); t.vout[0].scriptPubKey.resize(Script::MAX_P2S_SCRIPT_SIZE + 1); for (auto &byte : t.vout[0].scriptPubKey) byte = static_cast(OP_NOP); QVERIFY(!IsStandardTx(t, reason)); QCOMPARE(reason, std::string("scriptpubkey")); t.vout[0].scriptPubKey = GetScriptForDestination(key.getPubKey().getKeyId()); t.vin[0].scriptSig.resize(1651); QVERIFY(IsStandardTx(t, reason)); t.vin[0].scriptSig.resize(MAX_SCRIPT_SIZE + 1); QVERIFY(!IsStandardTx(t, reason)); QCOMPARE(reason, std::string("scriptsig-size")); t.vin[0].scriptSig = CScript() << std::vector(65, 0); // MAX_OP_RETURN_RELAY-byte TX_NULL_DATA (standard) t.vout[0].scriptPubKey = CScript() << OP_RETURN << ParseHex("04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef3804678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38" "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); QCOMPARE(Settings::MaxOpReturnRelay, t.vout[0].scriptPubKey.size()); QVERIFY2(IsStandardTx(t, reason), reason.c_str()); // Multiple op-returns. t.vout.push_back(t.vout.at(0)); t.vout[0].scriptPubKey = CScript() << OP_RETURN // 58 + 2 bytes << ParseHex("04678afdb0fe55482719bc3f4cef3804678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38123456"); t.vout[1].scriptPubKey = CScript() << OP_RETURN // 103 + 3 bytes << ParseHex("04678afdb0fe55482719bc3f4cef3804678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38" "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); QVERIFY(IsStandardTx(t, reason)); // limit is currently 223 t.vout.push_back(t.vout.at(0)); t.vout[2].scriptPubKey = CScript() << OP_RETURN // 57 + 2 << ParseHex("04678afdb0fe55482719bc3f4cef3804678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38"); QVERIFY(IsStandardTx(t, reason)); // exactly within limit. // one byte over limit. t.vout[2].scriptPubKey = t.vout[1].scriptPubKey; QCOMPARE(IsStandardTx(t, reason), false); QCOMPARE(reason, "oversize-op-return"); t.vout.resize(1); // MAX_OP_RETURN_RELAY+1-byte TX_NULL_DATA (non-standard) t.vout[0].scriptPubKey = CScript() << OP_RETURN << ParseHex("04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef3804678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef3800" "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); QCOMPARE(Settings::MaxOpReturnRelay + 1, t.vout[0].scriptPubKey.size()); QVERIFY(!IsStandardTx(t, reason)); // Data payload can be encoded in any way... t.vout[0].scriptPubKey = CScript() << OP_RETURN << ParseHex(""); QVERIFY(IsStandardTx(t, reason)); t.vout[0].scriptPubKey = CScript() << OP_RETURN << ParseHex("00") << ParseHex("01"); QVERIFY(IsStandardTx(t, reason)); // OP_RESERVED *is* considered to be a PUSHDATA type opcode by IsPushOnly()! t.vout[0].scriptPubKey = CScript() << OP_RETURN << OP_RESERVED << -1 << 0 << ParseHex("01") << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << 10 << 11 << 12 << 13 << 14 << 15 << 16; QVERIFY(IsStandardTx(t, reason)); t.vout[0].scriptPubKey = CScript() << OP_RETURN << 0 << ParseHex("01") << 2 << ParseHex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); QVERIFY(IsStandardTx(t, reason)); // A short non-push OP_RETURN script is standard after Layla as P2S, not TX_NULL_DATA. t.vout[0].scriptPubKey = CScript() << OP_RETURN << OP_RETURN; QVERIFY(IsStandardTx(t, reason)); // TX_NULL_DATA w/o PUSHDATA t.vout.resize(1); t.vout[0].scriptPubKey = CScript() << OP_RETURN; QVERIFY(IsStandardTx(t, reason)); // Multiple TX_NULL_DATA outputs are standard when the aggregate size is within the relay limit. t.vout.resize(2); t.vout[1].nValue = 0; t.vout[0].scriptPubKey = CScript() << OP_RETURN << ParseHex("04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38"); t.vout[1].scriptPubKey = CScript() << OP_RETURN << ParseHex("04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38"); QVERIFY(IsStandardTx(t, reason)); t.vout[0].scriptPubKey = CScript() << OP_RETURN << ParseHex("04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38"); t.vout[1].scriptPubKey = CScript() << OP_RETURN; QVERIFY(IsStandardTx(t, reason)); t.vout[0].scriptPubKey = CScript() << OP_RETURN; t.vout[1].scriptPubKey = CScript() << OP_RETURN; QVERIFY(IsStandardTx(t, reason)); } void TransactionTests::transactionIter() { auto vch = getTestTx(); Streaming::BufferPool pl(vch.size()); memcpy(pl.begin(), vch.data(), vch.size()); Tx tx(pl.commit(vch.size())); auto iter = Tx::Iterator(tx); auto type = iter.next(); QCOMPARE(type, Tx::TxVersion); QCOMPARE(iter.intData(), 1); type = iter.next(); QCOMPARE(type, Tx::PrevTxHash); QCOMPARE(iter.byteData().size(), 32); auto prev = iter.uint256Data(); QVERIFY(prev == uint256S("0xb4749f017444b051c44dfd2720e88f314ff94f3dd6d56d40ef65854fcd7fff6b")); type = iter.next(); QCOMPARE(type, Tx::PrevTxIndex); QCOMPARE(iter.intData(), 0); type = iter.next(); QCOMPARE(type, Tx::TxInScript); QCOMPARE(iter.byteData().size(), 140); type = iter.next(); QCOMPARE(type, Tx::Sequence); QCOMPARE(iter.uintData(), 0xffffffff); QCOMPARE(iter.longData(), (uint64_t) 0xffffffff); type = iter.next(); QCOMPARE(type, Tx::OutputValue); QCOMPARE(iter.longData(), (uint64_t) 244623243); type = iter.next(); QCOMPARE(type, Tx::OutputScript); QCOMPARE(iter.byteData().size(), 25); type = iter.next(); QCOMPARE(type, Tx::OutputValue); QCOMPARE(iter.longData(), (uint64_t) 44602432); type = iter.next(); QCOMPARE(type, Tx::OutputScript); QCOMPARE(iter.byteData().size(), 25); type = iter.next(); QCOMPARE(type, Tx::LockTime); type = iter.next(); QCOMPARE(type, Tx::End); } void TransactionTests::transactionIter2() { // coinbase-tx Block genesisBlock = Params(CBaseChainParams::MAIN).GenesisBlock(); QCOMPARE(genesisBlock.size(), 285); auto iter = Tx::Iterator(genesisBlock); auto type = iter.next(); QCOMPARE(type, Tx::TxVersion); QCOMPARE(iter.intData(), 1); type = iter.next(); QCOMPARE(type, Tx::PrevTxHash); QCOMPARE(iter.byteData().size(), 32); auto prev = iter.uint256Data(); QVERIFY(prev == uint256S("0x0000000000000000000000000000000000000000000000000000000000000000")); type = iter.next(); QCOMPARE(type, Tx::PrevTxIndex); QCOMPARE(iter.intData(), -1); type = iter.next(); QCOMPARE(type, Tx::TxInScript); QCOMPARE(iter.byteData().size(), 77); type = iter.next(); QCOMPARE(type, Tx::Sequence); QCOMPARE(iter.uintData(), 0xffffffff); QCOMPARE(iter.longData(), (uint64_t) 0xffffffff); type = iter.next(); QCOMPARE(type, Tx::OutputValue); QCOMPARE(iter.longData(), (uint64_t) 50 * COIN); type = iter.next(); QCOMPARE(type, Tx::OutputScript); QCOMPARE(iter.byteData().size(), 67); type = iter.next(); QCOMPARE(type, Tx::LockTime); QCOMPARE(iter.intData(), 0); type = iter.next(); QCOMPARE(type, Tx::End); Tx tx = iter.prevTx(); QCOMPARE(tx.size(), 204); logFatal() << tx.createHash(); QVERIFY(tx.createHash() == uint256S("0x4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b")); genesisBlock.findTransactions(); QCOMPARE(genesisBlock.transactions().size(), (size_t) 1); QCOMPARE(genesisBlock.transactions().front().size(), 204); QVERIFY(genesisBlock.transactions().front().createHash() == uint256S("0x4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b")); } void TransactionTests::txIterWithTokens() { // we test the first ever cashtoken transaction that has landed on mainchain. // TxId: 2edbe1d87de2a93f26cf4764342f67b0fdba51f05ae3cc067c1ac3746454afcb auto pool = Streaming::pool(350); pool->writeHex("010000000113232ccb0acabfaf3b2936814a9ab60d5218ff2105bb06099a9ab120ea2ee183000000006441647ee7a32d8d1cf46ac15e61ac48b79300e57f107a4e2d1ae4478ac4e8c180a70ed87dbb0bf00c230100191ddf779a5912c14cd7e33f0d27e05f0d4d7e5743cf412103eaa57987b59cb1e2d31aa3daa9a746c26b6016317000749486e06c3048c86528feffffff037364557e000000001976a91464efa2b10c9c167323ef39a4f1a40d167400447288ac200300000000000045ef13232ccb0acabfaf3b2936814a9ab60d5218ff2105bb06099a9ab120ea2ee1837204b33ff00dfe00752b7d76a91464efa2b10c9c167323ef39a4f1a40d167400447288ac00000000000000004c6a0442434d5220a8dd9f3f77efec7d4c159cbd28f6181e704dd0c298baea0cc3488cd1a176dc912463332d736f66742e636f6d2f746f6b656e732f66616c6c6f75745f636f696e2e6a736f6ec3180c00"); Tx tx(pool->commit()); QCOMPARE(tx.size(), 348); auto iter = Tx::Iterator(tx); auto type = iter.next(); QCOMPARE(type, Tx::TxVersion); QCOMPARE(iter.intData(), 1); type = iter.next(); QCOMPARE(type, Tx::PrevTxHash); type = iter.next(); QCOMPARE(type, Tx::PrevTxIndex); QCOMPARE(iter.intData(), 0); type = iter.next(); QCOMPARE(type, Tx::TxInScript); QCOMPARE(iter.byteData().size(), 100); type = iter.next(); QCOMPARE(type, Tx::Sequence); // output one is boringly normal. type = iter.next(); QCOMPARE(type, Tx::OutputValue); QCOMPARE(iter.longData(), (uint64_t) 2119525491); type = iter.next(); QCOMPARE(type, Tx::OutputScript); QCOMPARE(iter.dataLength(), 25); // output two is a token. type = iter.next(); QCOMPARE(type, Tx::OutputValue); QCOMPARE(iter.longData(), (uint64_t) 800); type = iter.next(); QCOMPARE(type, Tx::CashTokenCategory); QCOMPARE(iter.uint256Data(), uint256S("83e12eea20b19a9a0906bb0521ff18520db69a4a8136293bafbfca0acb2c2313")); type = iter.next(); QCOMPARE(type, Tx::CashTokenBitfield); QCOMPARE(iter.dataLength(), 1); auto bits = iter.bitfieldData(); QVERIFY(bits & Tx::HasNft); QVERIFY(bits & Tx::HasCommitment); QVERIFY(bits & Tx::HasFtAmount); QVERIFY(bits & Tx::NftMintBaton); QVERIFY((bits & Tx::NftMutableBaton) == 0); QCOMPARE(bits, 114); type = iter.next(); QCOMPARE(type, Tx::CashTokenCommitment); QCOMPARE(iter.dataLength(), 4); type = iter.next(); QCOMPARE(type, Tx::CashTokenAmount); QCOMPARE(iter.longData(), (uint64_t) 2100000000); type = iter.next(); QCOMPARE(type, Tx::OutputScript); QCOMPARE(iter.dataLength(), 25); // 3rd and last output is an op-return (bcmr) type = iter.next(); QCOMPARE(type, Tx::OutputValue); QCOMPARE(iter.longData(), (uint64_t) 0); type = iter.next(); QCOMPARE(type, Tx::OutputScript); QCOMPARE(iter.dataLength(), 76); auto bytes = iter.byteData(); QCOMPARE(bytes[0], 106); // op-return QCOMPARE(bytes[1], 4); // magic marker part one. // magic marker part 2. QCOMPARE(std::string(bytes.begin() + 2, 4), std::string("BCMR")); type = iter.next(); QCOMPARE(type, Tx::LockTime); type = iter.next(); QCOMPARE(type, Tx::End); auto iter2 = Tx::Iterator(tx); auto output1 = iter2.nextOutput(); QCOMPARE(output1.hasToken(), false); QCOMPARE(output1.outputValue, (int64_t) 2119525491); QCOMPARE(output1.outputScript().size(), 25); auto output2 = iter2.nextOutput(); QCOMPARE(output2.hasToken(), true); QCOMPARE(output2.outputValue, (int64_t) 800); QCOMPARE(output2.outputScript().size(), 25); QCOMPARE(tx.output(1).outputValue, (int64_t) 800); auto skippedOutput = Tx::Iterator(tx).nextOutput(1); QCOMPARE(skippedOutput.outputValue, (int64_t) 800); auto output3 = iter2.nextOutput(); QCOMPARE(output3.hasToken(), false); QCOMPARE(output3.outputValue, (int64_t) 0); QCOMPARE(output3.outputScript().size(), 76); QCOMPARE(output3.outputScript(), bytes); auto end = iter2.nextOutput(); QCOMPARE(end.outputValue, (int64_t) -1); auto token = output2.token(); QCOMPARE(token.category, uint256S("83e12eea20b19a9a0906bb0521ff18520db69a4a8136293bafbfca0acb2c2313")); QCOMPARE(token.amount, 2100000000); QCOMPARE(token.commitment.size(), 4); QCOMPARE(token.hasAmount(), true); QCOMPARE(token.hasNft(), true); QCOMPARE(token.hasCommitment(), true); QCOMPARE(token.isImmutableNft(), false); QCOMPARE(token.isMutableNft(), false); QCOMPARE(token.isMintingNft(), true); } static Tx makeTransactionOfSize(int size, int version = CTransaction::CURRENT_VERSION) { CMutableTransaction tx; tx.nVersion = version; tx.vin.resize(1); tx.vin[0].prevout = COutPoint(uint256S("01"), 0); tx.vout.resize(1); tx.vout[0].scriptPubKey = CScript() << OP_TRUE; int txSize = static_cast(::GetSerializeSize(CTransaction(tx), SER_NETWORK, PROTOCOL_VERSION)); if (txSize > size) throw std::runtime_error("test transaction template is larger than requested size"); tx.vin[0].scriptSig.resize(size - txSize); return Tx::fromOldTransaction(tx); } static void expectContextualTxReject(const Tx &tx, ValidationFlags flags, const std::string &reason) { try { ValidationPrivate::checkContextualTransaction(tx, flags); QFAIL("transaction was expected to be rejected"); } catch (const Validation::Exception &e) { QCOMPARE(std::string(e.what()), reason); } } static CScript tokenOutputScript(const uint256 &category, uint8_t bitfield, uint64_t amount = 0, const std::vector &commitment = {}) { CScript script; script.push_back(0xef); script.insert(script.end(), category.begin(), category.end()); script.push_back(bitfield); if (bitfield & Tx::HasCommitment) { script.push_back(static_cast(commitment.size())); script.insert(script.end(), commitment.begin(), commitment.end()); } if (bitfield & Tx::HasFtAmount) { if (amount < 253) { script.push_back(static_cast(amount)); } else if (amount <= std::numeric_limits::max()) { script.push_back(253); script.push_back(static_cast(amount)); script.push_back(static_cast(amount >> 8)); } else if (amount <= std::numeric_limits::max()) { script.push_back(254); for (int i = 0; i < 4; ++i) script.push_back(static_cast(amount >> (8 * i))); } else { script.push_back(255); for (int i = 0; i < 8; ++i) script.push_back(static_cast(amount >> (8 * i))); } } script << OP_TRUE; return script; } static Tx txWithOutputScript(const CScript &outputScript, bool coinbase = false) { CMutableTransaction tx; tx.nVersion = CTransaction::CURRENT_VERSION; tx.vin.resize(1); if (coinbase) { tx.vin[0].prevout.SetNull(); tx.vin[0].scriptSig = CScript() << OP_TRUE; } else { tx.vin[0].prevout = COutPoint(uint256S("01"), 0); } tx.vout.resize(1); tx.vout[0].nValue = 1000; tx.vout[0].scriptPubKey = outputScript; return Tx::fromOldTransaction(CTransaction(tx)); } static Tx txWithOutputScriptAndInputPadding(const CScript &outputScript) { CMutableTransaction tx; tx.nVersion = CTransaction::CURRENT_VERSION; tx.vin.resize(1); tx.vin[0].prevout = COutPoint(uint256S("01"), 0); tx.vin[0].scriptSig.resize(80); tx.vout.resize(1); tx.vout[0].nValue = 1000; tx.vout[0].scriptPubKey = outputScript; return Tx::fromOldTransaction(CTransaction(tx)); } void TransactionTests::may2023_cashtoken_output_structure_rules() { ValidationFlags hf2023Flags; hf2023Flags.hf201811Active = true; hf2023Flags.hf202305Active = true; const uint256 category = uint256S("0x0000000000000000000000000000000000000000000000000000000000001234"); ValidationPrivate::checkContextualTransaction(txWithOutputScript(tokenOutputScript(category, Tx::HasFtAmount, 1)), hf2023Flags); expectContextualTxReject(txWithOutputScript(tokenOutputScript(category, Tx::HasFtAmount, 1), true), hf2023Flags, "bad-txns-coinbase-has-tokens"); expectContextualTxReject(txWithOutputScript(tokenOutputScript(category, 0x80)), hf2023Flags, "bad-txns-token-bad-bitfield"); expectContextualTxReject(txWithOutputScript(tokenOutputScript(category, Tx::HasFtAmount, 0)), hf2023Flags, "bad-txns-token-non-nft-amount-zero"); expectContextualTxReject(txWithOutputScript(tokenOutputScript(category, Tx::HasNft | Tx::HasCommitment)), hf2023Flags, "bad-txns-token-commitment-bitfield-mismatch"); } void TransactionTests::may2023_cashtoken_output_structure_edge_cases() { ValidationFlags hf2023Flags; hf2023Flags.hf201811Active = true; hf2023Flags.hf202305Active = true; ValidationFlags hf2026Flags = hf2023Flags; hf2026Flags.hf202605Active = true; const uint256 category = uint256S("0x0000000000000000000000000000000000000000000000000000000000001234"); ValidationPrivate::checkContextualTransaction(txWithOutputScript(tokenOutputScript(category, Tx::HasNft)), hf2023Flags); ValidationPrivate::checkContextualTransaction(txWithOutputScript(tokenOutputScript(category, Tx::HasNft | Tx::NftMutableBaton)), hf2023Flags); ValidationPrivate::checkContextualTransaction(txWithOutputScript(tokenOutputScript(category, Tx::HasNft | Tx::NftMintBaton)), hf2023Flags); ValidationPrivate::checkContextualTransaction(txWithOutputScript(tokenOutputScript(category, Tx::HasNft | Tx::HasCommitment | Tx::HasFtAmount, 253, {0x01, 0x02})), hf2023Flags); ValidationPrivate::checkContextualTransaction(txWithOutputScript(tokenOutputScript(category, Tx::HasFtAmount, static_cast(std::numeric_limits::max()))), hf2023Flags); expectContextualTxReject(txWithOutputScript(tokenOutputScript(category, 0)), hf2023Flags, "bad-txns-token-bad-bitfield"); expectContextualTxReject(txWithOutputScript(tokenOutputScript(category, Tx::HasFtAmount | Tx::NftMutableBaton, 1)), hf2023Flags, "bad-txns-token-bad-bitfield"); expectContextualTxReject(txWithOutputScript(tokenOutputScript(category, Tx::HasNft | 3)), hf2023Flags, "bad-txns-token-bad-bitfield"); expectContextualTxReject(txWithOutputScript(tokenOutputScript(category, Tx::HasCommitment | Tx::HasFtAmount, 1, {0x01})), hf2023Flags, "bad-txns-token-bad-bitfield"); expectContextualTxReject(txWithOutputScript(tokenOutputScript(category, Tx::HasNft | Tx::HasFtAmount, 0)), hf2023Flags, "bad-txns-token-amount-bitfield-mismatch"); expectContextualTxReject(txWithOutputScript(tokenOutputScript(category, Tx::HasFtAmount, static_cast(std::numeric_limits::max()) + 1)), hf2023Flags, "bad-txns-token-amount-outofrange"); expectContextualTxReject(txWithOutputScript(tokenOutputScript(category, Tx::HasNft | Tx::HasCommitment, 0, std::vector(41, 0x01))), hf2023Flags, "bad-txns-token-commitment-oversized"); ValidationPrivate::checkContextualTransaction(txWithOutputScript(tokenOutputScript(category, Tx::HasNft | Tx::HasCommitment, 0, std::vector(128, 0x01))), hf2026Flags); expectContextualTxReject(txWithOutputScript(tokenOutputScript(category, Tx::HasNft | Tx::HasCommitment, 0, std::vector(129, 0x01))), hf2026Flags, "bad-txns-token-commitment-oversized"); CScript shortPrefix; shortPrefix.push_back(0xef); shortPrefix << OP_TRUE; expectContextualTxReject(txWithOutputScriptAndInputPadding(shortPrefix), hf2023Flags, "bad-tx-data"); } static CTransaction tokenInputSpendTx() { CMutableTransaction tx; tx.nVersion = CTransaction::CURRENT_VERSION; tx.vin.resize(1); tx.vin[0].prevout = COutPoint(uint256S("01"), 1); tx.vout.resize(1); tx.vout[0].nValue = 900; tx.vout[0].scriptPubKey = CScript() << OP_TRUE; return CTransaction(tx); } static CTransaction tokenSpendTx(const std::vector &prevouts, const std::vector &outputScripts) { CMutableTransaction tx; tx.nVersion = CTransaction::CURRENT_VERSION; tx.vin.resize(prevouts.size()); for (size_t i = 0; i < prevouts.size(); ++i) tx.vin[i].prevout = prevouts.at(i); tx.vout.resize(outputScripts.size()); for (size_t i = 0; i < outputScripts.size(); ++i) { tx.vout[i].nValue = 100; tx.vout[i].scriptPubKey = outputScripts.at(i); } return CTransaction(tx); } static UnspentOutputData tokenPrevout(const CScript &outputScript, int blockHeight) { UnspentOutputData out(Streaming::bufferFrom(outputScript), 1000); out.blockHeight = blockHeight; return out; } static UnspentOutputData plainPrevout(int blockHeight) { return tokenPrevout(CScript() << OP_TRUE, blockHeight); } static void expectTokenInputReject(const CTransaction tx, const Tx &newTx, const std::vector &unspents, ValidationFlags flags, const std::string &reason) { int64_t fees = 0; uint32_t sigChecks = 0; bool spendsCoinbase = false; try { ValidationPrivate::validateTransactionInputs(tx, newTx, unspents, Params().GetConsensus().hf202305Height, flags, fees, sigChecks, spendsCoinbase, false); QFAIL("transaction inputs were expected to be rejected"); } catch (const Validation::Exception &e) { QCOMPARE(std::string(e.what()), reason); } } void TransactionTests::may2023_cashtoken_input_structure_rules() { ValidationFlags beforeMay2023; ValidationFlags hf2023Flags; hf2023Flags.hf201811Active = true; hf2023Flags.hf202305Active = true; const int activationHeight = Params().GetConsensus().hf202305Height; const uint256 category = uint256S("0x0000000000000000000000000000000000000000000000000000000000001234"); std::vector unspents; unspents.push_back(tokenPrevout(tokenOutputScript(category, Tx::HasFtAmount, 1), activationHeight)); int64_t fees = 0; uint32_t sigChecks = 0; bool spendsCoinbase = false; const auto old = tokenInputSpendTx(); auto tx = Tx::fromOldTransaction(old); ValidationPrivate::validateTransactionInputs(old, tx, unspents, activationHeight, hf2023Flags, fees, sigChecks, spendsCoinbase, false); QCOMPARE(fees, static_cast(100)); unspents[0] = tokenPrevout(tokenOutputScript(category, 0x80), activationHeight); expectTokenInputReject(old, tx, unspents, hf2023Flags, "bad-txns-token-bad-bitfield"); } void TransactionTests::may2023_cashtoken_amount_accounting_rules() { ValidationFlags hf2023Flags; hf2023Flags.hf201811Active = true; hf2023Flags.hf202305Active = true; const int activationHeight = Params().GetConsensus().hf202305Height; const uint256 category = uint256S("0x0000000000000000000000000000000000000000000000000000000000001234"); const uint256 otherCategory = uint256S("0x0000000000000000000000000000000000000000000000000000000000005678"); std::vector unspents; unspents.push_back(tokenPrevout(tokenOutputScript(category, Tx::HasFtAmount, 100), activationHeight)); auto validSpend = tokenSpendTx({COutPoint(uint256S("01"), 1)}, {tokenOutputScript(category, Tx::HasFtAmount, 70)}); auto tx = Tx::fromOldTransaction(validSpend); int64_t fees = 0; uint32_t sigChecks = 0; bool spendsCoinbase = false; ValidationPrivate::validateTransactionInputs(validSpend, tx, unspents, activationHeight, hf2023Flags, fees, sigChecks, spendsCoinbase, false); QCOMPARE(fees, static_cast(900)); auto old = tokenSpendTx({COutPoint(uint256S("01"), 1)}, {tokenOutputScript(category, Tx::HasFtAmount, 101)}); tx = Tx::fromOldTransaction(old); expectTokenInputReject(old, tx, unspents, hf2023Flags, "bad-txns-token-in-belowout"); old = tokenSpendTx({COutPoint(uint256S("01"), 1)}, {tokenOutputScript(otherCategory, Tx::HasFtAmount, 1)}); tx = Tx::fromOldTransaction(old); expectTokenInputReject(old, tx, unspents, hf2023Flags, "bad-txns-token-invalid-category"); unspents[0] = plainPrevout(activationHeight); auto genesis = tokenSpendTx({COutPoint(otherCategory, 0)}, {tokenOutputScript(otherCategory, Tx::HasFtAmount, 10)}); tx = Tx::fromOldTransaction(genesis); ValidationPrivate::validateTransactionInputs(genesis, tx, unspents, activationHeight, hf2023Flags, fees, sigChecks, spendsCoinbase, false); unspents[0] = tokenPrevout(tokenOutputScript(otherCategory, Tx::HasFtAmount, 10), activationHeight); old = tokenSpendTx({COutPoint(otherCategory, 0)}, {tokenOutputScript(otherCategory, Tx::HasFtAmount, 1)}); tx = Tx::fromOldTransaction(old); expectTokenInputReject(old, tx, unspents, hf2023Flags, "bad-txns-token-dupe-genesis"); } void TransactionTests::may2023_cashtoken_amount_accounting_edge_cases() { ValidationFlags hf2023Flags; hf2023Flags.hf201811Active = true; hf2023Flags.hf202305Active = true; const int activationHeight = Params().GetConsensus().hf202305Height; const uint256 category = uint256S("0x0000000000000000000000000000000000000000000000000000000000001234"); std::vector unspents; unspents.push_back(tokenPrevout(tokenOutputScript(category, Tx::HasFtAmount, 60), activationHeight)); unspents.push_back(tokenPrevout(tokenOutputScript(category, Tx::HasFtAmount, 40), activationHeight)); auto splitSpend = tokenSpendTx({COutPoint(uint256S("01"), 1), COutPoint(uint256S("02"), 1)}, {tokenOutputScript(category, Tx::HasFtAmount, 1), tokenOutputScript(category, Tx::HasFtAmount, 99)}); int64_t fees = 0; uint32_t sigChecks = 0; bool spendsCoinbase = false; auto tx = Tx::fromOldTransaction(splitSpend); ValidationPrivate::validateTransactionInputs(splitSpend, tx, unspents, activationHeight, hf2023Flags, fees, sigChecks, spendsCoinbase, false); QCOMPARE(fees, static_cast(1800)); unspents[0] = tokenPrevout(tokenOutputScript(category, Tx::HasFtAmount, static_cast(std::numeric_limits::max())), activationHeight); unspents[1] = tokenPrevout(tokenOutputScript(category, Tx::HasFtAmount, 1), activationHeight); auto old = tokenSpendTx({COutPoint(uint256S("01"), 1), COutPoint(uint256S("02"), 1)}, {CScript() << OP_TRUE}); tx = Tx::fromOldTransaction(old); expectTokenInputReject(old, tx, unspents, hf2023Flags, "bad-txns-token-amount-overflow"); unspents = {tokenPrevout(tokenOutputScript(category, Tx::HasFtAmount, 100), activationHeight)}; old = tokenSpendTx({COutPoint(uint256S("01"), 1)}, {tokenOutputScript(category, Tx::HasFtAmount, 50), tokenOutputScript(category, Tx::HasFtAmount, 51)}); tx = Tx::fromOldTransaction(old); expectTokenInputReject(old, tx, unspents, hf2023Flags, "bad-txns-token-in-belowout"); } void TransactionTests::may2023_cashtoken_nft_accounting_rules() { ValidationFlags hf2023Flags; hf2023Flags.hf201811Active = true; hf2023Flags.hf202305Active = true; const int activationHeight = Params().GetConsensus().hf202305Height; const uint256 category = uint256S("0x0000000000000000000000000000000000000000000000000000000000001234"); const std::vector commitmentA = {0xaa}; const std::vector commitmentB = {0xbb}; std::vector unspents; unspents.push_back(tokenPrevout(tokenOutputScript(category, Tx::HasNft | Tx::HasCommitment, 0, commitmentA), activationHeight)); int64_t fees = 0; uint32_t sigChecks = 0; bool spendsCoinbase = false; auto immutableTransfer = tokenSpendTx({COutPoint(uint256S("01"), 1)}, {tokenOutputScript(category, Tx::HasNft | Tx::HasCommitment, 0, commitmentA)}); auto tx = Tx::fromOldTransaction(immutableTransfer); ValidationPrivate::validateTransactionInputs(immutableTransfer, tx, unspents, activationHeight, hf2023Flags, fees, sigChecks, spendsCoinbase, false); auto old = tokenSpendTx({COutPoint(uint256S("01"), 1)}, {tokenOutputScript(category, Tx::HasNft | Tx::HasCommitment, 0, commitmentB)}); tx = Tx::fromOldTransaction(old); expectTokenInputReject(old, tx, unspents, hf2023Flags, "bad-txns-token-nft-ex-nihilo"); unspents[0] = tokenPrevout(tokenOutputScript(category, Tx::HasNft | Tx::NftMutableBaton), activationHeight); auto mutableToImmutable = tokenSpendTx({COutPoint(uint256S("01"), 1)}, {tokenOutputScript(category, Tx::HasNft | Tx::HasCommitment, 0, commitmentB)}); tx = Tx::fromOldTransaction(mutableToImmutable); ValidationPrivate::validateTransactionInputs(mutableToImmutable, tx, unspents, activationHeight, hf2023Flags, fees, sigChecks, spendsCoinbase, false); old = tokenSpendTx({COutPoint(uint256S("01"), 1)}, {tokenOutputScript(category, Tx::HasNft | Tx::NftMintBaton)}); tx = Tx::fromOldTransaction(old); expectTokenInputReject(old, tx, unspents, hf2023Flags, "bad-txns-token-nft-ex-nihilo"); old = tokenSpendTx({COutPoint(uint256S("01"), 1)}, {tokenOutputScript(category, Tx::HasNft | Tx::NftMutableBaton), tokenOutputScript(category, Tx::HasNft | Tx::NftMutableBaton)}); tx = Tx::fromOldTransaction(old); expectTokenInputReject(old, tx, unspents, hf2023Flags, "bad-txns-token-nft-ex-nihilo"); unspents[0] = tokenPrevout(tokenOutputScript(category, Tx::HasNft | Tx::NftMintBaton), activationHeight); auto mintingCreatesMany = tokenSpendTx({COutPoint(uint256S("01"), 1)}, {tokenOutputScript(category, Tx::HasNft | Tx::NftMintBaton), tokenOutputScript(category, Tx::HasNft | Tx::NftMutableBaton), tokenOutputScript(category, Tx::HasNft | Tx::HasCommitment, 0, commitmentB)}); tx = Tx::fromOldTransaction(mintingCreatesMany); ValidationPrivate::validateTransactionInputs(mintingCreatesMany, tx, unspents, activationHeight, hf2023Flags, fees, sigChecks, spendsCoinbase, false); unspents[0] = plainPrevout(activationHeight); auto genesisNft = tokenSpendTx({COutPoint(category, 0)}, {tokenOutputScript(category, Tx::HasNft | Tx::HasCommitment, 0, commitmentB)}); tx = Tx::fromOldTransaction(genesisNft); ValidationPrivate::validateTransactionInputs(genesisNft, tx, unspents, activationHeight, hf2023Flags, fees, sigChecks, spendsCoinbase, false); } void TransactionTests::may2023_cashtoken_nft_accounting_edge_cases() { ValidationFlags hf2023Flags; hf2023Flags.hf201811Active = true; hf2023Flags.hf202305Active = true; const int activationHeight = Params().GetConsensus().hf202305Height; const uint256 category = uint256S("0x0000000000000000000000000000000000000000000000000000000000001234"); const std::vector commitmentA = {0xaa}; const std::vector commitmentB = {0xbb}; int64_t fees = 0; uint32_t sigChecks = 0; bool spendsCoinbase = false; std::vector unspents; unspents.push_back(tokenPrevout(tokenOutputScript(category, Tx::HasNft | Tx::NftMutableBaton), activationHeight)); auto mutableToMutable = tokenSpendTx({COutPoint(uint256S("01"), 1)}, {tokenOutputScript(category, Tx::HasNft | Tx::NftMutableBaton)}); auto tx = Tx::fromOldTransaction(mutableToMutable); ValidationPrivate::validateTransactionInputs(mutableToMutable, tx, unspents, activationHeight, hf2023Flags, fees, sigChecks, spendsCoinbase, false); unspents[0] = tokenPrevout(tokenOutputScript(category, Tx::HasNft | Tx::NftMintBaton), activationHeight); auto burnMintingBaton = tokenSpendTx({COutPoint(uint256S("01"), 1)}, {CScript() << OP_TRUE}); tx = Tx::fromOldTransaction(burnMintingBaton); ValidationPrivate::validateTransactionInputs(burnMintingBaton, tx, unspents, activationHeight, hf2023Flags, fees, sigChecks, spendsCoinbase, false); unspents[0] = tokenPrevout(tokenOutputScript(category, Tx::HasFtAmount, 10), activationHeight); auto old = tokenSpendTx({COutPoint(uint256S("01"), 1)}, {tokenOutputScript(category, Tx::HasNft | Tx::HasCommitment, 0, commitmentA)}); tx = Tx::fromOldTransaction(old); expectTokenInputReject(old, tx, unspents, hf2023Flags, "bad-txns-token-nft-ex-nihilo"); unspents[0] = plainPrevout(activationHeight); auto genesisCreatesMany = tokenSpendTx({COutPoint(category, 0)}, {tokenOutputScript(category, Tx::HasNft | Tx::HasCommitment, 0, commitmentA), tokenOutputScript(category, Tx::HasNft | Tx::NftMutableBaton), tokenOutputScript(category, Tx::HasNft | Tx::NftMintBaton), tokenOutputScript(category, Tx::HasNft | Tx::HasCommitment | Tx::HasFtAmount, 25, commitmentB)}); tx = Tx::fromOldTransaction(genesisCreatesMany); ValidationPrivate::validateTransactionInputs(genesisCreatesMany, tx, unspents, activationHeight, hf2023Flags, fees, sigChecks, spendsCoinbase, false); } void TransactionTests::may2023_contextual_transaction_rules() { /** The minimum allowed transaction size after the Nov 2018 upgrade and before May 2023 */ static constexpr unsigned int MIN_TX_SIZE_FROM2018 = 100; /** The minimum allowed transaction size after the May 2023 upgrade */ static constexpr unsigned int MIN_TX_SIZE_FROM2023 = 65; ValidationFlags noUpgrade; ValidationFlags hf2018Flags; hf2018Flags.hf201811Active = true; ValidationFlags hf2023Flags = hf2018Flags; hf2023Flags.hf202305Active = true; const Tx tx64 = makeTransactionOfSize(MIN_TX_SIZE_FROM2023 - 1); QCOMPARE(tx64.size(), 64); const Tx tx65 = makeTransactionOfSize(MIN_TX_SIZE_FROM2023); QCOMPARE(tx65.size(), 65); const Tx tx100 = makeTransactionOfSize(MIN_TX_SIZE_FROM2018); QCOMPARE(tx100.size(), 100); const Tx version2 = makeTransactionOfSize(120, 2); const Tx version8 = makeTransactionOfSize(120, 8); ValidationPrivate::checkContextualTransaction(tx64, noUpgrade); ValidationPrivate::checkContextualTransaction(tx65, noUpgrade); ValidationPrivate::checkContextualTransaction(tx100, noUpgrade); try { ValidationPrivate::checkContextualTransaction(tx64, hf2018Flags); } catch (const Validation::Exception &e) { QCOMPARE(std::string(e.what()), "bad-txns-undersize"); } try { ValidationPrivate::checkContextualTransaction(tx65, hf2018Flags); } catch (const Validation::Exception &e) { QCOMPARE(std::string(e.what()), "bad-txns-undersize"); } try { ValidationPrivate::checkContextualTransaction(tx100, hf2018Flags); ValidationPrivate::checkContextualTransaction(version2, hf2018Flags); ValidationPrivate::checkContextualTransaction(version8, hf2018Flags); } catch (const Validation::Exception &e) { QFAIL("no!"); } try { ValidationPrivate::checkContextualTransaction(tx64, hf2023Flags); } catch (const Validation::Exception &e) { QCOMPARE(std::string(e.what()), "bad-txns-undersize"); } try { ValidationPrivate::checkContextualTransaction(version2, hf2023Flags); } catch (const Validation::Exception &e) { QCOMPARE(std::string(e.what()), "bad-txns-version"); } try { ValidationPrivate::checkContextualTransaction(version8, hf2023Flags); } catch (const Validation::Exception &e) { QCOMPARE(std::string(e.what()), "bad-txns-version"); } try { ValidationPrivate::checkContextualTransaction(tx65, hf2018Flags); ValidationPrivate::checkContextualTransaction(tx100, hf2018Flags); } catch (const Validation::Exception &e) { QFAIL("no!"); } }