Files
thehub/testing/bitcoin-protocol/native_introspection_tests.cpp
tomFlowee 3bf765e2a3 Make sure we have access to Tx
Similar to the previous commit; make sure that the ValidationContext has
access to the new Tx format so new code can use it directly.

In practically all cases (outside of the unit tests) callers already had
a Tx instance. Making it just a matter of sending it with.

Notice that the Tx object is immutable and implicitly shared which makes
it cheaper to pass around.
2026-05-14 13:27:17 +02:00

405 lines
16 KiB
C++

/*
* This file is part of the Flowee project
* Copyright (C) 2026 The Flowee developers
*
* 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/>.
*/
#include "native_introspection_tests.h"
#include <primitives/transaction.h>
#include <primitives/ScriptDefines.h>
#include <script/interpreter.h>
#include <utilstrencodings.h>
static bool EvalScript(uint32_t flags, std::vector<std::vector<uint8_t>> &stack, const CScript &script,
const BaseSignatureChecker &checker, ScriptError &error)
{
Script::State state(flags);
const bool result = Script::eval(stack, script, checker, state);
error = state.error;
return result;
}
static std::vector<uint8_t> ToScriptNumVch(int64_t value)
{
return CScriptNum(value).getvch();
}
static std::vector<uint8_t> ToBytes(const CScript &script)
{
return std::vector<uint8_t>(script.begin(), script.end());
}
static std::vector<uint8_t> ToBytes(const uint256 &value)
{
return std::vector<uint8_t>(value.begin(), value.end());
}
static bool EvalOne(const CTransaction &tx, int inputIndex, const std::vector<Tx::Output> &spentOutputs,
const CScript &script, std::vector<std::vector<uint8_t>> &stack, ScriptError &error)
{
auto newTx = Tx::fromOldTransaction(tx);
TransactionSignatureChecker checker(&tx, newTx, inputIndex, spentOutputs.at(inputIndex).outputValue, &spentOutputs);
return EvalScript(SCRIPT_ENABLE_NATIVE_INTROSPECTION | SCRIPT_ENABLE_CASHTOKENS, stack, script, checker, error);
}
static uint256 TokenCategoryA()
{
return uint256S("0x0000000000000000000000000000000000000000000000000000000000001234");
}
static uint256 TokenCategoryB()
{
return uint256S("0x0000000000000000000000000000000000000000000000000000000000005678");
}
static std::vector<uint8_t> TokenCategoryBytes(const Tx::Token &token)
{
std::vector<uint8_t> bytes(token.category.begin(), token.category.end());
if (token.isMutableNft() || token.isMintingNft())
bytes.push_back(token.bitfield & 0x0f);
return bytes;
}
static CScript TokenScript(const uint256 &category, uint8_t bitfield, const std::vector<uint8_t> &commitment,
uint64_t amount, const CScript &bytecode)
{
std::vector<uint8_t> bytes;
bytes.reserve(1 + 32 + 1 + 1 + commitment.size() + 1 + bytecode.size());
bytes.push_back(0xef);
bytes.insert(bytes.end(), category.begin(), category.end());
bytes.push_back(bitfield);
if (bitfield & Tx::HasCommitment) {
bytes.push_back(static_cast<uint8_t>(commitment.size()));
bytes.insert(bytes.end(), commitment.begin(), commitment.end());
}
if (bitfield & Tx::HasFtAmount)
bytes.push_back(static_cast<uint8_t>(amount));
bytes.insert(bytes.end(), bytecode.begin(), bytecode.end());
return CScript(bytes.begin(), bytes.end());
}
static CMutableTransaction BuildTransaction()
{
CMutableTransaction tx;
tx.nVersion = 2;
tx.nLockTime = 500;
tx.vin.emplace_back(COutPoint(uint256S("0x010203040506070809101112131415161718191a1b1c1d1e1f20212223242526"), 7),
CScript() << OP_2, 123);
tx.vin.emplace_back(COutPoint(uint256S("0x515253545556575859606162636465666768696a6b6c6d6e6f70717273747576"), 9),
CScript() << OP_3 << OP_4, CTxIn::SEQUENCE_FINAL - 1);
tx.vout.emplace_back(1111, CScript() << OP_5);
tx.vout.emplace_back(2222, TokenScript(TokenCategoryB(),
Tx::HasNft | Tx::HasCommitment | Tx::NftMutableBaton | Tx::HasFtAmount,
{0x12, 0x34}, 42, CScript() << OP_6 << OP_7));
return tx;
}
static std::vector<Tx::Output> BuildSpentOutputs()
{
return {
Tx::Output(Streaming::bufferFrom(TokenScript(TokenCategoryA(), Tx::HasFtAmount, {}, 77,
CScript() << OP_8)), 3333),
Tx::Output(Streaming::bufferFrom(CScript() << OP_9 << OP_10), 4444)
};
}
void NativeIntrospectionTests::testOpcodes_data()
{
QTest::addColumn<CScript>("script");
QTest::addColumn<std::vector<uint8_t>>("expected");
const CMutableTransaction mutableTx = BuildTransaction();
const CTransaction tx(mutableTx);
const std::vector<Tx::Output> spentOutputs = BuildSpentOutputs();
// === Nullary opcodes ===
QTest::newRow("OP_INPUTINDEX")
<< (CScript() << OP_INPUTINDEX)
<< ToScriptNumVch(1);
QTest::newRow("OP_TXVERSION")
<< (CScript() << OP_TXVERSION)
<< ToScriptNumVch(2);
QTest::newRow("OP_TXINPUTCOUNT")
<< (CScript() << OP_TXINPUTCOUNT)
<< ToScriptNumVch(2);
QTest::newRow("OP_TXOUTPUTCOUNT")
<< (CScript() << OP_TXOUTPUTCOUNT)
<< ToScriptNumVch(2);
QTest::newRow("OP_TXLOCKTIME")
<< (CScript() << OP_TXLOCKTIME)
<< ToScriptNumVch(500);
const CScript activeBytecodeScript = CScript() << OP_CODESEPARATOR << OP_ACTIVEBYTECODE;
QTest::newRow("OP_ACTIVEBYTECODE")
<< activeBytecodeScript
<< std::vector<uint8_t>{static_cast<unsigned char>(OP_ACTIVEBYTECODE)};
// === Unary input opcodes ===
QTest::newRow("OP_UTXOVALUE input 0")
<< (CScript() << OP_0 << OP_UTXOVALUE)
<< ToScriptNumVch(3333);
QTest::newRow("OP_UTXOVALUE input 1")
<< (CScript() << OP_1 << OP_UTXOVALUE)
<< ToScriptNumVch(4444);
QTest::newRow("OP_UTXOBYTECODE input 1")
<< (CScript() << OP_1 << OP_UTXOBYTECODE)
<< ToBytes(spentOutputs.at(1).outputScript());
QTest::newRow("OP_UTXOTOKENCATEGORY FT input")
<< (CScript() << OP_0 << OP_UTXOTOKENCATEGORY)
<< TokenCategoryBytes(spentOutputs.at(0).token());
QTest::newRow("OP_UTXOTOKENCOMMITMENT FT-only input")
<< (CScript() << OP_0 << OP_UTXOTOKENCOMMITMENT)
<< std::vector<uint8_t>{};
QTest::newRow("OP_UTXOTOKENAMOUNT input")
<< (CScript() << OP_0 << OP_UTXOTOKENAMOUNT)
<< ToScriptNumVch(77);
QTest::newRow("OP_OUTPOINTTXHASH input 0")
<< (CScript() << OP_0 << OP_OUTPOINTTXHASH)
<< ToBytes(tx.vin.at(0).prevout.hash);
QTest::newRow("OP_OUTPOINTINDEX input 1")
<< (CScript() << OP_1 << OP_OUTPOINTINDEX)
<< ToScriptNumVch(9);
QTest::newRow("OP_INPUTBYTECODE input 1")
<< (CScript() << OP_1 << OP_INPUTBYTECODE)
<< ToBytes(mutableTx.vin.at(1).scriptSig);
QTest::newRow("OP_INPUTSEQUENCENUMBER input 0")
<< (CScript() << OP_0 << OP_INPUTSEQUENCENUMBER)
<< ToScriptNumVch(123);
// === Unary output opcodes ===
QTest::newRow("OP_OUTPUTVALUE output 1")
<< (CScript() << OP_1 << OP_OUTPUTVALUE)
<< ToScriptNumVch(2222);
QTest::newRow("OP_OUTPUTBYTECODE output 0")
<< (CScript() << OP_0 << OP_OUTPUTBYTECODE)
<< ToBytes(mutableTx.vout.at(0).scriptPubKey);
Tx outputTx = Tx::fromOldTransaction(tx);
const auto tokenOutput = outputTx.output(1);
QTest::newRow("OP_OUTPUTBYTECODE token output")
<< (CScript() << OP_1 << OP_OUTPUTBYTECODE)
<< ToBytes(tokenOutput.outputScript());
QTest::newRow("OP_OUTPUTTOKENCATEGORY output")
<< (CScript() << OP_1 << OP_OUTPUTTOKENCATEGORY)
<< TokenCategoryBytes(tokenOutput.token());
QTest::newRow("OP_OUTPUTTOKENCOMMITMENT output")
<< (CScript() << OP_1 << OP_OUTPUTTOKENCOMMITMENT)
<< std::vector<uint8_t>{0x12, 0x34};
QTest::newRow("OP_OUTPUTTOKENAMOUNT output")
<< (CScript() << OP_1 << OP_OUTPUTTOKENAMOUNT)
<< ToScriptNumVch(42);
QTest::newRow("OP_OUTPUTTOKENAMOUNT no-token output")
<< (CScript() << OP_0 << OP_OUTPUTTOKENAMOUNT)
<< std::vector<uint8_t>{};
}
void NativeIntrospectionTests::testOpcodes()
{
QFETCH(CScript, script);
QFETCH(std::vector<uint8_t>, expected);
const CTransaction tx(BuildTransaction());
auto newTx = Tx::fromOldTransaction(tx);
const std::vector<Tx::Output> spentOutputs = BuildSpentOutputs();
TransactionSignatureChecker checker(&tx, newTx, 1, spentOutputs.at(1).outputValue, &spentOutputs);
ScriptError error = SCRIPT_ERR_OK;
std::vector<std::vector<uint8_t>> stack;
QVERIFY(EvalScript(SCRIPT_ENABLE_NATIVE_INTROSPECTION | SCRIPT_ENABLE_CASHTOKENS, stack, script, checker, error));
QCOMPARE(error, SCRIPT_ERR_OK);
QCOMPARE(stack.size(), size_t(1));
QCOMPARE(stack.back(), expected);
}
void NativeIntrospectionTests::cashtoken_opcode_variants()
{
CMutableTransaction mutableTx = BuildTransaction();
mutableTx.vout.clear();
mutableTx.vout.emplace_back(1000, CScript() << OP_TRUE);
mutableTx.vout.emplace_back(1000, TokenScript(TokenCategoryA(), Tx::HasNft | Tx::HasCommitment,
{0x01, 0x02, 0x03}, 0, CScript() << OP_1));
mutableTx.vout.emplace_back(1000, TokenScript(TokenCategoryA(), Tx::HasNft | Tx::NftMutableBaton,
{}, 0, CScript() << OP_2));
mutableTx.vout.emplace_back(1000, TokenScript(TokenCategoryA(), Tx::HasNft | Tx::NftMintBaton,
{}, 0, CScript() << OP_3));
mutableTx.vout.emplace_back(1000, TokenScript(TokenCategoryB(), Tx::HasFtAmount,
{}, 99, CScript() << OP_4));
const CTransaction tx(mutableTx);
const std::vector<Tx::Output> spentOutputs = {
Tx::Output(Streaming::bufferFrom(TokenScript(TokenCategoryA(), Tx::HasNft | Tx::HasCommitment,
{0xaa}, 0, CScript() << OP_5)), 1111),
Tx::Output(Streaming::bufferFrom(TokenScript(TokenCategoryA(), Tx::HasNft | Tx::NftMutableBaton,
{}, 0, CScript() << OP_6)), 2222)
};
ScriptError error = SCRIPT_ERR_OK;
std::vector<std::vector<uint8_t>> stack;
QVERIFY(EvalOne(tx, 0, spentOutputs, CScript() << OP_0 << OP_UTXOTOKENCATEGORY, stack, error));
QCOMPARE(stack.back(), TokenCategoryBytes(spentOutputs.at(0).token()));
stack.clear();
QVERIFY(EvalOne(tx, 0, spentOutputs, CScript() << OP_0 << OP_UTXOTOKENCOMMITMENT, stack, error));
QCOMPARE(stack.back(), std::vector<uint8_t>{0xaa});
stack.clear();
QVERIFY(EvalOne(tx, 0, spentOutputs, CScript() << OP_1 << OP_UTXOTOKENCATEGORY, stack, error));
QCOMPARE(stack.back(), TokenCategoryBytes(spentOutputs.at(1).token()));
Tx outputTx = Tx::fromOldTransaction(tx);
stack.clear();
QVERIFY(EvalOne(tx, 0, spentOutputs, CScript() << OP_0 << OP_OUTPUTTOKENCATEGORY, stack, error));
QCOMPARE(stack.back(), std::vector<uint8_t>{});
stack.clear();
QVERIFY(EvalOne(tx, 0, spentOutputs, CScript() << OP_1 << OP_OUTPUTTOKENCATEGORY, stack, error));
QCOMPARE(stack.back(), TokenCategoryBytes(outputTx.output(1).token()));
stack.clear();
QVERIFY(EvalOne(tx, 0, spentOutputs, CScript() << OP_1 << OP_OUTPUTTOKENCOMMITMENT, stack, error));
QCOMPARE(stack.back(), std::vector<uint8_t>({0x01, 0x02, 0x03}));
stack.clear();
QVERIFY(EvalOne(tx, 0, spentOutputs, CScript() << OP_2 << OP_OUTPUTTOKENCATEGORY, stack, error));
QCOMPARE(stack.back(), TokenCategoryBytes(outputTx.output(2).token()));
stack.clear();
QVERIFY(EvalOne(tx, 0, spentOutputs, CScript() << OP_3 << OP_OUTPUTTOKENCATEGORY, stack, error));
QCOMPARE(stack.back(), TokenCategoryBytes(outputTx.output(3).token()));
stack.clear();
QVERIFY(EvalOne(tx, 0, spentOutputs, CScript() << OP_4 << OP_OUTPUTTOKENAMOUNT, stack, error));
QCOMPARE(stack.back(), ToScriptNumVch(99));
stack.clear();
QVERIFY(EvalOne(tx, 0, spentOutputs, CScript() << OP_4 << OP_OUTPUTTOKENCOMMITMENT, stack, error));
QCOMPARE(stack.back(), std::vector<uint8_t>{});
}
void NativeIntrospectionTests::error_conditions()
{
const CTransaction tx(BuildTransaction());
auto newTx = Tx::fromOldTransaction(tx);
const std::vector<Tx::Output> spentOutputs = BuildSpentOutputs();
TransactionSignatureChecker checker(&tx, newTx, 1, spentOutputs.at(1).outputValue, &spentOutputs);
ScriptError error = SCRIPT_ERR_OK;
std::vector<std::vector<uint8_t>> stack;
QVERIFY(!EvalScript(0, stack, CScript() << OP_TXVERSION, checker, error));
QCOMPARE(error, SCRIPT_ERR_BAD_OPCODE);
BaseSignatureChecker noContextChecker;
stack.clear();
QVERIFY(!EvalScript(SCRIPT_ENABLE_NATIVE_INTROSPECTION, stack, CScript() << OP_TXVERSION, noContextChecker, error));
QCOMPARE(error, SCRIPT_ERR_CONTEXT_NOT_PRESENT);
stack.clear();
QVERIFY(!EvalScript(SCRIPT_ENABLE_NATIVE_INTROSPECTION, stack, CScript() << OP_UTXOVALUE, checker, error));
QCOMPARE(error, SCRIPT_ERR_INVALID_STACK_OPERATION);
stack.clear();
QVERIFY(!EvalScript(SCRIPT_ENABLE_NATIVE_INTROSPECTION, stack, CScript() << OP_0 << OP_OUTPUTTOKENAMOUNT,
checker, error));
QCOMPARE(error, SCRIPT_ERR_BAD_OPCODE);
stack.clear();
QVERIFY(!EvalScript(SCRIPT_ENABLE_NATIVE_INTROSPECTION | SCRIPT_ENABLE_CASHTOKENS, stack,
CScript() << OP_1NEGATE << OP_UTXOTOKENCATEGORY, checker, error));
QCOMPARE(error, SCRIPT_ERR_INVALID_TX_INPUT_INDEX);
stack.clear();
QVERIFY(!EvalScript(SCRIPT_ENABLE_NATIVE_INTROSPECTION | SCRIPT_ENABLE_CASHTOKENS, stack,
CScript() << 2 << OP_UTXOTOKENCOMMITMENT, checker, error));
QCOMPARE(error, SCRIPT_ERR_INVALID_TX_INPUT_INDEX);
stack.clear();
QVERIFY(!EvalScript(SCRIPT_ENABLE_NATIVE_INTROSPECTION | SCRIPT_ENABLE_CASHTOKENS, stack,
CScript() << 2 << OP_OUTPUTTOKENCATEGORY, checker, error));
QCOMPARE(error, SCRIPT_ERR_INVALID_TX_OUTPUT_INDEX);
stack.clear();
QVERIFY(!EvalScript(SCRIPT_ENABLE_NATIVE_INTROSPECTION, stack, CScript() << OP_1NEGATE << OP_UTXOVALUE, checker, error));
QCOMPARE(error, SCRIPT_ERR_INVALID_TX_INPUT_INDEX);
stack.clear();
QVERIFY(!EvalScript(SCRIPT_ENABLE_NATIVE_INTROSPECTION, stack, CScript() << 2 << OP_UTXOVALUE, checker, error));
QCOMPARE(error, SCRIPT_ERR_INVALID_TX_INPUT_INDEX);
stack.clear();
QVERIFY(!EvalScript(SCRIPT_ENABLE_NATIVE_INTROSPECTION, stack, CScript() << 2 << OP_OUTPUTVALUE, checker, error));
QCOMPARE(error, SCRIPT_ERR_INVALID_TX_OUTPUT_INDEX);
const std::vector<uint8_t> largeIndex = CScriptNum(int64_t(1) << 32).getvch();
stack.clear();
QVERIFY(!EvalScript(SCRIPT_ENABLE_NATIVE_INTROSPECTION, stack, CScript() << largeIndex << OP_OUTPUTVALUE, checker, error));
QCOMPARE(error, SCRIPT_ERR_INVALID_TX_OUTPUT_INDEX);
TransactionSignatureChecker limitedChecker(&tx, newTx, 0, spentOutputs.at(0).outputValue);
stack.clear();
QVERIFY(!EvalScript(SCRIPT_ENABLE_NATIVE_INTROSPECTION, stack, CScript() << OP_1 << OP_UTXOVALUE, limitedChecker, error));
QCOMPARE(error, SCRIPT_ERR_LIMITED_CONTEXT_NO_SIBLING_INFO);
stack.clear();
QVERIFY(!EvalScript(SCRIPT_ENABLE_NATIVE_INTROSPECTION | SCRIPT_ENABLE_CASHTOKENS, stack,
CScript() << OP_1 << OP_UTXOTOKENAMOUNT, limitedChecker, error));
QCOMPARE(error, SCRIPT_ERR_LIMITED_CONTEXT_NO_SIBLING_INFO);
CMutableTransaction oversizedInputTx = BuildTransaction();
for (unsigned int i = 0; i < MAX_SCRIPT_ELEMENT_SIZE + 1; ++i)
oversizedInputTx.vin.at(1).scriptSig << OP_0;
const CTransaction oversizedInput(oversizedInputTx);
newTx = Tx::fromOldTransaction(oversizedInput);
TransactionSignatureChecker oversizedInputChecker(&oversizedInput, newTx, 1, spentOutputs.at(1).outputValue, &spentOutputs);
stack.clear();
QVERIFY(!EvalScript(SCRIPT_ENABLE_NATIVE_INTROSPECTION, stack, CScript() << OP_1 << OP_INPUTBYTECODE,
oversizedInputChecker, error));
QCOMPARE(error, SCRIPT_ERR_PUSH_SIZE);
CMutableTransaction oversizedOutputTx = BuildTransaction();
for (unsigned int i = 0; i < MAX_SCRIPT_ELEMENT_SIZE + 1; ++i)
oversizedOutputTx.vout.at(1).scriptPubKey << OP_0;
const CTransaction oversizedOutput(oversizedOutputTx);
newTx = Tx::fromOldTransaction(oversizedInput);
TransactionSignatureChecker oversizedOutputChecker(&oversizedOutput, newTx, 1, spentOutputs.at(1).outputValue, &spentOutputs);
stack.clear();
QVERIFY(!EvalScript(SCRIPT_ENABLE_NATIVE_INTROSPECTION, stack, CScript() << OP_1 << OP_OUTPUTBYTECODE,
oversizedOutputChecker, error));
QCOMPARE(error, SCRIPT_ERR_PUSH_SIZE);
}