From 70e098af04f25bbe5d85c1ed721bbbb75a54a3ba Mon Sep 17 00:00:00 2001 From: janb84 Date: Sun, 14 Jun 2026 20:41:17 +0200 Subject: [PATCH] kernel: replace BlockTreeEntry by Chain Back btck_Chain by a CBlockIndex instead of a CChain, so a chain becomes a snapshot identified by its tip block. A chain and the block tree entry at its tip then describe the same thing, making btck_BlockTreeEntry redundant; remove it and fold its accessors into btck_Chain. A null btck_Chain* now denotes the empty chain: all chain accessors are null-safe and chain-returning functions return the empty chain instead of an error, so ARG_NONNULL is dropped from chain parameters. In the C++ wrapper, rename ChainView to Chain and model it as a std::ranges range of its block headers (value_type = BlockHeader): operator[] indexes by height, size() - 1 is the height, and emptiness is observable via operator bool(). Mismatch mirrors std::ranges::mismatch but uses find_fork (O(log n)). --- src/bitcoin-chainstate.cpp | 2 +- src/kernel/bitcoinkernel.cpp | 150 ++++++++++-------- src/kernel/bitcoinkernel.h | 244 ++++++++++++++--------------- src/kernel/bitcoinkernel_wrapper.h | 179 ++++++++++++--------- src/test/kernel/test_kernel.cpp | 236 +++++++++++++++++++++------- 5 files changed, 493 insertions(+), 318 deletions(-) diff --git a/src/bitcoin-chainstate.cpp b/src/bitcoin-chainstate.cpp index ae215c198eda..9e53b4bc7be5 100644 --- a/src/bitcoin-chainstate.cpp +++ b/src/bitcoin-chainstate.cpp @@ -110,7 +110,7 @@ class TestValidationInterface : public ValidationInterface class TestKernelNotifications : public KernelNotifications { public: - void BlockTipHandler(SynchronizationState, const BlockTreeEntry, double) override + void BlockTipHandler(SynchronizationState, const Chain, double) override { std::cout << "Block tip changed" << std::endl; } diff --git a/src/kernel/bitcoinkernel.cpp b/src/kernel/bitcoinkernel.cpp index ef2c7cc6eea4..b60194aeca14 100644 --- a/src/kernel/bitcoinkernel.cpp +++ b/src/kernel/bitcoinkernel.cpp @@ -146,10 +146,10 @@ struct Handle { } // namespace -struct btck_BlockTreeEntry: Handle {}; struct btck_Block : Handle> {}; struct btck_BlockValidationState : Handle {}; struct btck_TxValidationState : Handle {}; +struct btck_Chain : Handle {}; namespace { @@ -303,7 +303,7 @@ class KernelNotifications final : public kernel::Notifications kernel::InterruptResult blockTip(SynchronizationState state, const CBlockIndex& index, double verification_progress) override { - if (m_cbs.block_tip) m_cbs.block_tip(m_cbs.user_data, cast_state(state), btck_BlockTreeEntry::ref(&index), verification_progress); + if (m_cbs.block_tip) m_cbs.block_tip(m_cbs.user_data, cast_state(state), btck_Chain::ref(&index), verification_progress); return {}; } void headerTip(SynchronizationState state, int64_t height, int64_t timestamp, bool presync) override @@ -363,7 +363,7 @@ class KernelValidationInterface final : public CValidationInterface if (m_cbs.pow_valid_block) { m_cbs.pow_valid_block(m_cbs.user_data, btck_Block::copy(btck_Block::ref(&block)), - btck_BlockTreeEntry::ref(pindex)); + btck_Chain::ref(pindex)); } } @@ -372,7 +372,7 @@ class KernelValidationInterface final : public CValidationInterface if (m_cbs.block_connected) { m_cbs.block_connected(m_cbs.user_data, btck_Block::copy(btck_Block::ref(&block)), - btck_BlockTreeEntry::ref(pindex)); + btck_Chain::ref(pindex)); } } @@ -381,7 +381,7 @@ class KernelValidationInterface final : public CValidationInterface if (m_cbs.block_disconnected) { m_cbs.block_disconnected(m_cbs.user_data, btck_Block::copy(btck_Block::ref(&block)), - btck_BlockTreeEntry::ref(pindex)); + btck_Chain::ref(pindex)); } } }; @@ -494,7 +494,6 @@ struct btck_Context : Handle> {}; 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 {}; @@ -895,23 +894,6 @@ void btck_context_destroy(btck_Context* context) delete context; } -const btck_BlockTreeEntry* btck_block_tree_entry_get_previous(const btck_BlockTreeEntry* entry) -{ - if (!btck_BlockTreeEntry::get(entry).pprev) { - LogInfo("Genesis block has no previous."); - return nullptr; - } - - return btck_BlockTreeEntry::ref(btck_BlockTreeEntry::get(entry).pprev); -} - -const btck_BlockTreeEntry* btck_block_tree_entry_get_ancestor(const btck_BlockTreeEntry* block_tree_entry, int32_t height) -{ - const auto* ancestor{btck_BlockTreeEntry::get(block_tree_entry).GetAncestor(height)}; - assert(ancestor); - return btck_BlockTreeEntry::ref(ancestor); -} - btck_BlockValidationState* btck_block_validation_state_create() { return btck_BlockValidationState::create(); @@ -1062,7 +1044,7 @@ 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) +const btck_Chain* btck_chainstate_manager_find(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))); @@ -1070,13 +1052,13 @@ const btck_BlockTreeEntry* btck_chainstate_manager_get_block_tree_entry_by_hash( LogDebug(BCLog::KERNEL, "A block with the given hash is not indexed."); return nullptr; } - return btck_BlockTreeEntry::ref(block_index); + return btck_Chain::ref(block_index); } -const btck_BlockTreeEntry* btck_chainstate_manager_get_best_entry(const btck_ChainstateManager* chainstate_manager) +const btck_Chain* btck_chainstate_manager_get_best_chain(const btck_ChainstateManager* chainstate_manager) { auto& chainman = *btck_ChainstateManager::get(chainstate_manager).m_chainman; - return btck_BlockTreeEntry::ref(WITH_LOCK(chainman.GetMutex(), return chainman.m_best_header)); + return btck_Chain::ref(WITH_LOCK(chainman.GetMutex(), return chainman.m_best_header)); } void btck_chainstate_manager_destroy(btck_ChainstateManager* chainman) @@ -1186,36 +1168,20 @@ void btck_block_destroy(btck_Block* block) delete block; } -btck_Block* btck_block_read(const btck_ChainstateManager* chainman, const btck_BlockTreeEntry* entry) +btck_Block* btck_block_read(const btck_ChainstateManager* chainman, const btck_Chain* chain) { + if (!chain) { + LogDebug(BCLog::KERNEL, "The empty chain has no tip block to read."); + return nullptr; + } auto block{std::make_shared()}; - if (!btck_ChainstateManager::get(chainman).m_chainman->m_blockman.ReadBlock(*block, btck_BlockTreeEntry::get(entry))) { + if (!btck_ChainstateManager::get(chainman).m_chainman->m_blockman.ReadBlock(*block, btck_Chain::get(chain))) { LogError("Failed to read block."); return nullptr; } return btck_Block::create(block); } -btck_BlockHeader* btck_block_tree_entry_get_block_header(const btck_BlockTreeEntry* entry) -{ - return btck_BlockHeader::create(btck_BlockTreeEntry::get(entry).GetBlockHeader()); -} - -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); -} - -int btck_block_tree_entry_equals(const btck_BlockTreeEntry* entry1, const btck_BlockTreeEntry* entry2) -{ - return &btck_BlockTreeEntry::get(entry1) == &btck_BlockTreeEntry::get(entry2); -} - btck_BlockHash* btck_block_hash_create(const unsigned char block_hash[32]) { return btck_BlockHash::create(std::span{block_hash, 32}); @@ -1241,14 +1207,18 @@ 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) +btck_BlockSpentOutputs* btck_block_spent_outputs_read(const btck_ChainstateManager* chainman, const btck_Chain* chain) { + if (!chain) { + LogDebug(BCLog::KERNEL, "The empty chain has no tip block to read spent outputs for."); + return nullptr; + } auto block_undo{std::make_shared()}; - if (btck_BlockTreeEntry::get(entry).nHeight < 1) { + if (btck_Chain::get(chain).nHeight == 0) { 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))) { + if (!btck_ChainstateManager::get(chainman).m_chainman->m_blockman.ReadBlockUndo(*block_undo, btck_Chain::get(chain))) { LogError("Failed to read block spent outputs data."); return nullptr; } @@ -1355,25 +1325,83 @@ btck_BlockValidationState* btck_chainstate_manager_process_block_header( const btck_Chain* btck_chainstate_manager_get_active_chain(const btck_ChainstateManager* chainman) { - return btck_Chain::ref(&WITH_LOCK(btck_ChainstateManager::get(chainman).m_chainman->GetMutex(), return btck_ChainstateManager::get(chainman).m_chainman->ActiveChain())); + return btck_Chain::ref(WITH_LOCK(btck_ChainstateManager::get(chainman).m_chainman->GetMutex(), return btck_ChainstateManager::get(chainman).m_chainman->ActiveChain().Tip())); } int32_t btck_chain_get_height(const btck_Chain* chain) { - LOCK(::cs_main); - return btck_Chain::get(chain).Height(); + // A null chain is the empty chain, which has no blocks and so a height of -1. + return chain ? btck_Chain::get(chain).nHeight : -1; } -const btck_BlockTreeEntry* btck_chain_get_by_height(const btck_Chain* chain, int32_t height) +btck_BlockHeader* btck_chain_get_block_header(const btck_Chain* chain, int32_t height) { - LOCK(::cs_main); - return btck_BlockTreeEntry::ref(btck_Chain::get(chain)[height]); + const auto* index{chain ? btck_Chain::get(chain).GetAncestor(height) : nullptr}; + if (!index) { + LogDebug(BCLog::KERNEL, "The requested height is not in the chain."); + return nullptr; + } + return btck_BlockHeader::create(index->GetBlockHeader()); +} + +const btck_BlockHash* btck_chain_get_block_hash(const btck_Chain* chain, int32_t height) +{ + const auto* index{chain ? btck_Chain::get(chain).GetAncestor(height) : nullptr}; + if (!index) { + LogDebug(BCLog::KERNEL, "The requested height is not in the chain."); + return nullptr; + } + return btck_BlockHash::ref(index->phashBlock); +} + +const btck_Chain* btck_chain_get_ancestor(const btck_Chain* chain, int32_t height) +{ + const auto* ancestor{chain ? btck_Chain::get(chain).GetAncestor(height) : nullptr}; + if (!ancestor) { + LogDebug(BCLog::KERNEL, "The requested height is not in the chain."); + return nullptr; + } + return btck_Chain::ref(ancestor); +} + +const btck_Chain* btck_chain_get_parent(const btck_Chain* chain) +{ + // The parent of the empty chain, and of the genesis-only chain, is the empty + // chain. + const auto* pprev{chain ? btck_Chain::get(chain).pprev : nullptr}; + if (!pprev) { + return nullptr; + } + return btck_Chain::ref(pprev); +} + +int btck_chain_starts_with(const btck_Chain* chain, const btck_Chain* prefix) +{ + // The empty chain is a prefix of every chain; a non-empty prefix cannot be a + // prefix of the empty chain. + if (!prefix) return 1; + if (!chain) return 0; + const auto& tip{btck_Chain::get(chain)}; + const auto& prefix_tip{btck_Chain::get(prefix)}; + return tip.GetAncestor(prefix_tip.nHeight) == &prefix_tip ? 1 : 0; +} + +const btck_Chain* btck_chain_find_fork(const btck_Chain* chain1, const btck_Chain* chain2) +{ + // The empty chain shares no block with any chain, so the fork is empty. + if (!chain1 || !chain2) return nullptr; + const auto* common{LastCommonAncestor(&btck_Chain::get(chain1), &btck_Chain::get(chain2))}; + if (!common) { + return nullptr; + } + return btck_Chain::ref(common); } -int btck_chain_contains(const btck_Chain* chain, const btck_BlockTreeEntry* entry) +int btck_chain_equals(const btck_Chain* chain1, const btck_Chain* chain2) { - LOCK(::cs_main); - return btck_Chain::get(chain).Contains(btck_BlockTreeEntry::get(entry)) ? 1 : 0; + // Chains are identified by their tip, which a btck_Chain* aliases directly, + // so pointer equality is chain equality (and the empty chain equals itself). + return chain1 == chain2 ? 1 : 0; } btck_BlockHeader* btck_block_header_create(const void* raw_block_header, size_t raw_block_header_len) diff --git a/src/kernel/bitcoinkernel.h b/src/kernel/bitcoinkernel.h index fc0eb9e82d3f..f769a9d9c48f 100644 --- a/src/kernel/bitcoinkernel.h +++ b/src/kernel/bitcoinkernel.h @@ -193,18 +193,6 @@ typedef struct btck_ContextOptions btck_ContextOptions; */ typedef struct btck_Context btck_Context; -/** - * Opaque data structure for holding a block tree entry. - * - * This is a pointer to an element in the block index currently in memory of - * the chainstate manager. It is valid for the lifetime of the chainstate - * manager it was retrieved from. The entry is part of a tree-like structure - * that is maintained internally. Every entry, besides the genesis, points to a - * single parent. Multiple entries may share a parent, thus forming a tree. - * Each entry corresponds to a single block and may be used to retrieve its - * data and validation status. - */ -typedef struct btck_BlockTreeEntry btck_BlockTreeEntry; /** * Opaque data structure for holding options for creating a new chainstate @@ -245,8 +233,17 @@ typedef struct btck_BlockValidationState btck_BlockValidationState; typedef struct btck_ConsensusParams btck_ConsensusParams; /** - * Opaque data structure for holding the currently known best-chain associated - * with a chainstate. + * Opaque data structure for holding a chain. + * + * A chain is the linear sequence of blocks from the genesis block up to and + * including a particular block (its tip). Because every block has a unique + * path back to the genesis block, a chain is fully determined by its tip. + * + * This is a view into the block index currently held in memory by the + * chainstate manager and is valid for the lifetime of the chainstate manager + * it was retrieved from. The chains exposed by this API are snapshots: once + * obtained, a chain always describes the same sequence of blocks, even as the + * chainstate manager's active chain advances. */ typedef struct btck_Chain btck_Chain; @@ -358,7 +355,7 @@ typedef void (*btck_DestroyCallback)(void* user_data); /** * Function signatures for the kernel notifications. */ -typedef void (*btck_NotifyBlockTip)(void* user_data, btck_SynchronizationState state, const btck_BlockTreeEntry* entry, double verification_progress); +typedef void (*btck_NotifyBlockTip)(void* user_data, btck_SynchronizationState state, const btck_Chain* chain, double verification_progress); typedef void (*btck_NotifyHeaderTip)(void* user_data, btck_SynchronizationState state, int64_t height, int64_t timestamp, int presync); typedef void (*btck_NotifyProgress)(void* user_data, const char* title, size_t title_len, int progress_percent, int resume_possible); typedef void (*btck_NotifyWarningSet)(void* user_data, btck_Warning warning, const char* message, size_t message_len); @@ -370,9 +367,9 @@ typedef void (*btck_NotifyFatalError)(void* user_data, const char* message, size * Function signatures for the validation interface. */ typedef void (*btck_ValidationInterfaceBlockChecked)(void* user_data, btck_Block* block, const btck_BlockValidationState* state); -typedef void (*btck_ValidationInterfacePoWValidBlock)(void* user_data, btck_Block* block, const btck_BlockTreeEntry* entry); -typedef void (*btck_ValidationInterfaceBlockConnected)(void* user_data, btck_Block* block, const btck_BlockTreeEntry* entry); -typedef void (*btck_ValidationInterfaceBlockDisconnected)(void* user_data, btck_Block* block, const btck_BlockTreeEntry* entry); +typedef void (*btck_ValidationInterfacePoWValidBlock)(void* user_data, btck_Block* block, const btck_Chain* chain); +typedef void (*btck_ValidationInterfaceBlockConnected)(void* user_data, btck_Block* block, const btck_Chain* chain); +typedef void (*btck_ValidationInterfaceBlockDisconnected)(void* user_data, btck_Block* block, const btck_Chain* chain); /** * Function signature for serializing data. @@ -1075,72 +1072,6 @@ BITCOINKERNEL_API void btck_context_destroy(btck_Context* context); ///@} -/** @name BlockTreeEntry - * Functions for working with block tree entries. - */ -///@{ - -/** - * @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. - * @return The previous block tree entry, or null on error or if the current block tree entry is the genesis block. - */ -BITCOINKERNEL_API const btck_BlockTreeEntry* btck_block_tree_entry_get_previous( - const btck_BlockTreeEntry* block_tree_entry) BITCOINKERNEL_ARG_NONNULL(1); - -/** - * @brief Return the btck_BlockHeader associated with this entry. - * - * @param[in] block_tree_entry Non-null. - * @return btck_BlockHeader. - */ -BITCOINKERNEL_API btck_BlockHeader* BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_tree_entry_get_block_header( - 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 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* btck_block_tree_entry_get_block_hash( - const btck_BlockTreeEntry* block_tree_entry) BITCOINKERNEL_ARG_NONNULL(1); - -/** - * @brief Check if two block tree entries are equal. Two block tree entries are equal when they - * point to the same block. - * - * @param[in] entry1 Non-null. - * @param[in] entry2 Non-null. - * @return 1 if the block tree entries are equal, 0 otherwise. - */ -BITCOINKERNEL_API int btck_block_tree_entry_equals( - const btck_BlockTreeEntry* entry1, const btck_BlockTreeEntry* entry2) BITCOINKERNEL_ARG_NONNULL(1, 2); - -/** - * @brief Return the ancestor of a btck_BlockTreeEntry at the given height. - * - * @param[in] block_tree_entry Non-null. - * @param[in] height The height of the requested ancestor. - * @return The ancestor at the given height. - */ -BITCOINKERNEL_API const btck_BlockTreeEntry* btck_block_tree_entry_get_ancestor( - const btck_BlockTreeEntry* block_tree_entry, - int32_t height) BITCOINKERNEL_ARG_NONNULL(1); - -///@} - /** @name ChainstateManagerOptions * Functions for working with chainstate manager options. */ @@ -1237,13 +1168,14 @@ BITCOINKERNEL_API btck_ChainstateManager* BITCOINKERNEL_WARN_UNUSED_RESULT btck_ const btck_ChainstateManagerOptions* chainstate_manager_options) BITCOINKERNEL_ARG_NONNULL(1); /** - * @brief Get the btck_BlockTreeEntry whose associated btck_BlockHeader has the most - * known cumulative proof of work. + * @brief Get the chain ending at the block with the most known cumulative + * proof of work. This block may not be part of the active chain, e.g. if its + * data has not been validated yet. * * @param[in] chainstate_manager Non-null. - * @return The btck_BlockTreeEntry, or null if no block headers have been loaded. + * @return The chain, or null if no block headers have been loaded. */ -BITCOINKERNEL_API const btck_BlockTreeEntry* btck_chainstate_manager_get_best_entry( +BITCOINKERNEL_API const btck_Chain* btck_chainstate_manager_get_best_chain( const btck_ChainstateManager* chainstate_manager) BITCOINKERNEL_ARG_NONNULL(1); /** @@ -1296,15 +1228,11 @@ BITCOINKERNEL_API int BITCOINKERNEL_WARN_UNUSED_RESULT btck_chainstate_manager_p int* new_block) BITCOINKERNEL_ARG_NONNULL(1, 2, 3); /** - * @brief Returns the best known currently active chain. Its lifetime is - * dependent on the chainstate manager. It can be thought of as a view on a - * vector of block tree entries that form the best chain. The returned chain - * reference always points to the currently active best chain. However, state - * transitions within the chainstate manager (e.g., processing blocks) will - * update the chain's contents. Data retrieved from this chain is only - * consistent up to the point when new data is processed in the chainstate - * manager. It is the user's responsibility to guard against these - * inconsistencies. + * @brief Returns the currently active chain. + * + * The returned chain ends at the active tip at the time of this call. Like + * all chains in this API, it is a snapshot: subsequent state transitions in + * the chainstate manager do not change the contents of this chain object. * * @param[in] chainstate_manager Non-null. * @return The chain. @@ -1313,14 +1241,14 @@ BITCOINKERNEL_API const btck_Chain* 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. + * @brief Retrieve the chain ending at the block with the given 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. + * @return The chain ending at the block with the passed in hash, or the + * empty chain (null) if the block hash is not found. */ -BITCOINKERNEL_API const btck_BlockTreeEntry* btck_chainstate_manager_get_block_tree_entry_by_hash( +BITCOINKERNEL_API const btck_Chain* btck_chainstate_manager_find( const btck_ChainstateManager* chainstate_manager, const btck_BlockHash* block_hash) BITCOINKERNEL_ARG_NONNULL(1, 2); @@ -1337,16 +1265,17 @@ BITCOINKERNEL_API void btck_chainstate_manager_destroy(btck_ChainstateManager* c ///@{ /** - * @brief Reads the block the passed in block tree entry points to from disk and + * @brief Reads the block at the tip of the passed in chain from disk and * returns it. * * @param[in] chainstate_manager Non-null. - * @param[in] block_tree_entry Non-null. + * @param[in] chain The chain whose tip block to read; the empty chain (null) has no + * tip and yields null. * @return The read out block, or null on error. */ BITCOINKERNEL_API btck_Block* BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_read( const btck_ChainstateManager* chainstate_manager, - const btck_BlockTreeEntry* block_tree_entry) BITCOINKERNEL_ARG_NONNULL(1, 2); + const btck_Chain* chain) BITCOINKERNEL_ARG_NONNULL(1); /** * @brief Parse a serialized raw block into a new block object. @@ -1505,43 +1434,105 @@ BITCOINKERNEL_API void btck_block_validation_state_destroy( ///@} /** @name Chain - * Functions for working with the chain + * Functions for working with the chain. + * + * A null btck_Chain* denotes the empty chain: a chain containing no blocks. */ ///@{ /** * @brief Return the height of the tip of the chain. * - * @param[in] chain Non-null. - * @return The current height. + * @param[in] chain The chain, or null for the empty chain. + * @return The current height, or -1 for the empty chain. */ BITCOINKERNEL_API int32_t btck_chain_get_height( - const btck_Chain* chain) BITCOINKERNEL_ARG_NONNULL(1); + const btck_Chain* chain); + +/** + * @brief Return the block header of the block at the given height in the chain. + * + * @param[in] chain The chain, or null for the empty chain. + * @param[in] block_height Height of the block in the chain. + * @return The block header at the given height, or null if the height is out of bounds. + */ +BITCOINKERNEL_API btck_BlockHeader* BITCOINKERNEL_WARN_UNUSED_RESULT btck_chain_get_block_header( + const btck_Chain* chain, + int32_t block_height); + +/** + * @brief Return the block hash of the block at the given height in the chain. + * The returned hash is owned by the chain and must not be destroyed by the caller. + * + * @param[in] chain The chain, or null for the empty chain. + * @param[in] block_height Height of the block in the chain. + * @return The block hash at the given height, or null if the height is out of bounds. + */ +BITCOINKERNEL_API const btck_BlockHash* btck_chain_get_block_hash( + const btck_Chain* chain, + int32_t block_height); /** - * @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. + * @brief Return the sub-chain ending at the given height, i.e. the prefix of + * the chain from the genesis block up to and including the block at that + * height. * - * @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. + * @param[in] chain The chain, or null for the empty chain. + * @param[in] block_height Height of the block the returned chain ends at. + * @return The sub-chain ending at the given height, or the empty chain (null) if the height is out of bounds. */ -BITCOINKERNEL_API const btck_BlockTreeEntry* btck_chain_get_by_height( +BITCOINKERNEL_API const btck_Chain* btck_chain_get_ancestor( const btck_Chain* chain, - int32_t block_height) BITCOINKERNEL_ARG_NONNULL(1); + int32_t block_height); /** - * @brief Return true if the passed in chain contains the block tree entry. + * @brief Return the parent chain: the prefix of this chain with its tip + * removed, i.e. the chain ending at the block one lower. * - * @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. + * @param[in] chain The chain, or null for the empty chain. + * @return The parent chain, or the empty chain (null) if the chain consists of only the genesis block. + */ +BITCOINKERNEL_API const btck_Chain* btck_chain_get_parent( + const btck_Chain* chain); + +/** + * @brief Return whether the chain starts with the given prefix, i.e. whether + * every block of the prefix chain is also a block of the chain at the same + * height. Equivalently, this tests whether the tip of the prefix chain is part + * of the chain. * + * @param[in] chain The chain, or null for the empty chain. + * @param[in] prefix The prefix chain, or null for the empty chain (which is a prefix of every chain). + * @return 1 if the chain starts with the prefix, 0 otherwise. */ -BITCOINKERNEL_API int btck_chain_contains( +BITCOINKERNEL_API int btck_chain_starts_with( const btck_Chain* chain, - const btck_BlockTreeEntry* block_tree_entry) BITCOINKERNEL_ARG_NONNULL(1, 2); + const btck_Chain* prefix); + +/** + * @brief Return the fork point of two chains: the longest common prefix + * beginning at the genesis block, i.e. the sub-chain ending at the last block + * both chains have in common. + * + * @param[in] chain1 The chain, or null for the empty chain. + * @param[in] chain2 The chain, or null for the empty chain. + * @return The fork point of the two chains, or the empty chain (null) if they share no block. + */ +BITCOINKERNEL_API const btck_Chain* btck_chain_find_fork( + const btck_Chain* chain1, + const btck_Chain* chain2); + +/** + * @brief Check if two chains are equal. Two chains are equal when they end at + * the same block (two empty chains are equal). + * + * @param[in] chain1 The chain, or null for the empty chain. + * @param[in] chain2 The chain, or null for the empty chain. + * @return 1 if the chains are equal, 0 otherwise. + */ +BITCOINKERNEL_API int btck_chain_equals( + const btck_Chain* chain1, + const btck_Chain* chain2); ///@} @@ -1551,16 +1542,17 @@ BITCOINKERNEL_API int btck_chain_contains( ///@{ /** - * @brief Reads the block spent coins data the passed in block tree entry points to from - * disk and returns it. + * @brief Reads the block spent coins data of the block at the tip of the + * passed in chain from disk and returns it. * * @param[in] chainstate_manager Non-null. - * @param[in] block_tree_entry Non-null. + * @param[in] chain The chain whose tip block to read; the empty chain (null) has no + * tip and yields 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); + const btck_Chain* chain) BITCOINKERNEL_ARG_NONNULL(1); /** * @brief Copy a block's spent outputs. diff --git a/src/kernel/bitcoinkernel_wrapper.h b/src/kernel/bitcoinkernel_wrapper.h index ff7899d1d69a..c8eb9506db02 100644 --- a/src/kernel/bitcoinkernel_wrapper.h +++ b/src/kernel/bitcoinkernel_wrapper.h @@ -7,11 +7,13 @@ #include +#include #include #include #include #include #include +#include #include #include #include @@ -243,7 +245,7 @@ concept IndexedContainer = requires(const Container& c, SizeFunc size_func, GetF template requires IndexedContainer -class Range +class Range : public std::ranges::view_interface> { public: using value_type = std::invoke_result_t; @@ -268,8 +270,6 @@ class Range size_t size() const { return std::invoke(SizeFunc, *m_container); } - bool empty() const { return size() == 0; } - value_type operator[](size_t index) const { return std::invoke(GetFunc, *m_container, index); } value_type at(size_t index) const @@ -279,9 +279,6 @@ class Range } return (*this)[index]; } - - value_type front() const { return (*this)[0]; } - value_type back() const { return (*this)[size() - 1]; } }; #define MAKE_RANGE_METHOD(method_name, ContainerType, SizeFunc, GetFunc, container_expr) \ @@ -762,6 +759,12 @@ class BlockHeaderApi return BlockHash{btck_block_header_get_hash(impl())}; } + //! Two block headers are equal when they hash to the same block. + bool operator==(const BlockHeaderApi& other) const + { + return Hash() == other.Hash(); + } + BlockHashView PrevHash() const { return BlockHashView{btck_block_header_get_prev_hash(impl())}; @@ -908,46 +911,106 @@ class Logger : UniqueHandle +class Chain : public std::ranges::view_interface { + const btck_Chain* m_ptr; + public: - BlockTreeEntry(const btck_BlockTreeEntry* entry) - : View{entry} + //! A Chain is a non-owning snapshot over a borrowed btck_Chain, valid for as + //! long as the chainstate manager that produced it. + explicit Chain(const btck_Chain* ptr) : m_ptr{ptr} {} + + const btck_Chain* get() const { return m_ptr; } + + bool operator==(const Chain& other) const { + return btck_chain_equals(get(), other.get()) != 0; } - bool operator==(const BlockTreeEntry& other) const + //! A chain is a random-access range of the block headers of its blocks, from + //! the genesis header (front()) up to and including the tip header (back()). + using value_type = BlockHeader; + using difference_type = std::ptrdiff_t; + using iterator = Iterator; + using const_iterator = iterator; + + iterator begin() const { return iterator(this, 0); } + iterator end() const { return iterator(this, size()); } + const_iterator cbegin() const { return begin(); } + const_iterator cend() const { return end(); } + + size_t size() const { return static_cast(CountEntries()); } + + BlockHeader operator[](size_t index) const { return GetHeader(static_cast(index)); } + + BlockHeader at(size_t index) const { - return btck_block_tree_entry_equals(get(), other.get()) != 0; + if (index >= size()) { + throw std::out_of_range("Index out of range"); + } + return (*this)[index]; } - std::optional GetPrevious() const + int32_t Height() const { - auto entry{btck_block_tree_entry_get_previous(get())}; - if (!entry) return std::nullopt; - return entry; + return btck_chain_get_height(get()); } - int32_t GetHeight() const + //! Number of blocks in the chain (tip height + 1, including the genesis block). + int32_t CountEntries() const { - return btck_block_tree_entry_get_height(get()); + return btck_chain_get_height(get()) + 1; } - BlockHashView GetHash() const + BlockHeader GetHeader(int32_t height) const { - return BlockHashView{btck_block_tree_entry_get_block_hash(get())}; + auto header{btck_chain_get_block_header(get(), height)}; + if (!header) throw std::runtime_error("No block in the chain at the provided height"); + return header; } - BlockHeader GetHeader() const + BlockHashView GetHash(int32_t height) const + { + auto hash{btck_chain_get_block_hash(get(), height)}; + if (!hash) throw std::runtime_error("No block in the chain at the provided height"); + return BlockHashView{hash}; + } + + //! The sub-chain ending at the given height (a prefix of this chain), or the + //! empty chain if the height is out of range. O(log n). + Chain GetAncestor(int32_t height) const { - return BlockHeader{btck_block_tree_entry_get_block_header(get())}; + return Chain{btck_chain_get_ancestor(get(), height)}; } - BlockTreeEntry GetAncestor(int32_t height) const + //! The parent chain (this chain with its tip removed), or the empty chain at + //! genesis. O(1). + Chain Parent() const { - return BlockTreeEntry{btck_block_tree_entry_get_ancestor(get(), height)}; + return Chain{btck_chain_get_parent(get())}; } + bool StartsWith(const Chain& prefix) const + { + return btck_chain_starts_with(get(), prefix.get()) != 0; + } + + Chain FindFork(const Chain& other) const + { + return Chain{btck_chain_find_fork(get(), other.get())}; + } + + std::ranges::mismatch_result + Mismatch(const Chain& other) const + { + auto fork{FindFork(other)}; + auto n{static_cast(fork.Height() + 1)}; + return {begin() + n, other.begin() + n}; + } + + //! Convenience accessors for the block at the chain's tip. + BlockHeader TipHeader() const { return GetHeader(Height()); } + BlockHashView TipHash() const { return GetHash(Height()); } }; class KernelNotifications @@ -955,7 +1018,7 @@ class KernelNotifications public: virtual ~KernelNotifications() = default; - virtual void BlockTipHandler(SynchronizationState state, BlockTreeEntry entry, double verification_progress) {} + virtual void BlockTipHandler(SynchronizationState state, Chain chain, double verification_progress) {} virtual void HeaderTipHandler(SynchronizationState state, int64_t height, int64_t timestamp, bool presync) {} @@ -1046,11 +1109,11 @@ class ValidationInterface virtual void BlockChecked(Block block, BlockValidationStateView state) {} - virtual void PowValidBlock(BlockTreeEntry entry, Block block) {} + virtual void PowValidBlock(Chain chain, Block block) {} - virtual void BlockConnected(Block block, BlockTreeEntry entry) {} + virtual void BlockConnected(Block block, Chain chain) {} - virtual void BlockDisconnected(Block block, BlockTreeEntry entry) {} + virtual void BlockDisconnected(Block block, Chain chain) {} }; class ChainParams : public Handle @@ -1086,7 +1149,7 @@ class ContextOptions : public UniqueHandle(user_data); }, - .block_tip = +[](void* user_data, btck_SynchronizationState state, const btck_BlockTreeEntry* entry, double verification_progress) { (*static_cast(user_data))->BlockTipHandler(static_cast(state), BlockTreeEntry{entry}, verification_progress); }, + .block_tip = +[](void* user_data, btck_SynchronizationState state, const btck_Chain* chain, double verification_progress) { (*static_cast(user_data))->BlockTipHandler(static_cast(state), Chain{chain}, verification_progress); }, .header_tip = +[](void* user_data, btck_SynchronizationState state, int64_t height, int64_t timestamp, int presync) { (*static_cast(user_data))->HeaderTipHandler(static_cast(state), height, timestamp, presync == 1); }, .progress = +[](void* user_data, const char* title, size_t title_len, int progress_percent, int resume_possible) { (*static_cast(user_data))->ProgressHandler({title, title_len}, progress_percent, resume_possible == 1); }, .warning_set = +[](void* user_data, btck_Warning warning, const char* message, size_t message_len) { (*static_cast(user_data))->WarningSetHandler(static_cast(warning), {message, message_len}); }, @@ -1108,9 +1171,9 @@ class ContextOptions : public UniqueHandle(user_data); }, .block_checked = +[](void* user_data, btck_Block* block, const btck_BlockValidationState* state) { (*static_cast(user_data))->BlockChecked(Block{block}, BlockValidationStateView{state}); }, - .pow_valid_block = +[](void* user_data, btck_Block* block, const btck_BlockTreeEntry* entry) { (*static_cast(user_data))->PowValidBlock(BlockTreeEntry{entry}, Block{block}); }, - .block_connected = +[](void* user_data, btck_Block* block, const btck_BlockTreeEntry* entry) { (*static_cast(user_data))->BlockConnected(Block{block}, BlockTreeEntry{entry}); }, - .block_disconnected = +[](void* user_data, btck_Block* block, const btck_BlockTreeEntry* entry) { (*static_cast(user_data))->BlockDisconnected(Block{block}, BlockTreeEntry{entry}); }, + .pow_valid_block = +[](void* user_data, btck_Block* block, const btck_Chain* chain) { (*static_cast(user_data))->PowValidBlock(Chain{chain}, Block{block}); }, + .block_connected = +[](void* user_data, btck_Block* block, const btck_Chain* chain) { (*static_cast(user_data))->BlockConnected(Block{block}, Chain{chain}); }, + .block_disconnected = +[](void* user_data, btck_Block* block, const btck_Chain* chain) { (*static_cast(user_data))->BlockDisconnected(Block{block}, Chain{chain}); }, }); } }; @@ -1159,37 +1222,6 @@ class ChainstateManagerOptions : public UniqueHandle -{ -public: - explicit ChainView(const btck_Chain* ptr) : View{ptr} {} - - int32_t Height() const - { - return btck_chain_get_height(get()); - } - - int32_t CountEntries() const - { - return btck_chain_get_height(get()) + 1; - } - - BlockTreeEntry GetByHeight(int32_t 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 class CoinApi { @@ -1325,33 +1357,32 @@ class ChainMan : UniqueHandle GetBlockTreeEntry(const BlockHash& block_hash) const + Chain Find(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; + auto chain{btck_chainstate_manager_find(get(), block_hash.get())}; + return Chain{chain}; } - BlockTreeEntry GetBestEntry() const + Chain GetBestChain() const { - return btck_chainstate_manager_get_best_entry(get()); + return Chain{btck_chainstate_manager_get_best_chain(get())}; } - std::optional ReadBlock(const BlockTreeEntry& entry) const + std::optional ReadBlock(const Chain& chain) const { - auto block{btck_block_read(get(), entry.get())}; + auto block{btck_block_read(get(), chain.get())}; if (!block) return std::nullopt; return block; } - BlockSpentOutputs ReadBlockSpentOutputs(const BlockTreeEntry& entry) const + BlockSpentOutputs ReadBlockSpentOutputs(const Chain& chain) const { - return btck_block_spent_outputs_read(get(), entry.get()); + return btck_block_spent_outputs_read(get(), chain.get()); } }; diff --git a/src/test/kernel/test_kernel.cpp b/src/test/kernel/test_kernel.cpp index 9050719c372c..152af702d91e 100644 --- a/src/test/kernel/test_kernel.cpp +++ b/src/test/kernel/test_kernel.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -200,17 +201,17 @@ class TestValidationInterface : public ValidationInterface } } - void BlockConnected(Block block, BlockTreeEntry entry) override + void BlockConnected(Block block, Chain chain) override { std::cout << "Block connected." << std::endl; } - void PowValidBlock(BlockTreeEntry entry, Block block) override + void PowValidBlock(Chain chain, Block block) override { std::cout << "Block passed pow verification" << std::endl; } - void BlockDisconnected(Block block, BlockTreeEntry entry) override + void BlockDisconnected(Block block, Chain chain) override { std::cout << "Block disconnected." << std::endl; } @@ -822,33 +823,33 @@ void chainman_reindex_test(TestDirectory& test_directory) // Sanity check some block retrievals auto chain{chainman->GetChain()}; - BOOST_CHECK_THROW(chain.GetByHeight(1000), std::runtime_error); - auto genesis_index{chain.Entries().front()}; - 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()}; + BOOST_CHECK(!chain.GetAncestor(1000)); // out-of-range ancestor is the empty chain + auto genesis{chain.GetAncestor(0)}; + BOOST_CHECK_EQUAL(genesis.Height(), 0); + auto genesis_block_raw{chainman->ReadBlock(genesis).value().ToBytes()}; + auto first{chain.GetAncestor(0)}; + auto first_block_raw{chainman->ReadBlock(genesis).value().ToBytes()}; check_equal(genesis_block_raw, first_block_raw); - auto height{first_index.GetHeight()}; + auto height{first.Height()}; 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.Entries().back()}; - 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 next{chain.GetAncestor(first.Height() + 1)}; + BOOST_CHECK(chain.StartsWith(next)); + auto next_block_data{chainman->ReadBlock(next).value().ToBytes()}; + auto tip{chain.GetAncestor(chain.Height())}; + auto tip_block_data{chainman->ReadBlock(tip).value().ToBytes()}; + auto second{chain.GetAncestor(1)}; + auto second_block{chainman->ReadBlock(second).value()}; auto second_block_data{second_block.ToBytes()}; - auto second_height{second_index.GetHeight()}; + auto second_height{second.Height()}; 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_hash{second.TipHash()}; + auto another_second{chainman->Find(second_hash)}; + BOOST_CHECK(another_second); // found: non-empty chain + auto another_second_height{another_second.Height()}; auto second_block_hash{second_block.GetHash()}; check_equal(second_block_hash.ToBytes(), second_hash.ToBytes()); BOOST_CHECK_EQUAL(second_height, another_second_height); @@ -912,19 +913,20 @@ void chainman_mainnet_validation_test(TestDirectory& test_directory) auto chain{chainman->GetChain()}; BOOST_CHECK_EQUAL(chain.Height(), 1); - auto tip{chain.Entries().back()}; + auto tip{chain.GetAncestor(chain.Height())}; auto read_block{chainman->ReadBlock(tip)}; BOOST_REQUIRE(read_block); check_equal(read_block.value().ToBytes(), raw_block); // Check that we can read the previous block - BlockTreeEntry tip_2{*tip.GetPrevious()}; + Chain tip_2{tip.Parent()}; 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.GetPrevious()); + // The genesis block has no parent + BOOST_CHECK_EQUAL(tip_2.Height(), 0); + BOOST_CHECK(!tip_2.Parent()); // If we try to validate it again, it should be a duplicate BOOST_CHECK(chainman->ProcessBlock(block, &new_block)); @@ -1013,9 +1015,9 @@ BOOST_AUTO_TEST_CASE(btck_block_hash_tests) CheckHandle(block_hash, block_hash_2); } -BOOST_AUTO_TEST_CASE(btck_block_tree_entry_tests) +BOOST_AUTO_TEST_CASE(btck_chain_tests) { - auto test_directory{TestDirectory{"block_tree_entry_test_bitcoin_kernel"}}; + auto test_directory{TestDirectory{"chain_test_bitcoin_kernel"}}; auto notifications{std::make_shared()}; auto context{create_context(notifications, ChainType::REGTEST)}; auto chainman{create_chainman( @@ -1035,29 +1037,151 @@ BOOST_AUTO_TEST_CASE(btck_block_tree_entry_tests) } auto chain{chainman->GetChain()}; - auto entry_0{chain.GetByHeight(0)}; - auto entry_1{chain.GetByHeight(1)}; - auto entry_2{chain.GetByHeight(2)}; + auto entry_0{chain.GetAncestor(0)}; + auto entry_1{chain.GetAncestor(1)}; + auto entry_2{chain.GetAncestor(2)}; // Test inequality BOOST_CHECK(entry_0 != entry_1); BOOST_CHECK(entry_1 != entry_2); BOOST_CHECK(entry_0 != entry_2); - // Test equality with same entry - BOOST_CHECK(entry_0 == chain.GetByHeight(0)); - BOOST_CHECK(entry_0 == BlockTreeEntry{entry_0}); + // Test equality with the same chain + BOOST_CHECK(entry_0 == chain.GetAncestor(0)); + BOOST_CHECK(entry_0 == Chain{entry_0}); BOOST_CHECK(entry_1 == entry_1); - // Test GetPrevious - auto prev{entry_1.GetPrevious()}; - BOOST_CHECK(prev.has_value()); - BOOST_CHECK(prev.value() == entry_0); + // Test the previous block via the O(1) parent chain + auto prev{entry_1.Parent()}; + BOOST_CHECK(prev); // non-empty: entry_1 has a parent + BOOST_CHECK(prev == entry_0); + BOOST_CHECK(!entry_0.Parent()); // genesis: parent is the empty chain // Test GetAncestor BOOST_CHECK(entry_2.GetAncestor(2) == entry_2); BOOST_CHECK(entry_2.GetAncestor(1) == entry_1); BOOST_CHECK(entry_2.GetAncestor(0) == entry_0); + + // Test StartsWith + BOOST_CHECK(entry_2.StartsWith(entry_1)); + BOOST_CHECK(entry_2.StartsWith(entry_0)); + BOOST_CHECK(!entry_0.StartsWith(entry_2)); + + // Test FindFork (the common prefix / fork point) + BOOST_CHECK(entry_2.FindFork(entry_1) == entry_1); + BOOST_CHECK(entry_0.FindFork(entry_2) == entry_0); + + // Test Mismatch (the first differing block on each side, returning iterators + // just like std::ranges::mismatch over the chains' entries). + // entry_2 vs entry_1: equal up to height 1; entry_2 diverges with its + // height-2 block, entry_1 is exhausted (a prefix, so its side is end()). + auto mm{entry_2.Mismatch(entry_1)}; + BOOST_CHECK(mm.in1 != entry_2.end()); + BOOST_CHECK(*mm.in1 == entry_2.TipHeader()); // header of entry_2's diverging block + BOOST_CHECK(mm.in2 == entry_1.end()); + + // entry_0 vs entry_2: entry_0 (genesis only) is exhausted at the fork (end()); + // entry_2 diverges with its height-1 block. + auto mm2{entry_0.Mismatch(entry_2)}; + BOOST_CHECK(mm2.in1 == entry_0.end()); + BOOST_CHECK(mm2.in2 != entry_2.end()); + BOOST_CHECK(*mm2.in2 == entry_1.TipHeader()); // header at entry_2's height-1 block + + // Two identical chains have no divergence: both sides reach end(). + auto mm3{entry_2.Mismatch(entry_2)}; + BOOST_CHECK(mm3.in1 == entry_2.end()); + BOOST_CHECK(mm3.in2 == entry_2.end()); +} + +BOOST_AUTO_TEST_CASE(btck_chain_empty_tests) +{ + auto test_directory{TestDirectory{"chain_empty_test_bitcoin_kernel"}}; + auto notifications{std::make_shared()}; + auto context{create_context(notifications, ChainType::REGTEST)}; + auto chainman{create_chainman( + test_directory, + /*reindex=*/false, + /*wipe_chainstate=*/false, + /*block_tree_db_in_memory=*/true, + /*chainstate_db_in_memory=*/true, + context)}; + + for (size_t i{0}; i < 3; i++) { + Block block{hex_string_to_byte_vec(REGTEST_BLOCK_DATA[i])}; + bool new_block{false}; + chainman->ProcessBlock(block, &new_block); + BOOST_CHECK(new_block); + } + + auto chain{chainman->GetChain()}; + auto genesis{chain.GetAncestor(0)}; + + // Find with a hash that is not in the block index yields the empty chain, + // observable through operator bool() / empty() rather than std::optional. + BlockHash unknown{std::array{}}; + auto empty{chainman->Find(unknown)}; + BOOST_CHECK(!empty); + BOOST_CHECK(empty.empty()); + BOOST_CHECK_EQUAL(empty.size(), 0u); + BOOST_CHECK_EQUAL(empty.Height(), -1); + BOOST_CHECK(empty.begin() == empty.end()); + + // A real chain is non-empty. + BOOST_CHECK(chain); + BOOST_CHECK(!chain.empty()); + + // Navigating off the ends of a chain produces the empty chain. + BOOST_CHECK(!genesis.Parent()); // genesis has no parent + BOOST_CHECK(!chain.GetAncestor(1000)); // height out of range + BOOST_CHECK(!chain.GetAncestor(-1)); // height out of range + + // The empty chain navigates to itself and equals other empty chains. + BOOST_CHECK(empty == genesis.Parent()); + BOOST_CHECK(!empty.Parent()); + BOOST_CHECK(!empty.GetAncestor(0)); + + // StartsWith: the empty chain is a prefix of every chain (including itself), + // but no non-empty chain is a prefix of the empty chain. + BOOST_CHECK(chain.StartsWith(empty)); + BOOST_CHECK(empty.StartsWith(empty)); + BOOST_CHECK(!empty.StartsWith(chain)); + + // The empty chain has no tip block to read. + BOOST_CHECK(!chainman->ReadBlock(empty)); +} + +BOOST_AUTO_TEST_CASE(btck_chain_mismatch_matches_std_ranges) +{ + auto test_directory{TestDirectory{"chain_mismatch_test_bitcoin_kernel"}}; + auto notifications{std::make_shared()}; + auto context{create_context(notifications, ChainType::REGTEST)}; + auto chainman{create_chainman( + test_directory, + /*reindex=*/false, + /*wipe_chainstate=*/false, + /*block_tree_db_in_memory=*/true, + /*chainstate_db_in_memory=*/true, + context)}; + + for (size_t i{0}; i < 3; i++) { + Block block{hex_string_to_byte_vec(REGTEST_BLOCK_DATA[i])}; + bool new_block{false}; + chainman->ProcessBlock(block, &new_block); + BOOST_CHECK(new_block); + } + + auto chain{chainman->GetChain()}; + auto chain1{chain.GetAncestor(2)}; + auto chain2{chain.GetAncestor(1)}; + + // The member Chain::Mismatch is exactly std::ranges::mismatch run over + // the two chains (which are random-access ranges of their block headers): + // same result type, same iterators. + auto res1 = chain1.Mismatch(chain2); + auto res2 = std::ranges::mismatch(chain1, chain2); + static_assert(std::same_as); + BOOST_CHECK(res1.in1 == res2.in1); + BOOST_CHECK(res1.in2 == res2.in2); } BOOST_AUTO_TEST_CASE(btck_chainman_in_memory_tests) @@ -1101,11 +1225,11 @@ BOOST_AUTO_TEST_CASE(btck_chainman_regtest_tests) BlockValidationState state = chainman->ProcessBlockHeader(header); BOOST_CHECK(state.GetValidationMode() == ValidationMode::VALID); BOOST_CHECK(state.GetBlockValidationResult() == BlockValidationResult::UNSET); - BlockTreeEntry entry{*chainman->GetBlockTreeEntry(header.Hash())}; - BOOST_CHECK(!chainman->GetChain().Contains(entry)); - BlockTreeEntry best_entry{chainman->GetBestEntry()}; - BlockHash hash{entry.GetHash()}; - BOOST_CHECK(hash == best_entry.GetHeader().Hash()); + Chain found{chainman->Find(header.Hash())}; + BOOST_CHECK(!chainman->GetChain().StartsWith(found)); + Chain best{chainman->GetBestChain()}; + BlockHash hash{found.TipHash()}; + BOOST_CHECK(hash == best.TipHeader().Hash()); } } @@ -1138,11 +1262,11 @@ BOOST_AUTO_TEST_CASE(btck_chainman_regtest_tests) } auto chain = chainman->GetChain(); - auto tip = chain.Entries().back(); + auto tip = chain.GetAncestor(chain.Height()); auto read_block = chainman->ReadBlock(tip).value(); check_equal(read_block.ToBytes(), hex_string_to_byte_vec(REGTEST_BLOCK_DATA[REGTEST_BLOCK_DATA.size() - 1])); - auto tip_2 = tip.GetPrevious().value(); + auto tip_2 = tip.Parent(); 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])); @@ -1154,8 +1278,8 @@ BOOST_AUTO_TEST_CASE(btck_chainman_regtest_tests) auto find_transaction = [&chainman](const TxidView& target_txid) -> std::optional { auto chain = chainman->GetChain(); - for (const auto block_tree_entry : chain.Entries()) { - auto block{chainman->ReadBlock(block_tree_entry)}; + for (int32_t height = 0; height <= chain.Height(); ++height) { + auto block{chainman->ReadBlock(chain.GetAncestor(height))}; for (const TransactionView transaction : block->Transactions()) { if (transaction.Txid() == target_txid) { return Transaction{transaction}; @@ -1165,8 +1289,8 @@ BOOST_AUTO_TEST_CASE(btck_chainman_regtest_tests) return std::nullopt; }; - for (const auto block_tree_entry : chain.Entries()) { - auto block{chainman->ReadBlock(block_tree_entry)}; + for (int32_t height = 0; height <= chain.Height(); ++height) { + auto block{chainman->ReadBlock(chain.GetAncestor(height))}; for (const auto transaction : block->Transactions()) { std::vector inputs; std::vector spent_outputs; @@ -1193,7 +1317,7 @@ BOOST_AUTO_TEST_CASE(btck_chainman_regtest_tests) // 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())}; + BlockSpentOutputs block_spent_outputs_prev{chainman->ReadBlockSpentOutputs(tip.Parent())}; 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); @@ -1231,10 +1355,8 @@ 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)}; + for (int32_t height = 0; height <= chain.Height(); ++height) { + std::optional block{chainman->ReadBlock(chain.GetAncestor(height))}; if (block) { for (const TransactionView transaction : block->Transactions()) { for (const TransactionOutputView output : transaction.Outputs()) { @@ -1248,9 +1370,11 @@ BOOST_AUTO_TEST_CASE(btck_chainman_regtest_tests) } } + // Iterating the chain yields its block headers in order; the header at each + // position matches the tip header of the sub-chain ending at that height. int32_t count{0}; - for (const auto entry : chain.Entries()) { - BOOST_CHECK_EQUAL(entry.GetHeight(), count); + for (const auto header : chain) { + BOOST_CHECK(header == chain.GetAncestor(count).TipHeader()); ++count; } BOOST_CHECK_EQUAL(count, chain.CountEntries());