Skip to content

Kernel: Remove BlockTreeEntry from the API#43

Open
janb84 wants to merge 1 commit into
2140-dev:masterfrom
janb84:2140/blocktreeentry-chain-equivalence
Open

Kernel: Remove BlockTreeEntry from the API#43
janb84 wants to merge 1 commit into
2140-dev:masterfrom
janb84:2140/blocktreeentry-chain-equivalence

Conversation

@janb84

@janb84 janb84 commented Jun 9, 2026

Copy link
Copy Markdown

This PR replaces the btck_BlockTreeEntry type in the kernel C API with a single chain-oriented abstraction, btck_Chain, matching the end state described in https://purplekarrot.net/blog/drop-block-tree-entry-from-bitcoin-kernel.html.

Since a block uniquely determines its path to genesis, a chain is fully determined by its tip and shares the same underlying CBlockIndex. Consolidating on btck_Chain also better hides the kernel's internals, exposing an opaque chain handle rather than leaking the block-index structure into the API (leaving the kernel free to change that without breaking the C API).

New chain functions:

btck_chainstate_manager_get_active_chain / _get_best_chain / _find -> return chains
btck_chain_get_height
btck_chain_get_block_header / _get_block_hash / _get_ancestor
btck_chain_get_parent
btck_chain_starts_with / _find_fork / _equals

This design follows @purpleKarrot's writeup and parts of the approach are based on his blogpost ofc. happy to add a Co-authored-by: if he's comfortable with it (quality wise) .

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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the block can be retrieved from the chain tip, should block even be passed in the callback?
Also, I wonder why the block is mutable here. Is this callback supposed to be able to modify the block?

Comment thread src/kernel/bitcoinkernel_wrapper.h Outdated
};

class BlockTreeEntry : public View<btck_BlockTreeEntry>
class ChainView : public View<btck_Chain>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ChainView should inherit from std::ranges::view_interface<ChainView>.

Comment thread src/kernel/bitcoinkernel_wrapper.h Outdated

BlockTreeEntry GetAncestor(int32_t height) const
//! The longest common prefix of this chain and another, beginning at genesis.
std::optional<ChainView> Mismatch(const ChainView& other) const

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mismatch should return std::ranges::mismatch_result.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes ok but performance, so what do you think about the compromise below. std::ranges::mismatch is O(n) this solution is O(log n) the 'compare' is walking from genesis to tip to find a mismatch (also side rant why from genesis ? such a deep reorg is unfeasible)

    std::optional<ChainView> FindFork(const ChainView& other) const
    {
        auto fork{btck_chain_find_fork(get(), other.get())};
        if (!fork) return std::nullopt;
        return ChainView{fork};
    }


    std::ranges::mismatch_result<std::optional<ChainView>, std::optional<ChainView>>
    Mismatch(const ChainView& other) const
    {
        auto fork{FindFork(other)};
        int32_t fork_height{fork ? fork->Height() : -1};
        std::optional<ChainView> in1{fork_height < Height() ? std::optional{GetAncestor(fork_height + 1)} : std::nullopt};
        std::optional<ChainView> in2{fork_height < other.Height() ? std::optional{other.GetAncestor(fork_height + 1)} : std::nullopt};
        return {in1, in2};
    }

(in this case btck_chain_mismatch() is renamed to btck_chain_find_fork() which also matches the result better)

@josibake josibake Jun 11, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes ok but performance

Knuth would like a word ;)

Take what I say with a grain of salt as I haven't dug into the code yet, but I am suspicious of performance if it sacrifices legibility and makes this hard to test. Even more suspicious if its not in a critical path (e.g., mining, IBD, block validation, etc).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What the mismatch member function does internally is an implementation detail and can be changed at any time. The only promise it should make is that it does produces the exact same outcome as std::ranges::mismatch but with a potentially better runtime complexity than $O(n)$.

There should be a test that covers this:

TEST_CASE("mismatch member matches standard free function")
{
  auto chain1 = ...;
  auto chain2 = ...;
  auto res1 = chain1.mismatch(chain2);
  auto res2 = std::ranges::mismatch(chain1, chain2);
  CHECK(std::same_as<decltype(res1), decltype(res2)>);
  CHECK(res1.in1 == res2.in1);
  CHECK(res1.in2 == res2.in2);
}

@janb84 janb84 force-pushed the 2140/blocktreeentry-chain-equivalence branch from b66cc0e to 08ea314 Compare June 10, 2026 08:48
@josibake

Copy link
Copy Markdown
Member

Concept ACK

Nice! I'll read the blog post before reviewing the implementation, but love to see it. This also seems like an excellent candidate to upstream.

@janb84 janb84 force-pushed the 2140/blocktreeentry-chain-equivalence branch from 08ea314 to 4a51f83 Compare June 14, 2026 14:36
@janb84

janb84 commented Jun 14, 2026

Copy link
Copy Markdown
Author

Did a re-read of the blogpost and the comments here, that made me change the approach (a bit) to match the idea of the blogpost better.

Additionally:
ChainView -> Chain.
Removed the std::optional (from Find, Parent, FindFork and GetAncestor) ,by allowing the empty range / empty chain
Mismatch produces the exact same outcome as std::ranges::mismatch

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)).
@janb84 janb84 force-pushed the 2140/blocktreeentry-chain-equivalence branch from 4a51f83 to 70e098a Compare June 14, 2026 18:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants