diff --git a/src/kernel/bitcoinkernel.cpp b/src/kernel/bitcoinkernel.cpp index b9c0acc8b4d..531df3c8be8 100644 --- a/src/kernel/bitcoinkernel.cpp +++ b/src/kernel/bitcoinkernel.cpp @@ -490,6 +490,7 @@ struct btck_Chain : Handle {}; struct btck_BlockSpentOutputs : Handle> {}; struct btck_TransactionSpentOutputs : Handle {}; struct btck_Coin : Handle {}; +struct btck_BlockHash : Handle {}; btck_Transaction* btck_transaction_create(const void* raw_transaction, size_t raw_transaction_len) { @@ -918,6 +919,17 @@ btck_ChainstateManager* btck_chainstate_manager_create( return btck_ChainstateManager::create(std::move(chainman), opts.m_context); } +const btck_BlockTreeEntry* btck_chainstate_manager_get_block_tree_entry_by_hash(const btck_ChainstateManager* chainman, const btck_BlockHash* block_hash) +{ + auto block_index = WITH_LOCK(btck_ChainstateManager::get(chainman).m_chainman->GetMutex(), + return btck_ChainstateManager::get(chainman).m_chainman->m_blockman.LookupBlockIndex(btck_BlockHash::get(block_hash))); + if (!block_index) { + LogDebug(BCLog::KERNEL, "A block with the given hash is not indexed."); + return nullptr; + } + return btck_BlockTreeEntry::ref(block_index); +} + void btck_chainstate_manager_destroy(btck_ChainstateManager* chainman) { { @@ -994,6 +1006,11 @@ int btck_block_to_bytes(const btck_Block* block, btck_WriteBytes writer, void* u } } +btck_BlockHash* btck_block_get_hash(const btck_Block* block) +{ + return btck_BlockHash::create(btck_Block::get(block)->GetHash()); +} + void btck_block_destroy(btck_Block* block) { delete block; @@ -1009,6 +1026,41 @@ btck_Block* btck_block_read(const btck_ChainstateManager* chainman, const btck_B return btck_Block::create(block); } +int32_t btck_block_tree_entry_get_height(const btck_BlockTreeEntry* entry) +{ + return btck_BlockTreeEntry::get(entry).nHeight; +} + +const btck_BlockHash* btck_block_tree_entry_get_block_hash(const btck_BlockTreeEntry* entry) +{ + return btck_BlockHash::ref(btck_BlockTreeEntry::get(entry).phashBlock); +} + +btck_BlockHash* btck_block_hash_create(const unsigned char block_hash[32]) +{ + return btck_BlockHash::create(std::span{block_hash, 32}); +} + +btck_BlockHash* btck_block_hash_copy(const btck_BlockHash* block_hash) +{ + return btck_BlockHash::copy(block_hash); +} + +void btck_block_hash_to_bytes(const btck_BlockHash* block_hash, unsigned char output[32]) +{ + std::memcpy(output, btck_BlockHash::get(block_hash).begin(), 32); +} + +int btck_block_hash_equals(const btck_BlockHash* hash1, const btck_BlockHash* hash2) +{ + return btck_BlockHash::get(hash1) == btck_BlockHash::get(hash2); +} + +void btck_block_hash_destroy(btck_BlockHash* hash) +{ + delete hash; +} + btck_BlockSpentOutputs* btck_block_spent_outputs_read(const btck_ChainstateManager* chainman, const btck_BlockTreeEntry* entry) { auto block_undo{std::make_shared()}; @@ -1121,3 +1173,21 @@ int btck_chain_get_height(const btck_Chain* chain) LOCK(::cs_main); return btck_Chain::get(chain).Height(); } + +const btck_BlockTreeEntry* btck_chain_get_genesis(const btck_Chain* chain) +{ + LOCK(::cs_main); + return btck_BlockTreeEntry::ref(btck_Chain::get(chain).Genesis()); +} + +const btck_BlockTreeEntry* btck_chain_get_by_height(const btck_Chain* chain, int height) +{ + LOCK(::cs_main); + return btck_BlockTreeEntry::ref(btck_Chain::get(chain)[height]); +} + +int btck_chain_contains(const btck_Chain* chain, const btck_BlockTreeEntry* entry) +{ + LOCK(::cs_main); + return btck_Chain::get(chain).Contains(&btck_BlockTreeEntry::get(entry)) ? 1 : 0; +} diff --git a/src/kernel/bitcoinkernel.h b/src/kernel/bitcoinkernel.h index b6cfb0dbd3e..e9283e3da3c 100644 --- a/src/kernel/bitcoinkernel.h +++ b/src/kernel/bitcoinkernel.h @@ -242,6 +242,13 @@ typedef struct btck_TransactionSpentOutputs btck_TransactionSpentOutputs; */ typedef struct btck_Coin btck_Coin; +/** + * Opaque data structure for holding a block hash. + * + * This is a type-safe identifier for a block. + */ +typedef struct btck_BlockHash btck_BlockHash; + /** Current sync state passed to tip changed callbacks. */ typedef uint8_t btck_SynchronizationState; #define btck_SynchronizationState_INIT_REINDEX ((btck_SynchronizationState)(0)) @@ -842,7 +849,7 @@ BITCOINKERNEL_API void btck_context_destroy(btck_Context* context); ///@{ /** - * @brief Returns the previous block tree entry in the chain, or null if the current + * @brief Returns the previous block tree entry in the tree, or null if the current * block tree entry is the genesis block. * * @param[in] block_tree_entry Non-null. @@ -851,6 +858,24 @@ BITCOINKERNEL_API void btck_context_destroy(btck_Context* context); BITCOINKERNEL_API const btck_BlockTreeEntry* BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_tree_entry_get_previous( const btck_BlockTreeEntry* block_tree_entry) BITCOINKERNEL_ARG_NONNULL(1); +/** + * @brief Return the height of a certain block tree entry. + * + * @param[in] block_tree_entry Non-null. + * @return The block height. + */ +BITCOINKERNEL_API int32_t BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_tree_entry_get_height( + const btck_BlockTreeEntry* block_tree_entry) BITCOINKERNEL_ARG_NONNULL(1); + +/** + * @brief Return the block hash associated with a block tree entry. + * + * @param[in] block_tree_entry Non-null. + * @return The block hash. + */ +BITCOINKERNEL_API const btck_BlockHash* BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_tree_entry_get_block_hash( + const btck_BlockTreeEntry* block_tree_entry) BITCOINKERNEL_ARG_NONNULL(1); + ///@} /** @name ChainstateManagerOptions @@ -1001,6 +1026,18 @@ BITCOINKERNEL_API int BITCOINKERNEL_WARN_UNUSED_RESULT btck_chainstate_manager_p BITCOINKERNEL_API const btck_Chain* BITCOINKERNEL_WARN_UNUSED_RESULT btck_chainstate_manager_get_active_chain( const btck_ChainstateManager* chainstate_manager) BITCOINKERNEL_ARG_NONNULL(1); +/** + * @brief Retrieve a block tree entry by its block hash. + * + * @param[in] chainstate_manager Non-null. + * @param[in] block_hash Non-null. + * @return The block tree entry of the block with the passed in hash, or null if + * the block hash is not found. + */ +BITCOINKERNEL_API const btck_BlockTreeEntry* BITCOINKERNEL_WARN_UNUSED_RESULT btck_chainstate_manager_get_block_tree_entry_by_hash( + const btck_ChainstateManager* chainstate_manager, + const btck_BlockHash* block_hash) BITCOINKERNEL_ARG_NONNULL(1, 2); + /** * Destroy the chainstate manager. */ @@ -1065,6 +1102,15 @@ BITCOINKERNEL_API size_t BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_count_trans BITCOINKERNEL_API const btck_Transaction* BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_get_transaction_at( const btck_Block* block, size_t transaction_index) BITCOINKERNEL_ARG_NONNULL(1); +/** + * @brief Calculate and return the hash of a block. + * + * @param[in] block Non-null. + * @return The block hash. + */ +BITCOINKERNEL_API btck_BlockHash* BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_get_hash( + const btck_Block* block) BITCOINKERNEL_ARG_NONNULL(1); + /** * @brief Serializes the block through the passed in callback to bytes. * This is consensus serialization that is also used for the P2P network. @@ -1130,6 +1176,40 @@ BITCOINKERNEL_API const btck_BlockTreeEntry* BITCOINKERNEL_WARN_UNUSED_RESULT bt BITCOINKERNEL_API int32_t BITCOINKERNEL_WARN_UNUSED_RESULT btck_chain_get_height( const btck_Chain* chain) BITCOINKERNEL_ARG_NONNULL(1); +/** + * @brief Get the block tree entry of the genesis block. + * + * @param[in] chain Non-null. + * @return The block tree entry of the genesis block, or null if the chain is empty. + */ +BITCOINKERNEL_API const btck_BlockTreeEntry* BITCOINKERNEL_WARN_UNUSED_RESULT btck_chain_get_genesis( + const btck_Chain* chain) BITCOINKERNEL_ARG_NONNULL(1); + +/** + * @brief Retrieve a block tree entry by its height in the currently active chain. + * Once retrieved there is no guarantee that it remains in the active chain. + * + * @param[in] chain Non-null. + * @param[in] block_height Height in the chain of the to be retrieved block tree entry. + * @return The block tree entry at a certain height in the currently active chain, or null + * if the height is out of bounds. + */ +BITCOINKERNEL_API const btck_BlockTreeEntry* BITCOINKERNEL_WARN_UNUSED_RESULT btck_chain_get_by_height( + const btck_Chain* chain, + int block_height) BITCOINKERNEL_ARG_NONNULL(1); + +/** + * @brief Return true if the passed in chain contains the block tree entry. + * + * @param[in] chain Non-null. + * @param[in] block_tree_entry Non-null. + * @return 1 if the block_tree_entry is in the chain, 0 otherwise. + * + */ +BITCOINKERNEL_API int BITCOINKERNEL_WARN_UNUSED_RESULT btck_chain_contains( + const btck_Chain* chain, + const btck_BlockTreeEntry* block_tree_entry) BITCOINKERNEL_ARG_NONNULL(1, 2); + ///@} /** @name BlockSpentOutputs @@ -1282,6 +1362,52 @@ BITCOINKERNEL_API void btck_coin_destroy(btck_Coin* coin); ///@} +/** @name BlockHash + * Functions for working with block hashes. + */ +///@{ + +/** + * @brief Create a block hash from its raw data. + */ +BITCOINKERNEL_API btck_BlockHash* BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_hash_create( + const unsigned char block_hash[32]) BITCOINKERNEL_ARG_NONNULL(1); + +/** + * @brief Check if two block hashes are equal. + * + * @param[in] hash1 Non-null. + * @param[in] hash2 Non-null. + * @return 0 if the block hashes are not equal. + */ +BITCOINKERNEL_API int BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_hash_equals( + const btck_BlockHash* hash1, const btck_BlockHash* hash2) BITCOINKERNEL_ARG_NONNULL(1, 2); + +/** + * @brief Copy a block hash. + * + * @param[in] block_hash Non-null. + * @return The copied block hash. + */ +BITCOINKERNEL_API btck_BlockHash* BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_hash_copy( + const btck_BlockHash* block_hash) BITCOINKERNEL_ARG_NONNULL(1); + +/** + * @brief Serializes the block hash to bytes. + * + * @param[in] block_hash Non-null. + * @param[in] output The serialized block hash. + */ +BITCOINKERNEL_API void btck_block_hash_to_bytes( + const btck_BlockHash* block_hash, unsigned char output[32]) BITCOINKERNEL_ARG_NONNULL(1, 2); + +/** + * Destroy the block hash. + */ +BITCOINKERNEL_API void btck_block_hash_destroy(btck_BlockHash* block_hash); + +///@} + #ifdef __cplusplus } // extern "C" #endif // __cplusplus diff --git a/src/kernel/bitcoinkernel_wrapper.h b/src/kernel/bitcoinkernel_wrapper.h index d9cacab8428..d06878fa6a5 100644 --- a/src/kernel/bitcoinkernel_wrapper.h +++ b/src/kernel/bitcoinkernel_wrapper.h @@ -7,6 +7,7 @@ #include +#include #include #include #include @@ -528,6 +529,53 @@ bool ScriptPubkeyApi::Verify(int64_t amount, return result == 1; } +template +class BlockHashApi +{ +private: + auto impl() const + { + return static_cast(this)->get(); + } + +public: + bool operator==(const Derived& other) const + { + return btck_block_hash_equals(impl(), other.get()) != 0; + } + + bool operator!=(const Derived& other) const + { + return btck_block_hash_equals(impl(), other.get()) == 0; + } + + std::array ToBytes() const + { + std::array hash; + btck_block_hash_to_bytes(impl(), reinterpret_cast(hash.data())); + return hash; + } +}; + +class BlockHashView: public View, public BlockHashApi +{ +public: + explicit BlockHashView(const btck_BlockHash* ptr) : View{ptr} {} +}; + +class BlockHash : public Handle, public BlockHashApi +{ +public: + explicit BlockHash(const std::array& hash) + : Handle{btck_block_hash_create(reinterpret_cast(hash.data()))} {} + + explicit BlockHash(btck_BlockHash* hash) + : Handle{hash} {} + + BlockHash(const BlockHashView& view) + : Handle{view} {} +}; + class Block : public Handle { public: @@ -550,6 +598,11 @@ public: MAKE_RANGE_METHOD(Transactions, Block, &Block::CountTransactions, &Block::GetTransaction, *this) + BlockHash GetHash() const + { + return BlockHash{btck_block_get_hash(get())}; + } + std::vector ToBytes() const { return write_bytes(get(), btck_block_to_bytes); @@ -613,6 +666,16 @@ public: if (!entry) return std::nullopt; return entry; } + + int32_t GetHeight() const + { + return btck_block_tree_entry_get_height(get()); + } + + BlockHashView GetHash() const + { + return BlockHashView{btck_block_tree_entry_get_block_hash(get())}; + } }; class KernelNotifications @@ -788,6 +851,30 @@ public: { return btck_chain_get_height(get()); } + + int CountEntries() const + { + return btck_chain_get_height(get()) + 1; + } + + BlockTreeEntry Genesis() const + { + return btck_chain_get_genesis(get()); + } + + BlockTreeEntry GetByHeight(int height) const + { + auto index{btck_chain_get_by_height(get(), height)}; + if (!index) throw std::runtime_error("No entry in the chain at the provided height"); + return index; + } + + bool Contains(BlockTreeEntry& entry) const + { + return btck_chain_contains(get(), entry.get()); + } + + MAKE_RANGE_METHOD(Entries, ChainView, &ChainView::CountEntries, &ChainView::GetByHeight, *this) }; template @@ -924,6 +1011,13 @@ public: return ChainView{btck_chainstate_manager_get_active_chain(get())}; } + std::optional GetBlockTreeEntry(const BlockHash& block_hash) const + { + auto entry{btck_chainstate_manager_get_block_tree_entry_by_hash(get(), block_hash.get())}; + if (!entry) return std::nullopt; + return entry; + } + std::optional ReadBlock(const BlockTreeEntry& entry) const { auto block{btck_block_read(get(), entry.get())}; diff --git a/src/test/kernel/test_kernel.cpp b/src/test/kernel/test_kernel.cpp index 525c5f9f93b..fca76f4fbc1 100644 --- a/src/test/kernel/test_kernel.cpp +++ b/src/test/kernel/test_kernel.cpp @@ -634,6 +634,39 @@ void chainman_reindex_test(TestDirectory& test_directory) std::vector import_files; BOOST_CHECK(chainman->ImportBlocks(import_files)); + + // Sanity check some block retrievals + auto chain{chainman->GetChain()}; + BOOST_CHECK_THROW(chain.GetByHeight(1000), std::runtime_error); + auto genesis_index{chain.Genesis()}; + BOOST_CHECK(!genesis_index.GetPrevious()); + auto genesis_block_raw{chainman->ReadBlock(genesis_index).value().ToBytes()}; + auto first_index{chain.GetByHeight(0)}; + auto first_block_raw{chainman->ReadBlock(genesis_index).value().ToBytes()}; + check_equal(genesis_block_raw, first_block_raw); + auto height{first_index.GetHeight()}; + BOOST_CHECK_EQUAL(height, 0); + + auto next_index{chain.GetByHeight(first_index.GetHeight() + 1)}; + BOOST_CHECK(chain.Contains(next_index)); + auto next_block_data{chainman->ReadBlock(next_index).value().ToBytes()}; + auto tip_index{chain.Tip()}; + auto tip_block_data{chainman->ReadBlock(tip_index).value().ToBytes()}; + auto second_index{chain.GetByHeight(1)}; + auto second_block{chainman->ReadBlock(second_index).value()}; + auto second_block_data{second_block.ToBytes()}; + auto second_height{second_index.GetHeight()}; + BOOST_CHECK_EQUAL(second_height, 1); + check_equal(next_block_data, tip_block_data); + check_equal(next_block_data, second_block_data); + + auto second_hash{second_index.GetHash()}; + auto another_second_index{chainman->GetBlockTreeEntry(second_hash)}; + BOOST_CHECK(another_second_index); + auto another_second_height{another_second_index->GetHeight()}; + auto second_block_hash{second_block.GetHash()}; + check_equal(second_block_hash.ToBytes(), second_hash.ToBytes()); + BOOST_CHECK_EQUAL(second_height, another_second_height); } void chainman_reindex_chainstate_test(TestDirectory& test_directory) @@ -722,6 +755,21 @@ BOOST_AUTO_TEST_CASE(btck_chainman_mainnet_tests) chainman_reindex_chainstate_test(test_directory); } +BOOST_AUTO_TEST_CASE(btck_block_hash_tests) +{ + std::array test_hash; + std::array test_hash_2; + for (int i = 0; i < 32; ++i) { + test_hash[i] = static_cast(i); + test_hash_2[i] = static_cast(i + 1); + } + BlockHash block_hash{test_hash}; + BlockHash block_hash_2{test_hash_2}; + BOOST_CHECK(block_hash != block_hash_2); + BOOST_CHECK(block_hash == block_hash); + CheckHandle(block_hash, block_hash_2); +} + BOOST_AUTO_TEST_CASE(btck_chainman_in_memory_tests) { auto in_memory_test_directory{TestDirectory{"in-memory_test_bitcoin_kernel"}}; @@ -824,6 +872,31 @@ BOOST_AUTO_TEST_CASE(btck_chainman_regtest_tests) } } + CheckRange(chain.Entries(), chain.CountEntries()); + + for (const BlockTreeEntry entry : chain.Entries()) { + std::optional block{chainman->ReadBlock(entry)}; + if (block) { + for (const TransactionView transaction : block->Transactions()) { + for (const TransactionOutputView output : transaction.Outputs()) { + // skip data carrier outputs + if ((unsigned char)output.GetScriptPubkey().ToBytes()[0] == 0x6a) { + continue; + } + BOOST_CHECK_GT(output.Amount(), 1); + } + } + } + } + + int32_t count{0}; + for (const auto entry : chain.Entries()) { + BOOST_CHECK_EQUAL(entry.GetHeight(), count); + ++count; + } + BOOST_CHECK_EQUAL(count, chain.CountEntries()); + + std::filesystem::remove_all(test_directory.m_directory / "blocks" / "blk00000.dat"); BOOST_CHECK(!chainman->ReadBlock(tip_2).has_value()); std::filesystem::remove_all(test_directory.m_directory / "blocks" / "rev00000.dat");