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);