diff --git a/Makefile.am b/Makefile.am
index 2b10aad5..b9312360 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -55,6 +55,8 @@ src_libbitcoin_node_la_SOURCES = \
src/chasers/chaser_transaction.cpp \
src/chasers/chaser_validate.cpp \
src/chasers/chaser_validate_batch.cpp \
+ src/chasers/chaser_validate_capture.cpp \
+ src/chasers/chaser_validate_parallel.cpp \
src/messages/block.cpp \
src/messages/transaction.cpp \
src/protocols/protocol.cpp \
diff --git a/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj b/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj
index edac1c25..b9353557 100644
--- a/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj
+++ b/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj
@@ -136,6 +136,8 @@
+
+
diff --git a/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj.filters
index 29e098fb..d03ee456 100644
--- a/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj.filters
+++ b/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj.filters
@@ -108,6 +108,12 @@
src\chasers
+
+ src\chasers
+
+
+ src\chasers
+
src
diff --git a/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj b/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj
index 0f61c134..f5467cf1 100644
--- a/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj
+++ b/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj
@@ -136,6 +136,8 @@
+
+
diff --git a/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj.filters b/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj.filters
index 29e098fb..d03ee456 100644
--- a/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj.filters
+++ b/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj.filters
@@ -108,6 +108,12 @@
src\chasers
+
+ src\chasers
+
+
+ src\chasers
+
src
diff --git a/include/bitcoin/node/chase.hpp b/include/bitcoin/node/chase.hpp
index 891be398..6a2d6449 100644
--- a/include/bitcoin/node/chase.hpp
+++ b/include/bitcoin/node/chase.hpp
@@ -111,6 +111,10 @@ 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 046a2a34..69a6b23f 100644
--- a/include/bitcoin/node/chasers/chaser_validate.hpp
+++ b/include/bitcoin/node/chasers/chaser_validate.hpp
@@ -81,10 +81,7 @@ class BCN_API chaser_validate
/// Batching.
virtual code start_batch() NOEXCEPT;
virtual void process_batch(bool residual) NOEXCEPT;
- virtual bool process_valids(bool residual) NOEXCEPT;
virtual void push_batch(const header_link& link, size_t height) NOEXCEPT;
- virtual bool process_invalids(const header_links& invalids) NOEXCEPT;
- virtual signatures get_capture(const header_link& link) NOEXCEPT;
// Override base class strand because it sits on the network thread pool.
network::asio::strand& strand() NOEXCEPT override;
@@ -97,6 +94,11 @@ class BCN_API chaser_validate
using threshold_group = signatures::threshold_group;
using missed = signatures::miss;
+ /// Batching helpers.
+ bool is_maximum() NOEXCEPT;
+ bool process_valids(bool residual) NOEXCEPT;
+ bool process_invalids(const header_links& invalids) NOEXCEPT;
+
// Capture handlers.
void do_log(const system::chain::script& missed) NOEXCEPT;
void do_fire(missed miss, size_t count) NOEXCEPT;
@@ -114,6 +116,7 @@ class BCN_API chaser_validate
const atomic_counter_ptr& sequence) NOEXCEPT;
// Capture helpers.
+ signatures get_capture(const header_link& link) NOEXCEPT;
std::string log_rate(const std::string& name, size_t numerator,
size_t denominator) const NOEXCEPT;
std::string log_ratio(const std::string& name, size_t numerator,
@@ -135,12 +138,15 @@ class BCN_API chaser_validate
std::atomic missed_schnorr_{};
std::atomic missed_multisig_{};
std::atomic missed_threshold_{};
- std::atomic backlog_{};
+ std::atomic validate_backlog_{};
+ std::atomic batch_backlog_{};
+ std::atomic_bool maximum_posted_{};
network::asio::strand validation_strand_;
const uint32_t subsidy_interval_;
const uint64_t initial_subsidy_;
const size_t maximum_backlog_;
+ const size_t maximum_height_;
const uint64_t batch_target_;
const bool batch_enabled_;
const bool node_witness_;
diff --git a/src/chasers/chaser_check.cpp b/src/chasers/chaser_check.cpp
index 64a20229..65b483a2 100644
--- a/src/chasers/chaser_check.cpp
+++ b/src/chasers/chaser_check.cpp
@@ -155,6 +155,7 @@ bool chaser_check::handle_chase(const code&, chase event_,
break;
}
case chase::valid:
+ case chase::prevalid:
{
BC_ASSERT(std::holds_alternative(value));
POST(do_advanced, std::get(value));
diff --git a/src/chasers/chaser_validate.cpp b/src/chasers/chaser_validate.cpp
index fa1d9899..8b76efe3 100644
--- a/src/chasers/chaser_validate.cpp
+++ b/src/chasers/chaser_validate.cpp
@@ -43,6 +43,7 @@ chaser_validate::chaser_validate(full_node& node) NOEXCEPT
subsidy_interval_(node.system_settings().subsidy_interval_blocks),
initial_subsidy_(node.system_settings().initial_subsidy()),
maximum_backlog_(node.node_settings().maximum_concurrency_()),
+ maximum_height_(node.node_settings().maximum_height_()),
batch_target_(node.node_settings().batch_signatures),
batch_enabled_(node.node_settings().batch_signatures_enabled()),
node_witness_(node.network_settings().witness_node()),
@@ -151,7 +152,7 @@ void chaser_validate::do_bumped(height_t height) NOEXCEPT
// Bypass until next event if validation backlog is full.
// Stop when suspended as write error does not terminate asynchronous loop.
- while ((backlog_ < maximum_backlog_) && !closed() && !suspended())
+ while ((validate_backlog_ < maximum_backlog_) && !closed() && !suspended())
{
const auto link = query.to_candidate(height);
const auto ec = query.get_block_state(link);
@@ -161,6 +162,10 @@ void chaser_validate::do_bumped(height_t height) NOEXCEPT
if (ec == database::error::unassociated)
return;
+ // The last job requiring validation has been posted.
+ if (height == maximum_height_)
+ maximum_posted_ = true;
+
const auto bypass = is_under_checkpoint(height) ||
query.is_milestone(link);
@@ -169,10 +174,15 @@ void chaser_validate::do_bumped(height_t height) NOEXCEPT
case database::error::unvalidated:
case database::error::unknown_state:
{
- if (!bypass || filter_)
- post_block(link, bypass);
- else
+ if (bypass && !filter_)
+ {
complete_block(error::success, link, height, true);
+ }
+ else
+ {
+ ++validate_backlog_;
+ post_block(link, bypass);
+ }
break;
}
case database::error::block_valid:
@@ -203,147 +213,21 @@ void chaser_validate::post_block(const header_link& link,
bool bypass) NOEXCEPT
{
BC_ASSERT(stranded());
-
- backlog_.fetch_add(one, relaxed);
PARALLEL(validate_block, link, bypass);
}
-// Unstranded (concurrent by block)
-// ----------------------------------------------------------------------------
-
-void chaser_validate::validate_block(const header_link& link,
- bool bypass) NOEXCEPT
-{
- if (closed())
- return;
-
- code ec{};
- chain::context ctx{};
- bool batched{}, faulted{}, enabled{};
- auto& query = archive();
-
- // TODO: implement allocator parameter resulting in full allocation to
- // shared_ptr, to optimize deallocate (12% of milestone/filter).
- const auto block = query.get_block(link, node_witness_);
-
- if (!block)
- {
- ec = error::validate2;
- }
- else if (!query.get_context(ctx, link))
- {
- ec = error::validate3;
- }
- else if ((ec = populate(bypass, *block, ctx)))
- {
- if (!query.set_block_unconfirmable(link))
- ec = error::validate4;
- }
- else if ((ec = validate(batched, faulted, enabled, bypass, *block, link, ctx)))
- {
- if (!query.set_block_unconfirmable(link))
- ec = error::validate5;
- }
-
- complete_block(ec, link, ctx.height, bypass, batched, faulted, enabled);
-
- // Prevent stall by posting internal event, avoiding external handlers.
- if (is_one(backlog_.fetch_sub(one, relaxed)))
- handle_chase({}, chase::bump, height_t{});
-}
-
-code chaser_validate::populate(bool bypass, const chain::block& block,
- const chain::context& ctx) NOEXCEPT
-{
- const auto& query = archive();
-
- if (bypass)
- {
- // Populating for filters only (no validation metadata required).
- block.populate(ctx);
- if (!query.populate_without_metadata(block))
- return system::error::missing_previous_output;
- }
- else
- {
- // Internal maturity and time locks are verified here because they are
- // the only necessary confirmation checks for internal spends.
- if (const auto ec = block.populate(ctx))
- return ec;
-
- // Metadata identifies internal spends allowing confirmation bypass.
- if (!query.populate_with_metadata(block))
- return system::error::missing_previous_output;
- }
-
- return error::success;
-}
-
-code chaser_validate::validate(bool& batched, bool& faulted, bool& capturing,
- bool bypass, const chain::block& block, const header_link& link,
- const chain::context& ctx) NOEXCEPT
-{
- auto& query = archive();
-
- if (!bypass)
- {
- code ec{};
- if (((ec = block.check(false))) || ((ec = block.check(ctx, false))))
- return ec;
-
- if ((ec = block.accept(ctx, subsidy_interval_, initial_subsidy_)))
- return ec;
-
- // Initialize block capture.
- const auto capture = get_capture(link);
-
- // Signature capture is enabled.
- capturing = capture.enabled;
-
- // 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 not stop during connect, so shutdown will wait on the
- // completion (block consistency) of all signature captures. But
- // the faulted state of batch is not persisted (because disk full).
- if ((ec = block.connect(ctx, capture)))
- return ec;
-
- // At least one signature batch was attempted (defer completion).
- batched = capture.batched;
-
- // Threshold batch commit failed, block otherwise passed (retry).
- faulted = capture.faulted;
- }
- // ================================================================
-
- // Prevouts optimize confirmation.
- // Block will be retried if batch is faulted.
- if (!faulted && !query.set_prevouts(link, block))
- return error::validate6;
- }
-
- // Block will be retried if batch is faulted.
- if (!faulted && !query.set_filter_body(link, block))
- return error::validate7;
-
- // Defer block state change when batched (or faulted).
- // Valid must be set after set_prevouts and set_filter_body.
- if (!batched && !bypass && !query.set_block_valid(link))
- return error::validate8;
-
- return error::success;
-}
-
// May be either concurrent or stranded.
void chaser_validate::complete_block(const code& ec, const header_link& link,
size_t height, bool bypass, bool batched, bool faulted,
bool capturing) NOEXCEPT
{
+ // Not stranded when called from validate_block.
+ if (is_zero(validate_backlog_.load()) && !stranded())
+ {
+ // Prevent stall by posting internal event, avoiding external handlers.
+ handle_chase({}, chase::bump, height_t{});
+ }
+
// Node errors are fatal (or disk full recoverable).
if (ec && node::error::error_category::contains(ec))
{
@@ -352,41 +236,48 @@ void chaser_validate::complete_block(const code& ec, const header_link& link,
return;
}
- // Prioritize non-signature block validation failures.
+ // Prioritize non-signature validation failures over batch result.
if (ec)
{
notify_block(ec, height, link, bypass);
return;
}
- // At least one unrecoverable (threshold) capture failed during script
- // validations, and there was no other failure. This is only caused by a
- // store fault - possibly a disk full condition. In the case of disk full
- // the node will pause, otherwise it will halt. Assume disk full here,
- // requiring a repost for block validation.
+ // Falls through to trigger residual batch processing.
+ if (!batched)
+ {
+ notify_block({}, height, link, bypass);
+ }
+
+ // Batch jobs (all posting from unstranded).
+ // ------------------------------------------------------------------------
+
+ // Avoid posting new work when closing.
+ if (closed() || !batch_enabled_)
+ return;
+
+ // Retry faulted threshold, re-enters backlog (presumes disk full).
if (faulted)
{
- // retry, no notify_block() this time.
+ ++validate_backlog_;
POST(post_block, link, bypass);
return;
}
- // Push block link to batched_, process_batch will verify via batch.
- // If block is missed it will be picked up on next batch, or on restart.
+ // Queue block and process batch if ready.
if (batched)
{
- // notify_block() success comes from process_invalids() and fail is
- // split beween push_batch() and process_valids().
+ ++batch_backlog_;
POST(push_batch, link, height);
return;
}
- // Not failed/invalid/batched/faulted, so block is complete (maybe valid).
- notify_block({}, height, link, bypass);
+ // 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;
- // Batch enabled not bypassed implies that the block is current and not
- // batched. Each such block triggers residual batch processing (no push).
- if (batch_enabled_ && !stopping_ && !capturing && !bypass)
+ // Drain batch when recent (current, or maximum reached without backlog).
+ if (current || is_maximum())
{
POST(process_batch, true);
}
@@ -395,6 +286,8 @@ void chaser_validate::complete_block(const code& ec, const header_link& link,
void chaser_validate::notify_block(const code& ec, size_t height,
const header_link& link, bool bypass) NOEXCEPT
{
+ // Not stranded when complete_block is called from validate_block.
+
if (ec)
{
// INVALID BLOCK (not a fault but discontinue)
diff --git a/src/chasers/chaser_validate_batch.cpp b/src/chasers/chaser_validate_batch.cpp
index a397a7c0..244a38af 100644
--- a/src/chasers/chaser_validate_batch.cpp
+++ b/src/chasers/chaser_validate_batch.cpp
@@ -28,18 +28,16 @@ namespace node {
#define CLASS chaser_validate
using namespace system;
-using namespace system::chain;
using namespace database;
using namespace std::chrono;
-using namespace std::placeholders;
BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT)
-BC_PUSH_WARNING(SMART_PTR_NOT_NEEDED)
-BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR)
+
+// Batching.
+// ----------------------------------------------------------------------------
+// protected
// TODO: ecdsa can be retained, as they don't fault, so set batched_ here.
-// TODO: scnorr can be retained if each threshold carries total sig count.
-// TODO: if so we can detect faulted (ignore) if full set is missing for block.
// Cannot know if archived batch is faulted, despite being otherwise full, as
// faulted is a non-persistent state. So we must purge batches at start.
code chaser_validate::start_batch() NOEXCEPT
@@ -54,14 +52,18 @@ code chaser_validate::start_batch() NOEXCEPT
void chaser_validate::push_batch(const header_link& link, size_t height) NOEXCEPT
{
BC_ASSERT(stranded());
+
+ if (closed()) return;
batched_.push_back(link);
+ --batch_backlog_;
- // chase portion of notify_block(success).
- notify({}, chase::valid, possible_wide_cast(height));
+ // Unblocks check chaser.
+ notify({}, chase::prevalid, possible_wide_cast(height));
// Process both tables when one hits target, allowing batched_ clearance
- // and therefore forward confirmation progress.
- process_batch(false);
+ // and therefore forward confirmation progress. Drain batch if no backlogs
+ // and maximum hash been posted.
+ process_batch(is_maximum());
}
void chaser_validate::process_batch(bool residual) NOEXCEPT
@@ -70,7 +72,7 @@ void chaser_validate::process_batch(bool residual) NOEXCEPT
// Test outside of lock to prevent reader contention for nearly all calls.
auto& query = archive();
- if (stopping_ || (!residual &&
+ if (closed() || (!residual &&
(query.ecdsa_records() < batch_target_) &&
(query.schnorr_records() < batch_target_)))
return;
@@ -82,7 +84,7 @@ void chaser_validate::process_batch(bool residual) NOEXCEPT
const std::unique_lock lock{ mutex_ };
// Must retest inside the lock as table updates are running concurrently.
- if (stopping_) return;
+ if (closed()) return;
const auto ecdsa = query.ecdsa_records();
const auto schnorr = query.schnorr_records();
if (!residual && (ecdsa < batch_target_) && (schnorr < batch_target_))
@@ -151,6 +153,17 @@ void chaser_validate::process_batch(bool residual) NOEXCEPT
// ========================================================================
}
+// Batching helpers.
+// ----------------------------------------------------------------------------
+// private
+
+bool chaser_validate::is_maximum() NOEXCEPT
+{
+ return maximum_posted_.load() &&
+ is_zero(batch_backlog_.load()) &&
+ is_zero(validate_backlog_.load());
+}
+
// 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 (!).
@@ -199,9 +212,7 @@ bool chaser_validate::process_valids(bool residual) NOEXCEPT
!query.set_block_valid(link))
return false;
- // logging portion of notify_block(success).
- fire(events::block_validated, height);
- LOGV("Block validated: " << height << " (batch)");
+ notify_block(system::error::success, height, link, false);
}
batched_.clear();
@@ -211,124 +222,6 @@ bool chaser_validate::process_valids(bool residual) NOEXCEPT
return true;
}
-signatures chaser_validate::get_capture(const header_link& link) NOEXCEPT
-{
- if (!batch_enabled_ || is_current_header(link))
- return { false };
-
- const auto sequence = to_shared();
- return signatures
- {
- .enabled = true,
- .log = BIND_THIS(do_log, _1),
- .fire = BIND_THIS(do_fire, _1, _2),
- .ecdsa = BIND_THIS(do_ecdsa, _1, _2, _3, link),
- .schnorr = BIND_THIS(do_schnorr, _1, _2, _3, link),
- .multisig = BIND_THIS(do_multisig, _1, _2, _3, link, sequence),
- .threshold = BIND_THIS(do_threshold, _1, link, sequence)
- };
-}
-
-// private
-// ----------------------------------------------------------------------------
-
-void chaser_validate::do_log(const script& ) NOEXCEPT
-{
- // Enable for a game of whack-a-mole.
- ////LOGA("Sigop @ " << ctx.height << " -> "
- //// << missed.to_string(flags::all_rules));
-}
-
-void chaser_validate::do_fire(missed miss, size_t count) NOEXCEPT
-{
- switch (miss)
- {
- case missed::ecdsa:
- missed_ecdsa_ += count;
- break;
- case missed::multisig:
- missed_multisig_ += count;
- break;
- case missed::schnorr:
- missed_schnorr_ += count;
- break;
- default:;
- }
-}
-
-bool chaser_validate::do_ecdsa(const hash_digest& digest,
- const ec_compressed& point, const ec_signature& sign,
- const header_link& link) NOEXCEPT
-{
- ++ecdsa_;
- const auto set = archive().set_signature(digest, point, sign, link);
- if (!set) fault(error::batch5);
- return set;
-}
-
-bool chaser_validate::do_schnorr(const hash_digest& digest,
- const ec_xonly& point, const ec_signature& sign,
- const header_link& link) NOEXCEPT
-{
- ++schnorr_;
- const auto set = archive().set_signature(digest, point, sign, link);
- if (!set) fault(error::batch6);
- return set;
-}
-
-bool chaser_validate::do_multisig(const hash_digest& ,
- const ec_compresseds& points, const ec_signatures& BC_DEBUG_ONLY(signs),
- const header_link& , const atomic_counter_ptr& ) NOEXCEPT
-{
- BC_ASSERT(points.size() == signs.size());
-
- multisig_ += points.size();
- ////const auto set = archive().set_signatures(digest, points, signs,
- //// (*sequence)++, link);
- ////if (!set) fault(error::batch7);
- ////return set;
- return true;
-}
-
-bool chaser_validate::do_threshold(const threshold_group& group,
- const header_link& , const atomic_counter_ptr& ) NOEXCEPT
-{
- threshold_ += group.entries.size();
- ////const auto set = archive().set_signatures(group, (*sequence)++, link);
- ////if (!set) fault(error::batch8);
- ////return set;
- return true;
-}
-
-std::string chaser_validate::log_rate(const std::string& name,
- size_t numerator, size_t denominator) const NOEXCEPT
-{
- const auto rate = numerator / greater(denominator, one);
- return (boost_format("%1% (%2% / %3%) = %4% sps") %
- name % numerator % denominator % rate).str();
-}
-
-std::string chaser_validate::log_ratio(const std::string& name,
- size_t numerator, size_t denominator) const NOEXCEPT
-{
- if (is_zero(denominator))
- return name;
-
- const auto ratio = (100.0 * numerator) / denominator;
- return (boost_format("%1% (%2% / %3%) = %4$.4f%%") %
- name % numerator % denominator % ratio).str();
-}
-
-void chaser_validate::log_captures() const NOEXCEPT
-{
- LOGV(log_ratio("Capture rate ecdsa.... ", ecdsa_, ecdsa_ + missed_ecdsa_));
- LOGV(log_ratio("Capture rate multisig. ", multisig_, multisig_ + missed_multisig_));
- LOGV(log_ratio("Capture rate schnorr.. ", schnorr_, schnorr_ + missed_schnorr_));
- LOGV(log_ratio("Capture rate threshold ", threshold_, threshold_ + zero));
-}
-
-BC_POP_WARNING()
-BC_POP_WARNING()
BC_POP_WARNING()
} // namespace node
diff --git a/src/chasers/chaser_validate_capture.cpp b/src/chasers/chaser_validate_capture.cpp
new file mode 100644
index 00000000..09efaa02
--- /dev/null
+++ b/src/chasers/chaser_validate_capture.cpp
@@ -0,0 +1,160 @@
+/**
+ * Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS)
+ *
+ * This file is part of libbitcoin.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+#include
+
+#include
+#include
+
+namespace libbitcoin {
+namespace node {
+
+#define CLASS chaser_validate
+
+using namespace system;
+using namespace system::chain;
+using namespace database;
+using namespace std::placeholders;
+
+BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT)
+
+// Capture handlers.
+// ----------------------------------------------------------------------------
+// private
+
+void chaser_validate::do_log(const script& ) NOEXCEPT
+{
+ // Enable for a game of whack-a-mole.
+ ////LOGA("Sigop @ " << ctx.height << " -> "
+ //// << missed.to_string(flags::all_rules));
+}
+
+void chaser_validate::do_fire(missed miss, size_t count) NOEXCEPT
+{
+ switch (miss)
+ {
+ case missed::ecdsa:
+ missed_ecdsa_ += count;
+ break;
+ case missed::multisig:
+ missed_multisig_ += count;
+ break;
+ case missed::schnorr:
+ missed_schnorr_ += count;
+ break;
+ default:;
+ }
+}
+
+bool chaser_validate::do_ecdsa(const hash_digest& digest,
+ const ec_compressed& point, const ec_signature& sign,
+ const header_link& link) NOEXCEPT
+{
+ ++ecdsa_;
+ const auto set = archive().set_signature(digest, point, sign, link);
+ if (!set) fault(error::batch5);
+ return set;
+}
+
+bool chaser_validate::do_schnorr(const hash_digest& digest,
+ const ec_xonly& point, const ec_signature& sign,
+ const header_link& link) NOEXCEPT
+{
+ ++schnorr_;
+ const auto set = archive().set_signature(digest, point, sign, link);
+ if (!set) fault(error::batch6);
+ return set;
+}
+
+bool chaser_validate::do_multisig(const hash_digest& ,
+ const ec_compresseds& points, const ec_signatures& BC_DEBUG_ONLY(signs),
+ const header_link& , const atomic_counter_ptr& ) NOEXCEPT
+{
+ BC_ASSERT(points.size() == signs.size());
+
+ multisig_ += points.size();
+ ////const auto set = archive().set_signatures(digest, points, signs,
+ //// (*sequence)++, link);
+ ////if (!set) fault(error::batch7);
+ ////return set;
+ return true;
+}
+
+bool chaser_validate::do_threshold(const threshold_group& group,
+ const header_link& , const atomic_counter_ptr& ) NOEXCEPT
+{
+ threshold_ += group.entries.size();
+ ////const auto set = archive().set_signatures(group, (*sequence)++, link);
+ ////if (!set) fault(error::batch8);
+ ////return set;
+ return true;
+}
+
+std::string chaser_validate::log_rate(const std::string& name,
+ size_t numerator, size_t denominator) const NOEXCEPT
+{
+ const auto rate = numerator / greater(denominator, one);
+ return (boost_format("%1% (%2% / %3%) = %4% sps") %
+ name % numerator % denominator % rate).str();
+}
+
+// Capture helpers.
+// ----------------------------------------------------------------------------
+// private
+
+signatures chaser_validate::get_capture(const header_link& link) NOEXCEPT
+{
+ if (!batch_enabled_ || is_current_header(link))
+ return { false };
+
+ const auto sequence = to_shared();
+ return signatures
+ {
+ .enabled = true,
+ .log = BIND_THIS(do_log, _1),
+ .fire = BIND_THIS(do_fire, _1, _2),
+ .ecdsa = BIND_THIS(do_ecdsa, _1, _2, _3, link),
+ .schnorr = BIND_THIS(do_schnorr, _1, _2, _3, link),
+ .multisig = BIND_THIS(do_multisig, _1, _2, _3, link, sequence),
+ .threshold = BIND_THIS(do_threshold, _1, link, sequence)
+ };
+}
+
+std::string chaser_validate::log_ratio(const std::string& name,
+ size_t numerator, size_t denominator) const NOEXCEPT
+{
+ if (is_zero(denominator))
+ return name;
+
+ const auto ratio = (100.0 * numerator) / denominator;
+ return (boost_format("%1% (%2% / %3%) = %4$.4f%%") %
+ name % numerator % denominator % ratio).str();
+}
+
+void chaser_validate::log_captures() const NOEXCEPT
+{
+ LOGV(log_ratio("Capture rate ecdsa.... ", ecdsa_, ecdsa_ + missed_ecdsa_));
+ LOGV(log_ratio("Capture rate multisig. ", multisig_, multisig_ + missed_multisig_));
+ LOGV(log_ratio("Capture rate schnorr.. ", schnorr_, schnorr_ + missed_schnorr_));
+ LOGV(log_ratio("Capture rate threshold ", threshold_, threshold_ + zero));
+}
+
+BC_POP_WARNING()
+
+} // namespace node
+} // namespace libbitcoin
diff --git a/src/chasers/chaser_validate_parallel.cpp b/src/chasers/chaser_validate_parallel.cpp
new file mode 100644
index 00000000..199e52f2
--- /dev/null
+++ b/src/chasers/chaser_validate_parallel.cpp
@@ -0,0 +1,163 @@
+/**
+ * Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS)
+ *
+ * This file is part of libbitcoin.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+#include
+
+#include
+#include
+
+namespace libbitcoin {
+namespace node {
+
+using namespace system;
+using namespace database;
+
+// Parallel execution path (concurrent by block).
+// ----------------------------------------------------------------------------
+
+void chaser_validate::validate_block(const header_link& link,
+ bool bypass) NOEXCEPT
+{
+ if (closed())
+ return;
+
+ code ec{};
+ chain::context ctx{};
+ bool batched{}, faulted{}, capturing{};
+ auto& query = archive();
+
+ // TODO: implement allocator parameter resulting in full allocation to
+ // shared_ptr, to optimize deallocate (12% of milestone/filter).
+ const auto block = query.get_block(link, node_witness_);
+
+ if (!block)
+ {
+ ec = error::validate2;
+ }
+ else if (!query.get_context(ctx, link))
+ {
+ ec = error::validate3;
+ }
+ else if ((ec = populate(bypass, *block, ctx)))
+ {
+ if (!query.set_block_unconfirmable(link))
+ ec = error::validate4;
+ }
+ else if ((ec = validate(batched, faulted, capturing, bypass, *block, link,
+ ctx)))
+ {
+ if (!query.set_block_unconfirmable(link))
+ ec = error::validate5;
+ }
+
+ --validate_backlog_;
+ complete_block(ec, link, ctx.height, bypass, batched, faulted, capturing);
+}
+
+// helpers
+// ----------------------------------------------------------------------------
+
+code chaser_validate::populate(bool bypass, const chain::block& block,
+ const chain::context& ctx) NOEXCEPT
+{
+ const auto& query = archive();
+
+ if (bypass)
+ {
+ // Populating for filters only (no validation metadata required).
+ block.populate(ctx);
+ if (!query.populate_without_metadata(block))
+ return system::error::missing_previous_output;
+ }
+ else
+ {
+ // Internal maturity and time locks are verified here because they are
+ // the only necessary confirmation checks for internal spends.
+ if (const auto ec = block.populate(ctx))
+ return ec;
+
+ // Metadata identifies internal spends allowing confirmation bypass.
+ if (!query.populate_with_metadata(block))
+ return system::error::missing_previous_output;
+ }
+
+ return error::success;
+}
+
+code chaser_validate::validate(bool& batched, bool& faulted, bool& capturing,
+ bool bypass, const chain::block& block, const header_link& link,
+ const chain::context& ctx) NOEXCEPT
+{
+ auto& query = archive();
+
+ if (!bypass)
+ {
+ code ec{};
+ if (((ec = block.check(false))) || ((ec = block.check(ctx, false))))
+ return ec;
+
+ if ((ec = block.accept(ctx, subsidy_interval_, initial_subsidy_)))
+ return ec;
+
+ // Initialize block capture.
+ const auto capture = get_capture(link);
+
+ // Signature capture is enabled.
+ capturing = capture.enabled;
+
+ // 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 not stop during connect, so shutdown will wait on the
+ // completion (block consistency) of all signature captures. But
+ // the faulted state of batch is not persisted (because disk full).
+ if ((ec = block.connect(ctx, capture)))
+ return ec;
+
+ // At least one signature batch was attempted (defer completion).
+ batched = capture.batched;
+
+ // Threshold batch commit failed, block otherwise passed (retry).
+ faulted = capture.faulted;
+ }
+ // ================================================================
+
+ // Prevouts optimize confirmation.
+ // Block will be retried if batch is faulted.
+ if (!faulted && !query.set_prevouts(link, block))
+ return error::validate6;
+ }
+
+ // Block will be retried if batch is faulted.
+ if (!faulted && !query.set_filter_body(link, block))
+ return error::validate7;
+
+ // Defer block state change when batched (or faulted).
+ // Valid must be set after set_prevouts and set_filter_body.
+ if (!batched && !bypass && !query.set_block_valid(link))
+ return error::validate8;
+
+ return error::success;
+}
+
+} // namespace node
+} // namespace libbitcoin