Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion src/script/interpreter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <util/hasher.h>
#include <script/interpreter.h>

#include <crypto/ripemd160.h>
Expand All @@ -13,6 +14,8 @@
#include <tinyformat.h>
#include <uint256.h>

#include <unordered_set>

typedef std::vector<unsigned char> valtype;

namespace {
Expand Down Expand Up @@ -1214,6 +1217,13 @@ bool EvalScript(std::vector<std::vector<unsigned char> >& stack, const CScript&
}
break;

case OP_CHECKCONSOLIDATION:
{
if (sigversion != SigVersion::TAPSCRIPT) return set_error(serror, SCRIPT_ERR_BAD_OPCODE);
stack.push_back(checker.CheckConsolidation() ? vchTrue : vchFalse);
}
break;

default:
return set_error(serror, SCRIPT_ERR_BAD_OPCODE);
}
Expand Down Expand Up @@ -1410,6 +1420,23 @@ void PrecomputedTransactionData::Init(const T& txTo, std::vector<CTxOut>&& spent
m_spent_outputs_ready = true;
}

// Precompute consolidation flags
if (m_spent_outputs_ready) {
m_consolidation_markers.resize(m_spent_outputs.size());
std::unordered_set<uint256, SaltedUint256Hasher> seen;
for (size_t i = 0; i < m_spent_outputs.size(); ++i) {
uint256 scriptHash = Hash(m_spent_outputs[i].scriptPubKey);
if (seen.contains(scriptHash)) {
m_consolidation_markers[i] = true;
} else {
m_consolidation_markers[i] = false;
seen.insert(scriptHash);
}
}
} else {
m_consolidation_markers.clear();
}

// Determine which precomputation-impacting features this transaction uses.
bool uses_bip143_segwit = force;
bool uses_bip341_taproot = force;
Expand Down Expand Up @@ -1825,6 +1852,13 @@ bool GenericTransactionSignatureChecker<T>::CheckSequence(const CScriptNum& nSeq
return true;
}

template <class T>
bool GenericTransactionSignatureChecker<T>::CheckConsolidation() const
{
assert(txdata && nIn < txdata->m_consolidation_markers.size());
return txdata->m_consolidation_markers[nIn];
}

