Skip to content

Commit 04b3c8f

Browse files
committed
nmc(P1f): HeaderChain::get_locator — BIP31 block locator over tip ancestry
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.
1 parent bd1f0e2 commit 04b3c8f

2 files changed

Lines changed: 140 additions & 2 deletions

File tree

src/impl/nmc/coin/header_chain.hpp

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,41 @@ class HeaderChain {
638638
return it->second;
639639
}
640640

641+
/// P1f: build a block locator for getheaders (BIP 31-style exponential
642+
/// backoff) by walking the tip's ancestry through m_previous_block. Walking
643+
/// parent links always follows the BEST chain we just fork-chose, so the
644+
/// locator stays correct across reorgs without a separate height index.
645+
/// Returns hashes tip..root: dense near the tip, doubling the stride after
646+
/// the first 10 entries, with the chain root always anchoring the list.
647+
std::vector<uint256> get_locator() const {
648+
std::lock_guard<std::mutex> lock(m_mutex);
649+
std::vector<uint256> locator;
650+
if (m_tip.IsNull()) return locator;
651+
652+
// Materialise the tip's ancestry (tip..root) via parent links.
653+
std::vector<uint256> ancestry;
654+
uint256 cursor = m_tip;
655+
while (true) {
656+
auto it = m_index.find(cursor);
657+
if (it == m_index.end()) break; // dangling link -- stop cleanly
658+
ancestry.push_back(cursor);
659+
if (it->second.height == 0) break; // reached chain root
660+
cursor = it->second.header.m_previous_block;
661+
}
662+
663+
// BIP 31-style exponential backoff over the ancestry.
664+
int64_t step = 1;
665+
for (size_t i = 0; i < ancestry.size(); i += static_cast<size_t>(step)) {
666+
locator.push_back(ancestry[i]);
667+
if (locator.size() > 10)
668+
step *= 2;
669+
}
670+
// Guarantee the chain root anchors the locator.
671+
if (locator.back() != ancestry.back())
672+
locator.push_back(ancestry.back());
673+
return locator;
674+
}
675+
641676
/// Add a single plain header.
642677
/// P0-DEFER: NO difficulty validation, NO PoW check, NO connection check,
643678
/// NO persistence. Structural skeleton only — returns false.
@@ -827,8 +862,8 @@ class HeaderChain {
827862
uint256 m_best_work;
828863

829864
// P0-DEFER: difficulty validation, AuxPow verification, LevelDB
830-
// persistence, height-index rebuild, and locator generation are all
831-
// deliberately absent in this structural leaf.
865+
// persistence and height-index rebuild are deliberately absent in this
866+
// structural leaf (locator generation landed in P1f).
832867
};
833868

834869
} // namespace coin

src/impl/nmc/test/auxpow_merkle_test.cpp

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,4 +1013,107 @@ TEST(NmcP1eForkChoice, FirstHeaderBecomesTipRegardlessOfPriorState)
10131013
EXPECT_EQ(chain_.cumulative_work(), nmc::coin::get_block_proof(kEasyBits));
10141014
}
10151015

