Companion to
00-overview,01-event-bus,02-chaser-header.
chaser_downloadis the dispatcher between the header chain and the peer block-download protocols. Conceptually:
- The header chaser commits to a candidate chain of headers.
chaser_downloadnotices which heights in that chain still lack block bodies (are unassociated) and partitions them into work-maps.- Peer protocols (
protocol_block_in_31800) pull maps viaget_hashes, download the blocks, and return any leftovers viaput_hashes.- As blocks arrive (
chase::checked), the position advances; when the position reaches the end of the request window it emitschase::windowed; as validation completes (chase::valid), the request window slides forward.
chaser_downloadalso runs the performance-policing loop: peers report their download speed, slow peers are detected by standard deviation, and starved peers cause work to be split off slow peers.
| File | Role |
|---|---|
include/bitcoin/node/chasers/chaser_download.hpp |
Class declaration; private types speeds, maps |
src/chasers/chaser_download.cpp |
Implementation |
1. State#
1.1 Configuration (immutable, set at construction)#
// chaser_download.hpp:96-102 / chaser_download.cpp:61-71
const float allowed_deviation_; // σ multiplier for "slow" cutoff
const size_t maximum_concurrency_; // download window size cap
const size_t maximum_height_; // hard ceiling (0 → no cap)
const size_t bypass_height_; // max(milestone height, top checkpoint height)
const size_t connections_; // target outbound count (or inbound if no outbound)
const size_t step_; // = min(max_concurrency, max_inventory × connections)
connections_ is derived from network config
(chaser_download.cpp:44-52): outbound + manual peer count if any, else
inbound count. step_ is the maximum number of hashes scanned when sizing
the inventory (:54-59).
1.2 Strand-protected runtime state#
// chaser_download.hpp:104-115
size_t inventory_{}; // per-channel inventory size — latched after first nonzero
size_t requested_{}; // height of last request issued
size_t advanced_{}; // count of chase::valid events received (NOT a height)
job::ptr job_{}; // network::race_all<const code&> token; null ⇒ "purging"
network::deadline::ptr compact_timer_{}; // defers download of a current compact top
database::header_link deferred_{}; // the compact top being deferred
database::header_link expired_{}; // the compact top whose deferral expired
speeds speeds_{}; // map<object_key channel, double kbps>
maps maps_{}; // deque<map_ptr> of pending download chunks
Plus position() from the base chaser — the height at which all blocks
below are associated (downloaded) on the candidate chain.
1.3 Why advanced_ is a counter, not a height#
do_advanced (chaser_download.cpp:369-379) increments advanced_ on every
chase::valid event, regardless of which height was validated. The
round is complete when advanced_ == requested_. This is correct
because:
-
Every block hash that is requested will eventually be validated or the peer will fail (and the hash is returned to the pool).
-
Once
requested_round-trips have completed, the request window can slide forward without leaving gaps below.
It does not track an in-order height; out-of-order validation is permitted as long as the count balances.
Invariant (Download-State-1).
position() ≤ requested_andadvanced_ ≤ requested_always hold on the strand.set_unassociatedshort-circuits if either falls behind (chaser_download.cpp:503-509).
1.4 The job_ token (purge semaphore)#
job_ is a network::race_all<const code&> whose completer is
handle_purged. Semantics:
- A
race_allinvokes its completer once all copies are dropped. -
chaser_downloadholds one copy injob_. Every peer protocol that callsget_hashesreceives a copy via themap_handler(chaser_download.cpp:455:handler(success, get_map(), job_)). -
When
chaser_downloaddrops its copy viastop_tracking()(:193-201), the completer fires only after every peer has also dropped its copy — i.e., released its current download work.
So job_ is effectively a purge barrier: "wait until every
in-flight download has either completed or surrendered, then resume".
Invariant (Download-Job-1).
job_ == nullptr⇒ chaser is purging (currently waiting for the barrier). While purging, all ofdo_bump,do_get_hashes,do_put_hashes, andset_unassociatedare no-ops (:393-394,:452-453,:463-464,:500-501). The transition out of purging is exactly thedo_handle_purgedcallback (:213-219) which callsstart_tracking()followed bydo_bump.
2. Public interface#
| External entry | Caller | Stranded? | Effect summary |
|---|---|---|---|
start() |
full_node::do_start |
on node strand at startup | start tracking, create compact timer, position = fork, requested/advanced = position, first set_unassociated; subscribe |
stopping(ec) |
full_node::do_close |
no | stop compact timer, drop job_ |
update(channel, speed, h) |
full_node::performance ← protocols |
posts to strand | record speed; detect slow channel; reply slow_channel if so |
get_hashes(h) |
full_node::get_hashes ← protocols |
posts to strand | hand a map and the job_ pointer to the caller |
put_hashes(map, h) |
full_node::put_hashes ← protocols |
posts to strand | re-queue the returned map; emit chase::download(map.size()) |
Bus subscription via handle_chase |
event_subscriber_ |
node strand, POSTs | dispatch to do_* |
3. Behaviour overview (3 cooperating state machines)#
These three are interleaved on the same strand. The Mermaid grouping is
conceptual; in code they share do_* handlers.
4. Bus integration (verified)#
Inputs (from 01-event-bus §2):
| Event | Source | Reaction |
|---|---|---|
chase::start |
full_node |
do_bump — kickstart |
chase::resume |
full_node |
do_bump |
chase::bump |
chaser_header, chaser_validate |
do_bump |
chase::headers |
chaser_header |
do_headers(branch_point) |
chase::checked |
protocol_block_in_31800, chaser_transaction |
do_checked(height) |
chase::valid |
chaser_validate |
do_advanced(height) |
chase::regressed/disorganized |
chaser_header |
do_regressed(branch_point) — rewind, purge work, emit purge |
chase::starved |
protocol_block_in_31800 |
do_starved(channel) — emit split or stall |
chase::stop |
service | unsubscribe (return false) |
Outputs:
| Event | When | Payload |
|---|---|---|
chase::download |
After set_unassociated adds new work (:420-421), or do_put_hashes re-queues (:466-467) |
count added |
chase::windowed |
do_bump advanced the position to requested_ (:406-407) |
height |
chase::purge |
After do_regressed purges all outstanding work (:363) |
branch_point |
chase::split |
(notify_one) On starvation, target the slowest tracked channel (:247) |
channel (= starved channel) |
chase::stall |
On starvation when no speeds are tracked (:252) |
channel (= starved channel) |
5. The download-window state machine#
Invariant (Download-Purge-1). A
chase::purgeemission is always followed (eventually, after barrier completion) by an idempotent return to the ACTIVE state withposition_rewound tomin(branch_point, position),requested_ = advanced_ = position, andmaps_empty (:348-364). While in PURGING, no work is handed out and no new work is computed.Invariant (Download-Purge-2).
do_regressedignores regressions at or abovemax(position, requested_)(:353-354). These are "inconsequential": neither the position nor the window has reached that height, so there is no work to purge. A regression inside the window but above the position still purges, because the window must follow the position or the gap it awaits is never requested.Invariant (Download-Window-1).
chase::windowed(height)is emitted exactly when the contiguous associated position reaches the last requested height (:401-408).chaser_validateuses it to know that no more blocks are coming for the window and drain its batches (doc 04 §5).
6. The performance / speed policy#
chaser_download is the only chaser that interacts with peer connections
beyond bus events: protocol_block_in_31800 calls
full_node::performance(channel, speed, handler) periodically, which
forwards to chaser_download::update. The reply code tells the peer
whether to keep going or terminate.
6.1 do_update decision tree#
speed == max_uint64: erase(channel); return exhausted_channel
speed == 0: erase(channel); return stalled_channel
otherwise: record speed = double(speed)
if count(speeds_) < 4: return success
compute mean, σ from speeds_
if speed ≥ mean: return success
if (mean - speed) > α·σ: return slow_channel
else: return success
(chaser_download.cpp:278-343; minimum_for_standard_deviation = 4,
chaser_download.hpp:83)
-
exhausted_channel: peer announces "I have nothing else useful for you". Peer pauses its timer; the record is released. -
stalled_channel: peer reports zero progress. Peer drops itself. slow_channel: peer is statistically below the herd. Returned only when ≥ 4 samples and the gap exceedsallowed_deviation × σ.
6.2 do_starved (when a peer has no work)#
protocol_block_in_31800 emits chase::starved(self) when it receives an
empty map. do_starved (:224-253):
-
Removes
selffromspeeds_(so it can't be picked as its own victim). -
Picks the slowest remaining channel by
speeds_min. -
If found: erases it and
notify_one(slow, chase::split, self)— direct that single channel to split its work and stop, leaving the starved channel something to claim. -
If
speeds_is empty: broadcastchase::stall(self)to all download-capable channels.
The receiver of split/stall is protocol_block_in_31800; see
06 §5.3.
7. set_unassociated — the work generator#
set_unassociated (chaser_download.cpp:496-571) is the heart of work
generation. It:
-
Early-exits if purging, if not all previously-requested work has cleared (
position < requested), or if validation has not caught up (advanced < requested) (:500-509). -
Latches
inventory_on first nonzero (:512-514→get_inventory_size). -
Computes a
stopheight:min(position + maximum_concurrency, maximum_height), clamped to the bypass height while the position is under it (:524-527). Bypassed blocks may archive unprobed, so the window must not extend above the bypass height until all blocks at or below it are associated. -
When current and
compact_timeoutis nonzero, if the top candidate is a compact-announced header that is not yet associated, clampsstopto one below it and starts the compact timer (once per link) (:530-547). This gives the compact fill a chance before the block is downloaded in full; when the timer fires,expired_records the link anddo_headersre-runs the generator without the clamp (:573-582). -
Loops
query.get_unassociated_above(requested_, inventory_, stop), chunking each result into amap_ptrand pushing ontomaps_, untilset_mapreturns false (empty map, end of unassociated headers belowstop) (:549-560). -
Returns total count added.
get_inventory_size (:584-593):
-
Returns 0 if no connections OR if
is_current_chain(false)is false — i.e. the candidate (header) chain isn't current. -
Otherwise:
ceilinged_divide(unassociated_count_above(fork, step), connections).
The candidate-current gate is not "wait until caught up" — issuing zero work until the node is caught up would just stall. Instead it guards against dividing work over a weak header chain: until headers are current, the partitioning of unassociated heights into per-peer inventory chunks could be meaningless, so block download is paused until header sync has produced a current (and therefore presumed near-canonical) candidate.
Invariant (Download-Inventory-1).
inventory_is computed at most once (latch on first nonzero result,:512-514). Peer inventory size is fixed for the run, after the first "currentness" of the chain.Invariant (Download-Bypass-1). No download window spans the bypass height: while
position < bypass_height,stop ≤ bypass_height(:526-527). Validation of bypassed blocks therefore never races ahead of their association.
8. Error inventory#
Unlike chaser_header, chaser_download does not define numbered
faults. Its errors are channel-class codes returned to the peer (the
chaser itself does not call fault):
Code returned by do_update |
Meaning |
|---|---|
error::exhausted_channel |
peer has no more work to offer |
error::stalled_channel |
peer reports zero progress |
error::slow_channel |
peer below statistical floor (mean − α·σ) |
network::error::service_stopped |
chaser is closed; update short-circuits |
error::success |
continue |
The peer-side reaction (drop connection vs. continue) lives in
protocol_performer (doc 06 §4).
9. Coupling diagram#
10. Spec view#
10.1 As a process#
A formal model can represent chaser_download as a process with:
- inputs:
- bus messages (
chase::*listed in §4) -
request RPCs:
update,get_hashes,put_hashes(each carries aresult_handlercontinuation) -
the compact timer tick
- outputs:
-
bus messages (
chase::download,chase::windowed,chase::purge,chase::split,chase::stall) -
returned codes to request RPCs
- state:
position,requested,advanced∈ ℕinventory∈ ℕ (latched after first nonzero)purging∈ {true, false} -- abstractsjob_deferred,expired∈ Link ∪ {⊥}-
maps: Queue ofmap-- eachmapis a non-empty set of(height, hash)pairs -
speeds: finite map from channel-id to ℝ⁺
10.2 Safety properties to prove#
-
No-overshoot:
position ≤ requestedandadvanced ≤ requestedare loop invariants. -
Purge soundness: while
purging,maps = ∅and nodownloadis emitted. -
Bypass clamp (Download-Bypass-1): no requested height above the bypass height while the position is under it.
-
Eventual progress: assuming peers honour
get_hashes/put_hashescontracts, the position increases monotonically when not purging. Liveness; requires fairness on peer scheduling. -
Split correctness:
chase::split(self)is sent only tonotify_one(slow)whereslow ≠ self(:228-247). -
Purge barrier completeness: every
chase::purgeis eventually followed by achase::download(or bystop). This depends on therace_allsemantics and peer compliance.
10.3 Liveness assumptions#
-
Peers eventually release
job_copies afterchase::purge— the protocols layer guarantees this on stop (doc 06 §5.2). -
query.get_unassociated_aboveterminates and is monotone inrequested_. -
The compact deferral is bounded by
compact_timeoutper top.
11. Implementation notes#
-
The three state machines (§3) are independent in their state space but interleaved in time — one actor per chaser with a single message-pump is sufficient.
-
The
race_alltoken (job_) is a non-trivial primitive. It is effectively a(condition-variable, ref-count)pair: each peer increments onget_hashesand decrements when done; the chaser waits on count → 0. -
set_unassociatedis naturally a generator/iterator (lazy sequence of maps).
12. Notes for the formal model#
-
All
do_*methods run on the strand. Race-freedom on internal state is by construction. -
The only externally-visible interaction beyond the bus is the peer speed/inventory protocol (§6, §7). Model the peer as a process that responds to
slow_channel/exhausted_channel/stalled_channelreturn values by dropping or pausing its connection. -
A model of
race_allrequires a counted semaphore or refcount; this is the only place inchaser_downloadthat needs more than single-threaded reasoning.
Cross-references#
00-overview§5 (chaser pipeline overview)-
01-event-bus§2.1 (work-shuffling events) and §2.2 (candidate-chain events) -
02-chaser-header§3.2 (organize emitsheaders/bump/regressed/disorganizedconsumed here) -
04-chaser-validate(issuer ofvalid; consumer ofwindowed) -
06-sessions-and-protocols§5 (protocol_block_in_31800— peer-side counterpart; consumer ofdownload/purge/split/stall/report) -
07-header-protocols§4 (compact block fill that the deferral timer waits for)