|
33 | 33 | // PPLNS math). per-coin isolation: src/impl/bch/coin/ only. |
34 | 34 | // --------------------------------------------------------------------------- |
35 | 35 |
|
| 36 | +#include <algorithm> |
36 | 37 | #include <cstddef> |
37 | 38 | #include <cstdint> |
38 | 39 | #include <iostream> |
| 40 | +#include <vector> |
39 | 41 |
|
40 | 42 | #include "../coin/header_sync.hpp" |
41 | 43 |
|
@@ -83,6 +85,105 @@ SyncRun drive_ibd(std::uint64_t gap, std::size_t cap = MAX_HEADERS_RESULTS, |
83 | 85 | return r; |
84 | 86 | } |
85 | 87 |
|
| 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 | + |
86 | 187 | } // namespace |
87 | 188 |
|
88 | 189 | int main() |
@@ -141,6 +242,46 @@ int main() |
141 | 242 | CHECK(empty.final_action == Followup::Idle); // nothing arrived -> Idle |
142 | 243 | } |
143 | 244 |
|
| 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 | + |
144 | 285 | if (failures == 0) |
145 | 286 | std::cout << "bch header_sync sync-to-peer-tip soak: ALL PASS\n"; |
146 | 287 | else |
|
0 commit comments