Skip to content

Commit f679bad

Browse files
committed
Merge bitcoin/bitcoin#33105: validation: detect witness stripping without re-running Script checks
27aefac validation: detect witness stripping without re-running Script checks (Antoine Poinsot) 2907b58 policy: introduce a helper to detect whether a transaction spends Segwit outputs (Antoine Poinsot) eb07320 qa: test witness stripping in p2p_segwit (Antoine Poinsot) Pull request description: Since it was introduced in 4eb5155 (#18044), the detection of a stripped witness relies on running the Script checks 3 times. In the worst case, this consists in running Script validation for every single input 3 times. Detection of a stripped witness is necessary because in this case wtxid==txid, and the transaction's wtxid must not be added to the reject filter or it could allow a malicious peer to interfere with txid-based orphan resolution as used in 1p1c package relay. However it is not necessary to run Script validation to detect a stripped witness (much less so doing it 3 times in a row). There are 3 types of witness program: defined program types (Taproot, P2WPKH and P2WSH), undefined types, and the Pay-to-anchor carve-out. For defined program types, Script validation with an empty witness will always fail (by consensus). For undefined program types, Script validation is always going to fail regardless of the witness (by standardness). For P2A, an empty witness is never going to lead to a failure. Therefore it holds that we can always detect a stripped witness without re-running Script validation. However this might lead to more "false positives" (cases where we return witness stripping for an otherwise invalid transaction) than the existing implementation. For instance a transaction with one P2PKH input with an invalid signature and one P2WPKH input with its witness stripped. The existing implementation would treat it as consensus invalid while the implementation in this PR would always consider it witness stripped. h/t AJ: this essentially implements a variant of bitcoin/bitcoin#33066 (comment). ACKs for top commit: sipa: re-ACK 27aefac Crypt-iQ: re-ACK 27aefac glozow: reACK 27aefac Tree-SHA512: 70cf76b655b52bc8fa2759133315a3f11140844b6b80d9de3c95f592050978cc01a87bd2446e3a9c25cc872efea7659d6da3337b1a709511771fece206e9f149
2 parents 63d604a + 27aefac commit f679bad

File tree

5 files changed

+211
-7
lines changed

5 files changed

+211
-7
lines changed

src/policy/policy.cpp

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,42 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs)
337337
return true;
338338
}
339339

340+
bool SpendsNonAnchorWitnessProg(const CTransaction& tx, const CCoinsViewCache& prevouts)
341+
{
342+
if (tx.IsCoinBase()) {
343+
return false;
344+
}
345+
346+
int version;
347+
std::vector<uint8_t> program;
348+
for (const auto& txin: tx.vin) {
349+
const auto& prev_spk{prevouts.AccessCoin(txin.prevout).out.scriptPubKey};
350+
351+
// Note this includes not-yet-defined witness programs.
352+
if (prev_spk.IsWitnessProgram(version, program) && !prev_spk.IsPayToAnchor(version, program)) {
353+
return true;
354+
}
355+
356+
// For P2SH extract the redeem script and check if it spends a non-Taproot witness program. Note
357+
// this is fine to call EvalScript (as done in AreInputsStandard/IsWitnessStandard) because this
358+
// function is only ever called after IsStandardTx, which checks the scriptsig is pushonly.
359+
if (prev_spk.IsPayToScriptHash()) {
360+
// If EvalScript fails or results in an empty stack, the transaction is invalid by consensus.
361+
std::vector <std::vector<uint8_t>> stack;
362+
if (!EvalScript(stack, txin.scriptSig, SCRIPT_VERIFY_NONE, BaseSignatureChecker{}, SigVersion::BASE)
363+
|| stack.empty()) {
364+
continue;
365+
}
366+
const CScript redeem_script{stack.back().begin(), stack.back().end()};
367+
if (redeem_script.IsWitnessProgram(version, program)) {
368+
return true;
369+
}
370+
}
371+
}
372+
373+
return false;
374+
}
375+
340376
int64_t GetVirtualTransactionSize(int64_t nWeight, int64_t nSigOpCost, unsigned int bytes_per_sigop)
341377
{
342378
return (std::max(nWeight, nSigOpCost * bytes_per_sigop) + WITNESS_SCALE_FACTOR - 1) / WITNESS_SCALE_FACTOR;

src/policy/policy.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,11 @@ bool AreInputsStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs)
166166
* Also enforce a maximum stack item size limit and no annexes for tapscript spends.
167167
*/
168168
bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs);
169+
/**
170+
* Check whether this transaction spends any witness program but P2A, including not-yet-defined ones.
171+
* May return `false` early for consensus-invalid transactions.
172+
*/
173+
bool SpendsNonAnchorWitnessProg(const CTransaction& tx, const CCoinsViewCache& prevouts);
169174

