From 5047083c3259af10e2d8219cc534f8403435295f Mon Sep 17 00:00:00 2001 From: evoskuil Date: Fri, 19 Jun 2026 17:14:23 -0400 Subject: [PATCH 1/7] Comments, style, improve validate close handling. --- src/chasers/chaser_validate.cpp | 42 +++++++++++++-------------- src/chasers/chaser_validate_batch.cpp | 6 ++-- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/chasers/chaser_validate.cpp b/src/chasers/chaser_validate.cpp index fa1d9899..e091d860 100644 --- a/src/chasers/chaser_validate.cpp +++ b/src/chasers/chaser_validate.cpp @@ -247,6 +247,7 @@ void chaser_validate::validate_block(const header_link& link, complete_block(ec, link, ctx.height, bypass, batched, faulted, enabled); + // Backlog does not account for post-validation (batch) processing. // Prevent stall by posting internal event, avoiding external handlers. if (is_one(backlog_.fetch_sub(one, relaxed))) handle_chase({}, chase::bump, height_t{}); @@ -352,44 +353,43 @@ 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. + // ------------------------------------------------------------------------ + // Avoid posting new work when closing. + if (closed() || !batch_enabled_) + return; + + // Trigger residual block batch processing. + if (!capturing && !bypass) + { + POST(process_batch, true); + return; + } + + // Retry faulted threshold (presumes disk full). if (faulted) { - // retry, no notify_block() this time. 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(). POST(push_batch, link, height); return; } - - // Not failed/invalid/batched/faulted, so block is complete (maybe valid). - notify_block({}, height, link, 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) - { - POST(process_batch, true); - } } void chaser_validate::notify_block(const code& ec, size_t height, diff --git a/src/chasers/chaser_validate_batch.cpp b/src/chasers/chaser_validate_batch.cpp index a397a7c0..c0b9b975 100644 --- a/src/chasers/chaser_validate_batch.cpp +++ b/src/chasers/chaser_validate_batch.cpp @@ -54,6 +54,8 @@ 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); // chase portion of notify_block(success). @@ -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_)) From e8abd0b73016f02c13915d030cb0b740874c0acc Mon Sep 17 00:00:00 2001 From: evoskuil Date: Fri, 19 Jun 2026 17:14:45 -0400 Subject: [PATCH 2/7] Stub maximum_height into validation chaser. --- include/bitcoin/node/chasers/chaser_validate.hpp | 1 + src/chasers/chaser_validate.cpp | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/include/bitcoin/node/chasers/chaser_validate.hpp b/include/bitcoin/node/chasers/chaser_validate.hpp index 046a2a34..666743d0 100644 --- a/include/bitcoin/node/chasers/chaser_validate.hpp +++ b/include/bitcoin/node/chasers/chaser_validate.hpp @@ -141,6 +141,7 @@ class BCN_API chaser_validate 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_validate.cpp b/src/chasers/chaser_validate.cpp index e091d860..82bd707d 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()), @@ -193,6 +194,13 @@ void chaser_validate::do_bumped(height_t height) NOEXCEPT } } + if (height == maximum_height_) + { + // TODO: the height limit has been queued for validation. + // TODO: once all queued validations are complete/batched, invoke + // TODO: POST(process_batch(true)) from point where that is known. + } + // All posted validations must complete or this is invalid. // So posted validations continue despite network suspension. set_position(height++); From 4e52e80e09a9be0f7682ad777bd2efa9009e01d2 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Fri, 19 Jun 2026 17:41:47 -0400 Subject: [PATCH 3/7] Style. --- src/chasers/chaser_validate.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/chasers/chaser_validate.cpp b/src/chasers/chaser_validate.cpp index 82bd707d..53e8be97 100644 --- a/src/chasers/chaser_validate.cpp +++ b/src/chasers/chaser_validate.cpp @@ -170,10 +170,10 @@ 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 + post_block(link, bypass); break; } case database::error::block_valid: From 99bfa8229421341fee1e2f7aed9fe264557ca91a Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sat, 20 Jun 2026 08:57:24 -0400 Subject: [PATCH 4/7] No post chase::valid for provisional valid (not yet confirmable). --- include/bitcoin/node/chase.hpp | 4 ++++ src/chasers/chaser_check.cpp | 1 + src/chasers/chaser_validate_batch.cpp | 8 +++----- 3 files changed, 8 insertions(+), 5 deletions(-) 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/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_batch.cpp b/src/chasers/chaser_validate_batch.cpp index c0b9b975..91f7654e 100644 --- a/src/chasers/chaser_validate_batch.cpp +++ b/src/chasers/chaser_validate_batch.cpp @@ -58,8 +58,8 @@ void chaser_validate::push_batch(const header_link& link, size_t height) NOEXCEP if (closed()) return; batched_.push_back(link); - // 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. @@ -201,9 +201,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(); From 4c15fd68875b789e1a41ce058899c10175fcd6dd Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sat, 20 Jun 2026 10:47:23 -0400 Subject: [PATCH 5/7] Further factor validation chaser, add recency batch bump. --- Makefile.am | 2 + .../libbitcoin-node/libbitcoin-node.vcxproj | 2 + .../libbitcoin-node.vcxproj.filters | 6 + .../libbitcoin-node/libbitcoin-node.vcxproj | 2 + .../libbitcoin-node.vcxproj.filters | 6 + .../bitcoin/node/chasers/chaser_validate.hpp | 13 +- src/chasers/chaser_validate.cpp | 172 +++--------------- src/chasers/chaser_validate_batch.cpp | 143 ++------------- src/chasers/chaser_validate_capture.cpp | 160 ++++++++++++++++ src/chasers/chaser_validate_parallel.cpp | 163 +++++++++++++++++ 10 files changed, 396 insertions(+), 273 deletions(-) create mode 100644 src/chasers/chaser_validate_capture.cpp create mode 100644 src/chasers/chaser_validate_parallel.cpp 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/chasers/chaser_validate.hpp b/include/bitcoin/node/chasers/chaser_validate.hpp index 666743d0..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,7 +138,9 @@ 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_; diff --git a/src/chasers/chaser_validate.cpp b/src/chasers/chaser_validate.cpp index 53e8be97..a074e2db 100644 --- a/src/chasers/chaser_validate.cpp +++ b/src/chasers/chaser_validate.cpp @@ -152,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); @@ -162,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); @@ -194,13 +198,6 @@ void chaser_validate::do_bumped(height_t height) NOEXCEPT } } - if (height == maximum_height_) - { - // TODO: the height limit has been queued for validation. - // TODO: once all queued validations are complete/batched, invoke - // TODO: POST(process_batch(true)) from point where that is known. - } - // All posted validations must complete or this is invalid. // So posted validations continue despite network suspension. set_position(height++); @@ -212,147 +209,22 @@ void chaser_validate::post_block(const header_link& link, { BC_ASSERT(stranded()); - backlog_.fetch_add(one, relaxed); + ++validate_backlog_; 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); - - // Backlog does not account for post-validation (batch) processing. - // 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)) { @@ -370,22 +242,29 @@ void chaser_validate::complete_block(const code& ec, const header_link& link, // Falls through to trigger residual batch processing. if (!batched) + { notify_block({}, height, link, bypass); + } - // Batch jobs. + // Batch jobs (all posting from unstranded). // ------------------------------------------------------------------------ + // Avoid posting new work when closing. if (closed() || !batch_enabled_) return; - // Trigger residual block batch processing. - if (!capturing && !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; + + // Drain batch when recent (current, or maximum reached without backlog). + if (current || is_maximum()) { POST(process_batch, true); return; } - // Retry faulted threshold (presumes disk full). + // Retry faulted threshold, re-enters backlog (presumes disk full). if (faulted) { POST(post_block, link, bypass); @@ -395,6 +274,7 @@ void chaser_validate::complete_block(const code& ec, const header_link& link, // Queue block and process batch if ready. if (batched) { + ++batch_backlog_; POST(push_batch, link, height); return; } @@ -403,6 +283,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 91f7654e..7e239a81 100644 --- a/src/chasers/chaser_validate_batch.cpp +++ b/src/chasers/chaser_validate_batch.cpp @@ -28,14 +28,14 @@ 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. @@ -57,13 +57,15 @@ void chaser_validate::push_batch(const header_link& link, size_t height) NOEXCEP if (closed()) return; batched_.push_back(link); + --batch_backlog_; // 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 @@ -153,6 +155,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 (!). @@ -211,124 +224,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 From f6507a22298a7d6cd6b99a9cbe0930448e62393d Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sat, 20 Jun 2026 12:19:51 -0400 Subject: [PATCH 6/7] Comment. --- src/chasers/chaser_validate_batch.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/chasers/chaser_validate_batch.cpp b/src/chasers/chaser_validate_batch.cpp index 7e239a81..244a38af 100644 --- a/src/chasers/chaser_validate_batch.cpp +++ b/src/chasers/chaser_validate_batch.cpp @@ -38,8 +38,6 @@ BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) // 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 From a164515bbe0baba609518f008118a3ebc82d8e5b Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sat, 20 Jun 2026 14:51:50 -0400 Subject: [PATCH 7/7] Fix recency batch bump. --- src/chasers/chaser_validate.cpp | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/chasers/chaser_validate.cpp b/src/chasers/chaser_validate.cpp index a074e2db..8b76efe3 100644 --- a/src/chasers/chaser_validate.cpp +++ b/src/chasers/chaser_validate.cpp @@ -175,9 +175,14 @@ void chaser_validate::do_bumped(height_t height) NOEXCEPT case database::error::unknown_state: { if (bypass && !filter_) + { complete_block(error::success, link, height, true); + } else + { + ++validate_backlog_; post_block(link, bypass); + } break; } case database::error::block_valid: @@ -208,8 +213,6 @@ void chaser_validate::post_block(const header_link& link, bool bypass) NOEXCEPT { BC_ASSERT(stranded()); - - ++validate_backlog_; PARALLEL(validate_block, link, bypass); } @@ -253,20 +256,10 @@ void chaser_validate::complete_block(const code& ec, const header_link& link, if (closed() || !batch_enabled_) 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). - if (current || is_maximum()) - { - POST(process_batch, true); - return; - } - // Retry faulted threshold, re-enters backlog (presumes disk full). if (faulted) { + ++validate_backlog_; POST(post_block, link, bypass); return; } @@ -278,6 +271,16 @@ void chaser_validate::complete_block(const code& ec, const header_link& link, POST(push_batch, link, height); 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). + if (current || is_maximum()) + { + POST(process_batch, true); + } } void chaser_validate::notify_block(const code& ec, size_t height,