// explicit instantiation
template class GenericTransactionSignatureChecker<CTransaction>;
template class GenericTransactionSignatureChecker<CMutableTransaction>;
Expand All @@ -1843,7 +1877,7 @@ static bool ExecuteWitnessScript(const std::span<const valtype>& stack_span, con
return set_error(serror, SCRIPT_ERR_BAD_OPCODE);
}
// New opcodes will be listed here. May use a different sigversion to modify existing opcodes.
if (IsOpSuccess(opcode)) {
if (IsOpSuccess(opcode) || (opcode == OP_CHECKCONSOLIDATION && (flags & SCRIPT_VERIFY_CC) == 0)) {
if (flags & SCRIPT_VERIFY_DISCOURAGE_OP_SUCCESS) {
return set_error(serror, SCRIPT_ERR_DISCOURAGE_OP_SUCCESS);
}
Expand Down
18 changes: 18 additions & 0 deletions src/script/interpreter.h
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ enum class script_verify_flag_name : uint8_t {
//
SCRIPT_VERIFY_TAPROOT,

// Verify OP_CHECKCONSOLIDATION
//
SCRIPT_VERIFY_CC,

// Making unknown Taproot leaf versions non-standard
//
SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_TAPROOT_VERSION,
Expand Down Expand Up @@ -181,6 +185,9 @@ struct PrecomputedTransactionData
//! Whether m_spent_outputs is initialized.
bool m_spent_outputs_ready = false;

// BIP-CC precomputed consolidation markers.
std::vector<bool> m_consolidation_markers;

PrecomputedTransactionData() = default;

/** Initialize this PrecomputedTransactionData with transaction data.
Expand Down Expand Up @@ -294,6 +301,11 @@ class BaseSignatureChecker
return false;
}

virtual bool CheckConsolidation() const
{
return false;
}

virtual ~BaseSignatureChecker() = default;
};

Expand Down Expand Up @@ -331,6 +343,7 @@ class GenericTransactionSignatureChecker : public BaseSignatureChecker
bool CheckSchnorrSignature(std::span<const unsigned char> sig, std::span<const unsigned char> pubkey, SigVersion sigversion, ScriptExecutionData& execdata, ScriptError* serror = nullptr) const override;
bool CheckLockTime(const CScriptNum& nLockTime) const override;
bool CheckSequence(const CScriptNum& nSequence) const override;
bool CheckConsolidation() const override;
};

using TransactionSignatureChecker = GenericTransactionSignatureChecker<CTransaction>;
Expand Down Expand Up @@ -362,6 +375,11 @@ class DeferringSignatureChecker : public BaseSignatureChecker
{
return m_checker.CheckSequence(nSequence);
}

bool CheckConsolidation() const override
{
return m_checker.CheckConsolidation();
}
};

/** Compute the BIP341 tapleaf hash from leaf version & script. */
Expand Down
5 changes: 4 additions & 1 deletion src/script/script.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ std::string GetOpName(opcodetype opcode)
// Opcode added by BIP 342 (Tapscript)
case OP_CHECKSIGADD : return "OP_CHECKSIGADD";

// Opcode added by BIP-CC (Tapscript)
case OP_CHECKCONSOLIDATION : return "OP_CHECKCONSOLIDATION";

case OP_INVALIDOPCODE : return "OP_INVALIDOPCODE";

default:
Expand Down Expand Up @@ -367,7 +370,7 @@ bool IsOpSuccess(const opcodetype& opcode)
return opcode == 80 || opcode == 98 || (opcode >= 126 && opcode <= 129) ||
(opcode >= 131 && opcode <= 134) || (opcode >= 137 && opcode <= 138) ||
(opcode >= 141 && opcode <= 142) || (opcode >= 149 && opcode <= 153) ||
(opcode >= 187 && opcode <= 254);
(opcode >= 188 && opcode <= 254);
}

bool CheckMinimalPush(const std::vector<unsigned char>& data, opcodetype opcode) {
Expand Down
5 changes: 4 additions & 1 deletion src/script/script.h
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,9 @@ enum opcodetype
// Opcode added by BIP 342 (Tapscript)
OP_CHECKSIGADD = 0xba,

// Opcode added by BIP-CC (Tapscript)
OP_CHECKCONSOLIDATION = 0xbb,

OP_INVALIDOPCODE = 0xff,
};

Expand Down Expand Up @@ -598,7 +601,7 @@ class CScriptID : public BaseHash<uint160>
explicit CScriptID(const uint160& in) : BaseHash(in) {}
};

/** Test for OP_SUCCESSx opcodes as defined by BIP342. */
/** Test for current OP_SUCCESSx opcodes. */
bool IsOpSuccess(const opcodetype& opcode);

bool CheckMinimalPush(const std::vector<unsigned char>& data, opcodetype opcode);
Expand Down
1 change: 1 addition & 0 deletions src/script/sign.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,7 @@ class DummySignatureChecker final : public BaseSignatureChecker
bool CheckSchnorrSignature(std::span<const unsigned char> sig, std::span<const unsigned char> pubkey, SigVersion sigversion, ScriptExecutionData& execdata, ScriptError* serror) const override { return sig.size() != 0; }
bool CheckLockTime(const CScriptNum& nLockTime) const override { return true; }
bool CheckSequence(const CScriptNum& nSequence) const override { return true; }
bool CheckConsolidation() const override { return true; }
};
}

