From bfbf1a7ef3ee64a345dc2b2c6cf4f85c3a739fae Mon Sep 17 00:00:00 2001 From: w0xlt <94266259+w0xlt@users.noreply.github.com> Date: Tue, 4 Nov 2025 14:41:17 -0800 Subject: [PATCH] kernel: Expose btck_transaction_check consensus function Add btck_transaction_check() to the libbitcoinkernel C API, exposing context-free transaction consensus validation (consensus/tx_check.h). Introduces btck_TxValidationState with introspection and lifecycle functions. btck_TxValidationResult is exposed for compatibility with existing validation-state APIs, though btck_transaction_check currently reaches only UNSET and CONSENSUS. Includes C++ wrapper and test coverage for btck_transaction_check using test vectors from tx_valid.json / tx_invalid.json. --- src/kernel/bitcoinkernel.cpp | 48 +++++++++++ src/kernel/bitcoinkernel.h | 82 +++++++++++++++++++ src/kernel/bitcoinkernel_wrapper.h | 38 +++++++++ src/test/kernel/test_kernel.cpp | 125 +++++++++++++++++++++++++++++ 4 files changed, 293 insertions(+) diff --git a/src/kernel/bitcoinkernel.cpp b/src/kernel/bitcoinkernel.cpp index f21ea0412b0..aa095cab74f 100644 --- a/src/kernel/bitcoinkernel.cpp +++ b/src/kernel/bitcoinkernel.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -146,6 +147,7 @@ struct Handle { struct btck_BlockTreeEntry: Handle {}; struct btck_Block : Handle> {}; struct btck_BlockValidationState : Handle {}; +struct btck_TxValidationState : Handle {}; 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 d3028729ba2..577c4c0a329 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 2be5e223865..3b3f5009fd5 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(flags), state.get()) == 1; } +class TxValidationState : public UniqueHandle +{ +public: + using UniqueHandle::UniqueHandle; // inherit ctor + explicit TxValidationState() : UniqueHandle{btck_tx_validation_state_create()} {} + + ValidationMode GetValidationMode() const + { + return static_cast(btck_tx_validation_state_get_validation_mode(get())); + } + + TxValidationResult GetTxValidationResult() const + { + return static_cast(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 97b0b91fa7d..0489839daca 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"); +}