From f5d5d1213cc4f4ef8bfe335736c665ed7bc3137d Mon Sep 17 00:00:00 2001 From: TheCharlatan Date: Sat, 1 Jun 2024 14:47:56 +0200 Subject: [PATCH] kernel: Add function to read block undo data from disk to C header This adds functions for reading the undo data from disk with a retrieved block tree entry. The undo data of a block contains all the spent script pubkeys of all the transactions in a block. For ease of understanding the undo data is renamed to spent outputs with seperate data structures exposed for a block's and a transaction's spent outputs. In normal operations undo data is used during re-orgs. This data might also be useful for building external indexes, or to scan for silent payment transactions. Internally the block undo data contains a vector of transaction undo data which contains a vector of the coins consumed. The coins are all int the order of the transaction inputs of the consuming transactions. Each coin can be used to retrieve a transaction output and in turn a script pubkey and amount. This translates to the three-level hierarchy the api provides: Block spent outputs contain transaction spent outputs, which contain individual coins. Each coin includes the associated output, the height of the block is contained in, and whether it is from a coinbase transaction. --- src/kernel/bitcoinkernel.cpp | 88 ++++++++++++++ src/kernel/bitcoinkernel.h | 180 +++++++++++++++++++++++++++++ src/kernel/bitcoinkernel_wrapper.h | 104 +++++++++++++++++ src/test/kernel/test_kernel.cpp | 52 ++++++++- 4 files changed, 420 insertions(+), 4 deletions(-) diff --git a/src/kernel/bitcoinkernel.cpp b/src/kernel/bitcoinkernel.cpp index 21193ce4948..b9c0acc8b4d 100644 --- a/src/kernel/bitcoinkernel.cpp +++ b/src/kernel/bitcoinkernel.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -28,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -485,6 +487,9 @@ struct btck_ChainParameters : Handle {}; struct btck_ChainstateManagerOptions : Handle {}; struct btck_ChainstateManager : Handle {}; struct btck_Chain : Handle {}; +struct btck_BlockSpentOutputs : Handle> {}; +struct btck_TransactionSpentOutputs : Handle {}; +struct btck_Coin : Handle {}; btck_Transaction* btck_transaction_create(const void* raw_transaction, size_t raw_transaction_len) { @@ -1004,6 +1009,89 @@ btck_Block* btck_block_read(const btck_ChainstateManager* chainman, const btck_B return btck_Block::create(block); } +btck_BlockSpentOutputs* btck_block_spent_outputs_read(const btck_ChainstateManager* chainman, const btck_BlockTreeEntry* entry) +{ + auto block_undo{std::make_shared()}; + if (btck_BlockTreeEntry::get(entry).nHeight < 1) { + LogDebug(BCLog::KERNEL, "The genesis block does not have any spent outputs."); + return btck_BlockSpentOutputs::create(block_undo); + } + if (!btck_ChainstateManager::get(chainman).m_chainman->m_blockman.ReadBlockUndo(*block_undo, btck_BlockTreeEntry::get(entry))) { + LogError("Failed to read block spent outputs data."); + return nullptr; + } + return btck_BlockSpentOutputs::create(block_undo); +} + +btck_BlockSpentOutputs* btck_block_spent_outputs_copy(const btck_BlockSpentOutputs* block_spent_outputs) +{ + return btck_BlockSpentOutputs::copy(block_spent_outputs); +} + +size_t btck_block_spent_outputs_count(const btck_BlockSpentOutputs* block_spent_outputs) +{ + return btck_BlockSpentOutputs::get(block_spent_outputs)->vtxundo.size(); +} + +const btck_TransactionSpentOutputs* btck_block_spent_outputs_get_transaction_spent_outputs_at(const btck_BlockSpentOutputs* block_spent_outputs, size_t transaction_index) +{ + assert(transaction_index < btck_BlockSpentOutputs::get(block_spent_outputs)->vtxundo.size()); + const auto* tx_undo{&btck_BlockSpentOutputs::get(block_spent_outputs)->vtxundo.at(transaction_index)}; + return btck_TransactionSpentOutputs::ref(tx_undo); +} + +void btck_block_spent_outputs_destroy(btck_BlockSpentOutputs* block_spent_outputs) +{ + delete block_spent_outputs; +} + +btck_TransactionSpentOutputs* btck_transaction_spent_outputs_copy(const btck_TransactionSpentOutputs* transaction_spent_outputs) +{ + return btck_TransactionSpentOutputs::copy(transaction_spent_outputs); +} + +size_t btck_transaction_spent_outputs_count(const btck_TransactionSpentOutputs* transaction_spent_outputs) +{ + return btck_TransactionSpentOutputs::get(transaction_spent_outputs).vprevout.size(); +} + +void btck_transaction_spent_outputs_destroy(btck_TransactionSpentOutputs* transaction_spent_outputs) +{ + delete transaction_spent_outputs; +} + +const btck_Coin* btck_transaction_spent_outputs_get_coin_at(const btck_TransactionSpentOutputs* transaction_spent_outputs, size_t coin_index) +{ + assert(coin_index < btck_TransactionSpentOutputs::get(transaction_spent_outputs).vprevout.size()); + const Coin* coin{&btck_TransactionSpentOutputs::get(transaction_spent_outputs).vprevout.at(coin_index)}; + return btck_Coin::ref(coin); +} + +btck_Coin* btck_coin_copy(const btck_Coin* coin) +{ + return btck_Coin::copy(coin); +} + +uint32_t btck_coin_confirmation_height(const btck_Coin* coin) +{ + return btck_Coin::get(coin).nHeight; +} + +int btck_coin_is_coinbase(const btck_Coin* coin) +{ + return btck_Coin::get(coin).IsCoinBase() ? 1 : 0; +} + +const btck_TransactionOutput* btck_coin_get_output(const btck_Coin* coin) +{ + return btck_TransactionOutput::ref(&btck_Coin::get(coin).out); +} + +void btck_coin_destroy(btck_Coin* coin) +{ + delete coin; +} + int btck_chainstate_manager_process_block( btck_ChainstateManager* chainman, const btck_Block* block, diff --git a/src/kernel/bitcoinkernel.h b/src/kernel/bitcoinkernel.h index e86945147ad..b6cfb0dbd3e 100644 --- a/src/kernel/bitcoinkernel.h +++ b/src/kernel/bitcoinkernel.h @@ -212,6 +212,36 @@ typedef struct btck_BlockValidationState btck_BlockValidationState; */ typedef struct btck_Chain btck_Chain; +/** + * Opaque data structure for holding a block's spent outputs. + * + * Contains all the previous outputs consumed by all transactions in a specific + * block. Internally it holds a nested vector. The top level vector has an + * entry for each transaction in a block (in order of the actual transactions + * of the block and without the coinbase transaction). This is exposed through + * @ref btck_TransactionSpentOutputs. Each btck_TransactionSpentOutputs is in + * turn a vector of all the previous outputs of a transaction (in order of + * their corresponding inputs). + */ +typedef struct btck_BlockSpentOutputs btck_BlockSpentOutputs; + +/** + * Opaque data structure for holding a transaction's spent outputs. + * + * Holds the coins consumed by a certain transaction. Retrieved through the + * @ref btck_BlockSpentOutputs. The coins are in the same order as the + * transaction's inputs consuming them. + */ +typedef struct btck_TransactionSpentOutputs btck_TransactionSpentOutputs; + +/** + * Opaque data structure for holding a coin. + * + * Holds information on the @ref btck_TransactionOutput held within, + * including the height it was spent at and whether it is a coinbase output. + */ +typedef struct btck_Coin btck_Coin; + /** Current sync state passed to tip changed callbacks. */ typedef uint8_t btck_SynchronizationState; #define btck_SynchronizationState_INIT_REINDEX ((btck_SynchronizationState)(0)) @@ -1102,6 +1132,156 @@ BITCOINKERNEL_API int32_t BITCOINKERNEL_WARN_UNUSED_RESULT btck_chain_get_height ///@} +/** @name BlockSpentOutputs + * Functions for working with block spent outputs. + */ +///@{ + +/** + * @brief Reads the block spent coins data the passed in block tree entry points to from + * disk and returns it. + * + * @param[in] chainstate_manager Non-null. + * @param[in] block_tree_entry Non-null. + * @return The read out block spent outputs, or null on error. + */ +BITCOINKERNEL_API btck_BlockSpentOutputs* BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_spent_outputs_read( + const btck_ChainstateManager* chainstate_manager, + const btck_BlockTreeEntry* block_tree_entry) BITCOINKERNEL_ARG_NONNULL(1, 2); + +/** + * @brief Copy a block's spent outputs. + * + * @param[in] block_spent_outputs Non-null. + * @return The copied block spent outputs. + */ +BITCOINKERNEL_API btck_BlockSpentOutputs* BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_spent_outputs_copy( + const btck_BlockSpentOutputs* block_spent_outputs) BITCOINKERNEL_ARG_NONNULL(1); + +/** + * @brief Returns the number of transaction spent outputs whose data is contained in + * block spent outputs. + * + * @param[in] block_spent_outputs Non-null. + * @return The number of transaction spent outputs data in the block spent outputs. + */ +BITCOINKERNEL_API size_t BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_spent_outputs_count( + const btck_BlockSpentOutputs* block_spent_outputs) BITCOINKERNEL_ARG_NONNULL(1); + +/** + * @brief Returns a transaction spent outputs contained in the block spent + * outputs at a certain index. The returned pointer is unowned and only valid + * for the lifetime of block_spent_outputs. + * + * @param[in] block_spent_outputs Non-null. + * @param[in] transaction_spent_outputs_index The index of the transaction spent outputs within the block spent outputs. + * @return A transaction spent outputs pointer. + */ +BITCOINKERNEL_API const btck_TransactionSpentOutputs* BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_spent_outputs_get_transaction_spent_outputs_at( + const btck_BlockSpentOutputs* block_spent_outputs, + size_t transaction_spent_outputs_index) BITCOINKERNEL_ARG_NONNULL(1); + +/** + * Destroy the block spent outputs. + */ +BITCOINKERNEL_API void btck_block_spent_outputs_destroy(btck_BlockSpentOutputs* block_spent_outputs); + +///@} + +/** @name TransactionSpentOutputs + * Functions for working with the spent coins of a transaction + */ +///@{ + +/** + * @brief Copy a transaction's spent outputs. + * + * @param[in] transaction_spent_outputs Non-null. + * @return The copied transaction spent outputs. + */ +BITCOINKERNEL_API btck_TransactionSpentOutputs* BITCOINKERNEL_WARN_UNUSED_RESULT btck_transaction_spent_outputs_copy( + const btck_TransactionSpentOutputs* transaction_spent_outputs) BITCOINKERNEL_ARG_NONNULL(1); + +/** + * @brief Returns the number of previous transaction outputs contained in the + * transaction spent outputs data. + * + * @param[in] transaction_spent_outputs Non-null + * @return The number of spent transaction outputs for the transaction. + */ +BITCOINKERNEL_API size_t BITCOINKERNEL_WARN_UNUSED_RESULT btck_transaction_spent_outputs_count( + const btck_TransactionSpentOutputs* transaction_spent_outputs) BITCOINKERNEL_ARG_NONNULL(1); + +/** + * @brief Returns a coin contained in the transaction spent outputs at a + * certain index. The returned pointer is unowned and only valid for the + * lifetime of transaction_spent_outputs. + * + * @param[in] transaction_spent_outputs Non-null. + * @param[in] coin_index The index of the to be retrieved coin within the + * transaction spent outputs. + * @return A coin pointer. + */ +BITCOINKERNEL_API const btck_Coin* BITCOINKERNEL_WARN_UNUSED_RESULT btck_transaction_spent_outputs_get_coin_at( + const btck_TransactionSpentOutputs* transaction_spent_outputs, + size_t coin_index) BITCOINKERNEL_ARG_NONNULL(1); + +/** + * Destroy the transaction spent outputs. + */ +BITCOINKERNEL_API void btck_transaction_spent_outputs_destroy(btck_TransactionSpentOutputs* transaction_spent_outputs); + +///@} + +/** @name Coin + * Functions for working with coins. + */ +///@{ + +/** + * @brief Copy a coin. + * + * @param[in] coin Non-null. + * @return The copied coin. + */ +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. + * + * @param[in] coin Non-null. + * @return The block height of the coin. + */ +BITCOINKERNEL_API uint32_t BITCOINKERNEL_WARN_UNUSED_RESULT btck_coin_confirmation_height( + const btck_Coin* coin) BITCOINKERNEL_ARG_NONNULL(1); + +/** + * @brief Returns whether the containing transaction was a coinbase. + * + * @param[in] coin Non-null. + * @return 1 if the coin is a coinbase coin, 0 otherwise. + */ +BITCOINKERNEL_API int BITCOINKERNEL_WARN_UNUSED_RESULT btck_coin_is_coinbase( + const btck_Coin* coin) BITCOINKERNEL_ARG_NONNULL(1); + +/** + * @brief Return the transaction output of a coin. The returned pointer is + * unowned and only valid for the lifetime of the coin. + * + * @param[in] coin Non-null. + * @return A transaction output pointer. + */ +BITCOINKERNEL_API const btck_TransactionOutput* BITCOINKERNEL_WARN_UNUSED_RESULT btck_coin_get_output( + const btck_Coin* coin) BITCOINKERNEL_ARG_NONNULL(1); + +/** + * Destroy the coin. + */ +BITCOINKERNEL_API void btck_coin_destroy(btck_Coin* coin); + +///@} + #ifdef __cplusplus } // extern "C" #endif // __cplusplus diff --git a/src/kernel/bitcoinkernel_wrapper.h b/src/kernel/bitcoinkernel_wrapper.h index 983a5e0737e..d9cacab8428 100644 --- a/src/kernel/bitcoinkernel_wrapper.h +++ b/src/kernel/bitcoinkernel_wrapper.h @@ -790,6 +790,105 @@ public: } }; +template +class CoinApi +{ +private: + auto impl() const + { + return static_cast(this)->get(); + } + + friend Derived; + CoinApi() = default; + +public: + uint32_t GetConfirmationHeight() const { return btck_coin_confirmation_height(impl()); } + + bool IsCoinbase() const { return btck_coin_is_coinbase(impl()) == 1; } + + TransactionOutputView GetOutput() const + { + return TransactionOutputView{btck_coin_get_output(impl())}; + } +}; + +class CoinView : public View, public CoinApi +{ +public: + explicit CoinView(const btck_Coin* ptr) : View{ptr} {} +}; + +class Coin : public Handle, public CoinApi +{ +public: + Coin(btck_Coin* coin) : Handle{coin} {} + + Coin(const CoinView& view) : Handle{view} {} +}; + +template +class TransactionSpentOutputsApi +{ +private: + auto impl() const + { + return static_cast(this)->get(); + } + + friend Derived; + TransactionSpentOutputsApi() = default; + +public: + size_t Count() const + { + return btck_transaction_spent_outputs_count(impl()); + } + + CoinView GetCoin(size_t index) const + { + return CoinView{btck_transaction_spent_outputs_get_coin_at(impl(), index)}; + } + + MAKE_RANGE_METHOD(Coins, Derived, &TransactionSpentOutputsApi::Count, &TransactionSpentOutputsApi::GetCoin, *static_cast(this)) +}; + +class TransactionSpentOutputsView : public View, public TransactionSpentOutputsApi +{ +public: + explicit TransactionSpentOutputsView(const btck_TransactionSpentOutputs* ptr) : View{ptr} {} +}; + +class TransactionSpentOutputs : public Handle, + public TransactionSpentOutputsApi +{ +public: + TransactionSpentOutputs(btck_TransactionSpentOutputs* transaction_spent_outputs) : Handle{transaction_spent_outputs} {} + + TransactionSpentOutputs(const TransactionSpentOutputsView& view) : Handle{view} {} +}; + +class BlockSpentOutputs : public Handle +{ +public: + BlockSpentOutputs(btck_BlockSpentOutputs* block_spent_outputs) + : Handle{block_spent_outputs} + { + } + + size_t Count() const + { + return btck_block_spent_outputs_count(get()); + } + + TransactionSpentOutputsView GetTxSpentOutputs(size_t tx_undo_index) const + { + return TransactionSpentOutputsView{btck_block_spent_outputs_get_transaction_spent_outputs_at(get(), tx_undo_index)}; + } + + MAKE_RANGE_METHOD(TxsSpentOutputs, BlockSpentOutputs, &BlockSpentOutputs::Count, &BlockSpentOutputs::GetTxSpentOutputs, *this) +}; + class ChainMan : UniqueHandle { public: @@ -831,6 +930,11 @@ public: if (!block) return std::nullopt; return block; } + + BlockSpentOutputs ReadBlockSpentOutputs(const BlockTreeEntry& entry) const + { + return btck_block_spent_outputs_read(get(), entry.get()); + } }; } // namespace btck diff --git a/src/test/kernel/test_kernel.cpp b/src/test/kernel/test_kernel.cpp index 29765812931..525c5f9f93b 100644 --- a/src/test/kernel/test_kernel.cpp +++ b/src/test/kernel/test_kernel.cpp @@ -701,11 +701,13 @@ void chainman_mainnet_validation_test(TestDirectory& test_directory) check_equal(read_block.value().ToBytes(), raw_block); // Check that we can read the previous block - auto tip_2{tip.GetPrevious()}; - auto read_block_2{chainman->ReadBlock(tip_2.value())}; + BlockTreeEntry tip_2{*tip.GetPrevious()}; + Block read_block_2{*chainman->ReadBlock(tip_2)}; + BOOST_CHECK_EQUAL(chainman->ReadBlockSpentOutputs(tip_2).Count(), 0); + BOOST_CHECK_EQUAL(chainman->ReadBlockSpentOutputs(tip).Count(), 0); // It should be an error if we go another block back, since the genesis has no ancestor - BOOST_CHECK(!tip_2.value().GetPrevious()); + BOOST_CHECK(!tip_2.GetPrevious()); // If we try to validate it again, it should be a duplicate BOOST_CHECK(chainman->ProcessBlock(block, &new_block)); @@ -782,6 +784,48 @@ 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])); + // 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())}; + CheckHandle(block_spent_outputs, block_spent_outputs_prev); + CheckRange(block_spent_outputs_prev.TxsSpentOutputs(), block_spent_outputs_prev.Count()); + BOOST_CHECK_EQUAL(block_spent_outputs.Count(), 1); + + // Get transaction spent outputs from the last transaction in the two blocks + TransactionSpentOutputsView transaction_spent_outputs{block_spent_outputs.GetTxSpentOutputs(block_spent_outputs.Count() - 1)}; + TransactionSpentOutputs owned_transaction_spent_outputs{transaction_spent_outputs}; + TransactionSpentOutputs owned_transaction_spent_outputs_prev{block_spent_outputs_prev.GetTxSpentOutputs(block_spent_outputs_prev.Count() - 1)}; + CheckHandle(owned_transaction_spent_outputs, owned_transaction_spent_outputs_prev); + CheckRange(transaction_spent_outputs.Coins(), transaction_spent_outputs.Count()); + + // Get the last coin from the transaction spent outputs + CoinView coin{transaction_spent_outputs.GetCoin(transaction_spent_outputs.Count() - 1)}; + BOOST_CHECK(!coin.IsCoinbase()); + Coin owned_coin{coin}; + Coin owned_coin_prev{owned_transaction_spent_outputs_prev.GetCoin(owned_transaction_spent_outputs_prev.Count() - 1)}; + CheckHandle(owned_coin, owned_coin_prev); + + // Validate coin properties + TransactionOutputView output = coin.GetOutput(); + uint32_t coin_height = coin.GetConfirmationHeight(); + BOOST_CHECK_EQUAL(coin_height, 205); + BOOST_CHECK_EQUAL(output.Amount(), 100000000); + + // Test script pubkey serialization + auto script_pubkey = output.GetScriptPubkey(); + auto script_pubkey_bytes{script_pubkey.ToBytes()}; + BOOST_CHECK_EQUAL(script_pubkey_bytes.size(), 22); + auto round_trip_script_pubkey{ScriptPubkey(script_pubkey_bytes)}; + BOOST_CHECK_EQUAL(round_trip_script_pubkey.ToBytes().size(), 22); + + for (const auto tx_spent_outputs : block_spent_outputs.TxsSpentOutputs()) { + for (const auto coins : tx_spent_outputs.Coins()) { + BOOST_CHECK_GT(coins.GetOutput().Amount(), 1); + } + } + std::filesystem::remove_all(test_directory.m_directory / "blocks" / "blk00000.dat"); - BOOST_CHECK(!chainman->ReadBlock(tip_2)); + BOOST_CHECK(!chainman->ReadBlock(tip_2).has_value()); + std::filesystem::remove_all(test_directory.m_directory / "blocks" / "rev00000.dat"); + BOOST_CHECK_THROW(chainman->ReadBlockSpentOutputs(tip), std::runtime_error); }