SH compact, header anti-DoS, and IBD stamp/pin rewind - #447
Conversation
Crash mid-finish leaves occupied head slots with alloc live_count 0; append skipped locate_head and dual-homed ingest over sealed main. Install compacted L1 before dropping L0 so locate_head never sees a hole, and occupancy walk includes L1. Co-authored-by: Cursor <cursoragent@cursor.com>
disconnect_to left chain_work_prefix at the old length, so an equal-height reorg ranked against the losing branch. Persist and pending ranking now require claimed nBits/POW (and MTP/bits when the parent is on the best chain) so a peer cannot inflate most-work with an unmined compact target. Co-authored-by: Cursor <cursoragent@cursor.com>
Stamp fail requeued the rest of the chunk without bodies after BQ take, then continued into the same lookup wave with clear_all identity. Pin fail did not clear_all at all. Requeue the tail with bodies, bump the feed epoch so in-channel loadq is stale, and clear speculative fks on both paths. Co-authored-by: Cursor <cursoragent@cursor.com>
Unreleased Fixed for the P1 datadir/index and IBD leftover-identity holes (compact L1 publish order, live_count=0 SH probe, chain-work prefix truncate, header POW persist/rank, stamp/pin wave rewind). Co-authored-by: Cursor <cursoragent@cursor.com>
35c80ed to
bc0957a
Compare
There was a problem hiding this comment.
Review
SH compact publish order, live_count==0 head probe, chain_work_prefix truncate, and claimed-nBits ranking all look right for the bugs they name. Required checks are green.
The stamp/pin leftover-identity slice does not restore the only production intake. I would not merge that part as written.
Must fix: stamp/pin rewind never returns wire to BQ
ConfirmFeed is readiness only. Lookup selects block_queue_unresolved_heights, then take_wave_items_for_load dequeues BQ before stamp/pin. load_fail_rewind_wave then:
clear_all(needed)feed.clear()(epoch bump drops later loadq chunks of the same wave without requeueing their wire)requeue_wire(tail)intofeed.readywith clonedBlocks
Nothing in spawn_confirm_engine reads feed.ready wire. Lookup will not see those heights until BQ is filled again. load_fail_rewind_keeps_tail_wire_and_clears_lookup only inspects the feed mutex, so it cannot catch this.
Recovery today is a different, untested path: reject rewinds lookup_taken_hi, densify demote_zombie_pending_for_fetch (pending + no matching BQ), re-getdata. That can unstick IBD, but it is not “keep tail wire because BQ is already taken,” it re-downloads the wave, and the cloned tail sits in ready unused.
Either:
- re-offer the tail (and stale-dropped loadq chunks) through
block_queue_offer/ promote, thenrequeue_hashes, or - drop the clones, document re-getdata as the recovery, and add an engine-level test that stamp/pin fail leaves those heights claimable again (BQ or densify), not merely
feed.ready.
(*w.block).clone() plus requeue_wire’s inner clone is a full Block copy off the Arc for no consumer.
Other
pending_path_claimed_pow_okfails open when the next hash is missing frompending(return true).work_of_header_pathfails closed (?). Needed becauseheader_path_meets_minworkis a no-op once minwork is unset or already met — keep this gate, but a hole should not skip the POW walk.shorter_higher_work_fork_is_not_hopelessnow asserts the attack (claimed mainnet nBits, nonce 0). The name and the old “real shorter higher-work still getdata” contract are gone. Rename; the legitimate case is still untested (regtest grind at a harder compact, or easierChainParams).disconnect_truncates_chain_work_prefix_to_keep_heightonly checkslenafter rewind to 0. The equal-height bug was stale work atlen == want. Truncate is the right mechanism; pinchain_work()after disconnect-to-fork + connect of a same-height winner.- CodeQL “hard-coded nonce” on
ensure_header_rejects_claimed_hard_bits_without_powis a test literal. Do not shuffle it; dismiss the alert. - Nit:
put_create_batch_append// Crash mid-finish…restates the new condition.
Looks correct
- Compact: install L1, then
takeL0 —locate_headcan see both, never neither. Occupancy walk visiting L1 matches 421 incompact_merges_two_sealed_global_ovf_files. - Tip append
entry_count() > 0 || !head_is_empty()is the crash-mid-finish hole;head_is_empty()only runs whenlive_countis already 0. ensure_header_fk:validate_headerwhenheight_of_hashhits (confirmed parent); else claimed POW vspow_limit. Ranking/work_of_header_pathno longer use nBits withoutvalidate_pow.- Pin fail now
clear_all+continueafter sleep (nosync_body_hwmon a discarded wave). That matches stamp fail.
Lookup never reads feed.ready wire after take_wave dequeues the BQ row. Re-serialize the rest of the wave (and epoch-stale loadq chunks) onto the RAM queue, rewind lookup_taken_hi to tip, and drop the Block clones that had nowhere to go. Co-authored-by: Cursor <cursoragent@cursor.com>
pending_path_claimed_pow_ok duplicated work_of_header_path and returned true when the walk hit a missing header, so getdata still fired when minwork was unset or already met. Gate fetchable on the existing walk and restore a real shorter higher-work getdata pin. Co-authored-by: Cursor <cursoragent@cursor.com>
The prefix-length assert after rewind-to-0 could not see stale work at len == want. Poison the losing tip, reconnect a same-height winner, and require chain_work() to match that branch. Drop a restating SH append comment. Co-authored-by: Cursor <cursoragent@cursor.com>
Fail-closed getdata still needs submitheader parents that live on the hub but not in the peer pending map. A true hole (neither pending nor hub) stays None. Co-authored-by: Cursor <cursoragent@cursor.com>
Do not merge until required checks are green.
Why
P1 leftover from the logical-error inventory (not covered by #443 / #444 / #445):
compact_sealed_ovftook L0, dropped it, then installed L1 on a second mutex.locate_headcould miss overflow keys. Occupancy walk never visited L1.live_count == 0. Crash mid-finish leaves occupied head slots. Append skippedlocate_headand dual-homed ingest over sealed main.disconnect_todid not truncatechain_work_prefix. When tip height came back the same,ensure_chain_work_prefixsawlen == wantand kept the losing branch's cumulative work.ensure_headeronly required a parent row. Pending ranking used claimed nBits, so an unmined compact target could look like more work and force getdata. Confirm still rejected the body; this is index/DoS.block=None(BQ already taken) and continued the same lookup wave afterclear_all. Pin fail did notclear_all. Next stamp hitparent create_fk unresolved.What
for_each_live_createwalks L1. Tip append probes when heads are occupied even iflive_count == 0.disconnect_totruncateschain_work_prefixtokeep_height+1.ensure_headerrunsvalidate_headerwhen the parent is on the best chain; otherwise claimed POW vspow_limit. Pending ranking and getdata skip headers whose hash does not meet claimed nBits.clear_allin-flight identity, bump the feed epoch so in-channel loadq is stale.Test plan
cargo test -p rbitcoin-store compact_merges_two_sealed_global_ovf_filescargo test -p rbitcoin-store append_after_zero_live_count_keeps_sealed_homecargo test -p rbitcoin-store --lib scripthash::cargo test -p rbitcoin-net --lib chain::testscargo test -p rbitcoin-net --lib ibd::confirm::testscargo test -p rbitcoin-net --lib ibd::eventscargo test -p rbitcoin-net --lib ibd::reorgOut of scope
lookup_taken_hi).gettxoutdeps(actions): bump the actions group with 2 updates #6/ibd: prune in-flight by confirmed tip #35, Esplora perf: BQ-ahead TipOnly head_fk; load stamps from hits #29/docs: bot fetch/push via HTTPS URL, leave origin on SSH #30,maxfeeratealready elsewhere).Made with Cursor