170175
/** Compute the virtual transaction size (weight reinterpreted as bytes). */
171176
int64_t GetVirtualTransactionSize(int64_t nWeight, int64_t nSigOpCost, unsigned int bytes_per_sigop);

src/test/transaction_tests.cpp

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,4 +1149,159 @@ BOOST_AUTO_TEST_CASE(max_standard_legacy_sigops)
11491149
BOOST_CHECK(!::AreInputsStandard(CTransaction(tx_max_sigops), coins));
11501150
}
11511151

1152+
/** Sanity check the return value of SpendsNonAnchorWitnessProg for various output types. */
1153+
BOOST_AUTO_TEST_CASE(spends_witness_prog)
1154+
{
1155+
CCoinsView coins_dummy;
1156+
CCoinsViewCache coins(&coins_dummy);
1157+
CKey key;
1158+
key.MakeNewKey(true);
1159+
const CPubKey pubkey{key.GetPubKey()};
1160+
CMutableTransaction tx_create{}, tx_spend{};
1161+
tx_create.vout.emplace_back(0, CScript{});
1162+
tx_spend.vin.emplace_back(Txid{}, 0);
1163+
std::vector<std::vector<uint8_t>> sol_dummy;
1164+
1165+
// CNoDestination, PubKeyDestination, PKHash, ScriptHash, WitnessV0ScriptHash, WitnessV0KeyHash,
1166+
// WitnessV1Taproot, PayToAnchor, WitnessUnknown.
1167+
static_assert(std::variant_size_v<CTxDestination> == 9);
1168+
1169+
// Go through all defined output types and sanity check SpendsNonAnchorWitnessProg.
1170+
1171+
// P2PK
1172+
tx_create.vout[0].scriptPubKey = GetScriptForDestination(PubKeyDestination{pubkey});
1173+
BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::PUBKEY);
1174+
tx_spend.vin[0].prevout.hash = tx_create.GetHash();
1175+
AddCoins(coins, CTransaction{tx_create}, 0, false);
1176+
BOOST_CHECK(!::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
1177+
1178+
// P2PKH
1179+
tx_create.vout[0].scriptPubKey = GetScriptForDestination(PKHash{pubkey});
1180+
BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::PUBKEYHASH);
1181+
tx_spend.vin[0].prevout.hash = tx_create.GetHash();
1182+
AddCoins(coins, CTransaction{tx_create}, 0, false);
1183+
BOOST_CHECK(!::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
1184+
1185+
// P2SH
1186+
auto redeem_script{CScript{} << OP_1 << OP_CHECKSIG};
1187+
tx_create.vout[0].scriptPubKey = GetScriptForDestination(ScriptHash{redeem_script});
1188+
BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::SCRIPTHASH);
1189+
tx_spend.vin[0].prevout.hash = tx_create.GetHash();
1190+
tx_spend.vin[0].scriptSig = CScript{} << OP_0 << ToByteVector(redeem_script);
1191+
AddCoins(coins, CTransaction{tx_create}, 0, false);
1192+
BOOST_CHECK(!::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
1193+
tx_spend.vin[0].scriptSig.clear();
1194+
1195+
// native P2WSH
1196+
const auto witness_script{CScript{} << OP_12 << OP_HASH160 << OP_DUP << OP_EQUAL};
1197+
tx_create.vout[0].scriptPubKey = GetScriptForDestination(WitnessV0ScriptHash{witness_script});
1198+
BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::WITNESS_V0_SCRIPTHASH);
1199+
tx_spend.vin[0].prevout.hash = tx_create.GetHash();
1200+
AddCoins(coins, CTransaction{tx_create}, 0, false);
1201+
BOOST_CHECK(::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
1202+
1203+
// P2SH-wrapped P2WSH
1204+
redeem_script = tx_create.vout[0].scriptPubKey;
1205+
tx_create.vout[0].scriptPubKey = GetScriptForDestination(ScriptHash(redeem_script));
1206+
BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::SCRIPTHASH);
1207+
tx_spend.vin[0].prevout.hash = tx_create.GetHash();
1208+
tx_spend.vin[0].scriptSig = CScript{} << ToByteVector(redeem_script);
1209+
AddCoins(coins, CTransaction{tx_create}, 0, false);
1210+
BOOST_CHECK(::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
1211+
tx_spend.vin[0].scriptSig.clear();
1212+
BOOST_CHECK(!::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
1213+
1214+
// native P2WPKH
1215+
tx_create.vout[0].scriptPubKey = GetScriptForDestination(WitnessV0KeyHash{pubkey});
1216+
BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::WITNESS_V0_KEYHASH);
1217+
tx_spend.vin[0].prevout.hash = tx_create.GetHash();
1218+
AddCoins(coins, CTransaction{tx_create}, 0, false);
1219+
BOOST_CHECK(::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
1220+
1221+
// P2SH-wrapped P2WPKH
1222+
redeem_script = tx_create.vout[0].scriptPubKey;
1223+
tx_create.vout[0].scriptPubKey = GetScriptForDestination(ScriptHash(redeem_script));
1224+
BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::SCRIPTHASH);
1225+
tx_spend.vin[0].prevout.hash = tx_create.GetHash();
1226+
tx_spend.vin[0].scriptSig = CScript{} << ToByteVector(redeem_script);
1227+
AddCoins(coins, CTransaction{tx_create}, 0, false);
1228+
BOOST_CHECK(::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
1229+
tx_spend.vin[0].scriptSig.clear();
1230+
BOOST_CHECK(!::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
1231+
1232+
// P2TR
1233+
tx_create.vout[0].scriptPubKey = GetScriptForDestination(WitnessV1Taproot{XOnlyPubKey{pubkey}});
1234+
BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::WITNESS_V1_TAPROOT);
1235+
tx_spend.vin[0].prevout.hash = tx_create.GetHash();
1236+
AddCoins(coins, CTransaction{tx_create}, 0, false);
1237+
BOOST_CHECK(::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
1238+
1239+
// P2SH-wrapped P2TR (undefined, non-standard)
1240+
redeem_script = tx_create.vout[0].scriptPubKey;
1241+
tx_create.vout[0].scriptPubKey = GetScriptForDestination(ScriptHash(redeem_script));
1242+
BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::SCRIPTHASH);
1243+
tx_spend.vin[0].prevout.hash = tx_create.GetHash();
1244+
tx_spend.vin[0].scriptSig = CScript{} << ToByteVector(redeem_script);
1245+
AddCoins(coins, CTransaction{tx_create}, 0, false);
1246+
BOOST_CHECK(::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
1247+
tx_spend.vin[0].scriptSig.clear();
1248+
BOOST_CHECK(!::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
1249+
1250+
// P2A
1251+
tx_create.vout[0].scriptPubKey = GetScriptForDestination(PayToAnchor{});
1252+
BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::ANCHOR);
1253+
tx_spend.vin[0].prevout.hash = tx_create.GetHash();
1254+
AddCoins(coins, CTransaction{tx_create}, 0, false);
1255+
BOOST_CHECK(!::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
1256+
1257+
// P2SH-wrapped P2A (undefined, non-standard)
1258+
redeem_script = tx_create.vout[0].scriptPubKey;
1259+
tx_create.vout[0].scriptPubKey = GetScriptForDestination(ScriptHash(redeem_script));
1260+
BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::SCRIPTHASH);
1261+
tx_spend.vin[0].prevout.hash = tx_create.GetHash();
1262+
tx_spend.vin[0].scriptSig = CScript{} << ToByteVector(redeem_script);
1263+
AddCoins(coins, CTransaction{tx_create}, 0, false);
1264+
BOOST_CHECK(::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
1265+
tx_spend.vin[0].scriptSig.clear();
1266+
1267+
// Undefined version 1 witness program
1268+
tx_create.vout[0].scriptPubKey = GetScriptForDestination(WitnessUnknown{1, {0x42, 0x42}});
1269+
BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::WITNESS_UNKNOWN);
1270+
tx_spend.vin[0].prevout.hash = tx_create.GetHash();
1271+
AddCoins(coins, CTransaction{tx_create}, 0, false);
1272+
BOOST_CHECK(::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
1273+
1274+
// P2SH-wrapped undefined version 1 witness program
1275+
redeem_script = tx_create.vout[0].scriptPubKey;
1276+
tx_create.vout[0].scriptPubKey = GetScriptForDestination(ScriptHash(redeem_script));
1277+
BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::SCRIPTHASH);
1278+
tx_spend.vin[0].prevout.hash = tx_create.GetHash();
1279+
tx_spend.vin[0].scriptSig = CScript{} << ToByteVector(redeem_script);
1280+
AddCoins(coins, CTransaction{tx_create}, 0, false);
1281+
BOOST_CHECK(::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
1282+
tx_spend.vin[0].scriptSig.clear();
1283+
BOOST_CHECK(!::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
1284+
1285+
// Various undefined version >1 32-byte witness programs.
1286+
const auto program{ToByteVector(XOnlyPubKey{pubkey})};
1287+
for (int i{2}; i <= 16; ++i) {
1288+
tx_create.vout[0].scriptPubKey = GetScriptForDestination(WitnessUnknown{i, program});
1289+
BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::WITNESS_UNKNOWN);
1290+
tx_spend.vin[0].prevout.hash = tx_create.GetHash();
1291+
AddCoins(coins, CTransaction{tx_create}, 0, false);
1292+
BOOST_CHECK(::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
1293+
1294+
// It's also detected within P2SH.
1295+
redeem_script = tx_create.vout[0].scriptPubKey;
1296+
tx_create.vout[0].scriptPubKey = GetScriptForDestination(ScriptHash(redeem_script));
1297+
BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::SCRIPTHASH);
1298+
tx_spend.vin[0].prevout.hash = tx_create.GetHash();
1299+
tx_spend.vin[0].scriptSig = CScript{} << ToByteVector(redeem_script);
1300+
AddCoins(coins, CTransaction{tx_create}, 0, false);
1301+
BOOST_CHECK(::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
1302+
tx_spend.vin[0].scriptSig.clear();
1303+
BOOST_CHECK(!::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
1304+
}
1305+
}
1306+
11521307
BOOST_AUTO_TEST_SUITE_END()

src/validation.cpp

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1254,13 +1254,8 @@ bool MemPoolAccept::PolicyScriptChecks(const ATMPArgs& args, Workspace& ws)
12541254
// Check input scripts and signatures.
12551255
// This is done last to help prevent CPU exhaustion denial-of-service attacks.
12561256
if (!CheckInputScripts(tx, state, m_view, scriptVerifyFlags, true, false, ws.m_precomputed_txdata, GetValidationCache())) {
1257-
// SCRIPT_VERIFY_CLEANSTACK requires SCRIPT_VERIFY_WITNESS, so we
1258-
// need to turn both off, and compare against just turning off CLEANSTACK
1259-
// to see if the failure is specifically due to witness validation.
1260-
TxValidationState state_dummy; // Want reported failures to be from first CheckInputScripts
1261-
if (!tx.HasWitness() && CheckInputScripts(tx, state_dummy, m_view, scriptVerifyFlags & ~(SCRIPT_VERIFY_WITNESS | SCRIPT_VERIFY_CLEANSTACK), true, false, ws.m_precomputed_txdata, GetValidationCache()) &&
1262-
!CheckInputScripts(tx, state_dummy, m_view, scriptVerifyFlags & ~SCRIPT_VERIFY_CLEANSTACK, true, false, ws.m_precomputed_txdata, GetValidationCache())) {
1263-
// Only the witness is missing, so the transaction itself may be fine.
1257+
// Detect a failure due to a missing witness so that p2p code can handle rejection caching appropriately.
1258+
if (!tx.HasWitness() && SpendsNonAnchorWitnessProg(tx, m_view)) {
12641259
state.Invalid(TxValidationResult::TX_WITNESS_STRIPPED,
12651260
state.GetRejectReason(), state.GetDebugMessage());
12661261
}

test/functional/p2p_segwit.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,12 @@ def test_p2sh_witness(self):
693693
expected_msgs=[spend_tx.txid_hex, 'was not accepted: mandatory-script-verify-flag-failed (Witness program was passed an empty witness)']):
694694
test_transaction_acceptance(self.nodes[0], self.test_node, spend_tx, with_witness=False, accepted=False)
695695

696+
# The transaction was detected as witness stripped above and not added to the reject
697+
# filter. Trying again will check it again and result in the same error.
698+
with self.nodes[0].assert_debug_log(
699+
expected_msgs=[spend_tx.txid_hex, 'was not accepted: mandatory-script-verify-flag-failed (Witness program was passed an empty witness)']):
700+
test_transaction_acceptance(self.nodes[0], self.test_node, spend_tx, with_witness=False, accepted=False)
701+
696702
# Try to put the witness script in the scriptSig, should also fail.
697703
spend_tx.vin[0].scriptSig = CScript([p2wsh_pubkey, b'a'])
698704
with self.nodes[0].assert_debug_log(
@@ -1245,6 +1251,13 @@ def test_tx_relay_after_segwit_activation(self):
12451251
test_transaction_acceptance(self.nodes[0], self.test_node, tx2, with_witness=True, accepted=True)
12461252
test_transaction_acceptance(self.nodes[0], self.test_node, tx3, with_witness=True, accepted=False)
12471253

1254+
# Now do the opposite: strip the witness entirely. This will be detected as witness stripping and
1255+
# the (w)txid won't be added to the reject filter: we can try again and get the same error.
1256+
tx3.wit.vtxinwit[0].scriptWitness.stack = []
1257+
reason = "was not accepted: mandatory-script-verify-flag-failed (Witness program was passed an empty witness)"
1258+
test_transaction_acceptance(self.nodes[0], self.test_node, tx3, with_witness=False, accepted=False, reason=reason)
1259+
test_transaction_acceptance(self.nodes[0], self.test_node, tx3, with_witness=False, accepted=False, reason=reason)
1260+
12481261
# Get rid of the extra witness, and verify acceptance.
12491262
tx3.wit.vtxinwit[0].scriptWitness.stack = [witness_script]
12501263
# Also check that old_node gets a tx announcement, even though this is

0 commit comments

Comments
 (0)