From 04b3c8f4e7501c57301f1ac477a317f9c679e150 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Fri, 19 Jun 2026 09:33:21 +0000 Subject: [PATCH] =?UTF-8?q?nmc(P1f):=20HeaderChain::get=5Flocator=20?= =?UTF-8?q?=E2=80=94=20BIP31=20block=20locator=20over=20tip=20ancestry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build a getheaders block locator by walking the tip ancestry through m_previous_block with BIP31-style exponential backoff: dense near the tip, doubling the stride past the first 10 entries, chain root always anchored. Walking parent links follows the best chain we just fork-chose, so the locator stays correct across reorgs without a separate height index. Mirrors src/impl/btc/coin/header_chain.hpp. +6 KATs (empty / single / dense-short / long-contiguous-anchored / backoff / follows-tip-after-reorg); suite 52 -> 58 green. --- src/impl/nmc/coin/header_chain.hpp | 39 ++++++++- src/impl/nmc/test/auxpow_merkle_test.cpp | 103 +++++++++++++++++++++++ 2 files changed, 140 insertions(+), 2 deletions(-) diff --git a/src/impl/nmc/coin/header_chain.hpp b/src/impl/nmc/coin/header_chain.hpp index 33c515d2f..efd682d09 100644 --- a/src/impl/nmc/coin/header_chain.hpp +++ b/src/impl/nmc/coin/header_chain.hpp @@ -638,6 +638,41 @@ class HeaderChain { return it->second; } + /// P1f: build a block locator for getheaders (BIP 31-style exponential + /// backoff) by walking the tip's ancestry through m_previous_block. Walking + /// parent links always follows the BEST chain we just fork-chose, so the + /// locator stays correct across reorgs without a separate height index. + /// Returns hashes tip..root: dense near the tip, doubling the stride after + /// the first 10 entries, with the chain root always anchoring the list. + std::vector get_locator() const { + std::lock_guard lock(m_mutex); + std::vector locator; + if (m_tip.IsNull()) return locator; + + // Materialise the tip's ancestry (tip..root) via parent links. + std::vector ancestry; + uint256 cursor = m_tip; + while (true) { + auto it = m_index.find(cursor); + if (it == m_index.end()) break; // dangling link -- stop cleanly + ancestry.push_back(cursor); + if (it->second.height == 0) break; // reached chain root + cursor = it->second.header.m_previous_block; + } + + // BIP 31-style exponential backoff over the ancestry. + int64_t step = 1; + for (size_t i = 0; i < ancestry.size(); i += static_cast(step)) { + locator.push_back(ancestry[i]); + if (locator.size() > 10) + step *= 2; + } + // Guarantee the chain root anchors the locator. + if (locator.back() != ancestry.back()) + locator.push_back(ancestry.back()); + return locator; + } + /// Add a single plain header. /// P0-DEFER: NO difficulty validation, NO PoW check, NO connection check, /// NO persistence. Structural skeleton only — returns false. @@ -827,8 +862,8 @@ class HeaderChain { uint256 m_best_work; // P0-DEFER: difficulty validation, AuxPow verification, LevelDB - // persistence, height-index rebuild, and locator generation are all - // deliberately absent in this structural leaf. + // persistence and height-index rebuild are deliberately absent in this + // structural leaf (locator generation landed in P1f). }; } // namespace coin diff --git a/src/impl/nmc/test/auxpow_merkle_test.cpp b/src/impl/nmc/test/auxpow_merkle_test.cpp index ac61dd12b..3f81cfa1c 100644 --- a/src/impl/nmc/test/auxpow_merkle_test.cpp +++ b/src/impl/nmc/test/auxpow_merkle_test.cpp @@ -1013,4 +1013,107 @@ TEST(NmcP1eForkChoice, FirstHeaderBecomesTipRegardlessOfPriorState) EXPECT_EQ(chain_.cumulative_work(), nmc::coin::get_block_proof(kEasyBits)); } +// ── P1f: HeaderChain::get_locator — BIP31 block locator over tip ancestry ── + +TEST(NmcP1fLocator, EmptyChainProducesEmptyLocator) +{ + HeaderChain chain_(params_activation(19200)); + EXPECT_TRUE(chain_.get_locator().empty()); +} + +TEST(NmcP1fLocator, SingleHeaderLocatorIsJustTheTip) +{ + HeaderChain chain_(params_activation(19200)); + uint256 z; z.SetNull(); + BlockHeaderType g = plain_header(z, kEasyBits, 1); + ASSERT_TRUE(chain_.add_header(g)); + auto loc = chain_.get_locator(); + ASSERT_EQ(loc.size(), 1u); + EXPECT_EQ(loc.front(), block_hash(g)); +} + +TEST(NmcP1fLocator, ShortChainLocatorIsDenseTipToRoot) +{ + HeaderChain chain_(params_activation(19200)); + uint256 z; z.SetNull(); + BlockHeaderType g = plain_header(z, kEasyBits, 1); + ASSERT_TRUE(chain_.add_header(g)); + BlockHeaderType a1 = plain_header(block_hash(g), kEasyBits, 2); + ASSERT_TRUE(chain_.add_header(a1)); + BlockHeaderType a2 = plain_header(block_hash(a1), kEasyBits, 3); + ASSERT_TRUE(chain_.add_header(a2)); + auto loc = chain_.get_locator(); + ASSERT_EQ(loc.size(), 3u); // dense: every block while <= 10 deep + EXPECT_EQ(loc[0], block_hash(a2)); // tip first + EXPECT_EQ(loc[1], block_hash(a1)); + EXPECT_EQ(loc[2], block_hash(g)); // root last +} + +TEST(NmcP1fLocator, LongChainFrontIsContiguousAndAnchoredAtRoot) +{ + HeaderChain chain_(params_activation(19200)); + uint256 z; z.SetNull(); + BlockHeaderType g = plain_header(z, kEasyBits, 1); + ASSERT_TRUE(chain_.add_header(g)); + std::vector hashes{block_hash(g)}; + uint256 prev = block_hash(g); + for (uint32_t i = 1; i <= 30; ++i) { // height 0..30 (31 headers) + BlockHeaderType h = plain_header(prev, kEasyBits, i + 1); + ASSERT_TRUE(chain_.add_header(h)); + prev = block_hash(h); + hashes.push_back(prev); + } + auto loc = chain_.get_locator(); + EXPECT_EQ(chain_.height(), 30u); + // First 11 entries are contiguous from the tip (step stays 1 until >10). + for (size_t i = 0; i < 11u; ++i) + EXPECT_EQ(loc[i], hashes[hashes.size() - 1 - i]); + // Backoff makes the locator far shorter than the chain, root-anchored. + EXPECT_LT(loc.size(), hashes.size()); + EXPECT_EQ(loc.back(), block_hash(g)); +} + +TEST(NmcP1fLocator, BackoffStrideDoublesPastTheDenseHead) +{ + HeaderChain chain_(params_activation(19200)); + uint256 z; z.SetNull(); + BlockHeaderType g = plain_header(z, kEasyBits, 1); + ASSERT_TRUE(chain_.add_header(g)); + uint256 prev = block_hash(g); + for (uint32_t i = 1; i <= 30; ++i) { + BlockHeaderType h = plain_header(prev, kEasyBits, i + 1); + ASSERT_TRUE(chain_.add_header(h)); + prev = block_hash(h); + } + auto loc = chain_.get_locator(); + // No duplicates, strictly descending toward the root, all known to the chain. + for (size_t i = 0; i < loc.size(); ++i) + EXPECT_TRUE(chain_.has_header(loc[i])); + for (size_t i = 1; i < loc.size(); ++i) + EXPECT_NE(loc[i], loc[i - 1]); +} + +TEST(NmcP1fLocator, LocatorFollowsTheTipAfterAWorkReorg) +{ + HeaderChain chain_(params_activation(19200)); + uint256 z; z.SetNull(); + BlockHeaderType g = plain_header(z, kEasyBits, 1); + ASSERT_TRUE(chain_.add_header(g)); + // Light branch a1<-a2 builds the initial tip. + BlockHeaderType a1 = plain_header(block_hash(g), kEasyBits, 2); + ASSERT_TRUE(chain_.add_header(a1)); + BlockHeaderType a2 = plain_header(block_hash(a1), kEasyBits, 3); + ASSERT_TRUE(chain_.add_header(a2)); + // One heavy sibling at height 1 reorgs the tip onto branch b. + BlockHeaderType b1 = plain_header(block_hash(g), kHardBits, 4); + ASSERT_TRUE(chain_.add_header(b1)); + ASSERT_EQ(chain_.tip()->block_hash, block_hash(b1)); + auto loc = chain_.get_locator(); + // Locator tracks the NEW tip's ancestry (b1 <- g), not the abandoned a-branch. + ASSERT_EQ(loc.size(), 2u); + EXPECT_EQ(loc[0], block_hash(b1)); + EXPECT_EQ(loc[1], block_hash(g)); + EXPECT_EQ(std::count(loc.begin(), loc.end(), block_hash(a2)), 0); +} + } // namespace