kernel: Expose btck_transaction_check consensus function
What changed, and why it matters
This commit adds a new public function to Bitcoin Core's libbitcoinkernel library that lets outside programs ask the kernel to perform basic, context-free checks on a transaction (for example, whether it has valid outputs and no duplicate inputs). It is purely an API addition: it exposes an existing internal validation routine through a C interface and adds tests. There is no bug fix, no change to consensus rules, and no indication of a security vulnerability.
No security action required. Treat as normal feature/API expansion. Reviewers may optionally verify that the wrapper correctly handles null pointers and that the state object is always reset, but these are code-quality rather than security concerns.
Security signals we found
No security-relevant behavior change: only new API surface exposing existing consensus check
No vulnerability fix, bounds-error correction, or privilege change present in diff
Added test coverage for newly exposed API
Evidence from the diff
The change introduces btck_transaction_check() and supporting btck_TxValidationState types to the libbitcoinkernel C API. The implementation wraps the existing CheckTransaction() from consensus/tx_check.h, which performs context-free structural consensus validation (null inputs, output value ranges, duplicate inputs, coinbase scriptSig size). The state object is reset on entry, and the function returns 1/0 for valid/invalid. A C++ wrapper and unit tests using tx_valid.json/tx_invalid.json vectors are added. No existing behavior is modified.
Changed components
src/kernel/bitcoinkernel.cppsrc/kernel/bitcoinkernel.hsrc/kernel/bitcoinkernel_wrapper.hsrc/test/kernel/test_kernel.cppInspect captured patch +293 / −0
diff --git a/src/kernel/bitcoinkernel.cpp b/src/kernel/bitcoinkernel.cpp
index f21ea041..aa095cab 100644
--- a/src/kernel/bitcoinkernel.cpp
+++ b/src/kernel/bitcoinkernel.cpp
@@ -8,6 +8,7 @@
#include <chain.h>
#include <coins.h>
+#include <consensus/tx_check.h>
#include <consensus/validation.h>
#include <dbwrapper.h>
#include <kernel/caches.h>
@@ -146,6 +147,7 @@ struct Handle {
struct btck_BlockTreeEntry: Handle<btck_BlockTreeEntry, CBlockIndex> {};
struct btck_Block : Handle<btck_Block, std::shared_ptr<const CBlock>> {};
struct btck_BlockValidationState : Handle<btck_BlockValidationState, BlockValidationState> {};
+struct btck_TxValidationState : Handle<btck_TxValidationState, TxValidationState> {};
namespace {
@@ -1444,3 +1446,49 @@ void btck_block_header_destroy(btck_BlockHeader* header)
{
delete header;
}
+
+btck_ValidationMode btck_tx_validation_state_get_validation_mode(const btck_TxValidationState* state_)
+{
+ const auto& state = btck_TxValidationState::get(state_);
+ if (state.IsValid()) return btck_ValidationMode_VALID;
+ if (state.IsInvalid()) return btck_ValidationMode_INVALID;
+ return btck_ValidationMode_INTERNAL_ERROR;
+}
+
+btck_TxValidationState* btck_tx_validation_state_create()
+{
+ return btck_TxValidationState::create();
+}
+
+btck_TxValidationResult btck_tx_validation_state_get_tx_validation_result(const btck_TxValidationState* state_)
+{
+ switch (btck_TxValidationState::get(state_).GetResult()) {
+ case TxValidationResult::TX_RESULT_UNSET: return btck_TxValidationResult_UNSET;
+ case TxValidationResult::TX_CONSENSUS: return btck_TxValidationResult_CONSENSUS;
+ case TxValidationResult::TX_INPUTS_NOT_STANDARD: return btck_TxValidationResult_INPUTS_NOT_STANDARD;
+ case TxValidationResult::TX_NOT_STANDARD: return btck_TxValidationResult_NOT_STANDARD;
+ case TxValidationResult::TX_MISSING_INPUTS: return btck_TxValidationResult_MISSING_INPUTS;
+ case TxValidationResult::TX_PREMATURE_SPEND: return btck_TxValidationResult_PREMATURE_SPEND;
+ case TxValidationResult::TX_WITNESS_MUTATED: return btck_TxValidationResult_WITNESS_MUTATED;
+ case TxValidationResult::TX_WITNESS_STRIPPED: return btck_TxValidationResult_WITNESS_STRIPPED;
+ case TxValidationResult::TX_CONFLICT: return btck_TxValidationResult_CONFLICT;
+ case TxValidationResult::TX_MEMPOOL_POLICY: return btck_TxValidationResult_MEMPOOL_POLICY;
+ case TxValidationResult::TX_NO_MEMPOOL: return btck_TxValidationResult_NO_MEMPOOL;
+ case TxValidationResult::TX_RECONSIDERABLE: return btck_TxValidationResult_RECONSIDERABLE;
+ case TxValidationResult::TX_UNKNOWN: return btck_TxValidationResult_UNKNOWN;
+ } // no default case, so the compiler can warn about missing cases
+ assert(false);
+}
+
+void btck_tx_validation_state_destroy(btck_TxValidationState* state)
+{
+ delete state;
+}
+
+int btck_transaction_check(const btck_Transaction* tx, btck_TxValidationState* validation_state)
+{
+ auto& state = btck_TxValidationState::get(validation_state);
+ state = TxValidationState{};
+ const bool ok = CheckTransaction(*btck_Transaction::get(tx), state);
+ return ok ? 1 : 0;
+}
diff --git a/src/kernel/bitcoinkernel.h b/src/kernel/bitcoinkernel.h
index d3028729..577c4c0a 100644
--- a/src/kernel/bitcoinkernel.h
+++ b/src/kernel/bitcoinkernel.h
@@ -242,6 +242,14 @@ typedef struct btck_ConsensusParams btck_ConsensusParams;
*/
typedef struct btck_Chain btck_Chain;
+/**
+ * Opaque data structure for holding the state of a transaction during validation.
+ *
+ * Contains information indicating whether validation was successful, and if not
+ * which step during transaction validation failed.
+ */
+typedef struct btck_TxValidationState btck_TxValidationState;
+
/**
* Opaque data structure for holding a block's spent outputs.
*
@@ -388,6 +396,25 @@ typedef uint32_t btck_BlockValidationResult;
#define btck_BlockValidationResult_TIME_FUTURE ((btck_BlockValidationResult)(7)) //!< block timestamp was > 2 hours in the future (or our clock is bad)
#define btck_BlockValidationResult_HEADER_LOW_WORK ((btck_BlockValidationResult)(8)) //!< the block header may be on a too-little-work chain
+/**
+ * Indicates the reason why a transaction failed validation. The subset of
+ * values reachable depends on which validation function was used.
+ */
+typedef uint32_t btck_TxValidationResult;
+#define btck_TxValidationResult_UNSET ((btck_TxValidationResult)(0)) //!< initial value. Tx has not yet been rejected
+#define btck_TxValidationResult_CONSENSUS ((btck_TxValidationResult)(1)) //!< invalid by consensus rules
+#define btck_TxValidationResult_INPUTS_NOT_STANDARD ((btck_TxValidationResult)(2)) //!< inputs (covered by txid) failed policy rules
+#define btck_TxValidationResult_NOT_STANDARD ((btck_TxValidationResult)(3)) //!< otherwise didn't meet local policy rules
+#define btck_TxValidationResult_MISSING_INPUTS ((btck_TxValidationResult)(4)) //!< transaction was missing some of its inputs
+#define btck_TxValidationResult_PREMATURE_SPEND ((btck_TxValidationResult)(5)) //!< transaction spends a coinbase too early, or violates locktime/sequence locks
+#define btck_TxValidationResult_WITNESS_MUTATED ((btck_TxValidationResult)(6)) //!< witness may have been malleated or is prior to SegWit activation
+#define btck_TxValidationResult_WITNESS_STRIPPED ((btck_TxValidationResult)(7)) //!< transaction is missing a witness
+#define btck_TxValidationResult_CONFLICT ((btck_TxValidationResult)(8)) //!< tx already in mempool or conflicts with a tx in the chain
+#define btck_TxValidationResult_MEMPOOL_POLICY ((btck_TxValidationResult)(9)) //!< violated mempool's fee/size/descendant/RBF/etc limits
+#define btck_TxValidationResult_NO_MEMPOOL ((btck_TxValidationResult)(10)) //!< this node does not have a mempool so can't validate the transaction
+#define btck_TxValidationResult_RECONSIDERABLE ((btck_TxValidationResult)(11)) //!< fails some policy, but might be acceptable if submitted in a (different) package
+#define btck_TxValidationResult_UNKNOWN ((btck_TxValidationResult)(12)) //!< transaction was not validated because package failed
+
/**
* Holds the validation interface callbacks. The user data pointer may be used
* to point to user-defined structures to make processing the validation
@@ -505,6 +532,40 @@ typedef uint8_t btck_ChainType;
#define btck_ChainType_SIGNET ((btck_ChainType)(3))
#define btck_ChainType_REGTEST ((btck_ChainType)(4))
+/** @name TxValidationState
+ * Introspection for transaction validation state.
+ */
+///@{
+
+/**
+ * Create a new btck_TxValidationState.
+ */
+BITCOINKERNEL_API btck_TxValidationState* BITCOINKERNEL_WARN_UNUSED_RESULT btck_tx_validation_state_create();
+
+/**
+ * Returns the validation mode from an opaque btck_TxValidationState pointer.
+ */
+BITCOINKERNEL_API btck_ValidationMode btck_tx_validation_state_get_validation_mode(
+ const btck_TxValidationState* state) BITCOINKERNEL_ARG_NONNULL(1);
+
+/**
+ * Returns the validation result from an opaque btck_TxValidationState pointer.
+ *
+ * btck_transaction_check currently produces only btck_TxValidationResult_UNSET
+ * for valid transactions and btck_TxValidationResult_CONSENSUS for invalid
+ * ones. Other values remain exposed for forward compatibility with higher-level
+ * validation entry points.
+ */
+BITCOINKERNEL_API btck_TxValidationResult btck_tx_validation_state_get_tx_validation_result(
+ const btck_TxValidationState* state) BITCOINKERNEL_ARG_NONNULL(1);
+
+/**
+ * Destroy the btck_TxValidationState.
+ */
+BITCOINKERNEL_API void btck_tx_validation_state_destroy(btck_TxValidationState* state);
+
+///@}
+
/** @name Transaction
* Functions for working with transactions.
*/
@@ -606,6 +667,27 @@ BITCOINKERNEL_API uint32_t BITCOINKERNEL_WARN_UNUSED_RESULT btck_transaction_get
BITCOINKERNEL_API const btck_Txid* BITCOINKERNEL_WARN_UNUSED_RESULT btck_transaction_get_txid(
const btck_Transaction* transaction) BITCOINKERNEL_ARG_NONNULL(1);
+/**
+ * @brief Run context-free consensus validation on a btck_Transaction.
+ *
+ * Performs basic structural consensus checks (consensus/tx_check::CheckTransaction)
+ * without requiring blockchain state.
+ *
+ * @param[in] tx Non-null, the transaction to validate.
+ * @param[out] validation_state Non-null, previously created with
+ * btck_tx_validation_state_create. Reset on
+ * entry (any prior contents are overwritten)
+ * and updated in-place with the validation
+ * result before this function returns.
+ * @return 1 if valid, 0 if invalid.
+ * @note Only btck_TxValidationResult_UNSET and
+ * btck_TxValidationResult_CONSENSUS are
+ * reachable via this function.
+ */
+BITCOINKERNEL_API int BITCOINKERNEL_WARN_UNUSED_RESULT btck_transaction_check(
+ const btck_Transaction* tx,
+ btck_TxValidationState* validation_state) BITCOINKERNEL_ARG_NONNULL(1, 2);
+
/**
* Destroy the transaction.
*/
diff --git a/src/kernel/bitcoinkernel_wrapper.h b/src/kernel/bitcoinkernel_wrapper.h
index 2be5e223..3b3f5009 100644
--- a/src/kernel/bitcoinkernel_wrapper.h
+++ b/src/kernel/bitcoinkernel_wrapper.h
@@ -79,6 +79,22 @@ enum class BlockValidationResult : btck_BlockValidationResult {
HEADER_LOW_WORK = btck_BlockValidationResult_HEADER_LOW_WORK
};
+enum class TxValidationResult : btck_TxValidationResult {
+ UNSET = btck_TxValidationResult_UNSET,
+ CONSENSUS = btck_TxValidationResult_CONSENSUS,
+ INPUTS_NOT_STANDARD = btck_TxValidationResult_INPUTS_NOT_STANDARD,
+ NOT_STANDARD = btck_TxValidationResult_NOT_STANDARD,
+ MISSING_INPUTS = btck_TxValidationResult_MISSING_INPUTS,
+ PREMATURE_SPEND = btck_TxValidationResult_PREMATURE_SPEND,
+ WITNESS_MUTATED = btck_TxValidationResult_WITNESS_MUTATED,
+ WITNESS_STRIPPED = btck_TxValidationResult_WITNESS_STRIPPED,
+ CONFLICT = btck_TxValidationResult_CONFLICT,
+ MEMPOOL_POLICY = btck_TxValidationResult_MEMPOOL_POLICY,
+ NO_MEMPOOL = btck_TxValidationResult_NO_MEMPOOL,
+ RECONSIDERABLE = btck_TxValidationResult_RECONSIDERABLE,
+ UNKNOWN = btck_TxValidationResult_UNKNOWN
+};
+
enum class ScriptVerifyStatus : btck_ScriptVerifyStatus {
OK = btck_ScriptVerifyStatus_OK,
ERROR_INVALID_FLAGS_COMBINATION = btck_ScriptVerifyStatus_ERROR_INVALID_FLAGS_COMBINATION,
@@ -999,6 +1015,28 @@ inline bool Block::Check(const ConsensusParamsView& consensus_params,
return btck_block_check(get(), consensus_params.get(), static_cast<btck_BlockCheckFlags>(flags), state.get()) == 1;
}
+class TxValidationState : public UniqueHandle<btck_TxValidationState, btck_tx_validation_state_destroy>
+{
+public:
+ using UniqueHandle::UniqueHandle; // inherit ctor
+ explicit TxValidationState() : UniqueHandle{btck_tx_validation_state_create()} {}
+
+ ValidationMode GetValidationMode() const
+ {
+ return static_cast<ValidationMode>(btck_tx_validation_state_get_validation_mode(get()));
+ }
+
+ TxValidationResult GetTxValidationResult() const
+ {
+ return static_cast<TxValidationResult>(btck_tx_validation_state_get_tx_validation_result(get()));
+ }
+};
+
+inline bool CheckTransaction(const Transaction& tx, TxValidationState& state)
+{
+ return btck_transaction_check(tx.get(), state.get()) == 1;
+}
+
class ValidationInterface
{
public:
diff --git a/src/test/kernel/test_kernel.cpp b/src/test/kernel/test_kernel.cpp
index 97b0b91f..0489839d 100644
--- a/src/test/kernel/test_kernel.cpp
+++ b/src/test/kernel/test_kernel.cpp
@@ -1266,3 +1266,128 @@ BOOST_AUTO_TEST_CASE(btck_chainman_regtest_tests)
fs::remove(test_directory.m_directory / "blocks" / "rev00000.dat");
BOOST_CHECK_THROW(chainman->ReadBlockSpentOutputs(tip), std::runtime_error);
}
+
+// -----------------------------------------------------------------------------
+// CheckTransaction tests
+//
+// Transaction hex below is copied from src/test/data/tx_invalid.json (entries
+// marked "BADTX") and tx_valid.json. CheckTransaction performs only basic context-free
+// consensus checks and can only produce two outcomes:
+// - VALID (ValidationMode::VALID, TxValidationResult::UNSET)
+// - INVALID (ValidationMode::INVALID, TxValidationResult::CONSENSUS)
+// Other TxValidationResult values are set by higher-level validation and are
+// not reachable through btck_transaction_check.
+// -----------------------------------------------------------------------------
+BOOST_AUTO_TEST_CASE(btck_transaction_check_tests)
+{
+ using namespace btck;
+
+ constexpr std::string_view valid_tx_hex{
+ "01000000010001000000000000000000000000000000000000000000000000000000000000"
+ "000000006a473044022067288ea50aa799543a536ff9306f8e1cba05b9c6b10951175b92"
+ "4f96732555ed022026d7b5265f38d21541519e4a1e55044d5b9e17e15cdbaf29ae3792e9"
+ "9e883e7a012103ba8c8b86dea131c22ab967e6dd99bdae8eff7a1f75a2c35f1f944109e3"
+ "fe5e22ffffffff010000000000000000015100000000"};
+ constexpr std::string_view no_outputs_tx_hex{
+ "01000000010001000000000000000000000000000000000000000000000000000000000000"
+ "000000006d483045022100f16703104aab4e4088317c862daec83440242411b039d14280e0"
+ "3dd33b487ab802201318a7be236672c5c56083eb7a5a195bc57a40af7923ff8545016cd3b5"
+ "71e2a601232103c40e5d339df3f30bf753e7e04450ae4ef76c9e45587d1d993bdc4cd06f06"
+ "51c7acffffffff0000000000"};
+
+ auto expect_valid = [](std::string_view hex) {
+ Transaction tx{hex_string_to_byte_vec(hex)};
+ TxValidationState st;
+ BOOST_CHECK(CheckTransaction(tx, st));
+ BOOST_CHECK(st.GetValidationMode() == ValidationMode::VALID);
+ BOOST_CHECK(st.GetTxValidationResult() == TxValidationResult::UNSET);
+ };
+
+ auto expect_invalid = [](std::string_view hex) {
+ Transaction tx{hex_string_to_byte_vec(hex)};
+ TxValidationState st;
+ BOOST_CHECK(!CheckTransaction(tx, st));
+ BOOST_CHECK(st.GetValidationMode() == ValidationMode::INVALID);
+ BOOST_CHECK(st.GetTxValidationResult() == TxValidationResult::CONSENSUS);
+ };
+
+ // Valid: simple 1-in 1-out transaction (from tx_valid.json)
+ expect_valid(valid_tx_hex);
+
+ // Valid coinbase with scriptSig size 2 (from tx_valid.json)
+ expect_valid(
+ "01000000010000000000000000000000000000000000000000000000000000000000000000"
+ "ffffffff025151ffffffff010000000000000000015100000000");
+
+ // No outputs (BADTX from tx_invalid.json)
+ expect_invalid(no_outputs_tx_hex);
+
+ {
+ Transaction valid_tx{hex_string_to_byte_vec(valid_tx_hex)};
+ Transaction invalid_tx{hex_string_to_byte_vec(no_outputs_tx_hex)};
+ TxValidationState state;
+
+ BOOST_CHECK(btck_transaction_check(valid_tx.get(), state.get()) == 1);
+ BOOST_CHECK(state.GetValidationMode() == ValidationMode::VALID);
+ BOOST_CHECK(state.GetTxValidationResult() == TxValidationResult::UNSET);
+
+ BOOST_CHECK(btck_transaction_check(invalid_tx.get(), state.get()) == 0);
+ BOOST_CHECK(state.GetValidationMode() == ValidationMode::INVALID);
+ BOOST_CHECK(state.GetTxValidationResult() == TxValidationResult::CONSENSUS);
+ }
+
+ // Negative output (BADTX)
+ expect_invalid(
+ "01000000010001000000000000000000000000000000000000000000000000000000000000"
+ "000000006d4830450220063222cbb128731fc09de0d7323746539166544d6c1df84d867cce"
+ "a84bcc8903022100bf568e8552844de664cd41648a031554327aa8844af34b4f27397c65b9"
+ "2c04de0123210243ec37dee0e2e053a9c976f43147e79bc7d9dc606ea51010af1ac80db6b0"
+ "69e1acffffffff01ffffffffffffffff015100000000");
+
+ // MAX_MONEY + 1 output (BADTX)
+ expect_invalid(
+ "01000000010001000000000000000000000000000000000000000000000000000000000000"
+ "000000006e493046022100e1eadba00d9296c743cb6ecc703fd9ddc9b3cd12906176a226ae"
+ "4c18d6b00796022100a71aef7d2874deff681ba6080f1b278bac7bb99c61b08a85f4311970"
+ "ffe7f63f012321030c0588dc44d92bdcbf8e72093466766fdc265ead8db64517b0c542275b"
+ "70fffbacffffffff010140075af0750700015100000000");
+
+ // MAX_MONEY output + 1 output: sum exceeds MAX_MONEY (BADTX)
+ expect_invalid(
+ "01000000010001000000000000000000000000000000000000000000000000000000000000"
+ "000000006d483045022027deccc14aa6668e78a8c9da3484fbcd4f9dcc9bb7d1b85146314b"
+ "21b9ae4d86022100d0b43dece8cfb07348de0ca8bc5b86276fa88f7f2138381128b7c36ab2"
+ "e42264012321029bb13463ddd5d2cc05da6e84e37536cb9525703cfd8f43afdb414988987a"
+ "92f6acffffffff020040075af075070001510001000000000000015100000000");
+
+ // Duplicate inputs (BADTX)
+ expect_invalid(
+ "01000000020001000000000000000000000000000000000000000000000000000000000000"
+ "000000006c47304402204bb1197053d0d7799bf1b30cd503c44b58d6240cccbdc85b6fe76d"
+ "087980208f02204beeed78200178ffc6c74237bb74b3f276bbb4098b5605d814304fe128bf"
+ "1431012321039e8815e15952a7c3fada1905f8cf55419837133bd7756c0ef14fc8dfe50c0d"
+ "eaacffffffff0001000000000000000000000000000000000000000000000000000000000000"
+ "000000006c47304402202306489afef52a6f62e90bf750bbcdf40c06f5c6b138286e6b6b8617"
+ "6bb9341802200dba98486ea68380f47ebb19a7df173b99e6bc9c681d6ccf3bde31465d1f16"
+ "b3012321039e8815e15952a7c3fada1905f8cf55419837133bd7756c0ef14fc8dfe50c0dea"
+ "acffffffff010000000000000000015100000000");
+
+ // Coinbase with scriptSig size 1: too small (BADTX)
+ expect_invalid(
+ "01000000010000000000000000000000000000000000000000000000000000000000000000"
+ "ffffffff0151ffffffff010000000000000000015100000000");
+
+ // Coinbase with scriptSig size 101: too large (BADTX)
+ expect_invalid(
+ "01000000010000000000000000000000000000000000000000000000000000000000000000"
+ "ffffffff6551515151515151515151515151515151515151515151515151515151515151515151"
+ "515151515151515151515151515151515151515151515151515151515151515151515151515151"
+ "51515151515151515151515151515151515151515151515151515151ffffffff01000000000000"
+ "0000015100000000");
+
+ // Null prevout in non-coinbase: two inputs, one is null (BADTX)
+ expect_invalid(
+ "01000000020000000000000000000000000000000000000000000000000000000000000000"
+ "ffffffff00ffffffff000100000000000000000000000000000000000000000000000000000000"
+ "00000000000000ffffffff010000000000000000015100000000");
+}
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.