Skip to content

Commit 8657bdf

Browse files
committed
bch(M5): fork-convergence soak proves back-off locator converges where single-hash stalls
The progress soak drove cold-start IBD on a clean chain; header_sync_test pinned choose_continue_locator as a pure function. Neither exercised the COMPOSITIONAL property PR #208 actually buys: that the IBD *loop*, driven through choose_continue_locator, CONVERGES to the peer tip when our latest header sits on a minority fork -- the silent-stall case the back-off locator fixes. Add three loop-level invariants over a live-shaped cursor (anchor 955700 -> peer tip 955822, the VM300 bchn-bch range), modeling the real BIP31 back-off (get_locator_internal) and a peer that anchors only at a locator entry on its main chain: inv.6 shallow fork: back-off locator anchors at the EXACT common ancestor, converges, re-walks ZERO already-held headers (false_evict analog == 0). inv.7 same fork, degraded single-hash fallback (no provider) STALLS and never converges -- proves the provider is load-bearing, not cosmetic. inv.8 deep 50-block fork still converges; re-walk bounded by one batch. Header-only over coin/header_sync.hpp; src/impl/bch/test/ only; impl_bch stays unregistered (bch skip-green). p2pool-merged-v36 surface: NONE.
1 parent 5f27c93 commit 8657bdf

1 file changed

Lines changed: 141 additions & 0 deletions

File tree

src/impl/bch/test/header_sync_progress_test.cpp

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,11 @@
3333
// PPLNS math). per-coin isolation: src/impl/bch/coin/ only.
3434
// ---------------------------------------------------------------------------
3535

36+
#include <algorithm>
3637
#include <cstddef>
3738
#include <cstdint>
3839
#include <iostream>
40+
#include <vector>
3941

4042
#include "../coin/header_sync.hpp"
4143

@@ -83,6 +85,105 @@ SyncRun drive_ibd(std::uint64_t gap, std::size_t cap = MAX_HEADERS_RESULTS,
8385
return r;
8486
}
8587

88+
// --- Fork-convergence soak: drive the IBD loop through choose_continue_locator
89+
// against a peer whose tip sits past a FORK from our latest header. The
90+
// COMPOSITIONAL counterpart to header_sync_test's pure choose_continue_locator
91+
// cases: proves the back-off locator makes the *loop* CONVERGE where the
92+
// degraded single-hash locator STALLS -- the silent-stall failure PR #208 fixed.
93+
// Header-layer "false_evict" analog = headers the peer re-serves that we already
94+
// hold; anchoring at the exact common ancestor keeps that at ZERO.
95+
using bch::coin::header_sync::choose_continue_locator;
96+
97+
// Hash stand-in: main-chain blocks hash to their height; our fork-only blocks
98+
// hash to height + FORK_TAG so the peer (which holds only the main chain) can
99+
// never anchor a fork-only locator entry.
100+
constexpr std::uint64_t FORK_TAG = 1'000'000'000ull;
101+
102+
inline std::uint64_t our_hash(std::uint64_t height, std::uint64_t fork_point) {
103+
return (height <= fork_point) ? height : height + FORK_TAG;
104+
}
105+
inline bool peer_can_anchor(std::uint64_t hash, std::uint64_t peer_tip) {
106+
return hash < FORK_TAG && hash <= peer_tip; // a main-chain hash the peer holds
107+
}
108+
109+
// Mirror of HeaderChain::get_locator_internal (BIP31 back-off): dense head of 11
110+
// consecutive heights from the tip, then the step doubles each entry down to 0.
111+
std::vector<std::uint64_t> make_chain_locator(std::uint64_t tip_height,
112+
std::uint64_t fork_point) {
113+
std::vector<std::uint64_t> loc;
114+
std::int64_t step = 1;
115+
std::int64_t h = static_cast<std::int64_t>(tip_height);
116+
while (h >= 0) {
117+
loc.push_back(our_hash(static_cast<std::uint64_t>(h), fork_point));
118+
if (h == 0) break;
119+
h -= step;
120+
if (h < 0) h = 0;
121+
if (loc.size() > 10) step *= 2;
122+
}
123+
return loc;
124+
}
125+
126+
struct ForkRun {
127+
std::uint64_t synced = 0; // highest MAIN-chain height we hold at halt
128+
std::size_t reissues = 0; // ContinueSync getheaders re-issues
129+
std::size_t redundant = 0; // already-held headers re-served (false_evict analog)
130+
bool converged = false;
131+
bool stalled = false;
132+
};
133+
134+
// Drive the headers-first IBD continuation against a forked peer.
135+
// fork_tip : height of OUR latest header (on a minority fork above fork_point)
136+
// fork_point : last height common to us and the peer (the true common ancestor)
137+
// peer_tip : the peer's main-chain tip we must converge to
138+
// use_provider: true -> robust HeaderChain back-off locator wired (THE FIX)
139+
// false -> degraded single-hash fallback (no provider) -> empty chain_locator
140+
ForkRun drive_forked_ibd(std::uint64_t fork_tip, std::uint64_t fork_point,
141+
std::uint64_t peer_tip, bool use_provider,
142+
std::size_t cap = MAX_HEADERS_RESULTS) {
143+
ForkRun r;
144+
std::uint64_t our_tip = fork_tip; // height of our latest header
145+
std::uint64_t main_progress = fork_point; // highest main height we already hold
146+
const std::size_t MAX_ROUNDS = 100000; // runaway guard: a real stall trips it
147+
std::size_t rounds = 0;
148+
while (rounds < MAX_ROUNDS) {
149+
++rounds;
150+
// Build the continuation locator exactly as NodeP2P now does.
151+
std::vector<std::uint64_t> chain_loc =
152+
use_provider ? make_chain_locator(our_tip, fork_point)
153+
: std::vector<std::uint64_t>{}; // no provider -> degraded
154+
std::vector<std::uint64_t> loc =
155+
choose_continue_locator(chain_loc, our_hash(our_tip, fork_point));
156+
157+
// Peer anchors at the FIRST (highest) locator entry it holds on its main
158+
// chain; if none, it returns nothing (the silent stall #208 addressed).
159+
std::int64_t anchor = -1;
160+
for (std::uint64_t hh : loc) {
161+
if (peer_can_anchor(hh, peer_tip)) { anchor = static_cast<std::int64_t>(hh); break; }
162+
}
163+
if (anchor < 0) { r.stalled = true; break; } // cannot anchor -> stall
164+
165+
std::uint64_t from = static_cast<std::uint64_t>(anchor);
166+
if (from >= peer_tip) break; // nothing new to serve -> caught up
167+
std::uint64_t want = peer_tip - from;
168+
std::size_t batch = (want >= cap) ? cap : static_cast<std::size_t>(want);
169+
std::uint64_t served_to = from + batch;
170+
171+
// Headers in (from, main_progress] are ones we already hold -> re-served.
172+
if (from < main_progress)
173+
r.redundant += static_cast<std::size_t>(std::min(served_to, main_progress) - from);
174+
175+
our_tip = served_to; // now on the peer's main chain
176+
if (served_to > main_progress) main_progress = served_to;
177+
r.synced = main_progress;
178+
179+
Followup f = classify_headers_batch(batch, DEFAULT_ANNOUNCE_THRESHOLD, cap);
180+
if (f == Followup::ContinueSync) { ++r.reissues; continue; }
181+
break; // converged / caught up
182+
}
183+
r.converged = (r.synced == peer_tip) && !r.stalled;
184+
return r;
185+
}
186+
86187
} // namespace
87188

