policy: update `AreInputsStandard` to return error string
What changed, and why it matters
This commit renames a Bitcoin Core internal function from AreInputsStandard to ValidateInputsStandardness and changes it from returning true/false to returning a structured result that includes an explanation of why an input was rejected. It does not change what transactions are accepted or rejected; it only improves the error messages returned to users and developers. There is no security vulnerability being fixed here.
No security action required. This is a maintainability/refactoring change. Reviewers may verify that all former return-false paths now correctly set an invalid TxValidationState and that callers propagate it properly.
Security signals we found
No change to policy thresholds or acceptance logic
Refactor only: boolean return replaced with structured validation state
Improved observability via detailed reject reasons and debug messages
No consensus code modified
No memory safety, cryptographic, or authorization changes
Evidence from the diff
The patch refactors AreInputsStandard into ValidateInputsStandardness, returning a TxValidationState object instead of a bool. Each previous ‘return false’ path now populates the state with a specific TX_INPUTS_NOT_STANDARD reject reason and a debug message (e.g., ‘non-witness sigops exceed bip54 limit’, ‘input N script unknown’, ‘p2sh redeemscript sigops exceed limit’). Callers in validation.cpp, benchmarks, fuzzers, and tests are updated to consume the new API. The functional test is updated to expect the more detailed RPC error string. The underlying policy rules and thresholds are unchanged.
Changed components
src/policy/policy.cppsrc/policy/policy.hsrc/validation.cppsrc/bench/ccoins_caching.cppsrc/test/fuzz/coins_view.cppsrc/test/fuzz/transaction.cppsrc/test/script_p2sh_tests.cppsrc/test/transaction_tests.cpptest/functional/mempool_sigoplimit.pyInspect captured patch +71 / −38
diff --git a/src/bench/ccoins_caching.cpp b/src/bench/ccoins_caching.cpp
index 35186153..673616a5 100644
--- a/src/bench/ccoins_caching.cpp
+++ b/src/bench/ccoins_caching.cpp
@@ -49,8 +49,7 @@ static void CCoinsCaching(benchmark::Bench& bench)
// Benchmark.
const CTransaction tx_1(t1);
bench.run([&] {
- bool success{AreInputsStandard(tx_1, coins)};
- assert(success);
+ assert(ValidateInputsStandardness(tx_1, coins).IsValid());
});
}
diff --git a/src/policy/policy.cpp b/src/policy/policy.cpp
index 3da6cb74..2f5f9fce 100644
--- a/src/policy/policy.cpp
+++ b/src/policy/policy.cpp
@@ -18,6 +18,7 @@
#include <script/solver.h>
#include <serialize.h>
#include <span.h>
+#include <tinyformat.h>
#include <algorithm>
#include <cstddef>
@@ -210,14 +211,16 @@ static bool CheckSigopsBIP54(const CTransaction& tx, const CCoinsViewCache& inpu
*
* We also check the total number of non-witness sigops across the whole transaction, as per BIP54.
*/
-bool AreInputsStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs)
+TxValidationState ValidateInputsStandardness(const CTransaction& tx, const CCoinsViewCache& mapInputs)
{
+ TxValidationState state;
if (tx.IsCoinBase()) {
- return true; // Coinbases don't use vin normally
+ return state; // Coinbases don't use vin normally
}
if (!CheckSigopsBIP54(tx, mapInputs)) {
- return false;
+ state.Invalid(TxValidationResult::TX_INPUTS_NOT_STANDARD, "bad-txns-nonstandard-inputs", "non-witness sigops exceed bip54 limit");
+ return state;
}
for (unsigned int i = 0; i < tx.vin.size(); i++) {
@@ -225,27 +228,38 @@ bool AreInputsStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs)
std::vector<std::vector<unsigned char> > vSolutions;
TxoutType whichType = Solver(prev.scriptPubKey, vSolutions);
- if (whichType == TxoutType::NONSTANDARD || whichType == TxoutType::WITNESS_UNKNOWN) {
+ if (whichType == TxoutType::NONSTANDARD) {
+ state.Invalid(TxValidationResult::TX_INPUTS_NOT_STANDARD, "bad-txns-nonstandard-inputs", strprintf("input %u script unknown", i));
+ return state;
+ } else if (whichType == TxoutType::WITNESS_UNKNOWN) {
// WITNESS_UNKNOWN failures are typically also caught with a policy
// flag in the script interpreter, but it can be helpful to catch
// this type of NONSTANDARD transaction earlier in transaction
// validation.
- return false;
+ state.Invalid(TxValidationResult::TX_INPUTS_NOT_STANDARD, "bad-txns-nonstandard-inputs", strprintf("input %u witness program is undefined", i));
+ return state;
} else if (whichType == TxoutType::SCRIPTHASH) {
std::vector<std::vector<unsigned char> > stack;
+ ScriptError serror;
// convert the scriptSig into a stack, so we can inspect the redeemScript
- if (!EvalScript(stack, tx.vin[i].scriptSig, SCRIPT_VERIFY_NONE, BaseSignatureChecker(), SigVersion::BASE))
- return false;
- if (stack.empty())
- return false;
+ if (!EvalScript(stack, tx.vin[i].scriptSig, SCRIPT_VERIFY_NONE, BaseSignatureChecker(), SigVersion::BASE, &serror)) {
+ state.Invalid(TxValidationResult::TX_INPUTS_NOT_STANDARD, "bad-txns-nonstandard-inputs", strprintf("p2sh scriptsig malformed (input %u: %s)", i, ScriptErrorString(serror)));
+ return state;
+ }
+ if (stack.empty()) {
+ state.Invalid(TxValidationResult::TX_INPUTS_NOT_STANDARD, "bad-txns-nonstandard-inputs", strprintf("input %u P2SH redeemscript missing", i));
+ return state;
+ }
CScript subscript(stack.back().begin(), stack.back().end());
- if (subscript.GetSigOpCount(true) > MAX_P2SH_SIGOPS) {
- return false;
+ unsigned int sigop_count = subscript.GetSigOpCount(true);
+ if (sigop_count > MAX_P2SH_SIGOPS) {
+ state.Invalid(TxValidationResult::TX_INPUTS_NOT_STANDARD, "bad-txns-nonstandard-inputs", strprintf("p2sh redeemscript sigops exceed limit (input %u: %u > %u)", i, sigop_count, MAX_P2SH_SIGOPS));
+ return state;
}
}
}
- return true;
+ return state;
}
bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs)
@@ -354,7 +368,7 @@ bool SpendsNonAnchorWitnessProg(const CTransaction& tx, const CCoinsViewCache& p
}
// 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
+ // this is fine to call EvalScript (as done in ValidateInputsStandardness/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.
diff --git a/src/policy/policy.h b/src/policy/policy.h
index 0131b56b..f10b120d 100644
--- a/src/policy/policy.h
+++ b/src/policy/policy.h
@@ -8,6 +8,7 @@
#include <consensus/amount.h>
#include <consensus/consensus.h>
+#include <consensus/validation.h>
#include <primitives/transaction.h>
#include <script/interpreter.h>
#include <script/solver.h>
@@ -154,11 +155,12 @@ static constexpr decltype(CTransaction::version) TX_MAX_STANDARD_VERSION{3};
*/
bool IsStandardTx(const CTransaction& tx, const std::optional<unsigned>& max_datacarrier_bytes, bool permit_bare_multisig, const CFeeRate& dust_relay_fee, std::string& reason);
/**
-* Check for standard transaction types
-* @param[in] mapInputs Map of previous transactions that have outputs we're spending
-* @return True if all inputs (scriptSigs) use only standard transaction forms
-*/
-bool AreInputsStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs);
+ * Check for standard transaction types
+ * @param[in] mapInputs Map of previous transactions that have outputs we're spending
+ * @returns valid TxValidationState if all inputs (scriptSigs) use only standard transaction forms else returns
+ * invalid TxValidationState which states why the first invalid input is not standard
+ */
+TxValidationState ValidateInputsStandardness(const CTransaction& tx, const CCoinsViewCache& mapInputs);
/**
* Check if the transaction is over standard P2WSH resources limit:
* 3600bytes witnessScript size, 80bytes per witness stack element, 100 witness stack elements
diff --git a/src/test/fuzz/coins_view.cpp b/src/test/fuzz/coins_view.cpp
index dacef9a7..d283224a 100644
--- a/src/test/fuzz/coins_view.cpp
+++ b/src/test/fuzz/coins_view.cpp
@@ -252,7 +252,7 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsView& backend
assert(expected_code_path);
},
[&] {
- (void)AreInputsStandard(CTransaction{random_mutable_transaction}, coins_view_cache);
+ (void)ValidateInputsStandardness(CTransaction{random_mutable_transaction}, coins_view_cache);
},
[&] {
TxValidationState state;
diff --git a/src/test/fuzz/transaction.cpp b/src/test/fuzz/transaction.cpp
index c9eb1122..78629c45 100644
--- a/src/test/fuzz/transaction.cpp
+++ b/src/test/fuzz/transaction.cpp
@@ -89,7 +89,7 @@ FUZZ_TARGET(transaction, .init = initialize_transaction)
CCoinsView coins_view;
const CCoinsViewCache coins_view_cache(&coins_view);
- (void)AreInputsStandard(tx, coins_view_cache);
+ (void)ValidateInputsStandardness(tx, coins_view_cache);
(void)IsWitnessStandard(tx, coins_view_cache);
if (tx.GetTotalSize() < 250'000) { // Avoid high memory usage (with msan) due to json encoding
diff --git a/src/test/script_p2sh_tests.cpp b/src/test/script_p2sh_tests.cpp
index b4641118..858723d4 100644
--- a/src/test/script_p2sh_tests.cpp
+++ b/src/test/script_p2sh_tests.cpp
@@ -275,7 +275,7 @@ BOOST_AUTO_TEST_CASE(switchover)
BOOST_CHECK_MESSAGE(err == SCRIPT_ERR_EQUALVERIFY, ScriptErrorString(err));
}
-BOOST_AUTO_TEST_CASE(AreInputsStandard)
+BOOST_AUTO_TEST_CASE(ValidateInputsStandardness)
{
CCoinsView coinsDummy;
CCoinsViewCache coins(&coinsDummy);
@@ -360,7 +360,7 @@ BOOST_AUTO_TEST_CASE(AreInputsStandard)
txTo.vin[3].scriptSig << OP_11 << OP_11 << std::vector<unsigned char>(oneAndTwo.begin(), oneAndTwo.end());
txTo.vin[4].scriptSig << std::vector<unsigned char>(fifteenSigops.begin(), fifteenSigops.end());
- BOOST_CHECK(::AreInputsStandard(CTransaction(txTo), coins));
+ BOOST_CHECK(::ValidateInputsStandardness(CTransaction(txTo), coins).IsValid());
// 22 P2SH sigops for all inputs (1 for vin[0], 6 for vin[3], 15 for vin[4]
BOOST_CHECK_EQUAL(GetP2SHSigOpCount(CTransaction(txTo), coins), 22U);
@@ -379,7 +379,7 @@ BOOST_AUTO_TEST_CASE(AreInputsStandard)
txToNonStd1.vin[0].prevout.hash = txFrom.GetHash();
txToNonStd1.vin[0].scriptSig << std::vector<unsigned char>(sixteenSigops.begin(), sixteenSigops.end());
- BOOST_CHECK(!::AreInputsStandard(CTransaction(txToNonStd1), coins));
+ BOOST_CHECK(::ValidateInputsStandardness(CTransaction(txToNonStd1), coins).IsInvalid());
BOOST_CHECK_EQUAL(GetP2SHSigOpCount(CTransaction(txToNonStd1), coins), 16U);
CMutableTransaction txToNonStd2;
@@ -391,7 +391,7 @@ BOOST_AUTO_TEST_CASE(AreInputsStandard)
txToNonStd2.vin[0].prevout.hash = txFrom.GetHash();
txToNonStd2.vin[0].scriptSig << std::vector<unsigned char>(twentySigops.begin(), twentySigops.end());
- BOOST_CHECK(!::AreInputsStandard(CTransaction(txToNonStd2), coins));
+ BOOST_CHECK(::ValidateInputsStandardness(CTransaction(txToNonStd2), coins).IsInvalid());
BOOST_CHECK_EQUAL(GetP2SHSigOpCount(CTransaction(txToNonStd2), coins), 20U);
}
diff --git a/src/test/transaction_tests.cpp b/src/test/transaction_tests.cpp
index b2a70057..5ff39fdc 100644
--- a/src/test/transaction_tests.cpp
+++ b/src/test/transaction_tests.cpp
@@ -411,7 +411,7 @@ BOOST_AUTO_TEST_CASE(test_Get)
t1.vout[0].nValue = 90*CENT;
t1.vout[0].scriptPubKey << OP_1;
- BOOST_CHECK(AreInputsStandard(CTransaction(t1), coins));
+ BOOST_CHECK(ValidateInputsStandardness(CTransaction(t1), coins).IsValid());
}
static void CreateCreditAndSpend(const FillableSigningProvider& keystore, const CScript& outscript, CTransactionRef& output, CMutableTransaction& input, bool success = true)
@@ -1052,7 +1052,7 @@ BOOST_AUTO_TEST_CASE(max_standard_legacy_sigops)
// 2490 sigops is below the limit.
BOOST_CHECK_EQUAL(GetP2SHSigOpCount(CTransaction(tx_max_sigops), coins), 2490);
- BOOST_CHECK(::AreInputsStandard(CTransaction(tx_max_sigops), coins));
+ BOOST_CHECK(::ValidateInputsStandardness(CTransaction(tx_max_sigops), coins).IsValid());
// Adding one more input will bump this to 2505, hitting the limit.
tx_create.vout.emplace_back(424242, max_sigops_p2sh);
@@ -1063,8 +1063,17 @@ BOOST_AUTO_TEST_CASE(max_standard_legacy_sigops)
tx_max_sigops.vin.emplace_back(prev_txid, p2sh_inputs_count, CScript() << ToByteVector(max_sigops_redeem_script));
AddCoins(coins, CTransaction(tx_create), 0, false);
BOOST_CHECK_GT((p2sh_inputs_count + 1) * MAX_P2SH_SIGOPS, MAX_TX_LEGACY_SIGOPS);
- BOOST_CHECK_EQUAL(GetP2SHSigOpCount(CTransaction(tx_max_sigops), coins), 2505);
- BOOST_CHECK(!::AreInputsStandard(CTransaction(tx_max_sigops), coins));
+ auto legacy_sigops_count = GetP2SHSigOpCount(CTransaction(tx_max_sigops), coins);
+ BOOST_CHECK_EQUAL(legacy_sigops_count, 2505);
+ std::string reject_reason("bad-txns-nonstandard-inputs");
+ std::string sigop_limit_reject_debug_message("non-witness sigops exceed bip54 limit");
+ {
+ auto validation_state = ValidateInputsStandardness(CTransaction(tx_max_sigops), coins);
+ BOOST_CHECK(validation_state.IsInvalid());
+ BOOST_CHECK_EQUAL(validation_state.GetRejectReason(), reject_reason);
+ BOOST_CHECK_EQUAL(validation_state.GetDebugMessage(), sigop_limit_reject_debug_message);
+ }
+
// Now, check the limit can be reached with regular P2PK outputs too. Use a separate
// preparation transaction, to demonstrate spending coins from a single tx is irrelevant.
@@ -1082,8 +1091,7 @@ BOOST_AUTO_TEST_CASE(max_standard_legacy_sigops)
AddCoins(coins, CTransaction(tx_create_p2pk), 0, false);
// The transaction now contains exactly 2500 sigops, the check should pass.
- BOOST_CHECK_EQUAL(p2sh_inputs_count * MAX_P2SH_SIGOPS + p2pk_inputs_count * 1, MAX_TX_LEGACY_SIGOPS);
- BOOST_CHECK(::AreInputsStandard(CTransaction(tx_max_sigops), coins));
+ BOOST_CHECK(::ValidateInputsStandardness(CTransaction(tx_max_sigops), coins).IsValid());
// Now, add some Segwit inputs. We add one for each defined Segwit output type. The limit
// is exclusively on non-witness sigops and therefore those should not be counted.
@@ -1099,7 +1107,7 @@ BOOST_AUTO_TEST_CASE(max_standard_legacy_sigops)
// The transaction now still contains exactly 2500 sigops, the check should pass.
AddCoins(coins, CTransaction(tx_create_segwit), 0, false);
- BOOST_REQUIRE(::AreInputsStandard(CTransaction(tx_max_sigops), coins));
+ BOOST_REQUIRE(::ValidateInputsStandardness(CTransaction(tx_max_sigops), coins).IsValid());
// Add one more P2PK input. We'll reach the limit.
tx_create_p2pk.vout.emplace_back(212121, p2pk_script);
@@ -1110,8 +1118,14 @@ BOOST_AUTO_TEST_CASE(max_standard_legacy_sigops)
tx_max_sigops.vin.emplace_back(prev_txid, i);
}
AddCoins(coins, CTransaction(tx_create_p2pk), 0, false);
- BOOST_CHECK_GT(p2sh_inputs_count * MAX_P2SH_SIGOPS + p2pk_inputs_count * 1, MAX_TX_LEGACY_SIGOPS);
- BOOST_CHECK(!::AreInputsStandard(CTransaction(tx_max_sigops), coins));
+ auto legacy_sigop_count_p2pk = p2sh_inputs_count * MAX_P2SH_SIGOPS + p2pk_inputs_count * 1;
+ BOOST_CHECK_GT(legacy_sigop_count_p2pk, MAX_TX_LEGACY_SIGOPS);
+ {
+ auto validation_state = ValidateInputsStandardness(CTransaction(tx_max_sigops), coins);
+ BOOST_CHECK(validation_state.IsInvalid());
+ BOOST_CHECK_EQUAL(validation_state.GetRejectReason(), reject_reason);
+ BOOST_CHECK_EQUAL(validation_state.GetDebugMessage(), sigop_limit_reject_debug_message);
+ }
}
/** Sanity check the return value of SpendsNonAnchorWitnessProg for various output types. */
diff --git a/src/validation.cpp b/src/validation.cpp
index 7f97f99b..433abd45 100644
--- a/src/validation.cpp
+++ b/src/validation.cpp
@@ -894,8 +894,11 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
return false; // state filled in by CheckTxInputs
}
- if (m_pool.m_opts.require_standard && !AreInputsStandard(tx, m_view)) {
- return state.Invalid(TxValidationResult::TX_INPUTS_NOT_STANDARD, "bad-txns-nonstandard-inputs");
+ if (m_pool.m_opts.require_standard) {
+ state = ValidateInputsStandardness(tx, m_view);
+ if (state.IsInvalid()) {
+ return false;
+ }
}
// Check for non-standard witnesses.
diff --git a/test/functional/mempool_sigoplimit.py b/test/functional/mempool_sigoplimit.py
index 6b2a0268..f07f7756 100755
--- a/test/functional/mempool_sigoplimit.py
+++ b/test/functional/mempool_sigoplimit.py
@@ -208,7 +208,8 @@ class BytesPerSigOpTest(BitcoinTestFramework):
nonstd_tx = CTransaction()
nonstd_tx.vin = [CTxIn(op, CScript([b"", packed_redeem_script])) for op in outpoints]
nonstd_tx.vout = [CTxOut(0, CScript([OP_RETURN, b""]))]
- assert_raises_rpc_error(-26, "bad-txns-nonstandard-inputs", self.nodes[0].sendrawtransaction, nonstd_tx.serialize().hex())
+ assert_raises_rpc_error(-26, "bad-txns-nonstandard-inputs, non-witness sigops exceed bip54 limit",
+ self.nodes[0].sendrawtransaction, nonstd_tx.serialize().hex())
# Spending one less accounts for 2490 legacy sigops and is standard.
std_tx = deepcopy(nonstd_tx)
Why this scored 19/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.