1016+
// ── P1f: HeaderChain::get_locator — BIP31 block locator over tip ancestry ──
1017+
1018+
TEST(NmcP1fLocator, EmptyChainProducesEmptyLocator)
1019+
{
1020+
HeaderChain chain_(params_activation(19200));
1021+
EXPECT_TRUE(chain_.get_locator().empty());
1022+
}
1023+
1024+
TEST(NmcP1fLocator, SingleHeaderLocatorIsJustTheTip)
1025+
{
1026+
HeaderChain chain_(params_activation(19200));
1027+
uint256 z; z.SetNull();
1028+
BlockHeaderType g = plain_header(z, kEasyBits, 1);
1029+
ASSERT_TRUE(chain_.add_header(g));
1030+
auto loc = chain_.get_locator();
1031+
ASSERT_EQ(loc.size(), 1u);
1032+
EXPECT_EQ(loc.front(), block_hash(g));
1033+
}
1034+
1035+
TEST(NmcP1fLocator, ShortChainLocatorIsDenseTipToRoot)
1036+
{
1037+
HeaderChain chain_(params_activation(19200));
1038+
uint256 z; z.SetNull();
1039+
BlockHeaderType g = plain_header(z, kEasyBits, 1);
1040+
ASSERT_TRUE(chain_.add_header(g));
1041+
BlockHeaderType a1 = plain_header(block_hash(g), kEasyBits, 2);
1042+
ASSERT_TRUE(chain_.add_header(a1));
1043+
BlockHeaderType a2 = plain_header(block_hash(a1), kEasyBits, 3);
1044+
ASSERT_TRUE(chain_.add_header(a2));
1045+
auto loc = chain_.get_locator();
1046+
ASSERT_EQ(loc.size(), 3u); // dense: every block while <= 10 deep
1047+
EXPECT_EQ(loc[0], block_hash(a2)); // tip first
1048+
EXPECT_EQ(loc[1], block_hash(a1));
1049+
EXPECT_EQ(loc[2], block_hash(g)); // root last
1050+
}
1051+
1052+
TEST(NmcP1fLocator, LongChainFrontIsContiguousAndAnchoredAtRoot)
1053+
{
1054+
HeaderChain chain_(params_activation(19200));
1055+
uint256 z; z.SetNull();
1056+
BlockHeaderType g = plain_header(z, kEasyBits, 1);
1057+
ASSERT_TRUE(chain_.add_header(g));
1058+
std::vector<uint256> hashes{block_hash(g)};
1059+
uint256 prev = block_hash(g);
1060+
for (uint32_t i = 1; i <= 30; ++i) { // height 0..30 (31 headers)
1061+
BlockHeaderType h = plain_header(prev, kEasyBits, i + 1);
1062+
ASSERT_TRUE(chain_.add_header(h));
1063+
prev = block_hash(h);
1064+
hashes.push_back(prev);
1065+
}
1066+
auto loc = chain_.get_locator();
1067+
EXPECT_EQ(chain_.height(), 30u);
1068+
// First 11 entries are contiguous from the tip (step stays 1 until >10).
1069+
for (size_t i = 0; i < 11u; ++i)
1070+
EXPECT_EQ(loc[i], hashes[hashes.size() - 1 - i]);
1071+
// Backoff makes the locator far shorter than the chain, root-anchored.
1072+
EXPECT_LT(loc.size(), hashes.size());
1073+
EXPECT_EQ(loc.back(), block_hash(g));
1074+
}
1075+
1076+
TEST(NmcP1fLocator, BackoffStrideDoublesPastTheDenseHead)
1077+
{
1078+
HeaderChain chain_(params_activation(19200));
1079+
uint256 z; z.SetNull();
1080+
BlockHeaderType g = plain_header(z, kEasyBits, 1);
1081+
ASSERT_TRUE(chain_.add_header(g));
1082+
uint256 prev = block_hash(g);
1083+
for (uint32_t i = 1; i <= 30; ++i) {
1084+
BlockHeaderType h = plain_header(prev, kEasyBits, i + 1);
1085+
ASSERT_TRUE(chain_.add_header(h));
1086+
prev = block_hash(h);
1087+
}
1088+
auto loc = chain_.get_locator();
1089+
// No duplicates, strictly descending toward the root, all known to the chain.
1090+
for (size_t i = 0; i < loc.size(); ++i)
1091+
EXPECT_TRUE(chain_.has_header(loc[i]));
1092+
for (size_t i = 1; i < loc.size(); ++i)
1093+
EXPECT_NE(loc[i], loc[i - 1]);
1094+
}
1095+
1096+
TEST(NmcP1fLocator, LocatorFollowsTheTipAfterAWorkReorg)
1097+
{
1098+
HeaderChain chain_(params_activation(19200));
1099+
uint256 z; z.SetNull();
1100+
BlockHeaderType g = plain_header(z, kEasyBits, 1);
1101+
ASSERT_TRUE(chain_.add_header(g));
1102+
// Light branch a1<-a2 builds the initial tip.
1103+
BlockHeaderType a1 = plain_header(block_hash(g), kEasyBits, 2);
1104+
ASSERT_TRUE(chain_.add_header(a1));
1105+
BlockHeaderType a2 = plain_header(block_hash(a1), kEasyBits, 3);
1106+
ASSERT_TRUE(chain_.add_header(a2));
1107+
// One heavy sibling at height 1 reorgs the tip onto branch b.
1108+
BlockHeaderType b1 = plain_header(block_hash(g), kHardBits, 4);
1109+
ASSERT_TRUE(chain_.add_header(b1));
1110+
ASSERT_EQ(chain_.tip()->block_hash, block_hash(b1));
1111+
auto loc = chain_.get_locator();
1112+
// Locator tracks the NEW tip's ancestry (b1 <- g), not the abandoned a-branch.
1113+
ASSERT_EQ(loc.size(), 2u);
1114+
EXPECT_EQ(loc[0], block_hash(b1));
1115+
EXPECT_EQ(loc[1], block_hash(g));
1116+
EXPECT_EQ(std::count(loc.begin(), loc.end(), block_hash(a2)), 0);
1117+
}
1118+
10161119
} // namespace

0 commit comments

Comments
 (0)