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/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_confirm.hpp b/include/bitcoin/node/chasers/chaser_confirm.hpp index 76684561..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, @@ -68,9 +70,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..3309cb04 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 @@ -42,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 @@ -50,45 +54,54 @@ 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; 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; + /// Validation. virtual void post_block(const database::header_link& link, 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; + + /// 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; bool stranded() const NOEXCEPT override; 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; 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) 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; @@ -98,20 +111,25 @@ 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; + // Capture helpers. void log_capture(const std::string_view& name, 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_{}; @@ -129,7 +147,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/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/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/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/chasers/chaser_check.cpp b/src/chasers/chaser_check.cpp index 68e72f98..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 @@ -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 @@ -372,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 @@ -386,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 @@ -402,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_confirm.cpp b/src/chasers/chaser_confirm.cpp index a0df5117..c55091be 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; } @@ -286,10 +281,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; @@ -326,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 eed2c0e2..e221f80a 100644 --- a/src/chasers/chaser_validate.cpp +++ b/src/chasers/chaser_validate.cpp @@ -18,7 +18,6 @@ */ #include -#include #include #include #include @@ -45,8 +44,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()) { } @@ -55,8 +53,10 @@ code chaser_validate::start() NOEXCEPT if (!node_settings().headers_first) return error::success; - const auto& query = archive(); - set_position(query.get_fork()); + if (const auto ec = start_batch()) + return ec; + + set_position(archive().get_fork()); SUBSCRIBE_CHASE(handle_chase, _1, _2, _3); return error::success; } @@ -88,6 +88,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: { @@ -121,6 +131,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()); @@ -148,7 +164,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); @@ -159,7 +175,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()) @@ -200,10 +216,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); } @@ -218,6 +233,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 @@ -234,24 +250,20 @@ 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))) + else if ((ec = validate(batched, faulted, 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); + 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, 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, @@ -281,40 +293,50 @@ 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(); 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; 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; @@ -322,31 +344,52 @@ 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)) + { + LOGF("Fault validating [" << height << "] " << ec.message()); + fault(ec); + return; + } + + // Prioritize non-signature block validation failures. if (ec) { - // Node errors are fatal. - if (node::error::error_category::contains(ec)) - { - // fault(ec) initiates recovery if caused by disk full condition. - LOGF("Fault validating [" << height << "] " << ec.message()); - fault(ec); - return; - } + notify_block(ec, height, link, bypass); + return; + } - if (ec == system::error::block_capture) - { - // 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); - 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; + } - // INVALID BLOCK (not a fault) + // 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::notify_block(const code& ec, size_t height, + const header_link& link, bool bypass) NOEXCEPT +{ + if (ec) + { + // INVALID BLOCK (not a fault but discontinue) notify(ec, chase::unvalid, link); fire(events::block_unconfirmable, height); LOGR("Invalid block [" << height << "] " << ec.message()); @@ -354,15 +397,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 @@ -394,146 +431,6 @@ bool chaser_validate::stranded() const NOEXCEPT return validation_strand_.running_in_this_thread(); } -// private -// ---------------------------------------------------------------------------- - -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(); - - 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 -{ - ////LOGA("Sigop @ " << ctx.height << " -> " - //// << missed.to_string(chain::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(system::error::block_capture); - 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(system::error::block_capture); - 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& id) 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); - return set; -} - -bool chaser_validate::do_threshold(const threshold_group& group, - const header_link& link, const atomic_counter_ptr& id) 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. - 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); -} - BC_POP_WARNING() } // namespace node 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 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/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; 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/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() 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);