Files

405 lines
16 KiB
C++
Raw Permalink Normal View History

/*
* 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>
2026-05-09 21:13:23 +02:00
#include <primitives/ScriptDefines.h>
#include <script/interpreter.h>
#include <utilstrencodings.h>
2026-05-12 16:07:59 +02:00
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;
}
2026-05-12 16:07:59 +02:00
static std::vector<uint8_t> ToScriptNumVch(int64_t value)
{
return CScriptNum(value).getvch();
}
2026-05-12 16:07:59 +02:00
static std::vector<uint8_t> ToBytes(const CScript &script)
{
2026-05-12 16:07:59 +02:00
return std::vector<uint8_t>(script.begin(), script.end());
}
2026-05-12 16:07:59 +02:00
static std::vector<uint8_t> ToBytes(const uint256 &value)
{
2026-05-12 16:07:59 +02:00
return std::vector<uint8_t>(value.begin(), value.end());
}
2026-05-14 08:52:55 +02:00
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)
{
2026-05-14 12:45:20 +02:00
auto newTx = Tx::fromOldTransaction(tx);
TransactionSignatureChecker checker(&tx, newTx, inputIndex, spentOutputs.at(inputIndex).outputValue, &spentOutputs);
2026-05-14 08:52:55 +02:00
return EvalScript(SCRIPT_ENABLE_NATIVE_INTROSPECTION | SCRIPT_ENABLE_CASHTOKENS, stack, script, checker, error);
}
2026-05-14 08:15:38 +02:00
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);
2026-05-14 08:15:38 +02:00
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;
}
2026-05-12 15:46:48 +02:00
static std::vector<Tx::Output> BuildSpentOutputs()
{
return {
2026-05-14 08:15:38 +02:00
Tx::Output(Streaming::bufferFrom(TokenScript(TokenCategoryA(), Tx::HasFtAmount, {}, 77,
CScript() << OP_8)), 3333),
2026-05-13 16:43:23 +02:00
Tx::Output(Streaming::bufferFrom(CScript() << OP_9 << OP_10), 4444)
};
}
2026-05-12 16:23:25 +02:00
void NativeIntrospectionTests::testOpcodes_data()
{
2026-05-12 16:23:25 +02:00
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)
2026-05-13 16:43:23 +02:00
<< ToBytes(spentOutputs.at(1).outputScript());
2026-05-12 16:23:25 +02:00
2026-05-14 08:15:38 +02:00
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);
2026-05-12 16:23:25 +02:00
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);
2026-05-14 08:15:38 +02:00
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>{};
2026-05-12 16:23:25 +02:00
}
void NativeIntrospectionTests::testOpcodes()
{
QFETCH(CScript, script);
QFETCH(std::vector<uint8_t>, expected);
const CTransaction tx(BuildTransaction());
2026-05-14 12:45:20 +02:00
auto newTx = Tx::fromOldTransaction(tx);
2026-05-12 16:23:25 +02:00
const std::vector<Tx::Output> spentOutputs = BuildSpentOutputs();
2026-05-14 12:45:20 +02:00
TransactionSignatureChecker checker(&tx, newTx, 1, spentOutputs.at(1).outputValue, &spentOutputs);
ScriptError error = SCRIPT_ERR_OK;
2026-05-12 16:07:59 +02:00
std::vector<std::vector<uint8_t>> stack;
2026-05-14 08:15:38 +02:00
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);
}
2026-05-14 08:52:55 +02:00
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>{});
}
2026-05-12 16:07:59 +02:00
void NativeIntrospectionTests::error_conditions()
{
2026-05-12 16:23:25 +02:00
const CTransaction tx(BuildTransaction());
2026-05-14 12:45:20 +02:00
auto newTx = Tx::fromOldTransaction(tx);
2026-05-12 15:46:48 +02:00
const std::vector<Tx::Output> spentOutputs = BuildSpentOutputs();
2026-05-14 12:45:20 +02:00
TransactionSignatureChecker checker(&tx, newTx, 1, spentOutputs.at(1).outputValue, &spentOutputs);
ScriptError error = SCRIPT_ERR_OK;
2026-05-12 16:07:59 +02:00
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);
2026-05-14 08:15:38 +02:00
stack.clear();
QVERIFY(!EvalScript(SCRIPT_ENABLE_NATIVE_INTROSPECTION, stack, CScript() << OP_0 << OP_OUTPUTTOKENAMOUNT,
checker, error));
QCOMPARE(error, SCRIPT_ERR_BAD_OPCODE);
2026-05-14 08:52:55 +02:00
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);
2026-05-12 16:07:59 +02:00
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);
2026-05-14 12:45:20 +02:00
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);
2026-05-14 08:15:38 +02:00
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);
2026-05-14 12:45:20 +02:00
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);
2026-05-14 12:45:20 +02:00
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);
}