Kernel: Add functions for working with outpoints
What changed, and why it matters
This commit adds new public programming interfaces (APIs) to the Bitcoin Core 'kernel' library so that developers can inspect transaction inputs, outpoints, and transaction IDs. It is a feature addition, not a fix for a security bug. The code does not change consensus rules, network behavior, or wallet handling. The new functions are covered by tests that verify they work correctly.
No security action required. Treat as normal feature/API expansion. Reviewers may want to confirm the lifetime/ownership documentation for returned const pointers is accurate and that the new wrappers follow the existing Handle/View patterns consistently.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit introduces opaque handle types btck_TransactionInput, btck_TransactionOutPoint, and btck_Txid plus copy/get/destroy helpers in src/kernel/bitcoinkernel.cpp and declarations in bitcoinkernel.h. It also adds C++ wrapper classes in bitcoinkernel_wrapper.h and unit tests in src/test/kernel/test_kernel.cpp. The test exercises retrieving inputs/outpoints and verifying scripts of every non-coinbase transaction on a regtest chain by scanning prior transactions. No existing behavior is modified; only new read-only accessors are added.
Changed components
src/kernel/bitcoinkernel.cppsrc/kernel/bitcoinkernel.hsrc/kernel/bitcoinkernel_wrapper.hsrc/test/kernel/test_kernel.cppInspect captured patch +417 / −1
diff --git a/src/kernel/bitcoinkernel.cpp b/src/kernel/bitcoinkernel.cpp
index 531df3c8..8bba3cf1 100644
--- a/src/kernel/bitcoinkernel.cpp
+++ b/src/kernel/bitcoinkernel.cpp
@@ -491,6 +491,9 @@ struct btck_BlockSpentOutputs : Handle<btck_BlockSpentOutputs, std::shared_ptr<C
struct btck_TransactionSpentOutputs : Handle<btck_TransactionSpentOutputs, CTxUndo> {};
struct btck_Coin : Handle<btck_Coin, Coin> {};
struct btck_BlockHash : Handle<btck_BlockHash, uint256> {};
+struct btck_TransactionInput : Handle<btck_TransactionInput, CTxIn> {};
+struct btck_TransactionOutPoint: Handle<btck_TransactionOutPoint, COutPoint> {};
+struct btck_Txid: Handle<btck_Txid, Txid> {};
btck_Transaction* btck_transaction_create(const void* raw_transaction, size_t raw_transaction_len)
{
@@ -519,6 +522,17 @@ size_t btck_transaction_count_inputs(const btck_Transaction* transaction)
return btck_Transaction::get(transaction)->vin.size();
}
+const btck_TransactionInput* btck_transaction_get_input_at(const btck_Transaction* transaction, size_t input_index)
+{
+ assert(input_index < btck_Transaction::get(transaction)->vin.size());
+ return btck_TransactionInput::ref(&btck_Transaction::get(transaction)->vin[input_index]);
+}
+
+const btck_Txid* btck_transaction_get_txid(const btck_Transaction* transaction)
+{
+ return btck_Txid::ref(&btck_Transaction::get(transaction)->GetHash());
+}
+
btck_Transaction* btck_transaction_copy(const btck_Transaction* transaction)
{
return btck_Transaction::copy(transaction);
@@ -637,6 +651,60 @@ int btck_script_pubkey_verify(const btck_ScriptPubkey* script_pubkey,
return result ? 1 : 0;
}
+btck_TransactionInput* btck_transaction_input_copy(const btck_TransactionInput* input)
+{
+ return btck_TransactionInput::copy(input);
+}
+
+const btck_TransactionOutPoint* btck_transaction_input_get_out_point(const btck_TransactionInput* input)
+{
+ return btck_TransactionOutPoint::ref(&btck_TransactionInput::get(input).prevout);
+}
+
+void btck_transaction_input_destroy(btck_TransactionInput* input)
+{
+ delete input;
+}
+
+btck_TransactionOutPoint* btck_transaction_out_point_copy(const btck_TransactionOutPoint* out_point)
+{
+ return btck_TransactionOutPoint::copy(out_point);
+}
+
+uint32_t btck_transaction_out_point_get_index(const btck_TransactionOutPoint* out_point)
+{
+ return btck_TransactionOutPoint::get(out_point).n;
+}
+
+const btck_Txid* btck_transaction_out_point_get_txid(const btck_TransactionOutPoint* out_point)
+{
+ return btck_Txid::ref(&btck_TransactionOutPoint::get(out_point).hash);
+}
+
+void btck_transaction_out_point_destroy(btck_TransactionOutPoint* out_point)
+{
+ delete out_point;
+}
+
+btck_Txid* btck_txid_copy(const btck_Txid* txid)
+{
+ return btck_Txid::copy(txid);
+}
+
+void btck_txid_to_bytes(const btck_Txid* txid, unsigned char output[32])
+{
+ std::memcpy(output, btck_Txid::get(txid).begin(), 32);
+}
+
+int btck_txid_equals(const btck_Txid* txid1, const btck_Txid* txid2)
+{
+ return btck_Txid::get(txid1) == btck_Txid::get(txid2);
+}
+
+void btck_txid_destroy(btck_Txid* txid)
+{
+ delete txid;
+}
void btck_logging_set_options(const btck_LoggingOptions options)
{
diff --git a/src/kernel/bitcoinkernel.h b/src/kernel/bitcoinkernel.h
index e9283e3d..14cd8145 100644
--- a/src/kernel/bitcoinkernel.h
+++ b/src/kernel/bitcoinkernel.h
@@ -249,6 +249,22 @@ typedef struct btck_Coin btck_Coin;
*/
typedef struct btck_BlockHash btck_BlockHash;
+/**
+ * Opaque data structure for holding a transaction input.
+ *
+ * Holds information on the @ref btck_TransactionOutPoint held within.
+ */
+typedef struct btck_TransactionInput btck_TransactionInput;
+
+/**
+ * Opaque data structure for holding a transaction out point.
+ *
+ * Holds the txid and output index it is pointing to.
+ */
+typedef struct btck_TransactionOutPoint btck_TransactionOutPoint;
+
+typedef struct btck_Txid btck_Txid;
+
/** Current sync state passed to tip changed callbacks. */
typedef uint8_t btck_SynchronizationState;
#define btck_SynchronizationState_INIT_REINDEX ((btck_SynchronizationState)(0))
@@ -498,6 +514,18 @@ BITCOINKERNEL_API size_t BITCOINKERNEL_WARN_UNUSED_RESULT btck_transaction_count
BITCOINKERNEL_API const btck_TransactionOutput* BITCOINKERNEL_WARN_UNUSED_RESULT btck_transaction_get_output_at(
const btck_Transaction* transaction, size_t output_index) BITCOINKERNEL_ARG_NONNULL(1);
+/**
+ * @brief Get the transaction input at the provided index. The returned
+ * transaction input is not owned and depends on the lifetime of the
+ * transaction.
+ *
+ * @param[in] transaction Non-null.
+ * @param[in] input_index The index of the transaction input to be retrieved.
+ * @return The transaction input
+ */
+BITCOINKERNEL_API const btck_TransactionInput* BITCOINKERNEL_WARN_UNUSED_RESULT btck_transaction_get_input_at(
+ const btck_Transaction* transaction, size_t input_index) BITCOINKERNEL_ARG_NONNULL(1);
+
/**
* @brief Get the number of inputs of a transaction.
*
@@ -507,6 +535,16 @@ BITCOINKERNEL_API const btck_TransactionOutput* BITCOINKERNEL_WARN_UNUSED_RESULT
BITCOINKERNEL_API size_t BITCOINKERNEL_WARN_UNUSED_RESULT btck_transaction_count_inputs(
const btck_Transaction* transaction) BITCOINKERNEL_ARG_NONNULL(1);
+/**
+ * @brief Get the txid of a transaction. The returned txid is not owned and
+ * depends on the lifetime of the transaction.
+ *
+ * @param[in] transaction Non-null.
+ * @return The txid.
+ */
+BITCOINKERNEL_API const btck_Txid* BITCOINKERNEL_WARN_UNUSED_RESULT btck_transaction_get_txid(
+ const btck_Transaction* transaction) BITCOINKERNEL_ARG_NONNULL(1);
+
/**
* Destroy the transaction.
*/
@@ -1313,6 +1351,119 @@ BITCOINKERNEL_API void btck_transaction_spent_outputs_destroy(btck_TransactionSp
///@}
+/** @name Transaction Input
+ * Functions for working with transaction inputs.
+ */
+///@{
+
+/**
+ * @brief Copy a transaction input.
+ *
+ * @param[in] transaction_input Non-null.
+ * @return The copied transaction input.
+ */
+BITCOINKERNEL_API btck_TransactionInput* BITCOINKERNEL_WARN_UNUSED_RESULT btck_transaction_input_copy(
+ const btck_TransactionInput* transaction_input) BITCOINKERNEL_ARG_NONNULL(1);
+
+/**
+ * @brief Get the transaction out point. The returned transaction out point is
+ * not owned and depends on the lifetime of the transaction.
+ *
+ * @param[in] transaction_input Non-null.
+ * @return The transaction out point.
+ */
+BITCOINKERNEL_API const btck_TransactionOutPoint* BITCOINKERNEL_WARN_UNUSED_RESULT btck_transaction_input_get_out_point(
+ const btck_TransactionInput* transaction_input) BITCOINKERNEL_ARG_NONNULL(1);
+
+/**
+ * Destroy the transaction input.
+ */
+BITCOINKERNEL_API void btck_transaction_input_destroy(btck_TransactionInput* transaction_input);
+
+///@}
+
+/** @name Transaction Out Point
+ * Functions for working with transaction out points.
+ */
+///@{
+
+/**
+ * @brief Copy a transaction out point.
+ *
+ * @param[in] transaction_out_point Non-null.
+ * @return The copied transaction out point.
+ */
+BITCOINKERNEL_API btck_TransactionOutPoint* BITCOINKERNEL_WARN_UNUSED_RESULT btck_transaction_out_point_copy(
+ const btck_TransactionOutPoint* transaction_out_point) BITCOINKERNEL_ARG_NONNULL(1);
+
+/**
+ * @brief Get the output position from the transaction out point.
+ *
+ * @param[in] transaction_out_point Non-null.
+ * @return The output index.
+ */
+BITCOINKERNEL_API uint32_t BITCOINKERNEL_WARN_UNUSED_RESULT btck_transaction_out_point_get_index(
+ const btck_TransactionOutPoint* transaction_out_point) BITCOINKERNEL_ARG_NONNULL(1);
+
+/**
+ * @brief Get the txid from the transaction out point. The returned txid is
+ * not owned and depends on the lifetime of the transaction out point.
+ *
+ * @param[in] transaction_out_point Non-null.
+ * @return The txid.
+ */
+BITCOINKERNEL_API const btck_Txid* BITCOINKERNEL_WARN_UNUSED_RESULT btck_transaction_out_point_get_txid(
+ const btck_TransactionOutPoint* transaction_out_point) BITCOINKERNEL_ARG_NONNULL(1);
+
+/**
+ * Destroy the transaction out point.
+ */
+BITCOINKERNEL_API void btck_transaction_out_point_destroy(btck_TransactionOutPoint* transaction_out_point);
+
+///@}
+
+/** @name Txid
+ * Functions for working with txids.
+ */
+///@{
+
+/**
+ * @brief Copy a txid.
+ *
+ * @param[in] txid Non-null.
+ * @return The copied txid.
+ */
+BITCOINKERNEL_API btck_Txid* BITCOINKERNEL_WARN_UNUSED_RESULT btck_txid_copy(
+ const btck_Txid* txid) BITCOINKERNEL_ARG_NONNULL(1);
+
+/**
+ * @brief Check if two txids are equal.
+ *
+ * @param[in] txid1 Non-null.
+ * @param[in] txid2 Non-null.
+ * @return 0 if the txid is not equal.
+ */
+BITCOINKERNEL_API int BITCOINKERNEL_WARN_UNUSED_RESULT btck_txid_equals(
+ const btck_Txid* txid1, const btck_Txid* txid2) BITCOINKERNEL_ARG_NONNULL(1, 2);
+
+/**
+ * @brief Serializes the txid to bytes.
+ *
+ * @param[in] txid Non-null.
+ * @param[out] output The serialized txid.
+ */
+BITCOINKERNEL_API void btck_txid_to_bytes(
+ const btck_Txid* txid, unsigned char output[32]) BITCOINKERNEL_ARG_NONNULL(1, 2);
+
+/**
+ * Destroy the txid.
+ */
+BITCOINKERNEL_API void btck_txid_destroy(btck_Txid* txid);
+
+///@}
+
+///@}
+
/** @name Coin
* Functions for working with coins.
*/
@@ -1328,7 +1479,8 @@ BITCOINKERNEL_API btck_Coin* BITCOINKERNEL_WARN_UNUSED_RESULT btck_coin_copy(
const btck_Coin* coin) BITCOINKERNEL_ARG_NONNULL(1);
/**
- * @brief Returns the height of the block that contains the coin's prevout.
+ * @brief Returns the block height where the transaction that
+ * created this coin was included in.
*
* @param[in] coin Non-null.
* @return The block height of the coin.
diff --git a/src/kernel/bitcoinkernel_wrapper.h b/src/kernel/bitcoinkernel_wrapper.h
index d06878fa..06a4ccfa 100644
--- a/src/kernel/bitcoinkernel_wrapper.h
+++ b/src/kernel/bitcoinkernel_wrapper.h
@@ -451,6 +451,119 @@ public:
: Handle(view) {}
};
+template <typename Derived>
+class TxidApi
+{
+private:
+ auto impl() const
+ {
+ return static_cast<const Derived*>(this)->get();
+ }
+
+ friend Derived;
+ TxidApi() = default;
+
+public:
+ bool operator==(const TxidApi& other) const
+ {
+ return btck_txid_equals(impl(), other.impl()) != 0;
+ }
+
+ bool operator!=(const TxidApi& other) const
+ {
+ return btck_txid_equals(impl(), other.impl()) == 0;
+ }
+
+ std::array<std::byte, 32> ToBytes() const
+ {
+ std::array<std::byte, 32> hash;
+ btck_txid_to_bytes(impl(), reinterpret_cast<unsigned char*>(hash.data()));
+ return hash;
+ }
+};
+
+class TxidView : public View<btck_Txid>, public TxidApi<TxidView>
+{
+public:
+ explicit TxidView(const btck_Txid* ptr) : View{ptr} {}
+};
+
+class Txid : public Handle<btck_Txid, btck_txid_copy, btck_txid_destroy>, public TxidApi<Txid>
+{
+public:
+ Txid(const TxidView& view)
+ : Handle(view) {}
+};
+
+template <typename Derived>
+class OutPointApi
+{
+private:
+ auto impl() const
+ {
+ return static_cast<const Derived*>(this)->get();
+ }
+
+ friend Derived;
+ OutPointApi() = default;
+
+public:
+ uint32_t index() const
+ {
+ return btck_transaction_out_point_get_index(impl());
+ }
+
+ TxidView Txid() const
+ {
+ return TxidView{btck_transaction_out_point_get_txid(impl())};
+ }
+};
+
+class OutPointView : public View<btck_TransactionOutPoint>, public OutPointApi<OutPointView>
+{
+public:
+ explicit OutPointView(const btck_TransactionOutPoint* ptr) : View{ptr} {}
+};
+
+class OutPoint : public Handle<btck_TransactionOutPoint, btck_transaction_out_point_copy, btck_transaction_out_point_destroy>, public OutPointApi<OutPoint>
+{
+public:
+ OutPoint(const OutPointView& view)
+ : Handle(view) {}
+};
+
+template <typename Derived>
+class TransactionInputApi
+{
+private:
+ auto impl() const
+ {
+ return static_cast<const Derived*>(this)->get();
+ }
+
+ friend Derived;
+ TransactionInputApi() = default;
+
+public:
+ OutPointView OutPoint() const
+ {
+ return OutPointView{btck_transaction_input_get_out_point(impl())};
+ }
+};
+
+class TransactionInputView : public View<btck_TransactionInput>, public TransactionInputApi<TransactionInputView>
+{
+public:
+ explicit TransactionInputView(const btck_TransactionInput* ptr) : View{ptr} {}
+};
+
+class TransactionInput : public Handle<btck_TransactionInput, btck_transaction_input_copy, btck_transaction_input_destroy>, public TransactionInputApi<TransactionInput>
+{
+public:
+ TransactionInput(const TransactionInputView& view)
+ : Handle(view) {}
+};
+
template <typename Derived>
class TransactionApi
{
@@ -476,8 +589,20 @@ public:
return TransactionOutputView{btck_transaction_get_output_at(impl(), index)};
}
+ TransactionInputView GetInput(size_t index) const
+ {
+ return TransactionInputView{btck_transaction_get_input_at(impl(), index)};
+ }
+
+ TxidView Txid() const
+ {
+ return TxidView{btck_transaction_get_txid(impl())};
+ }
+
MAKE_RANGE_METHOD(Outputs, Derived, &TransactionApi<Derived>::CountOutputs, &TransactionApi<Derived>::GetOutput, *static_cast<const Derived*>(this))
+ MAKE_RANGE_METHOD(Inputs, Derived, &TransactionApi<Derived>::CountInputs, &TransactionApi<Derived>::GetInput, *static_cast<const Derived*>(this))
+
std::vector<std::byte> ToBytes() const
{
return write_bytes(impl(), btck_transaction_to_bytes);
diff --git a/src/test/kernel/test_kernel.cpp b/src/test/kernel/test_kernel.cpp
index fca76f4f..d9875ee1 100644
--- a/src/test/kernel/test_kernel.cpp
+++ b/src/test/kernel/test_kernel.cpp
@@ -14,6 +14,7 @@
#include <cstdint>
#include <cstdlib>
#include <filesystem>
+#include <format>
#include <iostream>
#include <memory>
#include <optional>
@@ -61,6 +62,19 @@ std::vector<std::byte> hex_string_to_byte_vec(std::string_view hex)
return bytes;
}
+std::string byte_span_to_hex_string_reversed(std::span<const std::byte> bytes)
+{
+ std::ostringstream oss;
+
+ // Iterate in reverse order
+ for (auto it = bytes.rbegin(); it != bytes.rend(); ++it) {
+ oss << std::hex << std::setw(2) << std::setfill('0')
+ << static_cast<unsigned int>(static_cast<uint8_t>(*it));
+ }
+
+ return oss.str();
+}
+
constexpr auto VERIFY_ALL_PRE_SEGWIT{ScriptVerificationFlags::P2SH | ScriptVerificationFlags::DERSIG |
ScriptVerificationFlags::NULLDUMMY | ScriptVerificationFlags::CHECKLOCKTIMEVERIFY |
ScriptVerificationFlags::CHECKSEQUENCEVERIFY};
@@ -465,6 +479,18 @@ BOOST_AUTO_TEST_CASE(btck_transaction_output)
CheckHandle(output, output2);
}
+BOOST_AUTO_TEST_CASE(btck_transaction_input)
+{
+ Transaction tx{hex_string_to_byte_vec("020000000248c03e66fd371c7033196ce24298628e59ebefa00363026044e0f35e0325a65d000000006a473044022004893432347f39beaa280e99da595681ddb20fc45010176897e6e055d716dbfa022040a9e46648a5d10c33ef7cee5e6cf4b56bd513eae3ae044f0039824b02d0f44c012102982331a52822fd9b62e9b5d120da1d248558fac3da3a3c51cd7d9c8ad3da760efeffffffb856678c6e4c3c84e39e2ca818807049d6fba274b42af3c6d3f9d4b6513212d2000000006a473044022068bcedc7fe39c9f21ad318df2c2da62c2dc9522a89c28c8420ff9d03d2e6bf7b0220132afd752754e5cb1ea2fd0ed6a38ec666781e34b0e93dc9a08f2457842cf5660121033aeb9c079ea3e08ea03556182ab520ce5c22e6b0cb95cee6435ee17144d860cdfeffffff0260d50b00000000001976a914363cc8d55ea8d0500de728ef6d63804ddddbdc9888ac67040f00000000001976a914c303bdc5064bf9c9a8b507b5496bd0987285707988ac6acb0700")};
+ TransactionInput input_0 = tx.GetInput(0);
+ TransactionInput input_1 = tx.GetInput(1);
+ CheckHandle(input_0, input_1);
+ CheckRange(tx.Inputs(), tx.CountInputs());
+ OutPoint point_0 = input_0.OutPoint();
+ OutPoint point_1 = input_1.OutPoint();
+ CheckHandle(point_0, point_1);
+}
+
BOOST_AUTO_TEST_CASE(btck_script_verify_tests)
{
// Legacy transaction aca326a724eda9a461c10a876534ecd5ae7b27f10f26c3862fb996f80ea2d45d
@@ -702,6 +728,7 @@ void chainman_mainnet_validation_test(TestDirectory& test_directory)
auto raw_block = hex_string_to_byte_vec("010000006fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000982051fd1e4ba744bbbe680e1fee14677ba1a3c3540bf7b1cdb606e857233e0e61bc6649ffff001d01e362990101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000");
Block block{raw_block};
TransactionView tx{block.GetTransaction(block.CountTransactions() - 1)};
+ BOOST_CHECK_EQUAL(byte_span_to_hex_string_reversed(tx.Txid().ToBytes()), "0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098");
BOOST_CHECK_EQUAL(tx.CountInputs(), 1);
Transaction tx2 = tx;
BOOST_CHECK_EQUAL(tx2.CountInputs(), 1);
@@ -832,6 +859,50 @@ BOOST_AUTO_TEST_CASE(btck_chainman_regtest_tests)
auto read_block_2 = chainman->ReadBlock(tip_2).value();
check_equal(read_block_2.ToBytes(), hex_string_to_byte_vec(REGTEST_BLOCK_DATA[REGTEST_BLOCK_DATA.size() - 2]));
+ Txid txid = read_block.Transactions()[0].Txid();
+ Txid txid_2 = read_block_2.Transactions()[0].Txid();
+ BOOST_CHECK(txid != txid_2);
+ BOOST_CHECK(txid == txid);
+ CheckHandle(txid, txid_2);
+
+ auto find_transaction = [&chainman](const TxidView& target_txid) -> std::optional<Transaction> {
+ auto chain = chainman->GetChain();
+ for (const auto block_tree_entry : chain.Entries()) {
+ auto block{chainman->ReadBlock(block_tree_entry)};
+ for (const TransactionView transaction : block->Transactions()) {
+ if (transaction.Txid() == target_txid) {
+ return Transaction{transaction};
+ }
+ }
+ }
+ return std::nullopt;
+ };
+
+ for (const auto block_tree_entry : chain.Entries()) {
+ auto block{chainman->ReadBlock(block_tree_entry)};
+ for (const auto transaction : block->Transactions()) {
+ std::vector<TransactionInput> inputs;
+ std::vector<TransactionOutput> spent_outputs;
+ for (const auto input : transaction.Inputs()) {
+ OutPointView point = input.OutPoint();
+ if (point.index() == std::numeric_limits<uint32_t>::max()) {
+ continue;
+ }
+ inputs.emplace_back(input);
+ BOOST_CHECK(point.Txid() != transaction.Txid());
+ std::optional<Transaction> tx = find_transaction(point.Txid());
+ BOOST_CHECK(tx.has_value());
+ BOOST_CHECK(point.Txid() == tx->Txid());
+ spent_outputs.emplace_back(tx->GetOutput(point.index()));
+ }
+ BOOST_CHECK(inputs.size() == spent_outputs.size());
+ ScriptVerifyStatus status = ScriptVerifyStatus::OK;
+ for (size_t i{0}; i < inputs.size(); ++i) {
+ BOOST_CHECK(spent_outputs[i].GetScriptPubkey().Verify(spent_outputs[i].Amount(), transaction, spent_outputs, i, ScriptVerificationFlags::ALL, status));
+ }
+ }
+ }
+
// Read spent outputs for current tip and its previous block
BlockSpentOutputs block_spent_outputs{chainman->ReadBlockSpentOutputs(tip)};
BlockSpentOutputs block_spent_outputs_prev{chainman->ReadBlockSpentOutputs(*tip.GetPrevious())};
Why this scored 15/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.