From 7918a4d17d59eef5695f9cb8542bde3e3f6d24c9 Mon Sep 17 00:00:00 2001 From: TheCharlatan Date: Fri, 19 Sep 2025 22:14:54 +0200 Subject: [PATCH] Kernel: Add functions for working with outpoints This introduces the transaction outpoint, input and id types. This now allows a user to retrieve a transaction output from a prior transaction that a transaction outpoint is pointing to. This is exercised in the tests by verifying the script of every transaction in the test chain. --- src/kernel/bitcoinkernel.cpp | 69 +++++++++++++ src/kernel/bitcoinkernel.h | 149 +++++++++++++++++++++++++++++ src/kernel/bitcoinkernel_wrapper.h | 128 +++++++++++++++++++++++++ src/test/kernel/test_kernel.cpp | 69 +++++++++++++ 4 files changed, 415 insertions(+) diff --git a/src/kernel/bitcoinkernel.cpp b/src/kernel/bitcoinkernel.cpp index 77e46eb03f4..32e81708888 100644 --- a/src/kernel/bitcoinkernel.cpp +++ b/src/kernel/bitcoinkernel.cpp @@ -498,6 +498,9 @@ struct btck_BlockSpentOutputs : Handle {}; struct btck_Coin : Handle {}; struct btck_BlockHash : Handle {}; +struct btck_TransactionInput : Handle {}; +struct btck_TransactionOutPoint: Handle {}; +struct btck_Txid: Handle {}; btck_Transaction* btck_transaction_create(const void* raw_transaction, size_t raw_transaction_len) { @@ -526,6 +529,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); @@ -642,6 +656,61 @@ 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_level_category(btck_LogCategory category, btck_LogLevel level) { LOCK(cs_main); diff --git a/src/kernel/bitcoinkernel.h b/src/kernel/bitcoinkernel.h index 6a8196f8725..4bbd44d5fce 100644 --- a/src/kernel/bitcoinkernel.h +++ b/src/kernel/bitcoinkernel.h @@ -248,6 +248,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 transaction id 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)) @@ -493,6 +509,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. * @@ -502,6 +530,15 @@ 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. + * + * @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. */ @@ -1292,6 +1329,118 @@ 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 out point. + * + * @param[in] transaction_out_point Non-null. + * @return The output index. + */ +BITCOINKERNEL_API uint32_t btck_transaction_out_point_get_index( + const btck_TransactionOutPoint* transaction_out_point) BITCOINKERNEL_ARG_NONNULL(1); + +/** + * @brief Get the txid from the out point. + * + * @param[in] transaction_out_point Non-null. + * @return The txid. + */ +BITCOINKERNEL_API const btck_Txid* 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 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. */ diff --git a/src/kernel/bitcoinkernel_wrapper.h b/src/kernel/bitcoinkernel_wrapper.h index 749bed2e95c..ac6fcc0de1f 100644 --- a/src/kernel/bitcoinkernel_wrapper.h +++ b/src/kernel/bitcoinkernel_wrapper.h @@ -447,6 +447,119 @@ public: : Handle(view) {} }; +template +class TxidApi +{ +private: + auto impl() const + { + return static_cast(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 ToBytes() const + { + std::array hash; + btck_txid_to_bytes(impl(), reinterpret_cast(hash.data())); + return hash; + } +}; + +class TxidView : public View, public TxidApi +{ +public: + explicit TxidView(const btck_Txid* ptr) : View{ptr} {} +}; + +class Txid : public Handle, public TxidApi +{ +public: + Txid(const TxidView& view) + : Handle(view) {} +}; + +template +class OutPointApi +{ +private: + auto impl() const + { + return static_cast(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, public OutPointApi +{ +public: + explicit OutPointView(const btck_TransactionOutPoint* ptr) : View{ptr} {} +}; + +class OutPoint : public Handle, public OutPointApi +{ +public: + OutPoint(const OutPointView& view) + : Handle(view) {} +}; + +template +class TransactionInputApi +{ +private: + auto impl() const + { + return static_cast(this)->get(); + } + + friend Derived; + TransactionInputApi() = default; + +public: + OutPointView OutPoint() const + { + return OutPointView{btck_transaction_input_get_out_point(impl())}; + } +}; + +class TransactionInputView : public View, public TransactionInputApi +{ +public: + explicit TransactionInputView(const btck_TransactionInput* ptr) : View{ptr} {} +}; + +class TransactionInput : public Handle, public TransactionInputApi +{ +public: + TransactionInput(const TransactionInputView& view) + : Handle(view) {} +}; + template class TransactionApi { @@ -472,11 +585,26 @@ 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())}; + } + auto Outputs() const { return Range::CountOutputs, &TransactionApi::GetOutput>{*static_cast(this)}; } + auto Inputs() const + { + return Range::CountInputs, &TransactionApi::GetInput>{*static_cast(this)}; + } + std::vector 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 07b5e5b759a..9d7cbbf0eb8 100644 --- a/src/test/kernel/test_kernel.cpp +++ b/src/test/kernel/test_kernel.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -61,6 +62,19 @@ std::vector hex_string_to_byte_vec(std::string_view hex) return bytes; } +std::string byte_span_to_hex_string_reversed(std::span 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(static_cast(*it)); + } + + return oss.str(); +} + constexpr auto VERIFY_ALL_PRE_SEGWIT{ScriptVerificationFlags::P2SH | ScriptVerificationFlags::DERSIG | ScriptVerificationFlags::NULLDUMMY | ScriptVerificationFlags::CHECKLOCKTIMEVERIFY | ScriptVerificationFlags::CHECKSEQUENCEVERIFY}; @@ -476,6 +490,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 @@ -731,6 +757,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); @@ -869,6 +896,48 @@ BOOST_AUTO_TEST_CASE(btck_chainman_regtest_tests) auto read_block_2 = chainman->ReadBlock(tip_2).value(); check_equal(read_block_2.ToBytes(), as_bytes(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 { + for (const auto block_tree_entry : chainman->GetChain().Entries()) { + auto block{chainman->ReadBlock(block_tree_entry)}; + for (const auto transaction : block->Transactions()) { + if (transaction.Txid() == target_txid) { + return transaction; + } + } + } + return std::nullopt; + }; + + for (const auto block_tree_entry : chainman->GetChain().Entries()) { + auto block{chainman->ReadBlock(block_tree_entry)}; + for (const auto transaction : block->Transactions()) { + std::vector inputs; + std::vector spent_outputs; + for (const auto input : transaction.Inputs()) { + OutPointView point = input.OutPoint(); + if (point.index() == std::numeric_limits::max()) { + continue; + } + inputs.emplace_back(input); + BOOST_CHECK(point.Txid() != transaction.Txid()); + Transaction tx = *find_transaction(point.Txid()); + 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)); + } + } + } + BlockSpentOutputs block_spent_outputs{chainman->ReadBlockSpentOutputs(tip)}; BlockSpentOutputs block_spent_outputs_prev{chainman->ReadBlockSpentOutputs(*tip.GetPrevious())}; CheckHandle(block_spent_outputs, block_spent_outputs_prev);