From 8bec926ca4fd91184237ded75bbf286746bb66a5 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Tue, 16 Jun 2026 12:04:16 -0400 Subject: [PATCH 1/8] Remove three settings. --- include/bitcoin/node/settings.hpp | 3 --- src/settings.cpp | 3 --- test/settings.cpp | 3 --- 3 files changed, 9 deletions(-) diff --git a/include/bitcoin/node/settings.hpp b/include/bitcoin/node/settings.hpp index 10c2df96..9e887f03 100644 --- a/include/bitcoin/node/settings.hpp +++ b/include/bitcoin/node/settings.hpp @@ -41,9 +41,6 @@ class BCN_API settings bool memory_priority; bool allow_overlapped; bool batch_signatures; - bool mark_unconfirmable; - bool defer_validation; - bool defer_confirmation; float allowed_deviation; float minimum_fee_rate; float minimum_bump_rate; diff --git a/src/settings.cpp b/src/settings.cpp index dd1013fa..263e7234 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -37,9 +37,6 @@ settings::settings() NOEXCEPT thread_priority{ true }, allow_overlapped{ true }, batch_signatures{ true }, - mark_unconfirmable{ true }, - defer_validation{ false }, - defer_confirmation{ false }, minimum_fee_rate{ 0.0 }, minimum_bump_rate{ 0.0 }, allowed_deviation{ 1.5 }, diff --git a/test/settings.cpp b/test/settings.cpp index b22329d3..121e68f9 100644 --- a/test/settings.cpp +++ b/test/settings.cpp @@ -37,9 +37,6 @@ BOOST_AUTO_TEST_CASE(settings__node__default_context__expected) BOOST_REQUIRE_EQUAL(node.thread_priority, true); BOOST_REQUIRE_EQUAL(node.allow_overlapped, true); BOOST_REQUIRE_EQUAL(node.batch_signatures, true); - BOOST_REQUIRE_EQUAL(node.mark_unconfirmable, true); - BOOST_REQUIRE_EQUAL(node.defer_validation, false); - BOOST_REQUIRE_EQUAL(node.defer_confirmation, false); BOOST_REQUIRE_EQUAL(node.minimum_fee_rate, 0.0); BOOST_REQUIRE_EQUAL(node.minimum_bump_rate, 0.0); BOOST_REQUIRE_EQUAL(node.allowed_deviation, 1.5); From 0084d193f19a75b29590ba940421ef7cf4610e96 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Tue, 16 Jun 2026 12:05:05 -0400 Subject: [PATCH 2/8] Add 9 batch error codes. --- include/bitcoin/node/error.hpp | 10 +++++++++- src/error.cpp | 10 +++++++++- test/error.cpp | 10 +++++++--- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/include/bitcoin/node/error.hpp b/include/bitcoin/node/error.hpp index e3004567..df729100 100644 --- a/include/bitcoin/node/error.hpp +++ b/include/bitcoin/node/error.hpp @@ -108,7 +108,15 @@ enum error_t : uint8_t estimates_push2, estimates_pop1, estimates_pop2, - capture_fault + batch1, + batch2, + batch3, + batch4, + batch5, + batch6, + batch7, + batch8, + batch9 }; // No current need for error_code equivalence mapping. diff --git a/src/error.cpp b/src/error.cpp index 6746ca2d..691791df 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -98,7 +98,15 @@ DEFINE_ERROR_T_MESSAGE_MAP(error) { estimates_push2, "estimates_push2" }, { estimates_pop1, "estimates_pop1" }, { estimates_pop2, "estimates_pop2" }, - { capture_fault, "capture_fault" } + { batch1, "batch1" }, + { batch2, "batch2" }, + { batch3, "batch3" }, + { batch4, "batch4" }, + { batch5, "batch5" }, + { batch6, "batch6" }, + { batch7, "batch7" }, + { batch8, "batch8" }, + { batch9, "batch9" } }; DEFINE_ERROR_T_CATEGORY(error, "node", "node code") diff --git a/test/error.cpp b/test/error.cpp index ff94bc49..813aa4f1 100644 --- a/test/error.cpp +++ b/test/error.cpp @@ -297,13 +297,17 @@ BOOST_AUTO_TEST_CASE(error_t__code__estimates_pop2__true_expected_message) BOOST_REQUIRE_EQUAL(ec.message(), "estimates_pop2"); } -BOOST_AUTO_TEST_CASE(error_t__code__capture_fault__true_expected_message) +// batch + +BOOST_AUTO_TEST_CASE(error_t__code__batch1__true_expected_message) { - constexpr auto value = error::capture_fault; + constexpr auto value = error::batch1; const auto ec = code(value); BOOST_REQUIRE(ec); BOOST_REQUIRE(ec == value); - BOOST_REQUIRE_EQUAL(ec.message(), "capture_fault"); + BOOST_REQUIRE_EQUAL(ec.message(), "batch1"); } +// TODO: batch2-batch9 + BOOST_AUTO_TEST_SUITE_END() From a6e46f5f4c878638993131047e4c8272fbf6c82c Mon Sep 17 00:00:00 2001 From: evoskuil Date: Tue, 16 Jun 2026 12:09:02 -0400 Subject: [PATCH 3/8] Move mark_unconfirmable impl to database. --- include/bitcoin/node/impl/chasers/chaser_organize.ipp | 3 +-- src/chasers/chaser_confirm.cpp | 5 +---- src/chasers/chaser_validate.cpp | 10 +++------- src/protocols/protocol_block_in_31800.cpp | 4 +--- 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/include/bitcoin/node/impl/chasers/chaser_organize.ipp b/include/bitcoin/node/impl/chasers/chaser_organize.ipp index 807f9353..49675ffb 100644 --- a/include/bitcoin/node/impl/chasers/chaser_organize.ipp +++ b/include/bitcoin/node/impl/chasers/chaser_organize.ipp @@ -90,8 +90,7 @@ bool CLASS::handle_chase(const code&, chase event_, event_value value) NOEXCEPT case chase::unvalid: case chase::unconfirmable: { - // !mark_unconfirmable allows node to stall, preserving log. - if (!node_settings().mark_unconfirmable) + if (!database_settings().mark_unconfirmable) break; // Roll back the candidate chain to confirmed top (via fork point). diff --git a/src/chasers/chaser_confirm.cpp b/src/chasers/chaser_confirm.cpp index a0df5117..86df17a2 100644 --- a/src/chasers/chaser_confirm.cpp +++ b/src/chasers/chaser_confirm.cpp @@ -286,10 +286,7 @@ bool chaser_confirm::confirm_block(const header_link& link, size_t height, if (const auto ec = query.block_confirmable(link)) { - // !mark_unconfirmable allows node to stall, preserving log. - // Will continue to validate this block and fail to confirm here. - if (node_settings().mark_unconfirmable && - !query.set_block_unconfirmable(link)) + if (!query.set_block_unconfirmable(link)) { fault(error::confirm9); return false; diff --git a/src/chasers/chaser_validate.cpp b/src/chasers/chaser_validate.cpp index eed2c0e2..6cf8ace0 100644 --- a/src/chasers/chaser_validate.cpp +++ b/src/chasers/chaser_validate.cpp @@ -234,17 +234,13 @@ void chaser_validate::validate_block(const header_link& link, } else if ((ec = populate(bypass, *block, ctx))) { - // !mark_unconfirmable allows node to stall, preserving log. - if (node_settings().mark_unconfirmable && - !query.set_block_unconfirmable(link)) + if (!query.set_block_unconfirmable(link)) ec = error::validate4; } else if ((ec = validate(bypass, *block, link, ctx))) { - // !mark_unconfirmable allows node to stall, preserving log. - if (node_settings().mark_unconfirmable && - !query.set_block_unconfirmable(link)) - ec = error::validate5; + if (!query.set_block_unconfirmable(link)) + ec = error::validate5; } complete_block(ec, link, ctx.height, bypass); diff --git a/src/protocols/protocol_block_in_31800.cpp b/src/protocols/protocol_block_in_31800.cpp index ba6317df..c6709af5 100644 --- a/src/protocols/protocol_block_in_31800.cpp +++ b/src/protocols/protocol_block_in_31800.cpp @@ -317,9 +317,7 @@ bool protocol_block_in_31800::handle_receive_block(const code& ec, return false; } - // !mark_unconfirmable allows node to stall, preserving log. - if (node_settings().mark_unconfirmable && - !query.set_block_unconfirmable(link)) + if (!query.set_block_unconfirmable(link)) { stop(fault(error::protocol1)); return false; From c4625fe902be3a7eb8538953a9fdb66b87dc0daa Mon Sep 17 00:00:00 2001 From: evoskuil Date: Tue, 16 Jun 2026 12:14:28 -0400 Subject: [PATCH 4/8] Remove validate/confirm defer feature. --- include/bitcoin/node/chasers/chaser_confirm.hpp | 3 --- include/bitcoin/node/chasers/chaser_validate.hpp | 1 - src/chasers/chaser_confirm.cpp | 9 ++------- src/chasers/chaser_validate.cpp | 15 ++++----------- 4 files changed, 6 insertions(+), 22 deletions(-) diff --git a/include/bitcoin/node/chasers/chaser_confirm.hpp b/include/bitcoin/node/chasers/chaser_confirm.hpp index 76684561..fd027787 100644 --- a/include/bitcoin/node/chasers/chaser_confirm.hpp +++ b/include/bitcoin/node/chasers/chaser_confirm.hpp @@ -68,9 +68,6 @@ class BCN_API chaser_confirm bool roll_back(const header_links& popped, size_t fork_point, size_t top) NOEXCEPT; void announce(const header_link& link, height_t height) NOEXCEPT; - - // This is thread safe. - const bool defer_; }; } // namespace node diff --git a/include/bitcoin/node/chasers/chaser_validate.hpp b/include/bitcoin/node/chasers/chaser_validate.hpp index 7c2c835f..0e2a9045 100644 --- a/include/bitcoin/node/chasers/chaser_validate.hpp +++ b/include/bitcoin/node/chasers/chaser_validate.hpp @@ -129,7 +129,6 @@ class BCN_API chaser_validate const size_t maximum_backlog_; const bool batch_signatures_; const bool node_witness_; - const bool defer_; const bool filter_; }; diff --git a/src/chasers/chaser_confirm.cpp b/src/chasers/chaser_confirm.cpp index 86df17a2..686fe354 100644 --- a/src/chasers/chaser_confirm.cpp +++ b/src/chasers/chaser_confirm.cpp @@ -34,8 +34,7 @@ using namespace std::placeholders; BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) chaser_confirm::chaser_confirm(full_node& node) NOEXCEPT - : chaser(node), - defer_(node.node_settings().defer_confirmation) + : chaser(node) { } @@ -49,11 +48,7 @@ code chaser_confirm::start() NOEXCEPT LOGN("Node is current at startup block [" << position() << "]."); } - if (!defer_) - { - SUBSCRIBE_CHASE(handle_chase, _1, _2, _3); - } - + SUBSCRIBE_CHASE(handle_chase, _1, _2, _3); return error::success; } diff --git a/src/chasers/chaser_validate.cpp b/src/chasers/chaser_validate.cpp index 6cf8ace0..2fd5ac3f 100644 --- a/src/chasers/chaser_validate.cpp +++ b/src/chasers/chaser_validate.cpp @@ -45,8 +45,7 @@ chaser_validate::chaser_validate(full_node& node) NOEXCEPT maximum_backlog_(node.node_settings().maximum_concurrency_()), batch_signatures_(node.node_settings().batch_signatures), node_witness_(node.network_settings().witness_node()), - defer_(node.node_settings().defer_validation), - filter_(!defer_ && node.archive().filter_enabled()) + filter_(node.archive().filter_enabled()) { } @@ -159,7 +158,7 @@ void chaser_validate::do_bumped(height_t height) NOEXCEPT if (ec == database::error::unassociated) return; - const auto bypass = defer_ || is_under_checkpoint(height) || + const auto bypass = is_under_checkpoint(height) || query.is_milestone(link); switch (ec.value()) @@ -350,15 +349,9 @@ void chaser_validate::complete_block(const code& ec, const header_link& link, } // VALID BLOCK - // Under deferral there is no state change, but downloads will stall unless - // the window is closed out, so notify the check chaser of the increment. notify(ec, chase::valid, possible_wide_cast(height)); - - if (!defer_) - { - fire(events::block_validated, height); - LOGV("Block validated: " << height << (bypass ? " (bypass)" : "")); - } + fire(events::block_validated, height); + LOGV("Block validated: " << height << (bypass ? " (bypass)" : "")); } // Overrides due to independent priority thread pool From a3abc36ce4d3ac2a6c3aff7e5cb39cb30915de12 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Tue, 16 Jun 2026 12:29:39 -0400 Subject: [PATCH 5/8] Implement chase::advanced to bump batch verification. --- include/bitcoin/node/chase.hpp | 4 ++++ include/bitcoin/node/chasers/chaser_validate.hpp | 1 + src/chasers/chaser_check.cpp | 10 +++++++--- src/chasers/chaser_validate.cpp | 16 ++++++++++++++++ 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/include/bitcoin/node/chase.hpp b/include/bitcoin/node/chase.hpp index 891be398..d2416cf2 100644 --- a/include/bitcoin/node/chase.hpp +++ b/include/bitcoin/node/chase.hpp @@ -96,6 +96,10 @@ enum class chase /// Issued by 'organize' and handled by 'check', 'validate', 'confirm'. disorganized, + /// Download concurrency window completed, advancing to next (height_t). + /// Issued by 'check' and handled by 'validate'. + advanced, + /// Check/Identify. /// ----------------------------------------------------------------------- diff --git a/include/bitcoin/node/chasers/chaser_validate.hpp b/include/bitcoin/node/chasers/chaser_validate.hpp index 0e2a9045..296d9348 100644 --- a/include/bitcoin/node/chasers/chaser_validate.hpp +++ b/include/bitcoin/node/chasers/chaser_validate.hpp @@ -56,6 +56,7 @@ class BCN_API chaser_validate event_value value) NOEXCEPT; virtual void do_regressed(height_t branch_point) NOEXCEPT; + virtual void do_advanced(height_t height) NOEXCEPT; virtual void do_checked(height_t height) NOEXCEPT; virtual void do_bumped(height_t height) NOEXCEPT; virtual void do_bump(height_t height) NOEXCEPT; diff --git a/src/chasers/chaser_check.cpp b/src/chasers/chaser_check.cpp index 68e72f98..c0c41765 100644 --- a/src/chasers/chaser_check.cpp +++ b/src/chasers/chaser_check.cpp @@ -354,16 +354,20 @@ void chaser_check::do_regressed(height_t branch_point) NOEXCEPT // track downloaded in order (to move download window) // ---------------------------------------------------------------------------- -void chaser_check::do_advanced(height_t height) NOEXCEPT +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 set of requested hashes has been validated. + // The full count of requested hashes has been validated. if (advanced_ == requested_) - do_headers(height); + { + notify(error::success, chase::advanced, advanced_); + do_headers({}); + } } void chaser_check::do_checked(height_t height) NOEXCEPT diff --git a/src/chasers/chaser_validate.cpp b/src/chasers/chaser_validate.cpp index 2fd5ac3f..952a7799 100644 --- a/src/chasers/chaser_validate.cpp +++ b/src/chasers/chaser_validate.cpp @@ -87,6 +87,16 @@ bool chaser_validate::handle_chase(const code&, chase event_, POST(do_checked, std::get(value)); break; } + case chase::advanced: + { + if (!batch_signatures_) + break; + + // value is checked block height. + BC_ASSERT(std::holds_alternative(value)); + POST(do_advanced, std::get(value)); + break; + } case chase::regressed: case chase::disorganized: { @@ -120,6 +130,12 @@ void chaser_validate::do_regressed(height_t branch_point) NOEXCEPT set_position(branch_point); } +void chaser_validate::do_advanced(height_t) NOEXCEPT +{ + BC_ASSERT(stranded()); + process_batch(); +} + void chaser_validate::do_checked(height_t height) NOEXCEPT { BC_ASSERT(stranded()); From b4dd8dcf0748bb0debe511eef5ddd4585b150612 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Tue, 16 Jun 2026 12:29:54 -0400 Subject: [PATCH 6/8] Style, comments. --- .../bitcoin/node/chasers/chaser_validate.hpp | 5 +- src/chasers/chaser_check.cpp | 18 +-- src/chasers/chaser_validate.cpp | 128 +++++++----------- 3 files changed, 57 insertions(+), 94 deletions(-) diff --git a/include/bitcoin/node/chasers/chaser_validate.hpp b/include/bitcoin/node/chasers/chaser_validate.hpp index 296d9348..0b00039a 100644 --- a/include/bitcoin/node/chasers/chaser_validate.hpp +++ b/include/bitcoin/node/chasers/chaser_validate.hpp @@ -79,6 +79,7 @@ class BCN_API chaser_validate bool stranded() const NOEXCEPT override; private: + static constexpr auto relaxed = std::memory_order_relaxed; using atomic_counter = std::atomic; using atomic_counter_ptr = std::shared_ptr; using signatures = system::chain::signatures; @@ -99,10 +100,10 @@ class BCN_API chaser_validate bool do_multisig(const system::hash_digest& digest, const system::ec_compresseds& points, const system::ec_signatures& signs, const database::header_link& link, - const atomic_counter_ptr& id) NOEXCEPT; + const atomic_counter_ptr& sequence) NOEXCEPT; bool do_threshold(const threshold_group& group, const database::header_link& link, - const atomic_counter_ptr& id) NOEXCEPT; + const atomic_counter_ptr& sequence) NOEXCEPT; void log_capture(const std::string_view& name, size_t captured, size_t missed) const NOEXCEPT; diff --git a/src/chasers/chaser_check.cpp b/src/chasers/chaser_check.cpp index c0c41765..fee1745c 100644 --- a/src/chasers/chaser_check.cpp +++ b/src/chasers/chaser_check.cpp @@ -206,7 +206,7 @@ void chaser_check::do_handle_purged(const code&) NOEXCEPT BC_ASSERT(stranded()); start_tracking(); - do_bump(height_t{}); + do_bump({}); } // starved @@ -376,7 +376,7 @@ void chaser_check::do_checked(height_t height) NOEXCEPT // Candidate block was checked at the given height, advance. if (height == add1(position())) - do_bump(height); + do_bump({}); } void chaser_check::do_bump(height_t) NOEXCEPT @@ -390,13 +390,10 @@ 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 = add1(height))))) - { + while (!closed() && query.is_associated(query.to_candidate(++height))) set_position(height); - } - do_headers(sub1(height)); + do_headers({}); } // add headers @@ -406,11 +403,8 @@ void chaser_check::do_headers(height_t) NOEXCEPT { BC_ASSERT(stranded()); - const auto added = set_unassociated(); - if (is_zero(added)) - return; - - notify(error::success, chase::download, added); + if (const auto added = set_unassociated(); is_nonzero(added)) + notify(error::success, chase::download, added); } // get/put hashes diff --git a/src/chasers/chaser_validate.cpp b/src/chasers/chaser_validate.cpp index 952a7799..51135ee0 100644 --- a/src/chasers/chaser_validate.cpp +++ b/src/chasers/chaser_validate.cpp @@ -163,7 +163,7 @@ void chaser_validate::do_bumped(height_t height) NOEXCEPT const auto& query = archive(); // Bypass until next event if validation backlog is full. - // Stop when suspended as write error des not terminate asynchronous loop. + // Stop when suspended as write error does not terminate asynchronous loop. while ((backlog_ < maximum_backlog_) && !closed() && !suspended()) { const auto link = query.to_candidate(height); @@ -215,10 +215,9 @@ void chaser_validate::do_bumped(height_t height) NOEXCEPT void chaser_validate::post_block(const header_link& link, bool bypass) NOEXCEPT { - // may be called by do_bumped (stranded) or complete_block (not stranded). - ///BC_ASSERT(stranded()); + BC_ASSERT(stranded()); - backlog_.fetch_add(one, std::memory_order_relaxed); + backlog_.fetch_add(one, relaxed); PARALLEL(validate_block, link, bypass); } @@ -261,8 +260,8 @@ void chaser_validate::validate_block(const header_link& link, complete_block(ec, link, ctx.height, bypass); // Prevent stall by posting internal event, avoiding external handlers. - if (is_one(backlog_.fetch_sub(one, std::memory_order_relaxed))) - handle_chase(error::success, chase::bump, height_t{}); + if (is_one(backlog_.fetch_sub(one, relaxed))) + handle_chase({}, chase::bump, height_t{}); } code chaser_validate::populate(bool bypass, const chain::block& block, @@ -300,13 +299,6 @@ code chaser_validate::validate(bool bypass, const chain::block& block, if (!bypass) { code ec{}; - - // Skips identity validation (performed in downloader). - // This can be performed in downloader as well but moving it here with - // more order than required allows the downloader to avoid block parse - // which significantly reduces memory consumption and CPU during sync. - // This slightly increases check() computation under full validation - // because a redundant malleation guard is required when downloading. if (((ec = block.check(false))) || ((ec = block.check(ctx, false)))) return ec; @@ -370,35 +362,6 @@ void chaser_validate::complete_block(const code& ec, const header_link& link, LOGV("Block validated: " << height << (bypass ? " (bypass)" : "")); } -// Overrides due to independent priority thread pool -// ---------------------------------------------------------------------------- - -void chaser_validate::stopping(const code& ec) NOEXCEPT -{ - // Stop threadpool keep-alive, all work must self-terminate to affect join. - validation_threadpool_.stop(); - chaser::stopping(ec); -} - -void chaser_validate::stop() NOEXCEPT -{ - if (!validation_threadpool_.join()) - { - BC_ASSERT_MSG(false, "failed to join threadpool"); - std::abort(); - } -} - -network::asio::strand& chaser_validate::strand() NOEXCEPT -{ - return validation_strand_; -} - -bool chaser_validate::stranded() const NOEXCEPT -{ - return validation_strand_.running_in_this_thread(); -} - // private // ---------------------------------------------------------------------------- @@ -413,40 +376,19 @@ chain::signatures chaser_validate::get_capture( return signatures { - // Default struct is disabled. .enabled = true, - - // Enable for a game of whack-a-mole. .log = BIND_THIS(do_log, _1), - - // Update counters for missed capture. .fire = BIND_THIS(do_fire, _1, _2), - - // opcode::checksig/verify .ecdsa = BIND_THIS(do_ecdsa, _1, _2, _3, link), - - // opcode::checksigadd | opcode::checksig/verify .schnorr = BIND_THIS(do_schnorr, _1, _2, _3, link), - - // opcode::checkmultisig/verify .multisig = BIND_THIS(do_multisig, _1, _2, _3, link, id), - - // opcode::within - // opcode::numequal/verify - // opcode::numnotequal - // opcode::lessthan - // opcode::greaterthan - // opcode::lessthanorequal - // opcode::greaterthanorequal - // opcode::checksig (m of m) .threshold = BIND_THIS(do_threshold, _1, link, id) }; } -// Enable for a game of whack-a-mole. -void chaser_validate::do_log( - const chain::script& /* LOG_ONLY(missed) */) NOEXCEPT +void chaser_validate::do_log(const chain::script& ) NOEXCEPT { + // Enable for a game of whack-a-mole. ////LOGA("Sigop @ " << ctx.height << " -> " //// << missed.to_string(chain::flags::all_rules)); } @@ -474,7 +416,7 @@ bool chaser_validate::do_ecdsa(const hash_digest& digest, { ++ecdsa_; const auto set = archive().set_signature(digest, point, sign, link); - if (!set) fault(system::error::block_capture); + if (!set) fault(error::batch6); return set; } @@ -484,7 +426,7 @@ bool chaser_validate::do_schnorr(const hash_digest& digest, { ++schnorr_; const auto set = archive().set_signature(digest, point, sign, link); - if (!set) fault(system::error::block_capture); + if (!set) fault(error::batch7); return set; } @@ -493,26 +435,23 @@ BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR) bool chaser_validate::do_multisig(const hash_digest& digest, const ec_compresseds& points, const ec_signatures& signs, - const header_link& link, const atomic_counter_ptr& id) NOEXCEPT + const header_link& link, const atomic_counter_ptr& sequence) NOEXCEPT { BC_ASSERT(points.size() == signs.size()); multisig_ += points.size(); - const auto set = archive().set_signatures(digest, points, signs, (*id)++, - link); - if (!set) fault(system::error::block_capture); + const auto set = archive().set_signatures(digest, points, signs, + (*sequence)++, link); + if (!set) fault(error::batch8); return set; } bool chaser_validate::do_threshold(const threshold_group& group, - const header_link& link, const atomic_counter_ptr& id) NOEXCEPT + const header_link& link, const atomic_counter_ptr& sequence) NOEXCEPT { threshold_ += group.entries.size(); - const auto set = archive().set_signatures(group, (*id)++, link); - if (!set) fault(system::error::block_capture); - - // False here sets signatures.fault, causing block.connect(2) to - // return error::block_capture, causing block validation resubmit. + const auto set = archive().set_signatures(group, (*sequence)++, link); + if (!set) fault(error::batch9); return set; } @@ -533,12 +472,41 @@ void chaser_validate::log_capture(const std::string_view& name, void chaser_validate::log_captures() const NOEXCEPT { - log_capture("ecdsa.... ", ecdsa_, missed_ecdsa_); - log_capture("multisig. ", multisig_, missed_multisig_); - log_capture("schnorr.. ", schnorr_, missed_schnorr_); + log_capture("ecdsa.... ", ecdsa_, missed_ecdsa_); + log_capture("multisig. ", multisig_, missed_multisig_); + log_capture("schnorr.. ", schnorr_, missed_schnorr_); log_capture("threshold ", threshold_, zero); } +// Overrides due to independent priority thread pool +// ---------------------------------------------------------------------------- + +void chaser_validate::stopping(const code& ec) NOEXCEPT +{ + // Stop threadpool keep-alive, all work must self-terminate to affect join. + validation_threadpool_.stop(); + chaser::stopping(ec); +} + +void chaser_validate::stop() NOEXCEPT +{ + if (!validation_threadpool_.join()) + { + BC_ASSERT_MSG(false, "failed to join threadpool"); + std::abort(); + } +} + +network::asio::strand& chaser_validate::strand() NOEXCEPT +{ + return validation_strand_; +} + +bool chaser_validate::stranded() const NOEXCEPT +{ + return validation_strand_.running_in_this_thread(); +} + BC_POP_WARNING() } // namespace node From 349f1e5ca661aa9ca15cd05a833faf6c49c3f492 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Tue, 16 Jun 2026 12:28:56 -0400 Subject: [PATCH 7/8] Integrate signature capture and batch evaluation. --- .../bitcoin/node/chasers/chaser_validate.hpp | 24 ++- src/chasers/chaser_validate.cpp | 192 +++++++++++++++--- 2 files changed, 179 insertions(+), 37 deletions(-) diff --git a/include/bitcoin/node/chasers/chaser_validate.hpp b/include/bitcoin/node/chasers/chaser_validate.hpp index 0b00039a..03d07b9a 100644 --- a/include/bitcoin/node/chasers/chaser_validate.hpp +++ b/include/bitcoin/node/chasers/chaser_validate.hpp @@ -20,6 +20,7 @@ #define LIBBITCOIN_NODE_CHASERS_CHASER_VALIDATE_HPP #include +#include #include #include @@ -65,14 +66,18 @@ class BCN_API chaser_validate bool bypass) NOEXCEPT; virtual void validate_block(const database::header_link& link, bool bypass) NOEXCEPT; - virtual code validate(bool bypass, const system::chain::block& block, - const database::header_link& link, + virtual code validate(bool& batched, bool& faulted, bool bypass, + const system::chain::block& block, const database::header_link& link, const system::chain::context& ctx) NOEXCEPT; virtual code populate(bool bypass, const system::chain::block& block, const system::chain::context& ctx) NOEXCEPT; virtual void complete_block(const code& ec, - const database::header_link& link, size_t height, - bool bypassed) NOEXCEPT; + const database::header_link& link, size_t height, bool bypass, + bool batched=false, bool faulted=false) NOEXCEPT; + virtual void notify_block(const code& ec, size_t height, + const database::header_link& link, bool bypass) NOEXCEPT; + virtual void push_batch(const database::header_link& link) NOEXCEPT; + virtual void process_batch() NOEXCEPT; // Override base class strand because it sits on the network thread pool. network::asio::strand& strand() NOEXCEPT override; @@ -80,6 +85,8 @@ class BCN_API chaser_validate private: static constexpr auto relaxed = std::memory_order_relaxed; + using shared_lock = const std::shared_lock; + using shared_lock_cptr = std::shared_ptr; using atomic_counter = std::atomic; using atomic_counter_ptr = std::shared_ptr; using signatures = system::chain::signatures; @@ -90,7 +97,8 @@ class BCN_API chaser_validate // Handlers. void do_log(const system::chain::script& missed) NOEXCEPT; - void do_fire(missed miss, size_t count) NOEXCEPT; + void do_fire(missed miss, size_t count, + const shared_lock_cptr& lock) NOEXCEPT; bool do_ecdsa(const system::hash_digest& digest, const system::ec_compressed& point, const system::ec_signature& sign, const database::header_link& link) NOEXCEPT; @@ -109,11 +117,15 @@ class BCN_API chaser_validate size_t captured, size_t missed) const NOEXCEPT; void log_captures() const NOEXCEPT; - // This is protected by strand. + // These are protected by strand. + database::header_links batched_{}; network::threadpool validation_threadpool_; // These are thread safe. + // This prevents table updates during batch verify. + std::shared_mutex mutex_{}; + std::atomic ecdsa_{}; std::atomic schnorr_{}; std::atomic multisig_{}; diff --git a/src/chasers/chaser_validate.cpp b/src/chasers/chaser_validate.cpp index 51135ee0..16c5cd75 100644 --- a/src/chasers/chaser_validate.cpp +++ b/src/chasers/chaser_validate.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -54,7 +55,13 @@ code chaser_validate::start() NOEXCEPT if (!node_settings().headers_first) return error::success; - const auto& query = archive(); + // TODO: ecdsa can be retained, as they don't fault, so set batched_ here. + // 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. + auto& query = archive(); + if (batch_signatures_ && !query.purge_signatures()) + return error::batch1; + set_position(query.get_fork()); SUBSCRIBE_CHASE(handle_chase, _1, _2, _3); return error::success; @@ -232,6 +239,7 @@ void chaser_validate::validate_block(const header_link& link, code ec{}; chain::context ctx{}; + bool batched{}, faulted{}; auto& query = archive(); // TODO: implement allocator parameter resulting in full allocation to @@ -251,13 +259,13 @@ void chaser_validate::validate_block(const header_link& link, if (!query.set_block_unconfirmable(link)) ec = error::validate4; } - else if ((ec = validate(bypass, *block, link, ctx))) + else if ((ec = validate(batched, faulted, bypass, *block, link, ctx))) { if (!query.set_block_unconfirmable(link)) ec = error::validate5; } - complete_block(ec, link, ctx.height, bypass); + complete_block(ec, link, ctx.height, bypass, batched, faulted); // Prevent stall by posting internal event, avoiding external handlers. if (is_one(backlog_.fetch_sub(one, relaxed))) @@ -291,8 +299,9 @@ code chaser_validate::populate(bool bypass, const chain::block& block, return error::success; } -code chaser_validate::validate(bool bypass, const chain::block& block, - const header_link& link, const chain::context& ctx) NOEXCEPT +code chaser_validate::validate(bool& batched, bool& faulted, bool bypass, + const chain::block& block, const header_link& link, + const chain::context& ctx) NOEXCEPT { auto& query = archive(); @@ -305,19 +314,35 @@ code chaser_validate::validate(bool bypass, const chain::block& block, if ((ec = block.accept(ctx, subsidy_interval_, initial_subsidy_))) return ec; - if ((ec = block.connect(ctx, get_capture(link)))) + // Initialize block capture. + const auto capture = get_capture(link); + + // Sequentially connect block with signature capture (if enabled). + // There is not stop during connect, so a shutdown will wait on the + // completion (block consistency) of all signature captures. However + // the faulted state of a 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 block). + faulted = capture.faulted; + // Prevouts optimize confirmation. - if (!query.set_prevouts(link, block)) + // Block will be retried if batch is faulted. + if (!faulted && !query.set_prevouts(link, block)) return error::validate6; } - if (!query.set_filter_body(link, block)) + // 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 (!bypass && !query.set_block_valid(link)) + if (!batched && !bypass && !query.set_block_valid(link)) return error::validate8; return error::success; @@ -325,30 +350,135 @@ code chaser_validate::validate(bool bypass, const chain::block& block, // May be either concurrent or stranded. void chaser_validate::complete_block(const code& ec, const header_link& link, - size_t height, bool bypass) NOEXCEPT + size_t height, bool bypass, bool batched, bool faulted) NOEXCEPT { + // Node errors are fatal (or disk full recoverable). + if (ec && node::error::error_category::contains(ec)) + { + fault(ec); + return; + } + + // Prioritize non-signature block validation failures. if (ec) { - // Node errors are fatal. - if (node::error::error_category::contains(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. + if (faulted) + { + 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. + if (batched) + { + POST(push_batch, link); + return; + } + + // Not failed/invalid/batched/faulted, so block is complete (maybe valid). + notify_block({}, height, link, bypass); +} + +void chaser_validate::push_batch(const header_link& link) NOEXCEPT +{ + BC_ASSERT(stranded()); + batched_.push_back(link); +} + +// TODO: This is only invoked by check chaser advancement. But it's possible +// entries may be captured after that point. So this must be bumped. +void chaser_validate::process_batch() NOEXCEPT +{ + BC_ASSERT(stranded()); + auto& query = archive(); + + // 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. + std::unique_lock lock(mutex_); + + LOGN("Batch verification begin."); + + header_links invalids{}; + if (!query.verify_signatures(invalids)) + { + fault(error::batch2); + return; + } + + // 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 (!). + for (const auto& link: invalids) + { + size_t height{}; + if (!query.get_height(height, link) || + !query.set_block_unconfirmable(link)) { - // fault(ec) initiates recovery if caused by disk full condition. - LOGF("Fault validating [" << height << "] " << ec.message()); - fault(ec); + fault(error::batch3); return; } - if (ec == system::error::block_capture) + notify_block(system::error::invalid_signature, height, link, false); + } + + // Set all invalid links in batched_ to terminal. + std::ranges::replace_if(batched_, [&](const auto& link) NOEXCEPT + { + return contains(invalids, link); + }, header_link::terminal); + + // Set all batched blocks that aren't invalid to valid. + // May be ancestors of invalid, in which case they are also unconfirmable. + for (const auto& link: batched_) + { + // Terminal links are previously set invalid. + if (link == header_link::terminal) + continue; + + size_t height{}; + if (!query.get_height(height, link) || + !query.set_block_valid(link)) { - // 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. - post_block(link, bypass); + fault(error::batch4); return; } + notify_block(system::error::block_success, height, link, false); + } + + // All batched are processed, and since strand-protected is safe to clear. + batched_.clear(); + + // May have grown to maximum_concurrency and never used again once current. + if (is_current(true)) + batched_.shrink_to_fit(); + + // Purge all signature batch tables. + if (!query.purge_signatures()) + { + fault(error::batch5); + return; + } + + LOGN("Batch verification begin."); +} + +void chaser_validate::notify_block(const code& ec, size_t height, + const header_link& link, bool bypass) NOEXCEPT +{ + if (ec) + { // INVALID BLOCK (not a fault) notify(ec, chase::unvalid, link); fire(events::block_unconfirmable, height); @@ -368,21 +498,20 @@ void chaser_validate::complete_block(const code& ec, const header_link& link, chain::signatures chaser_validate::get_capture( const header_link& link) NOEXCEPT { - if (!batch_signatures_) - return {}; - - // Group identifier for block, incremented for each multisig/threshold. - const auto id = to_shared(); + if (!batch_signatures_ || is_current(link)) + return { false }; + const auto sequence = to_shared(); + const auto lock = emplace_shared(mutex_); return signatures { .enabled = true, .log = BIND_THIS(do_log, _1), - .fire = BIND_THIS(do_fire, _1, _2), + .fire = BIND_THIS(do_fire, _1, _2, lock), .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, id), - .threshold = BIND_THIS(do_threshold, _1, link, id) + .multisig = BIND_THIS(do_multisig, _1, _2, _3, link, sequence), + .threshold = BIND_THIS(do_threshold, _1, link, sequence) }; } @@ -393,7 +522,8 @@ void chaser_validate::do_log(const chain::script& ) NOEXCEPT //// << missed.to_string(chain::flags::all_rules)); } -void chaser_validate::do_fire(missed miss, size_t count) NOEXCEPT +void chaser_validate::do_fire(missed miss, size_t count, + const shared_lock_cptr&) NOEXCEPT { switch (miss) { From d93d7ceab4c35f9b9f981ff443e7117fe0f0720b Mon Sep 17 00:00:00 2001 From: evoskuil Date: Tue, 16 Jun 2026 13:25:14 -0400 Subject: [PATCH 8/8] Factor chaser_validate_batch.cpp, style, comments. --- Makefile.am | 1 + .../libbitcoin-node/libbitcoin-node.vcxproj | 1 + .../libbitcoin-node.vcxproj.filters | 3 + .../libbitcoin-node/libbitcoin-node.vcxproj | 1 + .../libbitcoin-node.vcxproj.filters | 3 + .../bitcoin/node/chasers/chaser_confirm.hpp | 4 +- .../bitcoin/node/chasers/chaser_validate.hpp | 18 +- src/chasers/chaser_confirm.cpp | 22 +- src/chasers/chaser_validate.cpp | 216 +-------------- src/chasers/chaser_validate_batch.cpp | 251 ++++++++++++++++++ 10 files changed, 293 insertions(+), 227 deletions(-) create mode 100644 src/chasers/chaser_validate_batch.cpp diff --git a/Makefile.am b/Makefile.am index 8aba8035..2b10aad5 100644 --- a/Makefile.am +++ b/Makefile.am @@ -54,6 +54,7 @@ src_libbitcoin_node_la_SOURCES = \ src/chasers/chaser_template.cpp \ src/chasers/chaser_transaction.cpp \ src/chasers/chaser_validate.cpp \ + src/chasers/chaser_validate_batch.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 884cf244..98a5e2d6 100644 --- a/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj @@ -135,6 +135,7 @@ + diff --git a/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj.filters index 5b8a5dde..5d2a740a 100644 --- a/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj.filters +++ b/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj.filters @@ -105,6 +105,9 @@ 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 3295d926..42a33c1d 100644 --- a/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj +++ b/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj @@ -135,6 +135,7 @@ + diff --git a/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj.filters b/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj.filters index 5b8a5dde..5d2a740a 100644 --- a/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj.filters +++ b/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj.filters @@ -105,6 +105,9 @@ src\chasers + + src\chasers + src diff --git a/include/bitcoin/node/chasers/chaser_confirm.hpp b/include/bitcoin/node/chasers/chaser_confirm.hpp index fd027787..ae8f769d 100644 --- a/include/bitcoin/node/chasers/chaser_confirm.hpp +++ b/include/bitcoin/node/chasers/chaser_confirm.hpp @@ -58,7 +58,9 @@ class BCN_API chaser_confirm virtual bool confirm_block(const header_link& link, size_t height, const header_links& popped, size_t fork_point) NOEXCEPT; virtual bool complete_block(const code& ec, const header_link& link, - size_t height, bool bypassed) NOEXCEPT; + size_t height, bool bypass) NOEXCEPT; + virtual bool notify_block(const code& ec, size_t height, + const database::header_link& link, bool bypass) NOEXCEPT; private: bool set_reorganized(const header_link& link, diff --git a/include/bitcoin/node/chasers/chaser_validate.hpp b/include/bitcoin/node/chasers/chaser_validate.hpp index 03d07b9a..3309cb04 100644 --- a/include/bitcoin/node/chasers/chaser_validate.hpp +++ b/include/bitcoin/node/chasers/chaser_validate.hpp @@ -43,6 +43,9 @@ class BCN_API chaser_validate void stop() NOEXCEPT override; protected: + using signatures = system::chain::signatures; + using race = network::race_unity; + /// Post a method in base or derived class in parallel (use PARALLEL). template inline auto parallel(Method&& method, Args&&... args) NOEXCEPT @@ -51,8 +54,6 @@ class BCN_API chaser_validate BIND_TO(method, args)); } - typedef network::race_unity race; - virtual bool handle_chase(const code& ec, chase event_, event_value value) NOEXCEPT; @@ -62,6 +63,7 @@ class BCN_API chaser_validate virtual void do_bumped(height_t height) NOEXCEPT; virtual void do_bump(height_t height) NOEXCEPT; + /// Validation. virtual void post_block(const database::header_link& link, bool bypass) NOEXCEPT; virtual void validate_block(const database::header_link& link, @@ -76,8 +78,12 @@ class BCN_API chaser_validate bool batched=false, bool faulted=false) NOEXCEPT; virtual void notify_block(const code& ec, size_t height, const database::header_link& link, bool bypass) NOEXCEPT; - virtual void push_batch(const database::header_link& link) NOEXCEPT; + + /// Batching. + virtual code start_batch() NOEXCEPT; virtual void process_batch() NOEXCEPT; + virtual void push_batch(const database::header_link& link) NOEXCEPT; + virtual signatures get_capture(const database::header_link& link) NOEXCEPT; // Override base class strand because it sits on the network thread pool. network::asio::strand& strand() NOEXCEPT override; @@ -89,13 +95,10 @@ class BCN_API chaser_validate using shared_lock_cptr = std::shared_ptr; using atomic_counter = std::atomic; using atomic_counter_ptr = std::shared_ptr; - using signatures = system::chain::signatures; using threshold_group = signatures::threshold_group; using missed = signatures::miss; - signatures get_capture(const database::header_link& link) NOEXCEPT; - - // Handlers. + // Capture handlers. void do_log(const system::chain::script& missed) NOEXCEPT; void do_fire(missed miss, size_t count, const shared_lock_cptr& lock) NOEXCEPT; @@ -113,6 +116,7 @@ class BCN_API chaser_validate const database::header_link& link, const atomic_counter_ptr& sequence) NOEXCEPT; + // Capture helpers. void log_capture(const std::string_view& name, size_t captured, size_t missed) const NOEXCEPT; void log_captures() const NOEXCEPT; diff --git a/src/chasers/chaser_confirm.cpp b/src/chasers/chaser_confirm.cpp index 686fe354..c55091be 100644 --- a/src/chasers/chaser_confirm.cpp +++ b/src/chasers/chaser_confirm.cpp @@ -318,16 +318,22 @@ bool chaser_confirm::complete_block(const code& ec, const header_link& link, { BC_ASSERT(stranded()); - if (ec) + // Database errors are fatal (or disk full recoverable). + if (ec && database::error::error_category::contains(ec)) { - // Database errors are fatal. - if (database::error::error_category::contains(ec)) - { - LOGF("Fault confirming [" << height << "] " << ec.message()); - fault(ec); - return false; - } + LOGF("Fault confirming [" << height << "] " << ec.message()); + fault(ec); + return false; + } + return notify_block(ec, height, link, bypass); +} + +bool chaser_confirm::notify_block(const code& ec, size_t height, + const header_link& link, bool bypass) NOEXCEPT +{ + if (ec) + { // UNCONFIRMABLE BLOCK (not a fault but discontinue) notify(ec, chase::unconfirmable, link); fire(events::block_unconfirmable, height); diff --git a/src/chasers/chaser_validate.cpp b/src/chasers/chaser_validate.cpp index 16c5cd75..e221f80a 100644 --- a/src/chasers/chaser_validate.cpp +++ b/src/chasers/chaser_validate.cpp @@ -18,8 +18,6 @@ */ #include -#include -#include #include #include #include @@ -55,14 +53,10 @@ code chaser_validate::start() NOEXCEPT if (!node_settings().headers_first) return error::success; - // TODO: ecdsa can be retained, as they don't fault, so set batched_ here. - // 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. - auto& query = archive(); - if (batch_signatures_ && !query.purge_signatures()) - return error::batch1; + if (const auto ec = start_batch()) + return ec; - set_position(query.get_fork()); + set_position(archive().get_fork()); SUBSCRIBE_CHASE(handle_chase, _1, _2, _3); return error::success; } @@ -355,6 +349,7 @@ void chaser_validate::complete_block(const code& ec, const header_link& link, // Node errors are fatal (or disk full recoverable). if (ec && node::error::error_category::contains(ec)) { + LOGF("Fault validating [" << height << "] " << ec.message()); fault(ec); return; } @@ -389,97 +384,12 @@ void chaser_validate::complete_block(const code& ec, const header_link& link, notify_block({}, height, link, bypass); } -void chaser_validate::push_batch(const header_link& link) NOEXCEPT -{ - BC_ASSERT(stranded()); - batched_.push_back(link); -} - -// TODO: This is only invoked by check chaser advancement. But it's possible -// entries may be captured after that point. So this must be bumped. -void chaser_validate::process_batch() NOEXCEPT -{ - BC_ASSERT(stranded()); - auto& query = archive(); - - // 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. - std::unique_lock lock(mutex_); - - LOGN("Batch verification begin."); - - header_links invalids{}; - if (!query.verify_signatures(invalids)) - { - fault(error::batch2); - return; - } - - // 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 (!). - for (const auto& link: invalids) - { - size_t height{}; - if (!query.get_height(height, link) || - !query.set_block_unconfirmable(link)) - { - fault(error::batch3); - return; - } - - notify_block(system::error::invalid_signature, height, link, false); - } - - // Set all invalid links in batched_ to terminal. - std::ranges::replace_if(batched_, [&](const auto& link) NOEXCEPT - { - return contains(invalids, link); - }, header_link::terminal); - - // Set all batched blocks that aren't invalid to valid. - // May be ancestors of invalid, in which case they are also unconfirmable. - for (const auto& link: batched_) - { - // Terminal links are previously set invalid. - if (link == header_link::terminal) - continue; - - size_t height{}; - if (!query.get_height(height, link) || - !query.set_block_valid(link)) - { - fault(error::batch4); - return; - } - - notify_block(system::error::block_success, height, link, false); - } - - // All batched are processed, and since strand-protected is safe to clear. - batched_.clear(); - - // May have grown to maximum_concurrency and never used again once current. - if (is_current(true)) - batched_.shrink_to_fit(); - - // Purge all signature batch tables. - if (!query.purge_signatures()) - { - fault(error::batch5); - return; - } - - LOGN("Batch verification begin."); -} - void chaser_validate::notify_block(const code& ec, size_t height, const header_link& link, bool bypass) NOEXCEPT { if (ec) { - // INVALID BLOCK (not a fault) + // INVALID BLOCK (not a fault but discontinue) notify(ec, chase::unvalid, link); fire(events::block_unconfirmable, height); LOGR("Invalid block [" << height << "] " << ec.message()); @@ -492,122 +402,6 @@ void chaser_validate::notify_block(const code& ec, size_t height, LOGV("Block validated: " << height << (bypass ? " (bypass)" : "")); } -// private -// ---------------------------------------------------------------------------- - -chain::signatures chaser_validate::get_capture( - const header_link& link) NOEXCEPT -{ - if (!batch_signatures_ || is_current(link)) - return { false }; - - const auto sequence = to_shared(); - const auto lock = emplace_shared(mutex_); - return signatures - { - .enabled = true, - .log = BIND_THIS(do_log, _1), - .fire = BIND_THIS(do_fire, _1, _2, lock), - .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) - }; -} - -void chaser_validate::do_log(const chain::script& ) NOEXCEPT -{ - // Enable for a game of whack-a-mole. - ////LOGA("Sigop @ " << ctx.height << " -> " - //// << missed.to_string(chain::flags::all_rules)); -} - -void chaser_validate::do_fire(missed miss, size_t count, - const shared_lock_cptr&) 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::batch6); - 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::batch7); - return set; -} - -BC_PUSH_WARNING(SMART_PTR_NOT_NEEDED) -BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR) - -bool chaser_validate::do_multisig(const hash_digest& digest, - const ec_compresseds& points, const ec_signatures& signs, - const header_link& link, const atomic_counter_ptr& sequence) 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::batch8); - return set; -} - -bool chaser_validate::do_threshold(const threshold_group& group, - const header_link& link, const atomic_counter_ptr& sequence) NOEXCEPT -{ - threshold_ += group.entries.size(); - const auto set = archive().set_signatures(group, (*sequence)++, link); - if (!set) fault(error::batch9); - return set; -} - -BC_POP_WARNING() -BC_POP_WARNING() - -void chaser_validate::log_capture(const std::string_view& name, - size_t captured, size_t missed) const NOEXCEPT -{ - if (to_bool(captured) || to_bool(missed)) - { - const auto rate = (100.0f * captured) / (captured + missed); - const auto text = (boost_format("%.4f") % rate).str(); - LOGV("Capture rate " << name << text << "% = " << captured - << "/(" << captured << "+" << missed << ")"); - } -} - -void chaser_validate::log_captures() const NOEXCEPT -{ - log_capture("ecdsa.... ", ecdsa_, missed_ecdsa_); - log_capture("multisig. ", multisig_, missed_multisig_); - log_capture("schnorr.. ", schnorr_, missed_schnorr_); - log_capture("threshold ", threshold_, zero); -} - // Overrides due to independent priority thread pool // ---------------------------------------------------------------------------- diff --git a/src/chasers/chaser_validate_batch.cpp b/src/chasers/chaser_validate_batch.cpp new file mode 100644 index 00000000..7a582e96 --- /dev/null +++ b/src/chasers/chaser_validate_batch.cpp @@ -0,0 +1,251 @@ +/** + * 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 +#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) +BC_PUSH_WARNING(SMART_PTR_NOT_NEEDED) +BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR) + +code chaser_validate::start_batch() NOEXCEPT +{ + // TODO: ecdsa can be retained, as they don't fault, so set batched_ here. + // 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. + return (batch_signatures_ && !archive().purge_signatures()) ? + error::batch1 : error::success; +} + +void chaser_validate::push_batch(const header_link& link) NOEXCEPT +{ + BC_ASSERT(stranded()); + batched_.push_back(link); +} + +// TODO: This is only invoked by check chaser advancement. But it's possible +// entries may be captured after that point. So this must be bumped. +void chaser_validate::process_batch() NOEXCEPT +{ + BC_ASSERT(stranded()); + auto& query = archive(); + + // 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. + std::unique_lock lock(mutex_); + + LOGN("Batch signature verify begin (" + << query.ecdsa_records() << ") ecdsa (" + << query.schnorr_records() << ") schnorr."); + + header_links invalids{}; + if (!query.verify_signatures(invalids)) + { + fault(error::batch2); + return; + } + + // 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 (!). + for (const auto& link: invalids) + { + size_t height{}; + if (!query.get_height(height, link) || + !query.set_block_unconfirmable(link)) + { + fault(error::batch3); + return; + } + + LOGR("Unverifiable signature in block (" << height << ")."); + notify_block(system::error::invalid_signature, height, link, false); + } + + // Set all invalid links in batched_ to terminal. + std::ranges::replace_if(batched_, [&](const auto& link) NOEXCEPT + { + return contains(invalids, link); + }, header_link::terminal); + + // Set all batched blocks that aren't invalid to valid. + // May be ancestors of invalid, in which case they are also unconfirmable. + for (const auto& link: batched_) + { + // Terminal links are previously set invalid. + if (link == header_link::terminal) + continue; + + size_t height{}; + if (!query.get_height(height, link) || + !query.set_block_valid(link)) + { + fault(error::batch4); + return; + } + + notify_block(system::error::block_success, height, link, false); + } + + // All batched are processed, and since strand-protected is safe to clear. + batched_.clear(); + + // May have grown to maximum_concurrency and never used again once current. + if (is_current(true)) + batched_.shrink_to_fit(); + + // Purge all signature batch tables. + if (!query.purge_signatures()) + { + fault(error::batch5); + return; + } + + LOGN("Batch signature verify end."); +} + +signatures chaser_validate::get_capture(const header_link& link) NOEXCEPT +{ + if (!batch_signatures_ || is_current(link)) + return { false }; + + const auto sequence = to_shared(); + const auto lock = emplace_shared(mutex_); + return signatures + { + .enabled = true, + .log = BIND_THIS(do_log, _1), + .fire = BIND_THIS(do_fire, _1, _2, lock), + .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)); +} + +// Captures shared lock on batch verification. +void chaser_validate::do_fire(missed miss, size_t count, + const shared_lock_cptr&) 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::batch6); + 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::batch7); + return set; +} + +bool chaser_validate::do_multisig(const hash_digest& digest, + const ec_compresseds& points, const ec_signatures& signs, + const header_link& link, const atomic_counter_ptr& sequence) 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::batch8); + return set; +} + +bool chaser_validate::do_threshold(const threshold_group& group, + const header_link& link, const atomic_counter_ptr& sequence) NOEXCEPT +{ + threshold_ += group.entries.size(); + const auto set = archive().set_signatures(group, (*sequence)++, link); + if (!set) fault(error::batch9); + return set; +} + +void chaser_validate::log_capture(const std::string_view& name, + size_t captured, size_t missed) const NOEXCEPT +{ + if (to_bool(captured) || to_bool(missed)) + { + const auto rate = (100.0f * captured) / (captured + missed); + const auto text = (boost_format("%.4f") % rate).str(); + LOGV("Capture rate " << name << text << "% = " << captured + << "/(" << captured << "+" << missed << ")"); + } +} + +void chaser_validate::log_captures() const NOEXCEPT +{ + log_capture("ecdsa.... ", ecdsa_, missed_ecdsa_); + log_capture("multisig. ", multisig_, missed_multisig_); + log_capture("schnorr.. ", schnorr_, missed_schnorr_); + log_capture("threshold ", threshold_, zero); +} + +BC_POP_WARNING() +BC_POP_WARNING() +BC_POP_WARNING() + +} // namespace node +} // namespace libbitcoin