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