Repository navigation
ibd: fetch again a BadPrev that races its parent commit - #1105
Conversation
There was a problem hiding this comment.
Review
Reviewed the top commit 668e9b76 only. Its parent is #1104 (41930dab); the GitHub file list also contains that SOCKS commit because this PR targets master.
prev_continues_own_path matches the race it describes. A BadPrev whose parent is the hub tip, or the hash already in height_to_hash at height - 1, stays on the path and is fetched again. A parent that is only some older path block still takes the evict arm (slot evicted, not re-get same hash). == binds tighter than ||, so the second arm is only the immediate predecessor. maybe_rewind_to_best_work still returns before that decision when a heavier fork wins. prev_is_hub_tip had one caller and it is gone.
bad_prev_racing_its_parents_commit_is_fetched_again fails on the parent commit: the old predicate drops height_to_hash[t + 2], and the new assertions require the slot, is_missing, and densify_scan_lo. WireRig::pump in the engine-fault test still stalls if the hash is never asked for.
Requesting changes because the retry uses a halt that treats this race as fatal.
| Severity | Location | Finding |
|---|---|---|
| High | crates/rbitcoin-net/src/ibd/events/mod.rs:1140 |
retry_tip_child calls apply_tip_child_bad_prev. The count key is (hash, hub tip bytes) and the halt is 3 (TIP_CHILD_HALT_AFTER). This race is exactly "hub tip still the grandparent": each BadPrev of the same child increments, and the third sets st.halt. The IBD loop takes that as NetError::Consensus (ibd/mod.rs:1171). note_confirm_stuck only records confirm_stuck_since; it does not delay the re-get. A parent still in write (the drain_fk / fence gap in the PR) can outlast three getdata round-trips of the child, and the new test applies the reject once. |
| Low | crates/rbitcoin-net/src/ibd/events/mod.rs:1090 |
The comment and the re-get log still say the parent is the hub tip when the new arm matched only the path slot at height - 1. |
Please count strikes only once the parent is the hub tip (the old prev_is_hub_tip case), and keep re-getting while the parent is still the uncommitted path slot. A test that applies the reject three times with a frozen tip should still show the hash missing and st.halt unset.
668e9b7 to
6f232cd
Compare
6f232cd to
3586aae
Compare
There was a problem hiding this comment.
Review
Reviewed only the top commit 3586aae3 (parent 1094e987, #1104). The predicate change matches the race, and the strike counter is the same one called out on 668e9b76. The previous high is unfixed. The commit message still says the child is fetched again under the three-strike halt.
prev_continues_own_path is true when the parent is the hub tip or the hash in height_to_hash at height - 1. A parent that is only an older path block still takes the evict arm. maybe_rewind_to_best_work still returns before that decision. prev_is_hub_tip is gone. bad_prev_racing_its_parents_commit_is_fetched_again still fails on the parent commit, and it still applies the reject once.
The height-1 arm sets retry_tip_child and calls apply_tip_child_bad_prev (events/mod.rs:1177). The count key is (hash, hub tip bytes) and TIP_CHILD_HALT_AFTER is 3 (:1267). This race is an unchanged hub tip, so the third BadPrev sets st.halt, and the IBD loop returns NetError::Consensus. note_confirm_stuck only records a timestamp. Strikes 1 and 2 call mark_missing immediately.
Count strikes only once the parent is the hub tip. Keep re-getting while the parent is still the uncommitted path slot. A test that applies the reject three times with a frozen tip should leave the hash missing and st.halt unset.
The re-get log at :1191 still says the parent is the hub tip when the match was only the path slot at height - 1.
|
Thanks. You're right, and the earlier commit message was wrong. Addressed in
The existing tip-child tests ( |
Load can stamp tip+2 while tip+1 is still being written (drain_fk at tip+1, fence at tip). That BadPrev can reach IBD before tip+1 Accepted, so prev_is_hub_tip saw the old tip, took the block for a fork, evicted it from the path, and never asked for it again. Catch-up stalled with asked=[] at tip+2. A BadPrev whose parent is this path's own block at height - 1 is not a fork, so prev_continues_own_path now keeps it on the path and fetches it again. Here it still shares the tip-child strike counter; the next commit takes it off that counter. batched_pin_engine_fault_retries_one_block_at_a_time discarded confirm events after the mend, so a dropped tip+2 could never be fetched. It now pumps IBD, which serves that fetch. Run as 120 parallel copies it failed 2 of 720 runs before and 0 of 600 after. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up. prev_continues_own_path folded "parent is the hub tip"
and "parent is the path block at height - 1, still committing" into one
retry_tip_child, so the second case also ran apply_tip_child_bad_prev.
Those strikes are keyed by (hash, hub tip), and in this race the tip has
not moved, so a third BadPrev would set st.halt and stop IBD.
bad_prev_parent now returns BadPrevParent::{Tip, Committing, OffPath}.
Only Tip takes the strikes. Committing is re-fetched without one and
leaves cascade_at alone, and the log names which case it was.
bad_prev_racing_its_parents_commit_is_fetched_again applies the reject
four times with a frozen tip and asserts no halt and the hash still
missing; it failed on the parent commit with "a committing parent is
not a strike".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
a3d8bd1 to
bb9b9f2
Compare
|
Reworded both commit messages; the trees are unchanged (
|
There was a problem hiding this comment.
Review
Reviewed a3d8bd10 on top of 3586aae3. The last change request is fixed.
bad_prev_parent splits the old predicate. A parent that is the hub tip is Tip and still goes through apply_tip_child_bad_prev, which halts at 3 while that tip is unchanged (tip_child_bad_prev_halts_on_the_third_retry). A parent that is only height_to_hash[height - 1] is Committing: the match arm does not touch the strike counter, then the hash is marked missing and densify is reopened. OffPath still drops the slot and takes the evict log. Tip is tested before Committing, so once the parent commit lands, further BadPrevs of the same child start the three-strike count. maybe_rewind_to_best_work still returns before either arm.
bad_prev_racing_its_parents_commit_is_fetched_again applies the reject four times with the hub tip frozen on the grandparent. It requires st.halt to stay unset, the hash to stay at t + 2, and the block to stay missing. That fails on 3586aae3, where the third reject sets the halt. An off-path parent still does not match Committing.
A Committing parent that never becomes the tip and is never removed from height_to_hash is re-fetched without that cap. That is the race window this commit is for: the classification changes when the parent is accepted or the slot is dropped.
Problem
batched_pin_engine_fault_retries_one_block_at_a_timefails intermittently in thecoveragejob (seen on #1104):Load stamps tip+2 while tip+1 is still being written (
drain_fkat tip+1, fence at tip), and gets a BadPrev. That reject can reach IBD before tip+1'sAccepted.prev_is_hub_tipthen reads the old tip, so IBD takes tip+2 for a competing fork and logsslot evicted, not re-get same hash. The block leaves the path and is never asked for again:That is a production stall, not only a test problem.
isolated_mutated_head_punishes_its_sender_when_lookup_runs_aheadfailed incoverageon #1104 with the sameSoftWire, 1 … asked=[]signature. I could not reproduce that one locally, so I'm not claiming this fixes it.Fix
bad_prev_parent(replacesprev_is_hub_tip) classifies the parent asBadPrevParent::Tip,Committing(this path's own block atheight - 1, not the tip yet) orOffPath.Tipis a tip child as before: it is fetched again under the existing three-strike halt.Committingis fetched again without a strike, because the tip has not moved during the race and the strike key(hash, tip)would otherwise halt IBD on the third repeat.OffPathis still a fork.Tests
bad_prev_racing_its_parents_commit_is_fetched_again: tip+1 on the path but not confirmed, then the same BadPrev at tip+2 four times with a frozen tip. The hash must stay on the path, missing, withst.haltunset. Onmasterit fails (left: None, the hash is dropped fromheight_to_hash). On3586aae3it fails with "a committing parent is not a strike".batched_pin_engine_fault_retries_one_block_at_a_timediscarded every confirm event after the mend, so a dropped tip+2 could never be fetched (docs/concurrency.md: after a load reject, bodies may be "fetched again from peers"). It now runsWireRig::pump, which applies the events and serves the fetch.isolated_mutated_head…passed 600 of 600 after the change.cargo fmt --check,cargo clippy --workspace --all-targets -D warnings,cargo test --workspace,./scripts/ci-os-smoke.shall pass.🤖 Generated with Claude Code