88189
int main()
@@ -141,6 +242,46 @@ int main()
141242
CHECK(empty.final_action == Followup::Idle); // nothing arrived -> Idle
142243
}
143244

245+
// ---- inv. 6: shallow fork -> back-off locator anchors at the EXACT common
246+
// ancestor, converges to peer tip, re-walks ZERO already-held headers
247+
// (false_evict analog == 0). Loop-level proof of PR #208 over a live-shaped
248+
// cursor (anchor 955700 -> peer tip 955822, the VM300 bchn-bch range).
249+
{
250+
const std::uint64_t peer_tip = 955822, fork_point = 955700;
251+
const std::uint64_t fork_tip = fork_point + 3; // 3-block minority fork
252+
ForkRun r = drive_forked_ibd(fork_tip, fork_point, peer_tip, /*use_provider=*/true);
253+
CHECK(r.converged); // reached peer tip (no stall)
254+
CHECK(r.synced == peer_tip);
255+
CHECK(r.redundant == 0); // anchored at exact ancestor: 0 re-walk
256+
CHECK(r.reissues == (peer_tip - fork_point - 1) / CAP); // bounded re-issues
257+
CHECK(!r.stalled);
258+
}
259+
260+
// ---- inv. 7: SAME fork, degraded single-hash locator (no provider) STALLS.
261+
// The peer cannot anchor our lone minority-fork tip -> serves nothing -> IBD
262+
// never converges. Proves the back-off provider is LOAD-BEARING, not cosmetic:
263+
// remove it and cold-start IBD across a fork silently hangs (the pre-#208 bug).
264+
{
265+
const std::uint64_t peer_tip = 955822, fork_point = 955700;
266+
const std::uint64_t fork_tip = fork_point + 3;
267+
ForkRun r = drive_forked_ibd(fork_tip, fork_point, peer_tip, /*use_provider=*/false);
268+
CHECK(r.stalled); // degraded path hangs on the fork
269+
CHECK(!r.converged);
270+
CHECK(r.synced < peer_tip);
271+
}
272+
273+
// ---- inv. 8: DEEP fork (50 blocks) still converges via back-off, never
274+
// stalls; the re-walk stays bounded by one batch, not unbounded.
275+
{
276+
const std::uint64_t peer_tip = 955822, fork_point = 955700;
277+
const std::uint64_t fork_tip = fork_point + 50; // deep minority fork
278+
ForkRun r = drive_forked_ibd(fork_tip, fork_point, peer_tip, /*use_provider=*/true);
279+
CHECK(r.converged);
280+
CHECK(r.synced == peer_tip);
281+
CHECK(!r.stalled);
282+
CHECK(r.redundant < CAP); // re-walk bounded, finite
283+
}
284+
144285
if (failures == 0)
145286
std::cout << "bch header_sync sync-to-peer-tip soak: ALL PASS\n";
146287
else

0 commit comments

Comments
 (0)