policy: introduce a helper to detect whether a transaction spends Segwit outputs
What changed, and why it matters
This commit adds a new internal helper function in Bitcoin Core that detects whether a transaction spends any Segregated Witness (Segwit) output, except for a special 'anchor' output type. It does not change network rules, user-visible behavior, or fix any bug by itself. It is a preparatory code change for future commits.
No immediate action required. Treat as routine refactoring/test addition. Monitor the follow-up commits that will consume this helper to understand the actual policy or security change.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit introduces SpendsNonAnchorWitnessProg() in src/policy/policy.cpp/.h and adds unit tests. The helper iterates over a transaction’s inputs, checks the previous output’s scriptPubKey, and returns true if it is a witness program other than PayToAnchor (P2A), including native P2WPKH/P2WSH, P2TR, and undefined future witness versions. It also handles P2SH-wrapped witness programs by extracting the redeemScript via EvalScript. The commit message explicitly states this is groundwork for later commits to detect witness stripping without repeatedly executing scripts.
Changed components
src/policy/policy.cppsrc/policy/policy.hsrc/test/transaction_tests.cppInspect captured patch +196 / −0
diff --git a/src/policy/policy.cpp b/src/policy/policy.cpp
index 48f2a6a7..fdb6bc5f 100644
--- a/src/policy/policy.cpp
+++ b/src/policy/policy.cpp
@@ -337,6 +337,42 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs)
return true;
}
+bool SpendsNonAnchorWitnessProg(const CTransaction& tx, const CCoinsViewCache& prevouts)
+{
+ if (tx.IsCoinBase()) {
+ return false;
+ }
+
+ int version;
+ std::vector<uint8_t> program;
+ for (const auto& txin: tx.vin) {
+ const auto& prev_spk{prevouts.AccessCoin(txin.prevout).out.scriptPubKey};
+
+ // Note this includes not-yet-defined witness programs.
+ if (prev_spk.IsWitnessProgram(version, program) && !prev_spk.IsPayToAnchor(version, program)) {
+ return true;
+ }
+
+ // For P2SH extract the redeem script and check if it spends a non-Taproot witness program. Note
+ // this is fine to call EvalScript (as done in AreInputsStandard/IsWitnessStandard) because this
+ // function is only ever called after IsStandardTx, which checks the scriptsig is pushonly.
+ if (prev_spk.IsPayToScriptHash()) {
+ // If EvalScript fails or results in an empty stack, the transaction is invalid by consensus.
+ std::vector <std::vector<uint8_t>> stack;
+ if (!EvalScript(stack, txin.scriptSig, SCRIPT_VERIFY_NONE, BaseSignatureChecker{}, SigVersion::BASE)
+ || stack.empty()) {
+ continue;
+ }
+ const CScript redeem_script{stack.back().begin(), stack.back().end()};
+ if (redeem_script.IsWitnessProgram(version, program)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
int64_t GetVirtualTransactionSize(int64_t nWeight, int64_t nSigOpCost, unsigned int bytes_per_sigop)
{
return (std::max(nWeight, nSigOpCost * bytes_per_sigop) + WITNESS_SCALE_FACTOR - 1) / WITNESS_SCALE_FACTOR;
diff --git a/src/policy/policy.h b/src/policy/policy.h
index f9a18561..ad787630 100644
--- a/src/policy/policy.h
+++ b/src/policy/policy.h
@@ -166,6 +166,11 @@ bool AreInputsStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs)
* Also enforce a maximum stack item size limit and no annexes for tapscript spends.
*/
bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs);
+/**
+ * Check whether this transaction spends any witness program but P2A, including not-yet-defined ones.
+ * May return `false` early for consensus-invalid transactions.
+ */
+bool SpendsNonAnchorWitnessProg(const CTransaction& tx, const CCoinsViewCache& prevouts);
/** Compute the virtual transaction size (weight reinterpreted as bytes). */
int64_t GetVirtualTransactionSize(int64_t nWeight, int64_t nSigOpCost, unsigned int bytes_per_sigop);
diff --git a/src/test/transaction_tests.cpp b/src/test/transaction_tests.cpp
index 5f03641e..137ec7fe 100644
--- a/src/test/transaction_tests.cpp
+++ b/src/test/transaction_tests.cpp
@@ -1149,4 +1149,159 @@ BOOST_AUTO_TEST_CASE(max_standard_legacy_sigops)
BOOST_CHECK(!::AreInputsStandard(CTransaction(tx_max_sigops), coins));
}
+/** Sanity check the return value of SpendsNonAnchorWitnessProg for various output types. */
+BOOST_AUTO_TEST_CASE(spends_witness_prog)
+{
+ CCoinsView coins_dummy;
+ CCoinsViewCache coins(&coins_dummy);
+ CKey key;
+ key.MakeNewKey(true);
+ const CPubKey pubkey{key.GetPubKey()};
+ CMutableTransaction tx_create{}, tx_spend{};
+ tx_create.vout.emplace_back(0, CScript{});
+ tx_spend.vin.emplace_back(Txid{}, 0);
+ std::vector<std::vector<uint8_t>> sol_dummy;
+
+ // CNoDestination, PubKeyDestination, PKHash, ScriptHash, WitnessV0ScriptHash, WitnessV0KeyHash,
+ // WitnessV1Taproot, PayToAnchor, WitnessUnknown.
+ static_assert(std::variant_size_v<CTxDestination> == 9);
+
+ // Go through all defined output types and sanity check SpendsNonAnchorWitnessProg.
+
+ // P2PK
+ tx_create.vout[0].scriptPubKey = GetScriptForDestination(PubKeyDestination{pubkey});
+ BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::PUBKEY);
+ tx_spend.vin[0].prevout.hash = tx_create.GetHash();
+ AddCoins(coins, CTransaction{tx_create}, 0, false);
+ BOOST_CHECK(!::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
+
+ // P2PKH
+ tx_create.vout[0].scriptPubKey = GetScriptForDestination(PKHash{pubkey});
+ BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::PUBKEYHASH);
+ tx_spend.vin[0].prevout.hash = tx_create.GetHash();
+ AddCoins(coins, CTransaction{tx_create}, 0, false);
+ BOOST_CHECK(!::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
+
+ // P2SH
+ auto redeem_script{CScript{} << OP_1 << OP_CHECKSIG};
+ tx_create.vout[0].scriptPubKey = GetScriptForDestination(ScriptHash{redeem_script});
+ BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::SCRIPTHASH);
+ tx_spend.vin[0].prevout.hash = tx_create.GetHash();
+ tx_spend.vin[0].scriptSig = CScript{} << OP_0 << ToByteVector(redeem_script);
+ AddCoins(coins, CTransaction{tx_create}, 0, false);
+ BOOST_CHECK(!::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
+ tx_spend.vin[0].scriptSig.clear();
+
+ // native P2WSH
+ const auto witness_script{CScript{} << OP_12 << OP_HASH160 << OP_DUP << OP_EQUAL};
+ tx_create.vout[0].scriptPubKey = GetScriptForDestination(WitnessV0ScriptHash{witness_script});
+ BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::WITNESS_V0_SCRIPTHASH);
+ tx_spend.vin[0].prevout.hash = tx_create.GetHash();
+ AddCoins(coins, CTransaction{tx_create}, 0, false);
+ BOOST_CHECK(::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
+
+ // P2SH-wrapped P2WSH
+ redeem_script = tx_create.vout[0].scriptPubKey;
+ tx_create.vout[0].scriptPubKey = GetScriptForDestination(ScriptHash(redeem_script));
+ BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::SCRIPTHASH);
+ tx_spend.vin[0].prevout.hash = tx_create.GetHash();
+ tx_spend.vin[0].scriptSig = CScript{} << ToByteVector(redeem_script);
+ AddCoins(coins, CTransaction{tx_create}, 0, false);
+ BOOST_CHECK(::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
+ tx_spend.vin[0].scriptSig.clear();
+ BOOST_CHECK(!::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
+
+ // native P2WPKH
+ tx_create.vout[0].scriptPubKey = GetScriptForDestination(WitnessV0KeyHash{pubkey});
+ BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::WITNESS_V0_KEYHASH);
+ tx_spend.vin[0].prevout.hash = tx_create.GetHash();
+ AddCoins(coins, CTransaction{tx_create}, 0, false);
+ BOOST_CHECK(::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
+
+ // P2SH-wrapped P2WPKH
+ redeem_script = tx_create.vout[0].scriptPubKey;
+ tx_create.vout[0].scriptPubKey = GetScriptForDestination(ScriptHash(redeem_script));
+ BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::SCRIPTHASH);
+ tx_spend.vin[0].prevout.hash = tx_create.GetHash();
+ tx_spend.vin[0].scriptSig = CScript{} << ToByteVector(redeem_script);
+ AddCoins(coins, CTransaction{tx_create}, 0, false);
+ BOOST_CHECK(::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
+ tx_spend.vin[0].scriptSig.clear();
+ BOOST_CHECK(!::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
+
+ // P2TR
+ tx_create.vout[0].scriptPubKey = GetScriptForDestination(WitnessV1Taproot{XOnlyPubKey{pubkey}});
+ BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::WITNESS_V1_TAPROOT);
+ tx_spend.vin[0].prevout.hash = tx_create.GetHash();
+ AddCoins(coins, CTransaction{tx_create}, 0, false);
+ BOOST_CHECK(::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
+
+ // P2SH-wrapped P2TR (undefined, non-standard)
+ redeem_script = tx_create.vout[0].scriptPubKey;
+ tx_create.vout[0].scriptPubKey = GetScriptForDestination(ScriptHash(redeem_script));
+ BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::SCRIPTHASH);
+ tx_spend.vin[0].prevout.hash = tx_create.GetHash();
+ tx_spend.vin[0].scriptSig = CScript{} << ToByteVector(redeem_script);
+ AddCoins(coins, CTransaction{tx_create}, 0, false);
+ BOOST_CHECK(::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
+ tx_spend.vin[0].scriptSig.clear();
+ BOOST_CHECK(!::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
+
+ // P2A
+ tx_create.vout[0].scriptPubKey = GetScriptForDestination(PayToAnchor{});
+ BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::ANCHOR);
+ tx_spend.vin[0].prevout.hash = tx_create.GetHash();
+ AddCoins(coins, CTransaction{tx_create}, 0, false);
+ BOOST_CHECK(!::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
+
+ // P2SH-wrapped P2A (undefined, non-standard)
+ redeem_script = tx_create.vout[0].scriptPubKey;
+ tx_create.vout[0].scriptPubKey = GetScriptForDestination(ScriptHash(redeem_script));
+ BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::SCRIPTHASH);
+ tx_spend.vin[0].prevout.hash = tx_create.GetHash();
+ tx_spend.vin[0].scriptSig = CScript{} << ToByteVector(redeem_script);
+ AddCoins(coins, CTransaction{tx_create}, 0, false);
+ BOOST_CHECK(::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
+ tx_spend.vin[0].scriptSig.clear();
+
+ // Undefined version 1 witness program
+ tx_create.vout[0].scriptPubKey = GetScriptForDestination(WitnessUnknown{1, {0x42, 0x42}});
+ BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::WITNESS_UNKNOWN);
+ tx_spend.vin[0].prevout.hash = tx_create.GetHash();
+ AddCoins(coins, CTransaction{tx_create}, 0, false);
+ BOOST_CHECK(::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
+
+ // P2SH-wrapped undefined version 1 witness program
+ redeem_script = tx_create.vout[0].scriptPubKey;
+ tx_create.vout[0].scriptPubKey = GetScriptForDestination(ScriptHash(redeem_script));
+ BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::SCRIPTHASH);
+ tx_spend.vin[0].prevout.hash = tx_create.GetHash();
+ tx_spend.vin[0].scriptSig = CScript{} << ToByteVector(redeem_script);
+ AddCoins(coins, CTransaction{tx_create}, 0, false);
+ BOOST_CHECK(::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
+ tx_spend.vin[0].scriptSig.clear();
+ BOOST_CHECK(!::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
+
+ // Various undefined version >1 32-byte witness programs.
+ const auto program{ToByteVector(XOnlyPubKey{pubkey})};
+ for (int i{2}; i <= 16; ++i) {
+ tx_create.vout[0].scriptPubKey = GetScriptForDestination(WitnessUnknown{i, program});
+ BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::WITNESS_UNKNOWN);
+ tx_spend.vin[0].prevout.hash = tx_create.GetHash();
+ AddCoins(coins, CTransaction{tx_create}, 0, false);
+ BOOST_CHECK(::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
+
+ // It's also detected within P2SH.
+ redeem_script = tx_create.vout[0].scriptPubKey;
+ tx_create.vout[0].scriptPubKey = GetScriptForDestination(ScriptHash(redeem_script));
+ BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::SCRIPTHASH);
+ tx_spend.vin[0].prevout.hash = tx_create.GetHash();
+ tx_spend.vin[0].scriptSig = CScript{} << ToByteVector(redeem_script);
+ AddCoins(coins, CTransaction{tx_create}, 0, false);
+ BOOST_CHECK(::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
+ tx_spend.vin[0].scriptSig.clear();
+ BOOST_CHECK(!::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
+ }
+}
+
BOOST_AUTO_TEST_SUITE_END()
Why this scored 12/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.