Skip to content
Merged
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: 36 additions & 0 deletions src/policy/policy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,42 @@ bool IsIssuanceInMoneyRange(const CTransaction& tx)
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)) {
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;
Expand Down
5 changes: 5 additions & 0 deletions src/policy/policy.h
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,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);

/* ELEMENTS
* Check if unblinded issuance/reissuance is in MoneyRange
Expand Down
138 changes: 138 additions & 0 deletions src/test/transaction_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -979,4 +979,142 @@ BOOST_AUTO_TEST_CASE(test_IsStandard)
}
}

/** 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(CConfidentialAsset(CAsset{}), CConfidentialValue(CAmount(0)), CScript{});
uint256 txid{};
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> == 8);

// Go through all defined output types and sanity check SpendsNonAnchorWitnessProg.

// 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));

// Undefined version 1 witness program
WitnessUnknown wu = {1, 2, {}, CPubKey()};
auto program1 = std::vector<unsigned char>{0x42, 0x42};
std::copy(program1.begin(), program1.end(), wu.program);
tx_create.vout[0].scriptPubKey = GetScriptForDestination(wu);
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) {
wu = WitnessUnknown{static_cast<unsigned int>(i), static_cast<unsigned int>(program.size()), {}, CPubKey()};
std::copy(program.begin(), program.end(), wu.program);
tx_create.vout[0].scriptPubKey = GetScriptForDestination(wu);
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()
9 changes: 2 additions & 7 deletions src/validation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1080,13 +1080,8 @@ bool MemPoolAccept::PolicyScriptChecks(const ATMPArgs& args, Workspace& ws)
// Check input scripts and signatures.
// This is done last to help prevent CPU exhaustion denial-of-service attacks.
if (!CheckInputScripts(tx, state, m_view, scriptVerifyFlags, true, false, ws.m_precomputed_txdata)) {
// SCRIPT_VERIFY_CLEANSTACK requires SCRIPT_VERIFY_WITNESS, so we
// need to turn both off, and compare against just turning off CLEANSTACK
// to see if the failure is specifically due to witness validation.
TxValidationState state_dummy; // Want reported failures to be from first CheckInputScripts
if (!tx.HasWitness() && CheckInputScripts(tx, state_dummy, m_view, scriptVerifyFlags & ~(SCRIPT_VERIFY_WITNESS | SCRIPT_VERIFY_CLEANSTACK), true, false, ws.m_precomputed_txdata) &&
!CheckInputScripts(tx, state_dummy, m_view, scriptVerifyFlags & ~SCRIPT_VERIFY_CLEANSTACK, true, false, ws.m_precomputed_txdata)) {
// Only the witness is missing, so the transaction itself may be fine.
// Detect a failure due to a missing witness so that p2p code can handle rejection caching appropriately.
if (!tx.HasWitness() && SpendsNonAnchorWitnessProg(tx, m_view)) {
state.Invalid(TxValidationResult::TX_WITNESS_STRIPPED,
state.GetRejectReason(), state.GetDebugMessage());
}
Expand Down
13 changes: 13 additions & 0 deletions test/functional/p2p_segwit.py
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,12 @@ def test_p2sh_witness(self):
expected_msgs=(spend_tx.hash, 'was not accepted: non-mandatory-script-verify-flag (Witness program was passed an empty witness)')):
test_transaction_acceptance(self.nodes[0], self.test_node, spend_tx, with_witness=False, accepted=False)

# The transaction was detected as witness stripped above and not added to the reject
# filter. Trying again will check it again and result in the same error.
with self.nodes[0].assert_debug_log(
expected_msgs=[spend_tx.getwtxid() , 'was not accepted: non-mandatory-script-verify-flag (Witness program was passed an empty witness)']):
test_transaction_acceptance(self.nodes[0], self.test_node, spend_tx, with_witness=False, accepted=False)

# Try to put the witness script in the scriptSig, should also fail.
spend_tx.vin[0].scriptSig = CScript([p2wsh_pubkey, b'a'])
spend_tx.rehash()
Expand Down Expand Up @@ -1317,6 +1323,13 @@ def test_tx_relay_after_segwit_activation(self):
test_transaction_acceptance(self.nodes[0], self.test_node, tx2, with_witness=True, accepted=True)
test_transaction_acceptance(self.nodes[0], self.test_node, tx3, with_witness=True, accepted=False)

# Now do the opposite: strip the witness entirely. This will be detected as witness stripping and
# the (w)txid won't be added to the reject filter: we can try again and get the same error.
tx3.wit.vtxinwit[0].scriptWitness.stack = []
reason = "was not accepted: non-mandatory-script-verify-flag (Witness program was passed an empty witness)"
test_transaction_acceptance(self.nodes[0], self.test_node, tx3, with_witness=False, accepted=False, reason=reason)
test_transaction_acceptance(self.nodes[0], self.test_node, tx3, with_witness=False, accepted=False, reason=reason)

# Get rid of the extra witness, and verify acceptance.
tx3.wit.vtxinwit[0].scriptWitness.stack = [witness_script]
# Also check that old_node gets a tx announcement, even though this is
Expand Down