From 2e09d66fbb7bb253ce90ffcda026ce58426ba4e4 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Wed, 1 Oct 2025 13:50:33 -0400 Subject: [PATCH 1/2] tests: add unit tests for CBlockIndex::GetAncestor and LastCommonAncestor --- src/test/CMakeLists.txt | 1 + src/test/chain_tests.cpp | 85 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 src/test/chain_tests.cpp diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index aacefb3f85c..b4c1ec69339 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -26,6 +26,7 @@ add_executable(test_bitcoin bloom_tests.cpp bswap_tests.cpp caches_tests.cpp + chain_tests.cpp chainstate_write_tests.cpp checkqueue_tests.cpp cluster_linearize_tests.cpp diff --git a/src/test/chain_tests.cpp b/src/test/chain_tests.cpp new file mode 100644 index 00000000000..a782e880f9f --- /dev/null +++ b/src/test/chain_tests.cpp @@ -0,0 +1,85 @@ +// Copyright (c) The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include + +#include + +BOOST_FIXTURE_TEST_SUITE(chain_tests, BasicTestingSetup) + +namespace { + +const CBlockIndex* NaiveGetAncestor(const CBlockIndex* a, int height) +{ + while (a->nHeight > height) { + a = a->pprev; + } + BOOST_REQUIRE_EQUAL(a->nHeight, height); + return a; +} + +const CBlockIndex* NaiveLastCommonAncestor(const CBlockIndex* a, const CBlockIndex* b) +{ + while (a->nHeight > b->nHeight) { + a = a->pprev; + } + while (b->nHeight > a->nHeight) { + b = b->pprev; + } + while (a != b) { + BOOST_REQUIRE_EQUAL(a->nHeight, b->nHeight); + a = a->pprev; + b = b->pprev; + } + BOOST_REQUIRE_EQUAL(a, b); + return a; +} + +} // namespace + +BOOST_AUTO_TEST_CASE(chain_test) +{ + FastRandomContext ctx; + std::vector> block_index; + // Run 10 iterations of the whole test. + for (int i = 0; i < 10; ++i) { + block_index.clear(); + // Create genesis block. + auto genesis = std::make_unique(); + genesis->nHeight = 0; + block_index.push_back(std::move(genesis)); + // Create 10000 more blocks. + for (int b = 0; b < 10000; ++b) { + auto new_index = std::make_unique(); + // 95% of blocks build on top of the last block; the others fork off randomly. + if (ctx.randrange(20) != 0) { + new_index->pprev = block_index.back().get(); + } else { + new_index->pprev = block_index[ctx.randrange(block_index.size())].get(); + } + new_index->nHeight = new_index->pprev->nHeight + 1; + new_index->BuildSkip(); + block_index.push_back(std::move(new_index)); + } + // Run 10000 random GetAncestor queries. + for (int q = 0; q < 10000; ++q) { + const CBlockIndex* block = block_index[ctx.randrange(block_index.size())].get(); + unsigned height = ctx.randrange(block->nHeight + 1); + const CBlockIndex* result = block->GetAncestor(height); + BOOST_CHECK(result == NaiveGetAncestor(block, height)); + } + // Run 10000 random LastCommonAncestor queries. + for (int q = 0; q < 10000; ++q) { + const CBlockIndex* block1 = block_index[ctx.randrange(block_index.size())].get(); + const CBlockIndex* block2 = block_index[ctx.randrange(block_index.size())].get(); + const CBlockIndex* result = LastCommonAncestor(block1, block2); + BOOST_CHECK(result == NaiveLastCommonAncestor(block1, block2)); + } + } +} + +BOOST_AUTO_TEST_SUITE_END() From 3635d62f5a935801e26a0d5fa2cb5e2dbbb42f9b Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Wed, 1 Oct 2025 13:51:22 -0400 Subject: [PATCH 2/2] chain: make use of pskip in LastCommonAncestor (optimization) By using the pskip pointer, which regularly allows jumping back much faster than pprev, the forking point between two CBlockIndex entries can be found much faster. A simulation shows that no more than 136 steps are needed to jump anywhere within the first 2^20 block heights, and on average 65 jumps for uniform forking points around that height. --- src/chain.cpp | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/chain.cpp b/src/chain.cpp index 4e2d1bf0ac8..3dd22634110 100644 --- a/src/chain.cpp +++ b/src/chain.cpp @@ -5,6 +5,7 @@ #include #include +#include #include std::string CBlockFileInfo::ToString() const @@ -158,18 +159,26 @@ int64_t GetBlockProofEquivalentTime(const CBlockIndex& to, const CBlockIndex& fr /** Find the last common ancestor two blocks have. * Both pa and pb must be non-nullptr. */ const CBlockIndex* LastCommonAncestor(const CBlockIndex* pa, const CBlockIndex* pb) { + // First rewind to the last common height (the forking point cannot be past one of the two). if (pa->nHeight > pb->nHeight) { pa = pa->GetAncestor(pb->nHeight); } else if (pb->nHeight > pa->nHeight) { pb = pb->GetAncestor(pa->nHeight); } - - while (pa != pb && pa && pb) { + while (pa != pb) { + // Jump back until pa and pb have a common "skip" ancestor. + while (pa->pskip != pb->pskip) { + // This logic relies on the property that equal-height blocks have equal-height skip + // pointers. + Assume(pa->nHeight == pb->nHeight); + Assume(pa->pskip->nHeight == pb->pskip->nHeight); + pa = pa->pskip; + pb = pb->pskip; + } + // At this point, pa and pb are different, but have equal pskip. The forking point lies in + // between pa/pb on the one end, and pa->pskip/pb->pskip on the other end. pa = pa->pprev; pb = pb->pprev; } - - // Eventually all chain branches meet at the genesis block. - assert(pa == pb); return pa; }