Expand Down
1 change: 1 addition & 0 deletions src/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ add_executable(test_bitcoin
coinstatsindex_tests.cpp
common_url_tests.cpp
compress_tests.cpp
consolidation_tests.cpp
crypto_tests.cpp
cuckoocache_tests.cpp
dbwrapper_tests.cpp
Expand Down
186 changes: 186 additions & 0 deletions src/test/consolidation_tests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// Copyright (c) 2025-present The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or https://opensource.org/license/mit.

#include <consensus/amount.h>
#include <policy/policy.h>
#include <primitives/transaction.h>
#include <script/interpreter.h>
#include <script/script.h>
#include <script/sign.h>
#include <script/signingprovider.h>
#include <uint256.h>
#include <util/strencodings.h>

#include <test/util/setup_common.h>

#include <boost/test/tools/old/interface.hpp>
#include <boost/test/unit_test.hpp>

#include <vector>

BOOST_FIXTURE_TEST_SUITE(consolidation_tests, BasicTestingSetup)

namespace {

struct ConsolidationTaproot
{
CKey key;
XOnlyPubKey internal_key;
CScript script_pubkey;
CScript tapscript;
std::vector<unsigned char> control;
uint256 merkle_root;
};

/** Build a taproot output with a key path and a single tapscript leaf of OP_CHECKCONSOLIDATION. */
ConsolidationTaproot MakeConsolidationTaproot()
{
ConsolidationTaproot out;
out.key.MakeNewKey(true);
out.internal_key = XOnlyPubKey(out.key.GetPubKey());
out.tapscript = CScript() << OP_CHECKCONSOLIDATION;

TaprootBuilder builder;
builder.Add(0, std::span<const unsigned char>{out.tapscript}, TAPROOT_LEAF_TAPSCRIPT);
builder.Finalize(out.internal_key);
const TaprootSpendData spenddata = builder.GetSpendData();

out.merkle_root = spenddata.merkle_root;
out.script_pubkey = GetScriptForDestination(builder.GetOutput());

auto it = spenddata.scripts.find({std::vector<unsigned char>(out.tapscript.begin(), out.tapscript.end()), TAPROOT_LEAF_TAPSCRIPT});
assert(it != spenddata.scripts.end());
out.control = *it->second.begin();
return out;
}

CMutableTransaction BuildBaseTx()
{
CMutableTransaction mtx;
// Key-path input
mtx.vin.emplace_back(COutPoint{Txid::FromUint256(uint256(0)), 0});
// Script-path input
mtx.vin.emplace_back(COutPoint{Txid::FromUint256(uint256(1)), 0});
// Output
mtx.vout.emplace_back(COIN*2, CScript() << OP_1 << std::vector<unsigned char>(32, 0x01));
return mtx;
}

void SetScriptPathSpend(CTxIn& in, const ConsolidationTaproot& tree)
{
in.scriptWitness.stack.clear();
in.scriptWitness.stack.emplace_back(tree.tapscript.begin(), tree.tapscript.end());
in.scriptWitness.stack.push_back(tree.control);
}

void SignKeyPathInput(CTxIn& in, const ConsolidationTaproot& tree, const CMutableTransaction& mtx, unsigned int vin,
const CTxOut& prevout, const PrecomputedTransactionData& txdata)
{
FlatSigningProvider provider;
provider.keys.emplace(tree.key.GetPubKey().GetID(), tree.key);
std::vector<unsigned char> sig;
MutableTransactionSignatureCreator creator(mtx, vin, prevout.nValue, &txdata, SIGHASH_DEFAULT);
BOOST_REQUIRE(creator.CreateSchnorrSig(provider, sig, tree.internal_key, nullptr, &tree.merkle_root, SigVersion::TAPROOT));
in.scriptWitness.stack.clear();
in.scriptWitness.stack.push_back(sig);
}

bool VerifyInput(const CTransaction& tx, unsigned int vin, const CTxOut& prevout,
const PrecomputedTransactionData& txdata, script_verify_flags flags)
{
ScriptError err = SCRIPT_ERR_OK;
return VerifyScript(tx.vin[vin].scriptSig, prevout.scriptPubKey, &tx.vin[vin].scriptWitness, flags,
TransactionSignatureChecker(&tx, vin, prevout.nValue, txdata, MissingDataBehavior::ASSERT_FAIL), &err);
}

void RunConsolidationCase(const ConsolidationTaproot& key_path_input, const ConsolidationTaproot& script_path_input, script_verify_flags flags)
{
// Build transaction
CMutableTransaction mtx = BuildBaseTx();
SetScriptPathSpend(mtx.vin[1], script_path_input);
const CTxOut prevout0{COIN, key_path_input.script_pubkey};
const CTxOut prevout1{COIN, script_path_input.script_pubkey};
PrecomputedTransactionData txdata;
txdata.Init(CTransaction{mtx}, {prevout0, prevout1}, true);
SignKeyPathInput(mtx.vin[0], key_path_input, mtx, 0, prevout0, txdata);

// Check consolidation markers
BOOST_REQUIRE_EQUAL(txdata.m_consolidation_markers.size(), 2U);
BOOST_CHECK(!txdata.m_consolidation_markers[0]);
const bool same_spk = (key_path_input.script_pubkey == script_path_input.script_pubkey);
BOOST_CHECK_EQUAL(txdata.m_consolidation_markers[1], same_spk);

// Verify inputs
const CTransaction tx{mtx};
// Key-path input should always verify
BOOST_CHECK(VerifyInput(tx, 0, prevout0, txdata, flags));
// Script-path input should verify when input SPKs are equal or when SCRIPT_VERIFY_CC is disabled
BOOST_CHECK_EQUAL(VerifyInput(tx, 1, prevout1, txdata, flags), same_spk || (flags & SCRIPT_VERIFY_CC) == 0);
}

}

BOOST_AUTO_TEST_CASE(same_spk)
{
// Same scriptPubKeys, post-activation
const ConsolidationTaproot input = MakeConsolidationTaproot();
RunConsolidationCase(input, input, MANDATORY_SCRIPT_VERIFY_FLAGS | SCRIPT_VERIFY_CC);
}

BOOST_AUTO_TEST_CASE(different_spks_post_activation)
{
// Different scriptPubKeys, post-activation
const ConsolidationTaproot key_path_input = MakeConsolidationTaproot();
const ConsolidationTaproot script_path_input = MakeConsolidationTaproot();
BOOST_REQUIRE(key_path_input.script_pubkey != script_path_input.script_pubkey);
RunConsolidationCase(key_path_input, script_path_input, MANDATORY_SCRIPT_VERIFY_FLAGS | SCRIPT_VERIFY_CC);
}

BOOST_AUTO_TEST_CASE(different_spks_pre_activation)
{
// Different scriptPubKeys, pre-activation
const ConsolidationTaproot key_path_input = MakeConsolidationTaproot();
const ConsolidationTaproot script_path_input = MakeConsolidationTaproot();
BOOST_REQUIRE(key_path_input.script_pubkey != script_path_input.script_pubkey);
RunConsolidationCase(key_path_input, script_path_input, MANDATORY_SCRIPT_VERIFY_FLAGS);
}

BOOST_AUTO_TEST_CASE(many_prevouts)
{
// 3 spks, not sorted
const ConsolidationTaproot spk1 = MakeConsolidationTaproot();
const ConsolidationTaproot spk2 = MakeConsolidationTaproot();
const ConsolidationTaproot spk3 = MakeConsolidationTaproot();
BOOST_REQUIRE(spk1.script_pubkey != spk2.script_pubkey);
BOOST_REQUIRE(spk2.script_pubkey != spk3.script_pubkey);
BOOST_REQUIRE(spk1.script_pubkey != spk3.script_pubkey);
const CTxOut prevout0{COIN, spk1.script_pubkey}; // 1st spk1, OP_CHECKCONSOLIDATION false
const CTxOut prevout1{COIN, spk2.script_pubkey}; // 1st spk2, OP_CHECKCONSOLIDATION false
const CTxOut prevout2{COIN, spk2.script_pubkey}; // 2nd spk2, OP_CHECKCONSOLIDATION true
const CTxOut prevout3{COIN, spk2.script_pubkey}; // 3rd spk2, OP_CHECKCONSOLIDATION true
const CTxOut prevout4{COIN, spk1.script_pubkey}; // 2nd spk1, OP_CHECKCONSOLIDATION true
const CTxOut prevout5{COIN, spk3.script_pubkey}; // 1st spk3, OP_CHECKCONSOLIDATION false

CMutableTransaction mtx;
mtx.vin.emplace_back(COutPoint{Txid::FromUint256(uint256(0)), 0});
mtx.vin.emplace_back(COutPoint{Txid::FromUint256(uint256(1)), 0});
mtx.vin.emplace_back(COutPoint{Txid::FromUint256(uint256(2)), 0});
mtx.vin.emplace_back(COutPoint{Txid::FromUint256(uint256(3)), 0});
mtx.vin.emplace_back(COutPoint{Txid::FromUint256(uint256(4)), 0});
mtx.vin.emplace_back(COutPoint{Txid::FromUint256(uint256(5)), 0});
mtx.vout.emplace_back(COIN*6, CScript() << OP_1 << std::vector<unsigned char>(32, 0x01));

PrecomputedTransactionData txdata;
txdata.Init(CTransaction{mtx}, {prevout0, prevout1, prevout2, prevout3, prevout4, prevout5}, true);

BOOST_REQUIRE_EQUAL(txdata.m_consolidation_markers.size(), 6U);
BOOST_CHECK(!txdata.m_consolidation_markers[0]);
BOOST_CHECK(!txdata.m_consolidation_markers[1]);
BOOST_CHECK(txdata.m_consolidation_markers[2]);
BOOST_CHECK(txdata.m_consolidation_markers[3]);
BOOST_CHECK(txdata.m_consolidation_markers[4]);
BOOST_CHECK(!txdata.m_consolidation_markers[5]);
}

BOOST_AUTO_TEST_SUITE_END()