Files

1137 lines
52 KiB
C++
Raw Permalink Normal View History

2017-11-09 19:34:51 +01:00
/*
* This file is part of the Flowee project
* Copyright (C) 2011-2015 The Bitcoin Core developers
2026-05-13 16:43:23 +02:00
* Copyright (C) 2016-2026 Tom Zander <tom@flowee.org>
2017-11-09 19:34:51 +01:00
*
* 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 <http://www.gnu.org/licenses/>.
*/
2013-04-13 00:13:08 -05:00
2018-12-28 12:10:28 +01:00
#include "transaction_tests.h"
2024-09-01 18:49:11 +02:00
#include <utils/streaming/BufferPools.h>
#include "TransactionBuilder.h"
#include "data/tx_invalid.json.h"
#include "data/tx_valid.json.h"
#include "core_io.h"
2026-04-14 23:43:29 +02:00
#include "main.h"
#include "validation/Engine.h" // For checkTransaction
#include "validation/BlockValidation_p.h"
#include "validation/ValidationException.h"
2014-10-11 22:41:05 +00:00
#include "policy/policy.h"
2024-09-07 18:10:13 +02:00
#include "primitives/Block.h"
#include "primitives/ScriptDefines.h"
#include "primitives/script.h"
#include "transaction_utils.h"
#include "chainparams.h"
2018-02-12 14:15:24 +01:00
#include <SettingsDefaults.h>
#include <utilstrencodings.h>
#include <clientversion.h>
2022-07-06 22:50:53 +02:00
#include <BitcoinVersion.h>
2013-04-13 00:13:08 -05:00
#include <boost/algorithm/string/classification.hpp>
#include <boost/algorithm/string/split.hpp>
2015-05-10 13:35:44 +02:00
#include <boost/assign/list_of.hpp>
#include <boost/assign/list_of.hpp>
2026-04-14 23:43:29 +02:00
#include <streaming/streams.h>
2026-05-14 08:52:55 +02:00
#include <limits>
2015-09-04 16:11:34 +02:00
#include <univalue.h>
2013-04-13 00:13:08 -05:00
2012-08-04 18:28:49 +02:00
// In script_tests.cpp
2015-05-13 21:29:19 +02:00
extern UniValue read_json(const std::string& jsondata);
2011-07-31 20:07:53 +02:00
static std::map<std::string, unsigned int> 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)
2017-07-24 22:40:28 +02:00
(std::string("SIGHASH_FORKID"), (unsigned int)SCRIPT_ENABLE_SIGHASH_FORKID);
2018-12-28 12:10:28 +01:00
unsigned int TransactionTests::parseScriptFlags(const std::string &strFlags)
{
if (strFlags.empty()) {
return 0;
}
unsigned int flags = 0;
std::vector<std::string> words;
boost::algorithm::split(words, strFlags, boost::algorithm::is_any_of(","));
2018-12-28 12:10:28 +01:00
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)
{
2018-12-28 12:10:28 +01:00
if (flags == 0)
return std::string();
std::string ret;
std::map<std::string, unsigned int>::const_iterator it = mapFlagNames.begin();
while (it != mapFlagNames.end()) {
if (flags & it->second) {
ret += it->first + ",";
}
it++;
}
return ret.substr(0, ret.size() - 1);
}
2011-07-31 20:07:53 +02:00
2018-12-28 12:10:28 +01:00
void TransactionTests::tx_valid()
2012-08-04 18:28:49 +02:00
{
// 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
2012-08-04 18:28:49 +02:00
// ... where all scripts are stringified scripts.
//
// verifyFlags is a comma separated list of script verification flags to apply, or "NONE"
2015-05-13 21:29:19 +02:00
UniValue tests = read_json(std::string(json_tests::tx_valid, json_tests::tx_valid + sizeof(json_tests::tx_valid)));
2012-08-04 18:28:49 +02:00
for (unsigned int idx = 0; idx < tests.size(); idx++) {
2015-05-13 21:29:19 +02:00
UniValue test = tests[idx];
std::string strTest = test.write();
2019-09-04 14:11:10 +02:00
if (test[0].isArray()) {
if (test.size() != 3 || !test[1].isStr() || !test[2].isStr() || test[1].get_str().empty() || test[2].get_str().empty()) {
2019-10-10 19:20:04 +02:00
logCritical() << strTest;
2018-12-28 12:10:28 +01:00
QFAIL("Bad test");
2012-08-04 18:28:49 +02:00
continue;
}
std::map<COutPoint, CScript> mapprevOutScriptPubKeys;
2015-05-13 21:29:19 +02:00
UniValue inputs = test[0].get_array();
2012-08-04 18:28:49 +02:00
bool fValid = true;
2019-09-04 14:11:10 +02:00
for (unsigned int inpIdx = 0; inpIdx < inputs.size(); inpIdx++) {
const UniValue& input = inputs[inpIdx];
if (!input.isArray()) {
2012-08-04 18:28:49 +02:00
fValid = false;
break;
}
2015-05-13 21:29:19 +02:00
UniValue vinput = input.get_array();
2019-09-04 14:11:10 +02:00
if (vinput.size() != 3) {
2012-08-04 18:28:49 +02:00
fValid = false;
break;
}
2014-12-16 14:50:05 +01:00
mapprevOutScriptPubKeys[COutPoint(uint256S(vinput[0].get_str()), vinput[1].get_int())] = ParseScript(vinput[2].get_str());
2012-08-04 18:28:49 +02:00
}
2018-12-28 12:10:28 +01:00
Q_ASSERT(fValid);
2012-08-04 18:28:49 +02:00
std::string transaction = test[1].get_str();
2012-08-04 18:28:49 +02:00
CDataStream stream(ParseHex(transaction), SER_NETWORK, PROTOCOL_VERSION);
CTransaction tx;
stream >> tx;
2026-05-14 12:45:20 +02:00
auto newTx = Tx::fromOldTransaction(tx);
2026-04-14 23:43:29 +02:00
try {
2026-05-14 12:45:20 +02:00
Validation::checkTransaction(newTx);
2026-04-14 23:43:29 +02:00
} catch (...) {
QFAIL("validation of tx failed");
}
2019-09-04 14:11:10 +02:00
for (unsigned int i = 0; i < tx.vin.size(); i++) {
2012-08-04 18:28:49 +02:00
if (!mapprevOutScriptPubKeys.count(tx.vin[i].prevout))
2018-12-28 12:10:28 +01:00
QFAIL("Bad test");
2012-08-04 18:28:49 +02:00
2021-01-20 19:21:53 +01:00
int64_t amount = 0;
2018-12-28 12:10:28 +01:00
unsigned int verify_flags = parseScriptFlags(test[2].get_str());
2020-04-11 18:35:43 +02:00
Script::State state(verify_flags);
const bool ok = Script::verify(tx.vin[i].scriptSig, mapprevOutScriptPubKeys[tx.vin[i].prevout],
2026-05-14 12:45:20 +02:00
TransactionSignatureChecker(&tx, newTx, i, amount), state);
2019-10-10 19:20:04 +02:00
if (!ok)
logDebug() << strTest;
2020-04-11 18:35:43 +02:00
QCOMPARE(state.errorString(), "No error");
2019-10-10 19:20:04 +02:00
QVERIFY(ok);
2012-08-04 18:28:49 +02:00
}
}
}
}
2018-12-28 12:10:28 +01:00
void TransactionTests::tx_invalid()
2012-08-04 18:28:49 +02:00
{
// 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
2012-08-04 18:28:49 +02:00
// ... where all scripts are stringified scripts.
//
// verifyFlags is a comma separated list of script verification flags to apply, or "NONE"
2015-05-13 21:29:19 +02:00
UniValue tests = read_json(std::string(json_tests::tx_invalid, json_tests::tx_invalid + sizeof(json_tests::tx_invalid)));
2012-08-04 18:28:49 +02:00
for (unsigned int idx = 0; idx < tests.size(); idx++) {
2015-05-13 21:29:19 +02:00
UniValue test = tests[idx];
std::string strTest = test.write();
if (test[0].isArray())
2012-08-04 18:28:49 +02:00
{
if (test.size() != 3 || !test[1].isStr() || !test[2].isStr())
2018-12-28 12:10:28 +01:00
QFAIL("Bad test");
2012-08-04 18:28:49 +02:00
std::map<COutPoint, CScript> mapprevOutScriptPubKeys;
2015-05-13 21:29:19 +02:00
UniValue inputs = test[0].get_array();
2012-08-04 18:28:49 +02:00
bool fValid = true;
2016-11-15 14:58:14 +01:00
for (unsigned int inpIdx = 0; inpIdx < inputs.size(); inpIdx++) {
const UniValue& input = inputs[inpIdx];
if (!input.isArray())
2012-08-04 18:28:49 +02:00
{
fValid = false;
break;
}
2015-05-13 21:29:19 +02:00
UniValue vinput = input.get_array();
2012-08-04 18:28:49 +02:00
if (vinput.size() != 3)
{
fValid = false;
break;
}
2014-12-16 14:50:05 +01:00
mapprevOutScriptPubKeys[COutPoint(uint256S(vinput[0].get_str()), vinput[1].get_int())] = ParseScript(vinput[2].get_str());
2012-08-04 18:28:49 +02:00
}
2018-12-28 12:10:28 +01:00
Q_ASSERT(fValid);
2012-08-04 18:28:49 +02:00
std::string transaction = test[1].get_str();
2012-08-04 18:28:49 +02:00
CDataStream stream(ParseHex(transaction), SER_NETWORK, PROTOCOL_VERSION);
CTransaction tx;
stream >> tx;
2026-05-14 12:45:20 +02:00
auto newTx = Tx::fromOldTransaction(tx);
2012-08-04 18:28:49 +02:00
2026-04-14 23:43:29 +02:00
try {
2026-05-14 12:45:20 +02:00
Validation::checkTransaction(newTx);
2026-04-14 23:43:29 +02:00
} catch (...) {
fValid = false;
}
2020-04-11 18:35:43 +02:00
Script::State scriptState;
2026-04-14 23:43:29 +02:00
for (unsigned int i = 0; i < tx.vin.size() && fValid; i++) {
2012-08-04 18:28:49 +02:00
if (!mapprevOutScriptPubKeys.count(tx.vin[i].prevout))
2018-12-28 12:10:28 +01:00
QFAIL("Bad test");
2012-08-04 18:28:49 +02:00
2021-01-20 19:21:53 +01:00
int64_t amount = 0;
2020-04-11 18:35:43 +02:00
scriptState.flags = parseScriptFlags(test[2].get_str());
fValid = Script::verify(tx.vin[i].scriptSig, mapprevOutScriptPubKeys[tx.vin[i].prevout],
2026-05-14 12:45:20 +02:00
TransactionSignatureChecker(&tx, newTx, i, amount), scriptState);
2020-04-11 18:35:43 +02:00
if (fValid)
QVERIFY(scriptState.error == SCRIPT_ERR_OK);
else
QVERIFY(scriptState.error != SCRIPT_ERR_OK);
2012-08-04 18:28:49 +02:00
}
2018-12-28 12:10:28 +01:00
QVERIFY(!fValid);
2012-08-04 18:28:49 +02:00
}
}
}
static std::vector<unsigned char> getTestTx()
2011-07-31 20:07:53 +02:00
{
// 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<unsigned char> vch(ch, ch + sizeof(ch) -1);
return vch;
}
2018-12-28 12:10:28 +01:00
void TransactionTests::basic_transaction_tests()
{
auto vch = getTestTx();
2012-04-17 20:37:47 +02:00
CDataStream stream(vch, SER_DISK, CLIENT_VERSION);
CMutableTransaction tx;
2011-07-31 20:07:53 +02:00
stream >> tx;
2026-04-14 23:43:29 +02:00
try {
// Simple deserialized transaction should be valid.
Validation::checkTransaction(Tx::fromOldTransaction(tx));
} catch (...) {
QFAIL("failed validation 1");
}
2011-07-31 20:07:53 +02:00
// Check that duplicate txins fail
tx.vin.push_back(tx.vin[0]);
2026-04-14 23:43:29 +02:00
try {
// Transaction with duplicate txins should be invalid.
Validation::checkTransaction(Tx::fromOldTransaction(tx));
QFAIL("Failed to fail validation checks");
} catch (...) { }
2011-07-31 20:07:53 +02:00
}
//
// 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<CMutableTransaction> SetupDummyInputs(std::vector<PrivateKey> &keys)
{
std::vector<CMutableTransaction> 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;
}
2018-12-28 12:10:28 +01:00
void TransactionTests::test_IsStandard()
{
2014-04-23 08:05:05 +02:00
LOCK(cs_main);
std::vector<PrivateKey> keystore;
std::vector<CMutableTransaction> 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<unsigned char>(65, 0);
t.vout.resize(1);
t.vout[0].nValue = 90*CENT;
2022-07-06 22:12:33 +02:00
PrivateKey key;
2022-05-11 13:46:15 +02:00
key.makeNewKey(true);
t.vout[0].scriptPubKey = GetScriptForDestination(key.getPubKey().getKeyId());
std::string reason;
2018-12-28 12:10:28 +01:00
QVERIFY(IsStandardTx(t, reason));
// Check dust with default relay fee:
2021-01-20 19:21:53 +01:00
int64_t nDustThreshold = 182 * minRelayTxFee.GetFeePerK()/1000 * 3;
QCOMPARE(nDustThreshold, (int64_t) 546);
// dust:
t.vout[0].nValue = nDustThreshold - 1;
2018-12-28 12:10:28 +01:00
QVERIFY(!IsStandardTx(t, reason));
// not dust:
t.vout[0].nValue = nDustThreshold;
2018-12-28 12:10:28 +01:00
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;
2018-12-28 12:10:28 +01:00
QVERIFY(!IsStandardTx(t, reason));
// not dust:
t.vout[0].nValue = 672;
2018-12-28 12:10:28 +01:00
QVERIFY(IsStandardTx(t, reason));
2018-02-12 14:15:24 +01:00
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<unsigned char>(OP_NOP);
2018-12-28 12:10:28 +01:00
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<unsigned char>(65, 0);
// MAX_OP_RETURN_RELAY-byte TX_NULL_DATA (standard)
2018-04-16 11:02:17 +02:00
t.vout[0].scriptPubKey = CScript() << OP_RETURN
<< ParseHex("04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef3804678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38"
"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000");
2021-03-13 16:52:04 +01:00
QCOMPARE(Settings::MaxOpReturnRelay, t.vout[0].scriptPubKey.size());
QVERIFY2(IsStandardTx(t, reason), reason.c_str());
2021-03-13 16:52:04 +01:00
// 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)
2018-04-16 11:02:17 +02:00
t.vout[0].scriptPubKey = CScript() << OP_RETURN
<< ParseHex("04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef3804678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef3800"
"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000");
2021-03-13 16:52:04 +01:00
QCOMPARE(Settings::MaxOpReturnRelay + 1, t.vout[0].scriptPubKey.size());
2018-12-28 12:10:28 +01:00
QVERIFY(!IsStandardTx(t, reason));
// Data payload can be encoded in any way...
t.vout[0].scriptPubKey = CScript() << OP_RETURN << ParseHex("");
2018-12-28 12:10:28 +01:00
QVERIFY(IsStandardTx(t, reason));
t.vout[0].scriptPubKey = CScript() << OP_RETURN << ParseHex("00") << ParseHex("01");
2018-12-28 12:10:28 +01:00
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;
2018-12-28 12:10:28 +01:00
QVERIFY(IsStandardTx(t, reason));
t.vout[0].scriptPubKey = CScript() << OP_RETURN << 0 << ParseHex("01") << 2 << ParseHex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
2018-12-28 12:10:28 +01:00
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;
2018-12-28 12:10:28 +01:00
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));
}
2018-12-28 12:10:28 +01:00
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();
2018-12-28 12:10:28 +01:00
QCOMPARE(type, Tx::TxVersion);
QCOMPARE(iter.intData(), 1);
type = iter.next();
2018-12-28 12:10:28 +01:00
QCOMPARE(type, Tx::PrevTxHash);
QCOMPARE(iter.byteData().size(), 32);
auto prev = iter.uint256Data();
2018-12-28 12:10:28 +01:00
QVERIFY(prev == uint256S("0xb4749f017444b051c44dfd2720e88f314ff94f3dd6d56d40ef65854fcd7fff6b"));
type = iter.next();
2018-12-28 12:10:28 +01:00
QCOMPARE(type, Tx::PrevTxIndex);
QCOMPARE(iter.intData(), 0);
type = iter.next();
2018-12-28 12:10:28 +01:00
QCOMPARE(type, Tx::TxInScript);
QCOMPARE(iter.byteData().size(), 140);
type = iter.next();
2018-12-28 12:10:28 +01:00
QCOMPARE(type, Tx::Sequence);
QCOMPARE(iter.uintData(), 0xffffffff);
QCOMPARE(iter.longData(), (uint64_t) 0xffffffff);
type = iter.next();
2018-12-28 12:10:28 +01:00
QCOMPARE(type, Tx::OutputValue);
QCOMPARE(iter.longData(), (uint64_t) 244623243);
type = iter.next();
2018-12-28 12:10:28 +01:00
QCOMPARE(type, Tx::OutputScript);
QCOMPARE(iter.byteData().size(), 25);
type = iter.next();
2018-12-28 12:10:28 +01:00
QCOMPARE(type, Tx::OutputValue);
QCOMPARE(iter.longData(), (uint64_t) 44602432);
type = iter.next();
2018-12-28 12:10:28 +01:00
QCOMPARE(type, Tx::OutputScript);
QCOMPARE(iter.byteData().size(), 25);
type = iter.next();
2018-12-28 12:10:28 +01:00
QCOMPARE(type, Tx::LockTime);
type = iter.next();
2018-12-28 12:10:28 +01:00
QCOMPARE(type, Tx::End);
}
2018-12-28 12:10:28 +01:00
void TransactionTests::transactionIter2()
{
// coinbase-tx
Block genesisBlock = Params(CBaseChainParams::MAIN).GenesisBlock();
QCOMPARE(genesisBlock.size(), 285);
auto iter = Tx::Iterator(genesisBlock);
auto type = iter.next();
2018-12-28 12:10:28 +01:00
QCOMPARE(type, Tx::TxVersion);
QCOMPARE(iter.intData(), 1);
type = iter.next();
2018-12-28 12:10:28 +01:00
QCOMPARE(type, Tx::PrevTxHash);
QCOMPARE(iter.byteData().size(), 32);
auto prev = iter.uint256Data();
2018-12-28 12:10:28 +01:00
QVERIFY(prev == uint256S("0x0000000000000000000000000000000000000000000000000000000000000000"));
type = iter.next();
2018-12-28 12:10:28 +01:00
QCOMPARE(type, Tx::PrevTxIndex);
QCOMPARE(iter.intData(), -1);
type = iter.next();
2018-12-28 12:10:28 +01:00
QCOMPARE(type, Tx::TxInScript);
QCOMPARE(iter.byteData().size(), 77);
type = iter.next();
2018-12-28 12:10:28 +01:00
QCOMPARE(type, Tx::Sequence);
QCOMPARE(iter.uintData(), 0xffffffff);
QCOMPARE(iter.longData(), (uint64_t) 0xffffffff);
type = iter.next();
2018-12-28 12:10:28 +01:00
QCOMPARE(type, Tx::OutputValue);
QCOMPARE(iter.longData(), (uint64_t) 50 * COIN);
type = iter.next();
2018-12-28 12:10:28 +01:00
QCOMPARE(type, Tx::OutputScript);
QCOMPARE(iter.byteData().size(), 67);
type = iter.next();
2018-12-28 12:10:28 +01:00
QCOMPARE(type, Tx::LockTime);
QCOMPARE(iter.intData(), 0);
type = iter.next();
2018-12-28 12:10:28 +01:00
QCOMPARE(type, Tx::End);
Tx tx = iter.prevTx();
QCOMPARE(tx.size(), 204);
logFatal() << tx.createHash();
QVERIFY(tx.createHash() == uint256S("0x4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"));
genesisBlock.findTransactions();
2018-12-28 12:10:28 +01:00
QCOMPARE(genesisBlock.transactions().size(), (size_t) 1);
QCOMPARE(genesisBlock.transactions().front().size(), 204);
QVERIFY(genesisBlock.transactions().front().createHash() == uint256S("0x4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"));
}
2024-09-01 18:49:11 +02:00
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);
2024-09-06 22:48:19 +02:00
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);
2024-09-01 18:49:11 +02:00
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);
2024-09-01 20:02:21 +02:00
auto iter2 = Tx::Iterator(tx);
2026-05-13 16:43:23 +02:00
auto output1 = iter2.nextOutput();
QCOMPARE(output1.hasToken(), false);
2024-09-01 20:02:21 +02:00
QCOMPARE(output1.outputValue, (int64_t) 2119525491);
2026-05-13 16:43:23 +02:00
QCOMPARE(output1.outputScript().size(), 25);
auto output2 = iter2.nextOutput();
QCOMPARE(output2.hasToken(), true);
2024-09-01 20:02:21 +02:00
QCOMPARE(output2.outputValue, (int64_t) 800);
2026-05-13 16:43:23 +02:00
QCOMPARE(output2.outputScript().size(), 25);
2026-05-14 08:09:17 +02:00
QCOMPARE(tx.output(1).outputValue, (int64_t) 800);
auto skippedOutput = Tx::Iterator(tx).nextOutput(1);
QCOMPARE(skippedOutput.outputValue, (int64_t) 800);
2026-05-13 16:43:23 +02:00
auto output3 = iter2.nextOutput();
QCOMPARE(output3.hasToken(), false);
2024-09-01 20:02:21 +02:00
QCOMPARE(output3.outputValue, (int64_t) 0);
2026-05-13 16:43:23 +02:00
QCOMPARE(output3.outputScript().size(), 76);
QCOMPARE(output3.outputScript(), bytes);
auto end = iter2.nextOutput();
2024-09-01 20:02:21 +02:00
QCOMPARE(end.outputValue, (int64_t) -1);
2026-05-13 16:43:23 +02:00
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);
2024-09-01 18:49:11 +02:00
}
2026-05-08 23:38:26 +02:00
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<int>(::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);
2026-05-08 23:38:26 +02:00
return Tx::fromOldTransaction(tx);
}
static void expectContextualTxReject(const Tx &tx, ValidationFlags flags, const std::string &reason)
{
try {
ValidationPrivate::checkContextualTransaction(tx, flags);
2026-05-14 10:53:55 +02:00
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<uint8_t> &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<unsigned char>(commitment.size()));
script.insert(script.end(), commitment.begin(), commitment.end());
}
2026-05-14 08:52:55 +02:00
if (bitfield & Tx::HasFtAmount) {
if (amount < 253) {
script.push_back(static_cast<unsigned char>(amount));
} else if (amount <= std::numeric_limits<uint16_t>::max()) {
script.push_back(253);
script.push_back(static_cast<unsigned char>(amount));
script.push_back(static_cast<unsigned char>(amount >> 8));
} else if (amount <= std::numeric_limits<uint32_t>::max()) {
script.push_back(254);
for (int i = 0; i < 4; ++i)
script.push_back(static_cast<unsigned char>(amount >> (8 * i)));
} else {
script.push_back(255);
for (int i = 0; i < 8; ++i)
script.push_back(static_cast<unsigned char>(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));
}
2026-05-14 08:52:55 +02:00
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");
}
2026-05-14 08:52:55 +02:00
void TransactionTests::may2023_cashtoken_output_structure_edge_cases()
{
ValidationFlags hf2023Flags;
hf2023Flags.hf201811Active = true;
hf2023Flags.hf202305Active = true;
ValidationFlags hf2026Flags = hf2023Flags;
hf2026Flags.hf202605Active = true;
2026-05-14 08:52:55 +02:00
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<uint64_t>(std::numeric_limits<int64_t>::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<uint64_t>(std::numeric_limits<int64_t>::max()) + 1)),
hf2023Flags, "bad-txns-token-amount-outofrange");
expectContextualTxReject(txWithOutputScript(tokenOutputScript(category, Tx::HasNft | Tx::HasCommitment,
0, std::vector<uint8_t>(41, 0x01))),
hf2023Flags, "bad-txns-token-commitment-oversized");
ValidationPrivate::checkContextualTransaction(txWithOutputScript(tokenOutputScript(category,
Tx::HasNft | Tx::HasCommitment, 0, std::vector<uint8_t>(128, 0x01))),
hf2026Flags);
expectContextualTxReject(txWithOutputScript(tokenOutputScript(category, Tx::HasNft | Tx::HasCommitment,
0, std::vector<uint8_t>(129, 0x01))),
hf2026Flags, "bad-txns-token-commitment-oversized");
2026-05-14 08:52:55 +02:00
CScript shortPrefix;
shortPrefix.push_back(0xef);
shortPrefix << OP_TRUE;
2026-05-14 10:53:55 +02:00
expectContextualTxReject(txWithOutputScriptAndInputPadding(shortPrefix), hf2023Flags, "bad-tx-data");
2026-05-14 08:52:55 +02:00
}
2026-05-14 08:11:19 +02:00
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);
}
2026-05-14 08:12:13 +02:00
static CTransaction tokenSpendTx(const std::vector<COutPoint> &prevouts, const std::vector<CScript> &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);
}
2026-05-14 08:11:19 +02:00
static UnspentOutputData tokenPrevout(const CScript &outputScript, int blockHeight)
{
UnspentOutputData out(Streaming::bufferFrom(outputScript), 1000);
out.blockHeight = blockHeight;
return out;
}
2026-05-14 08:12:13 +02:00
static UnspentOutputData plainPrevout(int blockHeight)
{
return tokenPrevout(CScript() << OP_TRUE, blockHeight);
}
2026-05-14 12:07:30 +02:00
static void expectTokenInputReject(const CTransaction tx, const Tx &newTx, const std::vector<UnspentOutputData> &unspents,
2026-05-14 08:11:19 +02:00
ValidationFlags flags, const std::string &reason)
{
int64_t fees = 0;
uint32_t sigChecks = 0;
bool spendsCoinbase = false;
try {
2026-05-14 12:07:30 +02:00
ValidationPrivate::validateTransactionInputs(tx, newTx, unspents, Params().GetConsensus().hf202305Height,
2026-05-14 08:11:19 +02:00
flags, fees, sigChecks, spendsCoinbase, false);
2026-05-14 10:53:55 +02:00
QFAIL("transaction inputs were expected to be rejected");
2026-05-14 08:11:19 +02:00
} 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<UnspentOutputData> unspents;
unspents.push_back(tokenPrevout(tokenOutputScript(category, Tx::HasFtAmount, 1), activationHeight));
int64_t fees = 0;
uint32_t sigChecks = 0;
bool spendsCoinbase = false;
2026-05-14 13:33:13 +02:00
const auto old = tokenInputSpendTx();
auto tx = Tx::fromOldTransaction(old);
2026-05-14 12:07:30 +02:00
ValidationPrivate::validateTransactionInputs(old, tx, unspents, activationHeight,
2026-05-14 08:11:19 +02:00
hf2023Flags, fees, sigChecks, spendsCoinbase, false);
QCOMPARE(fees, static_cast<int64_t>(100));
unspents[0] = tokenPrevout(tokenOutputScript(category, 0x80), activationHeight);
2026-05-14 12:07:30 +02:00
expectTokenInputReject(old, tx, unspents, hf2023Flags, "bad-txns-token-bad-bitfield");
2026-05-14 08:11:19 +02:00
}
2026-05-14 08:12:13 +02:00
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<UnspentOutputData> unspents;
unspents.push_back(tokenPrevout(tokenOutputScript(category, Tx::HasFtAmount, 100), activationHeight));
auto validSpend = tokenSpendTx({COutPoint(uint256S("01"), 1)},
{tokenOutputScript(category, Tx::HasFtAmount, 70)});
2026-05-14 12:07:30 +02:00
auto tx = Tx::fromOldTransaction(validSpend);
2026-05-14 08:12:13 +02:00
int64_t fees = 0;
uint32_t sigChecks = 0;
bool spendsCoinbase = false;
2026-05-14 12:07:30 +02:00
ValidationPrivate::validateTransactionInputs(validSpend, tx, unspents, activationHeight,
2026-05-14 08:12:13 +02:00
hf2023Flags, fees, sigChecks, spendsCoinbase, false);
QCOMPARE(fees, static_cast<int64_t>(900));
2026-05-14 12:07:30 +02:00
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");
2026-05-14 08:12:13 +02:00
unspents[0] = plainPrevout(activationHeight);
auto genesis = tokenSpendTx({COutPoint(otherCategory, 0)}, {tokenOutputScript(otherCategory, Tx::HasFtAmount, 10)});
2026-05-14 12:07:30 +02:00
tx = Tx::fromOldTransaction(genesis);
ValidationPrivate::validateTransactionInputs(genesis, tx, unspents, activationHeight,
2026-05-14 08:12:13 +02:00
hf2023Flags, fees, sigChecks, spendsCoinbase, false);
unspents[0] = tokenPrevout(tokenOutputScript(otherCategory, Tx::HasFtAmount, 10), activationHeight);
2026-05-14 12:07:30 +02:00
old = tokenSpendTx({COutPoint(otherCategory, 0)},
{tokenOutputScript(otherCategory, Tx::HasFtAmount, 1)});
tx = Tx::fromOldTransaction(old);
expectTokenInputReject(old, tx, unspents, hf2023Flags, "bad-txns-token-dupe-genesis");
2026-05-14 08:12:13 +02:00
}
2026-05-14 08:52:55 +02:00
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<UnspentOutputData> 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;
2026-05-14 12:07:30 +02:00
auto tx = Tx::fromOldTransaction(splitSpend);
ValidationPrivate::validateTransactionInputs(splitSpend, tx, unspents, activationHeight,
2026-05-14 08:52:55 +02:00
hf2023Flags, fees, sigChecks, spendsCoinbase, false);
QCOMPARE(fees, static_cast<int64_t>(1800));
unspents[0] = tokenPrevout(tokenOutputScript(category, Tx::HasFtAmount,
static_cast<uint64_t>(std::numeric_limits<int64_t>::max())), activationHeight);
unspents[1] = tokenPrevout(tokenOutputScript(category, Tx::HasFtAmount, 1), activationHeight);
2026-05-14 12:07:30 +02:00
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");
2026-05-14 08:52:55 +02:00
unspents = {tokenPrevout(tokenOutputScript(category, Tx::HasFtAmount, 100), activationHeight)};
2026-05-14 12:07:30 +02:00
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");
2026-05-14 08:52:55 +02:00
}
2026-05-14 08:12:58 +02:00
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<uint8_t> commitmentA = {0xaa};
const std::vector<uint8_t> commitmentB = {0xbb};
std::vector<UnspentOutputData> 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)},
2026-05-14 12:07:30 +02:00
{tokenOutputScript(category, Tx::HasNft | Tx::HasCommitment, 0, commitmentA)});
auto tx = Tx::fromOldTransaction(immutableTransfer);
ValidationPrivate::validateTransactionInputs(immutableTransfer, tx, unspents, activationHeight,
2026-05-14 08:12:58 +02:00
hf2023Flags, fees, sigChecks, spendsCoinbase, false);
2026-05-14 12:07:30 +02:00
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");
2026-05-14 08:12:58 +02:00
unspents[0] = tokenPrevout(tokenOutputScript(category, Tx::HasNft | Tx::NftMutableBaton), activationHeight);
auto mutableToImmutable = tokenSpendTx({COutPoint(uint256S("01"), 1)},
2026-05-14 12:07:30 +02:00
{tokenOutputScript(category, Tx::HasNft | Tx::HasCommitment, 0, commitmentB)});
tx = Tx::fromOldTransaction(mutableToImmutable);
ValidationPrivate::validateTransactionInputs(mutableToImmutable, tx, unspents, activationHeight,
2026-05-14 08:12:58 +02:00
hf2023Flags, fees, sigChecks, spendsCoinbase, false);
2026-05-14 12:07:30 +02:00
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");
2026-05-14 08:12:58 +02:00
unspents[0] = tokenPrevout(tokenOutputScript(category, Tx::HasNft | Tx::NftMintBaton), activationHeight);
auto mintingCreatesMany = tokenSpendTx({COutPoint(uint256S("01"), 1)},
2026-05-14 12:07:30 +02:00
{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,
2026-05-14 08:12:58 +02:00
hf2023Flags, fees, sigChecks, spendsCoinbase, false);
unspents[0] = plainPrevout(activationHeight);
auto genesisNft = tokenSpendTx({COutPoint(category, 0)},
2026-05-14 12:07:30 +02:00
{tokenOutputScript(category, Tx::HasNft | Tx::HasCommitment, 0, commitmentB)});
tx = Tx::fromOldTransaction(genesisNft);
ValidationPrivate::validateTransactionInputs(genesisNft, tx, unspents, activationHeight,
2026-05-14 08:12:58 +02:00
hf2023Flags, fees, sigChecks, spendsCoinbase, false);
}
2026-05-14 08:52:55 +02:00
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<uint8_t> commitmentA = {0xaa};
const std::vector<uint8_t> commitmentB = {0xbb};
int64_t fees = 0;
uint32_t sigChecks = 0;
bool spendsCoinbase = false;
std::vector<UnspentOutputData> unspents;
unspents.push_back(tokenPrevout(tokenOutputScript(category, Tx::HasNft | Tx::NftMutableBaton),
activationHeight));
auto mutableToMutable = tokenSpendTx({COutPoint(uint256S("01"), 1)},
2026-05-14 12:07:30 +02:00
{tokenOutputScript(category, Tx::HasNft | Tx::NftMutableBaton)});
auto tx = Tx::fromOldTransaction(mutableToMutable);
ValidationPrivate::validateTransactionInputs(mutableToMutable, tx, unspents, activationHeight,
2026-05-14 08:52:55 +02:00
hf2023Flags, fees, sigChecks, spendsCoinbase, false);
2026-05-14 12:07:30 +02:00
unspents[0] = tokenPrevout(tokenOutputScript(category, Tx::HasNft | Tx::NftMintBaton), activationHeight);
2026-05-14 08:52:55 +02:00
auto burnMintingBaton = tokenSpendTx({COutPoint(uint256S("01"), 1)}, {CScript() << OP_TRUE});
2026-05-14 12:07:30 +02:00
tx = Tx::fromOldTransaction(burnMintingBaton);
ValidationPrivate::validateTransactionInputs(burnMintingBaton, tx, unspents, activationHeight,
2026-05-14 08:52:55 +02:00
hf2023Flags, fees, sigChecks, spendsCoinbase, false);
unspents[0] = tokenPrevout(tokenOutputScript(category, Tx::HasFtAmount, 10), activationHeight);
2026-05-14 12:07:30 +02:00
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");
2026-05-14 08:52:55 +02:00
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)});
2026-05-14 12:07:30 +02:00
tx = Tx::fromOldTransaction(genesisCreatesMany);
ValidationPrivate::validateTransactionInputs(genesisCreatesMany, tx, unspents, activationHeight,
2026-05-14 08:52:55 +02:00
hf2023Flags, fees, sigChecks, spendsCoinbase, false);
}
void TransactionTests::may2023_contextual_transaction_rules()
{
2026-05-08 23:38:26 +02:00
/** 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;
2026-05-08 23:38:26 +02:00
ValidationFlags hf2018Flags;
hf2018Flags.hf201811Active = true;
ValidationFlags hf2023Flags = hf2018Flags;
hf2023Flags.hf202305Active = true;
2026-05-08 23:38:26 +02:00
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);
2026-05-08 23:38:26 +02:00
const Tx version2 = makeTransactionOfSize(120, 2);
const Tx version8 = makeTransactionOfSize(120, 8);
ValidationPrivate::checkContextualTransaction(tx64, noUpgrade);
2026-05-08 23:38:26 +02:00
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!");
}
2026-05-08 23:38:26 +02:00
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!");
}
}