From f0ab6eeef59653c2a5aa016b680fc20c72936f85 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sat, 11 Jul 2026 14:26:44 -0400 Subject: [PATCH 01/11] Avoid unnecessary table reads. --- src/chasers/chaser_validate_batch.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/chasers/chaser_validate_batch.cpp b/src/chasers/chaser_validate_batch.cpp index 931a38c3..540e8b53 100644 --- a/src/chasers/chaser_validate_batch.cpp +++ b/src/chasers/chaser_validate_batch.cpp @@ -275,12 +275,10 @@ bool chaser_validate::is_residual() NOEXCEPT bool chaser_validate::is_mature(bool residual) NOEXCEPT { - const auto& query = archive(); - const auto ecdsa = query.ecdsa_records(); - const auto schnorr = query.schnorr_records(); - // Verify non-residuals when mature. - return residual || (ecdsa >= batch_target_) || (schnorr >= batch_target_); + return residual || + (archive().ecdsa_records() >= batch_target_) || + (archive().schnorr_records() >= batch_target_); } std::string chaser_validate::log_rate(const std::string& name, From 45cc0047b6eadd63fbdaca8323c4e18dbd02266c Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sat, 11 Jul 2026 14:31:33 -0400 Subject: [PATCH 02/11] Style, comments. --- .../bitcoin/node/chasers/chaser_validate.hpp | 29 +++++++++++-------- src/chasers/chaser_check.cpp | 20 ++++++++----- src/chasers/chaser_validate_capture.cpp | 26 ++++++++++------- 3 files changed, 45 insertions(+), 30 deletions(-) diff --git a/include/bitcoin/node/chasers/chaser_validate.hpp b/include/bitcoin/node/chasers/chaser_validate.hpp index d4b8191e..ce671bf0 100644 --- a/include/bitcoin/node/chasers/chaser_validate.hpp +++ b/include/bitcoin/node/chasers/chaser_validate.hpp @@ -93,11 +93,23 @@ class BCN_API chaser_validate bool stranded() const NOEXCEPT override; private: + using atomic_counter = std::atomic; + using atomic_counter_ptr = std::shared_ptr; + struct counters + { + atomic_counter ecdsa_{}; + atomic_counter schnorr_{}; + atomic_counter multisig_{}; + atomic_counter threshold_{}; + atomic_counter missed_ecdsa_{}; + atomic_counter missed_schnorr_{}; + atomic_counter missed_multisig_{}; + atomic_counter missed_threshold_{}; + }; + static constexpr auto relaxed = std::memory_order_relaxed; using schnorr_link = database::schnorr_link; using schnorr_link_ptr = std::shared_ptr; - using atomic_counter = std::atomic; - using atomic_counter_ptr = std::shared_ptr; using cursor = system::chain::threshold::cursor; using missed = signatures::miss; @@ -138,17 +150,10 @@ class BCN_API chaser_validate // These are thread safe. stopper stopping_{}; + counters counters_{}; std::shared_mutex mutex_{}; - std::atomic ecdsa_{}; - std::atomic schnorr_{}; - std::atomic multisig_{}; - std::atomic threshold_{}; - std::atomic missed_ecdsa_{}; - std::atomic missed_schnorr_{}; - std::atomic missed_multisig_{}; - std::atomic missed_threshold_{}; - std::atomic validate_backlog_{}; - std::atomic batch_backlog_{}; + atomic_counter batch_backlog_{}; + atomic_counter validate_backlog_{}; std::atomic_bool maximum_posted_{}; std::atomic_bool recovering_{}; diff --git a/src/chasers/chaser_check.cpp b/src/chasers/chaser_check.cpp index 65b483a2..e0a68f7d 100644 --- a/src/chasers/chaser_check.cpp +++ b/src/chasers/chaser_check.cpp @@ -88,6 +88,9 @@ map_ptr chaser_check::split(const map_ptr& map) NOEXCEPT // start/stop // ---------------------------------------------------------------------------- +// `position` tracks the contiguous chain of associated candidate blocks. +// `requested` tracks the last height of the current download window. +// `advanced` tracks `requested` less window blocks not yet validated. code chaser_check::start() NOEXCEPT { start_tracking(); @@ -360,7 +363,6 @@ void chaser_check::do_advanced(height_t) NOEXCEPT BC_ASSERT(stranded()); // Validations are not ordered, so accumulate vs. compare height. - // Advancement through window is a quantity, not dedicated to a branch. ++advanced_; // The full count of requested hashes has been validated. @@ -484,21 +486,25 @@ size_t chaser_check::set_unassociated() NOEXCEPT if (closed() || purging()) return {}; - // Defer new work issuance until gaps filled and validation caught up. - if (position() < requested_ || advanced_ < requested_) + // Defer new work issuance until gaps filled. + if (position() < requested_) + return {}; + + // Defer new work until validation caught up to request. + if (advanced_ < requested_) return {}; // Inventory size gets set only once. if (is_zero(inventory_)) if (is_zero((inventory_ = get_inventory_size()))) - return zero; + return {}; // Due to previous downloads, validation can race ahead of last request. // The last request (requested_) stops at the last gap in the window, but // validation continues until the next gap. Start next scan above validated // not last requested, since all between are already downloaded. const auto& query = archive(); - const auto requested = requested_; + const auto previous = requested_; const auto step = ceilinged_add(position(), maximum_concurrency_); const auto stop = std::min(step, maximum_height_); size_t count{}; @@ -518,7 +524,7 @@ size_t chaser_check::set_unassociated() NOEXCEPT LOGN("Advance by (" << maximum_concurrency_ << ") above (" - << requested << ") from (" + << previous << ") from (" << position() << ") stop (" << stop << ") found (" << count << ") last (" @@ -530,7 +536,7 @@ size_t chaser_check::set_unassociated() NOEXCEPT size_t chaser_check::get_inventory_size() const NOEXCEPT { if (is_zero(connections_) || !is_current_chain(false)) - return zero; + return {}; const auto& query = archive(); const auto fork = query.get_fork(); diff --git a/src/chasers/chaser_validate_capture.cpp b/src/chasers/chaser_validate_capture.cpp index 77dadaac..e1dde2e5 100644 --- a/src/chasers/chaser_validate_capture.cpp +++ b/src/chasers/chaser_validate_capture.cpp @@ -52,13 +52,13 @@ void chaser_validate::do_fire(missed miss, size_t count) NOEXCEPT switch (miss) { case missed::ecdsa: - missed_ecdsa_ += count; + counters_.missed_ecdsa_ += count; break; case missed::multisig: - missed_multisig_ += count; + counters_.missed_multisig_ += count; break; case missed::schnorr: - missed_schnorr_ += count; + counters_.missed_schnorr_ += count; break; default:; } @@ -68,7 +68,7 @@ bool chaser_validate::do_ecdsa(const hash_digest& digest, const ec_compressed& point, const ec_signature& sign, const header_link& link, const atomic_counter_ptr& sequence) NOEXCEPT { - ++ecdsa_; + ++counters_.ecdsa_; const auto id = (*sequence)++; if (is_limited(id)) return false; const auto group = narrow_cast(id); @@ -81,7 +81,7 @@ bool chaser_validate::do_schnorr(const hash_digest& digest, const ec_xonly& point, const ec_signature& sign, const header_link& link) NOEXCEPT { - ++schnorr_; + ++counters_.schnorr_; const auto set = archive().set_signature(digest, point, sign, link); if (!set) fault(error::batch6); return set; @@ -92,7 +92,7 @@ bool chaser_validate::do_multisig(const hash_digest& digest, const std::span& signs, const header_link& link, const atomic_counter_ptr& sequence) NOEXCEPT { - multisig_ += points.size(); + counters_.multisig_ += points.size(); const auto id = (*sequence)++; if (is_limited(id)) return false; const auto group = narrow_cast(id); @@ -118,7 +118,7 @@ chaser_validate::cursor chaser_validate::open_threshold(size_t rows, if (fk->is_terminal()) return {}; - threshold_ += rows; + counters_.threshold_ += rows; return { .put = BIND_THIS(do_threshold, _1, _2, _3, fk, link), @@ -161,10 +161,14 @@ std::string chaser_validate::log_ratio(const std::string& name, void chaser_validate::log_captures() const NOEXCEPT { - LOGN(log_ratio("Capture ecdsa.... ", ecdsa_, ecdsa_ + missed_ecdsa_)); - LOGN(log_ratio("Capture multisig. ", multisig_, multisig_ + missed_multisig_)); - LOGN(log_ratio("Capture schnorr.. ", schnorr_, schnorr_ + missed_schnorr_)); - LOGN(log_ratio("Capture threshold ", threshold_, threshold_ + zero)); + LOGN(log_ratio("Capture ecdsa.... ", counters_.ecdsa_, + counters_.ecdsa_ + counters_.missed_ecdsa_)); + LOGN(log_ratio("Capture multisig. ", counters_.multisig_, + counters_.multisig_ + counters_.missed_multisig_)); + LOGN(log_ratio("Capture schnorr.. ", counters_.schnorr_, + counters_.schnorr_ + counters_.missed_schnorr_)); + LOGN(log_ratio("Capture threshold ", counters_.threshold_, + counters_.threshold_ + zero)); } BC_POP_WARNING() From 575cc62d14bdae96815d4b742dad52aaf094ee11 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sat, 11 Jul 2026 13:31:23 -0400 Subject: [PATCH 03/11] Disable chase::prevalid. --- include/bitcoin/node/chase.hpp | 6 +++--- src/chasers/chaser_check.cpp | 2 +- src/chasers/chaser_validate_batch.cpp | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/include/bitcoin/node/chase.hpp b/include/bitcoin/node/chase.hpp index 6f3d1eb8..3399a849 100644 --- a/include/bitcoin/node/chase.hpp +++ b/include/bitcoin/node/chase.hpp @@ -115,9 +115,9 @@ enum class chase /// Accept/Connect. /// ----------------------------------------------------------------------- - /// A branch has become provisionally valid, download unblocked (height_t). - /// Issued by 'validate' and handled by 'check'. - prevalid, + /////// A branch has become provisionally valid, download unblocked (height_t). + /////// Issued by 'validate' and handled by 'check'. + ////prevalid, /// A branch has become valid (height_t). /// Issued by 'validate' and handled by 'check', 'confirm', 'snapshot'. diff --git a/src/chasers/chaser_check.cpp b/src/chasers/chaser_check.cpp index e0a68f7d..58ebe7a4 100644 --- a/src/chasers/chaser_check.cpp +++ b/src/chasers/chaser_check.cpp @@ -158,7 +158,7 @@ bool chaser_check::handle_chase(const code&, chase event_, break; } case chase::valid: - case chase::prevalid: + ////case chase::prevalid: { BC_ASSERT(std::holds_alternative(value)); POST(do_advanced, std::get(value)); diff --git a/src/chasers/chaser_validate_batch.cpp b/src/chasers/chaser_validate_batch.cpp index 540e8b53..4cab7308 100644 --- a/src/chasers/chaser_validate_batch.cpp +++ b/src/chasers/chaser_validate_batch.cpp @@ -91,7 +91,7 @@ void chaser_validate::push_batch(const header_link& link, } // Unblocks check chaser for download while verifying. - notify({}, chase::prevalid, possible_wide_cast(height)); + ////notify({}, chase::prevalid, possible_wide_cast(height)); // Process both tables when one hits target, allowing batched_ clearance // and therefore forward confirmation progress. Drain batch if no backlogs From 9621f41d1a0ff452aa0f61a2411e2e139c815884 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sat, 11 Jul 2026 13:42:48 -0400 Subject: [PATCH 04/11] Check chaser issue notification that window is downloaded. --- include/bitcoin/node/chase.hpp | 4 ++++ src/chasers/chaser_check.cpp | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/include/bitcoin/node/chase.hpp b/include/bitcoin/node/chase.hpp index 3399a849..8634bd8b 100644 --- a/include/bitcoin/node/chase.hpp +++ b/include/bitcoin/node/chase.hpp @@ -112,6 +112,10 @@ enum class chase /// Issued by 'block_in_31800' and handled by 'organize'. unchecked, + /// A downloaded window is completed by check (height_t). + /// Issued by 'check' and handled by 'validate'. + windowed, + /// Accept/Connect. /// ----------------------------------------------------------------------- diff --git a/src/chasers/chaser_check.cpp b/src/chasers/chaser_check.cpp index 58ebe7a4..6f9d90e1 100644 --- a/src/chasers/chaser_check.cpp +++ b/src/chasers/chaser_check.cpp @@ -391,8 +391,14 @@ void chaser_check::do_bump(height_t) NOEXCEPT // TODO: query.is_associated() is expensive (hashmap search). // Skip checked blocks starting immediately after last checked. while (!closed() && query.is_associated(query.to_candidate(++height))) + { set_position(height); + // Notify validator that no more blocks are coming. + if (height == requested_) + notify(error::success, chase::windowed, height); + } + do_headers({}); } From 54640fb54e33826f853c2c742211f009c4e5128f Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sat, 11 Jul 2026 14:40:55 -0400 Subject: [PATCH 05/11] Drain batches on window completion. --- include/bitcoin/node/chasers/chaser_validate.hpp | 1 + src/chasers/chaser_validate.cpp | 9 +++++++++ src/chasers/chaser_validate_batch.cpp | 4 ++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/include/bitcoin/node/chasers/chaser_validate.hpp b/include/bitcoin/node/chasers/chaser_validate.hpp index ce671bf0..ec5719d0 100644 --- a/include/bitcoin/node/chasers/chaser_validate.hpp +++ b/include/bitcoin/node/chasers/chaser_validate.hpp @@ -154,6 +154,7 @@ class BCN_API chaser_validate std::shared_mutex mutex_{}; atomic_counter batch_backlog_{}; atomic_counter validate_backlog_{}; + std::atomic_bool window_archived_{}; std::atomic_bool maximum_posted_{}; std::atomic_bool recovering_{}; diff --git a/src/chasers/chaser_validate.cpp b/src/chasers/chaser_validate.cpp index 491667c0..cd70db10 100644 --- a/src/chasers/chaser_validate.cpp +++ b/src/chasers/chaser_validate.cpp @@ -96,10 +96,19 @@ bool chaser_validate::handle_chase(const code&, chase event_, case chase::checked: { // value is checked block height. + window_archived_.store(false); BC_ASSERT(std::holds_alternative(value)); POST(do_checked, std::get(value)); break; } + case chase::windowed: + { + // value is last height in window. + window_archived_.store(true); + BC_ASSERT(std::holds_alternative(value)); + POST(process_batch, is_residual()); + break; + } case chase::regressed: case chase::disorganized: { diff --git a/src/chasers/chaser_validate_batch.cpp b/src/chasers/chaser_validate_batch.cpp index 4cab7308..d923c320 100644 --- a/src/chasers/chaser_validate_batch.cpp +++ b/src/chasers/chaser_validate_batch.cpp @@ -267,8 +267,8 @@ bool chaser_validate::mark_valids(bool startup) NOEXCEPT bool chaser_validate::is_residual() NOEXCEPT { - // Verify residuals when recent. - return maximum_posted_.load() && + // Verify residuals when recent or window is fully archived. + return (maximum_posted_.load() || window_archived_.load(relaxed)) && is_zero(batch_backlog_.load()) && is_zero(validate_backlog_.load()); } From 18ff242c505f370438ef313fd855f5faf4a34017 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sat, 11 Jul 2026 14:43:45 -0400 Subject: [PATCH 06/11] Style, comments. --- include/bitcoin/node/chasers/chaser_validate.hpp | 9 +++++---- src/chasers/chaser_validate.cpp | 6 +++--- src/chasers/chaser_validate_batch.cpp | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/include/bitcoin/node/chasers/chaser_validate.hpp b/include/bitcoin/node/chasers/chaser_validate.hpp index ec5719d0..94834ebd 100644 --- a/include/bitcoin/node/chasers/chaser_validate.hpp +++ b/include/bitcoin/node/chasers/chaser_validate.hpp @@ -149,16 +149,17 @@ class BCN_API chaser_validate network::threadpool validation_threadpool_; // These are thread safe. - stopper stopping_{}; - counters counters_{}; + network::asio::strand validation_strand_; std::shared_mutex mutex_{}; atomic_counter batch_backlog_{}; atomic_counter validate_backlog_{}; + std::atomic_bool disk_recovering_{}; std::atomic_bool window_archived_{}; std::atomic_bool maximum_posted_{}; - std::atomic_bool recovering_{}; + counters counters_{}; + stopper stopping_{}; - network::asio::strand validation_strand_; + // These are thread safe. const uint32_t subsidy_interval_; const uint64_t initial_subsidy_; const size_t silent_start_height_; diff --git a/src/chasers/chaser_validate.cpp b/src/chasers/chaser_validate.cpp index cd70db10..e41eaff3 100644 --- a/src/chasers/chaser_validate.cpp +++ b/src/chasers/chaser_validate.cpp @@ -75,7 +75,7 @@ bool chaser_validate::handle_chase(const code&, chase event_, // Because in-flight blocks are lost, reset position when backlog clears. if (event_ == chase::unfull) { - recovering_.store(true); + disk_recovering_.store(true); return true; } @@ -155,9 +155,9 @@ void chaser_validate::do_bump(height_t) NOEXCEPT { BC_ASSERT(stranded()); - if (recovering_.load() && is_zero(validate_backlog_.load())) + if (disk_recovering_.load() && is_zero(validate_backlog_.load())) { - recovering_.store(false); + disk_recovering_.store(false); set_position(archive().get_fork()); } diff --git a/src/chasers/chaser_validate_batch.cpp b/src/chasers/chaser_validate_batch.cpp index d923c320..ea9b65c9 100644 --- a/src/chasers/chaser_validate_batch.cpp +++ b/src/chasers/chaser_validate_batch.cpp @@ -268,7 +268,7 @@ bool chaser_validate::mark_valids(bool startup) NOEXCEPT bool chaser_validate::is_residual() NOEXCEPT { // Verify residuals when recent or window is fully archived. - return (maximum_posted_.load() || window_archived_.load(relaxed)) && + return (maximum_posted_.load() || window_archived_.load()) && is_zero(batch_backlog_.load()) && is_zero(validate_backlog_.load()); } From d84dedfcbf8f672d0e822afa32949ea942ac2efc Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sat, 11 Jul 2026 14:46:52 -0400 Subject: [PATCH 07/11] Remove dead code. --- include/bitcoin/node/chase.hpp | 4 ---- include/bitcoin/node/chasers/chaser_validate.hpp | 3 +-- src/chasers/chaser_validate.cpp | 2 +- src/chasers/chaser_validate_batch.cpp | 6 +----- 4 files changed, 3 insertions(+), 12 deletions(-) diff --git a/include/bitcoin/node/chase.hpp b/include/bitcoin/node/chase.hpp index 8634bd8b..9ac311b8 100644 --- a/include/bitcoin/node/chase.hpp +++ b/include/bitcoin/node/chase.hpp @@ -119,10 +119,6 @@ enum class chase /// Accept/Connect. /// ----------------------------------------------------------------------- - /////// A branch has become provisionally valid, download unblocked (height_t). - /////// Issued by 'validate' and handled by 'check'. - ////prevalid, - /// A branch has become valid (height_t). /// Issued by 'validate' and handled by 'check', 'confirm', 'snapshot'. valid, diff --git a/include/bitcoin/node/chasers/chaser_validate.hpp b/include/bitcoin/node/chasers/chaser_validate.hpp index 94834ebd..feb990e0 100644 --- a/include/bitcoin/node/chasers/chaser_validate.hpp +++ b/include/bitcoin/node/chasers/chaser_validate.hpp @@ -81,7 +81,7 @@ class BCN_API chaser_validate /// Batching. virtual code start_batch() NOEXCEPT; virtual void close_batch() NOEXCEPT; - virtual void push_batch(const header_link& link, size_t height) NOEXCEPT; + virtual void push_batch(const header_link& link) NOEXCEPT; virtual void process_batch(bool residual) NOEXCEPT; virtual code do_process_batch(bool startup) NOEXCEPT; virtual bool mark_valids(bool startup) NOEXCEPT; @@ -107,7 +107,6 @@ class BCN_API chaser_validate atomic_counter missed_threshold_{}; }; - static constexpr auto relaxed = std::memory_order_relaxed; using schnorr_link = database::schnorr_link; using schnorr_link_ptr = std::shared_ptr; using cursor = system::chain::threshold::cursor; diff --git a/src/chasers/chaser_validate.cpp b/src/chasers/chaser_validate.cpp index e41eaff3..5764079b 100644 --- a/src/chasers/chaser_validate.cpp +++ b/src/chasers/chaser_validate.cpp @@ -282,7 +282,7 @@ void chaser_validate::complete_block(const code& ec, const header_link& link, if (batched) { ++batch_backlog_; - POST(push_batch, link, height); + POST(push_batch, link); return; } diff --git a/src/chasers/chaser_validate_batch.cpp b/src/chasers/chaser_validate_batch.cpp index ea9b65c9..bab770c1 100644 --- a/src/chasers/chaser_validate_batch.cpp +++ b/src/chasers/chaser_validate_batch.cpp @@ -75,8 +75,7 @@ void chaser_validate::close_batch() NOEXCEPT // batched_ is redundant with the combined set of ecdsa/schnorr unfailed block // identifiers stored in the two batch tables, so just a sort optimization. -void chaser_validate::push_batch(const header_link& link, - size_t height) NOEXCEPT +void chaser_validate::push_batch(const header_link& link) NOEXCEPT { BC_ASSERT(stranded()); @@ -90,9 +89,6 @@ void chaser_validate::push_batch(const header_link& link, return; } - // Unblocks check chaser for download while verifying. - ////notify({}, chase::prevalid, possible_wide_cast(height)); - // Process both tables when one hits target, allowing batched_ clearance // and therefore forward confirmation progress. Drain batch if no backlogs // and maximum hash been posted. From e0e2bbdfe4c38fdd2a330ac4ffc72874a8adf1d4 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sat, 11 Jul 2026 14:55:13 -0400 Subject: [PATCH 08/11] Fix stall under suspension recovery. --- src/chasers/chaser_validate.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/chasers/chaser_validate.cpp b/src/chasers/chaser_validate.cpp index 5764079b..3176c20b 100644 --- a/src/chasers/chaser_validate.cpp +++ b/src/chasers/chaser_validate.cpp @@ -79,6 +79,12 @@ bool chaser_validate::handle_chase(const code&, chase event_, return true; } + // Record durable state before the suspension gate. + if (event_ == chase::windowed) + window_archived_.store(true); + else if (event_ == chase::checked) + window_archived_.store(false); + // Stop generating query during suspension. // Incoming events may already be flushed to the strand at this point. if (suspended()) @@ -86,17 +92,21 @@ bool chaser_validate::handle_chase(const code&, chase event_, switch (event_) { - case chase::resume: case chase::start: case chase::bump: { POST(do_bump, height_t{}); break; } + case chase::resume: + { + POST(process_batch, is_residual()); + POST(do_bump, height_t{}); + break; + } case chase::checked: { // value is checked block height. - window_archived_.store(false); BC_ASSERT(std::holds_alternative(value)); POST(do_checked, std::get(value)); break; @@ -104,7 +114,6 @@ bool chaser_validate::handle_chase(const code&, chase event_, case chase::windowed: { // value is last height in window. - window_archived_.store(true); BC_ASSERT(std::holds_alternative(value)); POST(process_batch, is_residual()); break; From 17f41a6b63325a8f61934359f79969089a91435f Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sat, 11 Jul 2026 15:02:08 -0400 Subject: [PATCH 09/11] Comment. --- src/chasers/chaser_validate.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/chasers/chaser_validate.cpp b/src/chasers/chaser_validate.cpp index 3176c20b..7c30b913 100644 --- a/src/chasers/chaser_validate.cpp +++ b/src/chasers/chaser_validate.cpp @@ -79,7 +79,8 @@ bool chaser_validate::handle_chase(const code&, chase event_, return true; } - // Record durable state before the suspension gate. + // Record durable state before the suspension gate. A consequence is that + // recovery with window_archived_ drains small batches until next checked. if (event_ == chase::windowed) window_archived_.store(true); else if (event_ == chase::checked) From fbbbf979aa71a138eec14f6aa66a55627fde173a Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sat, 11 Jul 2026 15:24:20 -0400 Subject: [PATCH 10/11] Fix log storm and excess locking (prematurity). --- src/chasers/chaser_validate_batch.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/chasers/chaser_validate_batch.cpp b/src/chasers/chaser_validate_batch.cpp index bab770c1..310825d2 100644 --- a/src/chasers/chaser_validate_batch.cpp +++ b/src/chasers/chaser_validate_batch.cpp @@ -271,10 +271,18 @@ bool chaser_validate::is_residual() NOEXCEPT bool chaser_validate::is_mature(bool residual) NOEXCEPT { - // Verify non-residuals when mature. + const auto& query = archive(); + const auto ecdsa = query.ecdsa_records(); + const auto schnorr = query.schnorr_records(); + + // Nothing to verify and no residuals to release. + if (is_zero(ecdsa) && is_zero(schnorr) && batched_.empty()) + return false; + + // Verify residuals whenever, and non-residuals when mature. return residual || - (archive().ecdsa_records() >= batch_target_) || - (archive().schnorr_records() >= batch_target_); + (ecdsa >= batch_target_) || + (schnorr >= batch_target_); } std::string chaser_validate::log_rate(const std::string& name, From fb880fa7a31fcf036ca70f67a8a4707a0b4f47dd Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sat, 11 Jul 2026 23:19:47 -0400 Subject: [PATCH 11/11] Batch control. --- .../bitcoin/node/chasers/chaser_validate.hpp | 29 +-- include/bitcoin/node/error.hpp | 1 + src/chasers/chaser_validate.cpp | 25 +- src/chasers/chaser_validate_batch.cpp | 228 ++++++++---------- src/chasers/chaser_validate_capture.cpp | 3 +- src/chasers/chaser_validate_parallel.cpp | 51 ++-- src/error.cpp | 1 + test/error.cpp | 8 +- 8 files changed, 167 insertions(+), 179 deletions(-) diff --git a/include/bitcoin/node/chasers/chaser_validate.hpp b/include/bitcoin/node/chasers/chaser_validate.hpp index feb990e0..116f8ff6 100644 --- a/include/bitcoin/node/chasers/chaser_validate.hpp +++ b/include/bitcoin/node/chasers/chaser_validate.hpp @@ -20,7 +20,6 @@ #define LIBBITCOIN_NODE_CHASERS_CHASER_VALIDATE_HPP #include -#include #include #include @@ -78,15 +77,19 @@ class BCN_API chaser_validate virtual void notify_block(const code& ec, size_t height, const header_link& link, bool bypass, bool startup=false) NOEXCEPT; - /// Batching. + /// Batching (lock-free, self-serviced by completing pool threads). + /// Batch state is the store: sig tables carry rows, prevalid table + /// carries the per-block link set (write-through, crash-durable). virtual code start_batch() NOEXCEPT; - virtual void close_batch() NOEXCEPT; - virtual void push_batch(const header_link& link) NOEXCEPT; virtual void process_batch(bool residual) NOEXCEPT; virtual code do_process_batch(bool startup) NOEXCEPT; - virtual bool mark_valids(bool startup) NOEXCEPT; - virtual bool mark_invalids(const header_links& invalids, - bool startup) NOEXCEPT; + virtual bool mark_valids(header_links& prevalids, bool startup) NOEXCEPT; + virtual bool mark_invalids(header_links& prevalids, + const header_links& invalids, bool startup) NOEXCEPT; + + /// Turnstile (drain/capture exclusion without blocking writers). + virtual bool enter_capture() NOEXCEPT; + virtual void exit_capture() NOEXCEPT; // Override base class strand because it sits on the network thread pool. network::asio::strand& strand() NOEXCEPT override; @@ -139,22 +142,20 @@ class BCN_API chaser_validate // Batching helpers. bool is_residual() NOEXCEPT; bool is_mature(bool residual) NOEXCEPT; - std::string log_rate(const std::string& name, size_t numerator, - size_t denominator) const NOEXCEPT; + std::string log_rate(const std::string& name, size_t signatures, + size_t milliseconds) const NOEXCEPT; - // These are protected by strand. - header_links batched_{}; - header_links invalids_{}; + // This is not thread safe. network::threadpool validation_threadpool_; // These are thread safe. network::asio::strand validation_strand_; - std::shared_mutex mutex_{}; - atomic_counter batch_backlog_{}; atomic_counter validate_backlog_{}; std::atomic_bool disk_recovering_{}; std::atomic_bool window_archived_{}; std::atomic_bool maximum_posted_{}; + std::atomic_bool draining_{}; + atomic_counter writers_{}; counters counters_{}; stopper stopping_{}; diff --git a/include/bitcoin/node/error.hpp b/include/bitcoin/node/error.hpp index a4eddf6c..90f05f72 100644 --- a/include/bitcoin/node/error.hpp +++ b/include/bitcoin/node/error.hpp @@ -92,6 +92,7 @@ enum error_t : uint8_t validate7, validate8, validate9, + validate10, confirm1, confirm2, confirm3, diff --git a/src/chasers/chaser_validate.cpp b/src/chasers/chaser_validate.cpp index 7c30b913..5e68c89b 100644 --- a/src/chasers/chaser_validate.cpp +++ b/src/chasers/chaser_validate.cpp @@ -18,7 +18,6 @@ */ #include -#include #include #include #include @@ -101,7 +100,8 @@ bool chaser_validate::handle_chase(const code&, chase event_, } case chase::resume: { - POST(process_batch, is_residual()); + // A windowed drain may have been elided during suspension. + process_batch(is_residual()); POST(do_bump, height_t{}); break; } @@ -114,9 +114,12 @@ bool chaser_validate::handle_chase(const code&, chase event_, } case chase::windowed: { - // value is last height in window. + // value is last height in window. Called directly (not posted): + // the drain must never depend on scheduling, which saturated + // validations can exhaust for the entire window (strand for + // admission-side control, never for release-side). BC_ASSERT(std::holds_alternative(value)); - POST(process_batch, is_residual()); + process_batch(is_residual()); break; } case chase::regressed: @@ -282,28 +285,25 @@ void chaser_validate::complete_block(const code& ec, const header_link& link, notify_block({}, height, link, bypass); } - // Batch jobs (all posting from unstranded). + // Batch jobs (self-serviced from any thread). // ------------------------------------------------------------------------ if (closed() || !batch_enabled_) return; - // Queue block and process batch if ready. if (batched) { - ++batch_backlog_; - POST(push_batch, link); + process_batch(is_residual()); return; } // Capturing disabled when confirmed chain current (and not under bypass). - // When not in effect must drain last batch by last block validation. const auto current = !capturing && !bypass; - // Drain batch when recent (current, or maximum reached without backlog). + // Drain batch when recent (current, or window/maximum without backlog). if (current || is_residual()) { - POST(process_batch, true); + process_batch(true); } } @@ -332,9 +332,6 @@ void chaser_validate::notify_block(const code& ec, size_t height, void chaser_validate::stopping(const code& ec) NOEXCEPT { - // closed() is now true so time to bump batched_ drain. - POST(close_batch); - // Stop long-running batch validations. stopping_.store(true); diff --git a/src/chasers/chaser_validate_batch.cpp b/src/chasers/chaser_validate_batch.cpp index 310825d2..1213b78b 100644 --- a/src/chasers/chaser_validate_batch.cpp +++ b/src/chasers/chaser_validate_batch.cpp @@ -20,7 +20,7 @@ #include #include -#include +#include #include namespace libbitcoin { @@ -38,125 +38,96 @@ BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) // ---------------------------------------------------------------------------- // protected -// If there was a non-empty batch at startup, process it for invalids and set -// their states normally, then scan from fork point (position()) to association -// gap for all prevalids. Iterate over these setting their states to valid. code chaser_validate::start_batch() NOEXCEPT { - BC_ASSERT(batched_.empty()); if (!batch_enabled_) return {}; - auto& query = archive(); - batched_ = query.get_prevalids(); - if (!query.purge_prevalids()) - return error::batch1; - - if (is_zero(query.ecdsa_records()) && is_zero(query.schnorr_records())) + const auto& query = archive(); + if (is_zero(query.prevalid_records()) && + is_zero(query.ecdsa_records()) && + is_zero(query.schnorr_records())) return {}; return do_process_batch(true); } -// Shutdown drains batched_ to prevalid table, which is recovered on startup. -// Snapshot restoration purges sigs and prevalids as these are not append-only. -void chaser_validate::close_batch() NOEXCEPT +void chaser_validate::process_batch(bool residual) NOEXCEPT { - BC_ASSERT(stranded()); - BC_ASSERT(closed()); + // Cheap pre-test, retested in critical section. + if (closed() || !is_mature(residual)) + return; + + // Claim the drain (at most one, losers rely on the winner). + if (draining_.exchange(true)) + return; - // Set even if signature batch tables are empty. - if (is_zero(batch_backlog_.load())) + // Wait for captures to complete or abandon on close. + while (is_nonzero(writers_.load())) { - archive().set_prevalids(batched_); - batched_.clear(); - } -} + if (closed()) + { + draining_.store(false); + return; + } -// batched_ is redundant with the combined set of ecdsa/schnorr unfailed block -// identifiers stored in the two batch tables, so just a sort optimization. -void chaser_validate::push_batch(const header_link& link) NOEXCEPT -{ - BC_ASSERT(stranded()); + std::this_thread::yield(); + } - // Accumulate even if closed. Sacrifices stop speed to save validations. - batched_.push_back(link); - --batch_backlog_; + // Batch tables are now quiescent (no writers admitted, none in flight). + // ======================================================================== - if (closed()) + // Retest under the claim, another drain may have just emptied the tables. + if (!is_mature(residual)) { - close_batch(); + draining_.store(false); return; } - // Process both tables when one hits target, allowing batched_ clearance - // and therefore forward confirmation progress. Drain batch if no backlogs - // and maximum hash been posted. - const auto residual = is_residual(); - process_batch(residual); - - if (residual) - batched_.shrink_to_fit(); -} - -void chaser_validate::process_batch(bool residual) NOEXCEPT -{ - BC_ASSERT(stranded()); - - // Test outside of lock to prevent reader contention for nearly all calls. - if (closed() || !is_mature(residual)) + const auto ec = do_process_batch(false); + draining_.store(false); + if (ec == network::error::operation_canceled) return; - // Unique lock prevents batch table updates during evaluation, allowing the - // tables to be fully purged upon completion, and ensuring that evaluation - // does not operate over partial block records in the batch tables. - // ======================================================================== + if (ec) { - const std::unique_lock lock{ mutex_ }; - - // Must retest inside lock as table updates are running concurrently. - if (closed() || !is_mature(residual)) - return; - - if (const auto ec = do_process_batch(false)) - { - fault(ec); - return; - } + fault(ec); + return; } + // ======================================================================== - // Log outside of lock, and only when batch executes (non-verbose). + // Log outside of drain claim, and only when batch executes (non-verbose). log_captures(); } +// Guarded by the drain claim (or single-threaded at startup). code chaser_validate::do_process_batch(bool startup) NOEXCEPT { auto& query = archive(); + auto prevalids = query.get_prevalids(); const auto ecdsa = query.ecdsa_records(); if (is_nonzero(ecdsa)) { header_links invalids{}; const auto start = network::logger::now(); - if (!query.verify_ecdsa_signatures(stopping_, invalids)) - { - LOGN("Batch verify ecdsa canceled (" << ecdsa << ")."); - return network::error::operation_canceled; - } - - const auto end = network::logger::now(); - const auto elapsed = duration_cast(end - start).count(); - fire(events::ecdsa_secs, elapsed); + ////if (!query.verify_ecdsa_signatures(stopping_, invalids)) + ////{ + //// LOGN("Batch verify ecdsa canceled (" << ecdsa << ")."); + //// return network::error::operation_canceled; + ////} + const auto elapsed = network::logger::now() - start; + fire(events::ecdsa_secs, duration_cast(elapsed).count()); if (!startup) { - LOGN(log_rate("Batch verify rate ecdsa.... ", ecdsa, elapsed)); + LOGN(log_rate("Batch verify rate ecdsa.... ", ecdsa, + duration_cast(elapsed).count())); } - if (!mark_invalids(invalids, startup) || - !query.purge_ecdsa_signatures()) - return error::batch2; + if (!mark_invalids(prevalids, invalids, startup)) + return error::batch1; } const auto schnorr = query.schnorr_records(); @@ -164,42 +135,37 @@ code chaser_validate::do_process_batch(bool startup) NOEXCEPT { header_links invalids{}; const auto start = network::logger::now(); - if (!query.verify_schnorr_signatures(stopping_, invalids)) - { - LOGN("Batch verify schnorr canceled (" << schnorr << ")."); - return network::error::operation_canceled; - } - - const auto end = network::logger::now(); - const auto elapsed = duration_cast(end - start).count(); - fire(events::schnorr_secs, elapsed); + ////if (!query.verify_schnorr_signatures(stopping_, invalids)) + ////{ + //// LOGN("Batch verify schnorr canceled (" << schnorr << ")."); + //// return network::error::operation_canceled; + ////} + const auto elapsed = network::logger::now() - start; + fire(events::schnorr_secs, duration_cast(elapsed).count()); if (!startup) { - LOGN(log_rate("Batch verify rate schnorr.. ", schnorr, elapsed)); + LOGN(log_rate("Batch verify rate schnorr.. ", schnorr, + duration_cast(elapsed).count())); } - if (!mark_invalids(invalids, startup) || - !query.purge_schnorr_signatures()) - return error::batch3; + if (!mark_invalids(prevalids, invalids, startup)) + return error::batch2; } - return mark_valids(startup) ? error::success : error::batch4; + if (!mark_valids(prevalids, startup)) + return error::batch3; + + // Purge prevalids before signatures. + return query.purge_prevalids() && + query.purge_ecdsa_signatures() && + query.purge_schnorr_signatures() ? + error::success : error::batch4; } -// Invalids might not be included in batched, as link push is a race. -// Collected links are only required to set valid, not invalid, and do not -// need to coincide with the batch that is currently being processed (!). -// A batched_ value may not be present in the current batched_ set despite -// being invalid here, which is expected. However upon invalidation and failure -// to match here, this block id will subsequently land in batched_ and would -// then be reported as valid, overriding the unconfirmable block state set -// below, so invalids_ are cached for process lifetime. -bool chaser_validate::mark_invalids(const header_links& invalids, - bool startup) NOEXCEPT +bool chaser_validate::mark_invalids(header_links& prevalids, + const header_links& invalids, bool startup) NOEXCEPT { - BC_ASSERT(stranded()); - auto& query = archive(); for (const auto& link: invalids) { @@ -208,38 +174,36 @@ bool chaser_validate::mark_invalids(const header_links& invalids, !query.set_block_unconfirmable(link)) return false; - invalids_.push_back(link); const auto ec = system::error::invalid_signature; notify_block(ec, height, link, false, startup); } - // Set all invalid links in batched_ to terminal (to be skipped). - if (!invalids_.empty()) + // Exclude invalid links from marking, invalids is almost always empty. + if (!invalids.empty()) { - std::ranges::replace_if(batched_, [&](const auto& link) NOEXCEPT + std::ranges::replace_if(prevalids, [&](const header_link& link) NOEXCEPT { - return contains(invalids_, link); + return contains(invalids, link); }, header_link::terminal); } return true; } -// Set all batched blocks that aren't invalid to valid. +// Set all prevalid blocks that aren't invalid to valid. // May be ancestors of invalid, in which case they are also unconfirmable. -bool chaser_validate::mark_valids(bool startup) NOEXCEPT +bool chaser_validate::mark_valids(header_links& prevalids, + bool startup) NOEXCEPT { - BC_ASSERT(stranded()); - auto& query = archive(); std::atomic_bool fault{}; constexpr auto parallel = poolstl::execution::par; // Allow valids to drain when closed. - std::for_each(parallel, batched_.cbegin(), batched_.cend(), + std::for_each(parallel, prevalids.cbegin(), prevalids.cend(), [&](auto link) NOEXCEPT { - // terminal links are previously set invalid (to be skipped). + // Terminal links are those flagged as invalid (to be skipped). if (link == header_link::terminal) return; @@ -253,7 +217,6 @@ bool chaser_validate::mark_valids(bool startup) NOEXCEPT notify_block(system::error::success, height, link, false, startup); }); - batched_.clear(); return !fault.load(); } @@ -265,7 +228,6 @@ bool chaser_validate::is_residual() NOEXCEPT { // Verify residuals when recent or window is fully archived. return (maximum_posted_.load() || window_archived_.load()) && - is_zero(batch_backlog_.load()) && is_zero(validate_backlog_.load()); } @@ -275,8 +237,9 @@ bool chaser_validate::is_mature(bool residual) NOEXCEPT const auto ecdsa = query.ecdsa_records(); const auto schnorr = query.schnorr_records(); - // Nothing to verify and no residuals to release. - if (is_zero(ecdsa) && is_zero(schnorr) && batched_.empty()) + // Nothing to verify and no links to release. + if (is_zero(ecdsa) && is_zero(schnorr) && + is_zero(query.prevalid_records())) return false; // Verify residuals whenever, and non-residuals when mature. @@ -286,11 +249,32 @@ bool chaser_validate::is_mature(bool residual) NOEXCEPT } std::string chaser_validate::log_rate(const std::string& name, - size_t numerator, size_t denominator) const NOEXCEPT + size_t signatures, size_t milliseconds) const NOEXCEPT +{ + const auto rate = (signatures * 1000u) / greater(milliseconds, one); + return (boost_format("%1% (%2% / %3%ms) = %4% sps") % + name % signatures % (milliseconds) % rate).str(); +} + +// Turnstile. +// ---------------------------------------------------------------------------- +// protected + +bool chaser_validate::enter_capture() NOEXCEPT +{ + ++writers_; + if (draining_.load()) + { + --writers_; + return false; + } + + return true; +} + +void chaser_validate::exit_capture() NOEXCEPT { - const auto rate = numerator / greater(denominator, one); - return (boost_format("%1% (%2% / %3%) = %4% sps") % - name % numerator % denominator % rate).str(); + --writers_; } BC_POP_WARNING() diff --git a/src/chasers/chaser_validate_capture.cpp b/src/chasers/chaser_validate_capture.cpp index e1dde2e5..85fbe40e 100644 --- a/src/chasers/chaser_validate_capture.cpp +++ b/src/chasers/chaser_validate_capture.cpp @@ -18,7 +18,6 @@ */ #include -#include #include namespace libbitcoin { @@ -132,7 +131,7 @@ chaser_validate::cursor chaser_validate::open_threshold(size_t rows, signatures chaser_validate::get_capture(const header_link& link) NOEXCEPT { - if (!batch_enabled_ || is_current_header(link)) + if (!batch_enabled_ || link.is_terminal() || is_current_header(link)) return { false }; const auto sequence = to_shared(); diff --git a/src/chasers/chaser_validate_parallel.cpp b/src/chasers/chaser_validate_parallel.cpp index 2dd75740..8b427864 100644 --- a/src/chasers/chaser_validate_parallel.cpp +++ b/src/chasers/chaser_validate_parallel.cpp @@ -114,45 +114,50 @@ code chaser_validate::validate(bool& batched, bool& capturing, bool bypass, if ((ec = block.accept(ctx, subsidy_interval_, initial_subsidy_))) return ec; - // Initialize block capture. - const auto capture = get_capture(link); + // This critical section is mutually-exclusive with batch verification + // (turnstile). All batch table state written inside the capture epoch. + // ==================================================================== - // Signature capture is enabled. - capturing = capture.enabled; + // Enter the capture turnstile. If drain is in progress, full validate. + const auto entered = enter_capture(); + + // Initialize signature capture. + const auto capture = get_capture(entered ? link : header_link{}); + capturing = entered ? capture.enabled : true; + + ec = block.connect(ctx, capture); + + // At least one signature batch was attempted (batch completion). + batched = capture.batched; + + // Mark block as valid contingent on batch verification. + if (!ec && batched && !query.set_prevalid(link)) + ec = error::validate6; + + if (entered) + exit_capture(); - // This critical section is mutually-exclusive with batch verification. - // ==================================================================== - { - std::shared_lock lock{ mutex_, std::defer_lock }; - if (capturing) lock.lock(); - - // Sequentially connect block with signature capture (if enabled). - // There is no stop during connect, so shutdown will wait on the - // completion (block consistency) of all signature captures. - if ((ec = block.connect(ctx, capture))) - return ec; - - // At least one signature batch was attempted (defer completion). - batched = capture.batched; - } // ==================================================================== + if (ec) + return ec; + // Prevouts optimize confirmation. if (!query.set_prevouts(link, block)) - return error::validate6; + return error::validate7; } if (!query.set_filter_body(link, block)) - return error::validate7; + return error::validate8; if ((ctx.height >= silent_start_height_) && !query.set_silent(link, block)) - return error::validate8; + return error::validate9; // Defer block state change when batched. // Valid must be set after set_prevouts, set_filter_body, and set_silent. if (!batched && !bypass && !query.set_block_valid(link)) - return error::validate9; + return error::validate10; return error::success; } diff --git a/src/error.cpp b/src/error.cpp index 05c37c26..f7a2caf5 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -82,6 +82,7 @@ DEFINE_ERROR_T_MESSAGE_MAP(error) { validate7, "validate7" }, { validate8, "validate8" }, { validate9, "validate9" }, + { validate10, "validate0" }, { confirm1, "confirm1" }, { confirm2, "confirm2" }, { confirm3, "confirm3" }, diff --git a/test/error.cpp b/test/error.cpp index 3636a5ef..b75690a1 100644 --- a/test/error.cpp +++ b/test/error.cpp @@ -222,7 +222,7 @@ BOOST_AUTO_TEST_CASE(error_t__code__header1__true_expected_message) BOOST_REQUIRE_EQUAL(ec.message(), "header1"); } -// TODO: organize2-organize15 +// TODO: organize2-... BOOST_AUTO_TEST_CASE(error_t__code__organize1__true_expected_message) { constexpr auto value = error::organize1; @@ -232,7 +232,7 @@ BOOST_AUTO_TEST_CASE(error_t__code__organize1__true_expected_message) BOOST_REQUIRE_EQUAL(ec.message(), "organize1"); } -// TODO: validate2-validate6 +// TODO: validate2-... BOOST_AUTO_TEST_CASE(error_t__code__validate1__true_expected_message) { constexpr auto value = error::validate1; @@ -242,7 +242,7 @@ BOOST_AUTO_TEST_CASE(error_t__code__validate1__true_expected_message) BOOST_REQUIRE_EQUAL(ec.message(), "validate1"); } -// TODO: confirm2-confirm17 +// TODO: confirm2-... BOOST_AUTO_TEST_CASE(error_t__code__confirm1__true_expected_message) { constexpr auto value = error::confirm1; @@ -308,6 +308,6 @@ BOOST_AUTO_TEST_CASE(error_t__code__batch1__true_expected_message) BOOST_REQUIRE_EQUAL(ec.message(), "batch1"); } -// TODO: batch2-batch8 +// TODO: batch2-... BOOST_AUTO_TEST_SUITE_END()