From ff2140b2ca9a183c3ba32a5fe6df6c258b2acb68 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sun, 14 Jun 2026 13:00:47 -0400 Subject: [PATCH 1/8] Add mark_unconfirmable setting. --- include/bitcoin/node/settings.hpp | 1 + src/settings.cpp | 1 + test/settings.cpp | 1 + 3 files changed, 3 insertions(+) diff --git a/include/bitcoin/node/settings.hpp b/include/bitcoin/node/settings.hpp index 88983129..10c2df96 100644 --- a/include/bitcoin/node/settings.hpp +++ b/include/bitcoin/node/settings.hpp @@ -41,6 +41,7 @@ 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; diff --git a/src/settings.cpp b/src/settings.cpp index af900e3f..dd1013fa 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -37,6 +37,7 @@ 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 }, diff --git a/test/settings.cpp b/test/settings.cpp index f63af133..b22329d3 100644 --- a/test/settings.cpp +++ b/test/settings.cpp @@ -37,6 +37,7 @@ 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); From 0dcaae9c4609bc7e24024a76b726c2032a27731b Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sun, 14 Jun 2026 13:04:38 -0400 Subject: [PATCH 2/8] Integrate mark_unconfirmable. --- .../node/impl/chasers/chaser_organize.ipp | 4 ++++ src/chasers/chaser_confirm.cpp | 5 ++++- src/chasers/chaser_validate.cpp | 18 ++++++++++++++---- src/protocols/protocol_block_in_31800.cpp | 4 +++- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/include/bitcoin/node/impl/chasers/chaser_organize.ipp b/include/bitcoin/node/impl/chasers/chaser_organize.ipp index 0858a75b..807f9353 100644 --- a/include/bitcoin/node/impl/chasers/chaser_organize.ipp +++ b/include/bitcoin/node/impl/chasers/chaser_organize.ipp @@ -90,6 +90,10 @@ 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) + break; + // Roll back the candidate chain to confirmed top (via fork point). BC_ASSERT(std::holds_alternative(value)); POST(do_disorganize, std::get(value)); diff --git a/src/chasers/chaser_confirm.cpp b/src/chasers/chaser_confirm.cpp index 86df17a2..a0df5117 100644 --- a/src/chasers/chaser_confirm.cpp +++ b/src/chasers/chaser_confirm.cpp @@ -286,7 +286,10 @@ bool chaser_confirm::confirm_block(const header_link& link, size_t height, if (const auto ec = query.block_confirmable(link)) { - if (!query.set_block_unconfirmable(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)) { fault(error::confirm9); return false; diff --git a/src/chasers/chaser_validate.cpp b/src/chasers/chaser_validate.cpp index e8c13475..08596070 100644 --- a/src/chasers/chaser_validate.cpp +++ b/src/chasers/chaser_validate.cpp @@ -200,7 +200,9 @@ void chaser_validate::do_bumped(height_t height) NOEXCEPT void chaser_validate::post_block(const header_link& link, bool bypass) NOEXCEPT { - BC_ASSERT(stranded()); + // may be called by do_bumped (stranded) or complete_block (not stranded). + ///BC_ASSERT(stranded()); + backlog_.fetch_add(one, std::memory_order_relaxed); PARALLEL(validate_block, link, bypass); } @@ -232,13 +234,21 @@ void chaser_validate::validate_block(const header_link& link, } else if ((ec = populate(bypass, *block, ctx))) { - if (!query.set_block_unconfirmable(link)) + if (node_settings().mark_unconfirmable && + !query.set_block_unconfirmable(link)) ec = error::validate4; } else if ((ec = validate(bypass, *block, link, ctx))) { - if (!query.set_block_unconfirmable(link)) - ec = error::validate5; + // !mark_unconfirmable allows node to stall, preserving log. + // Will continue to validate blocks until the end of the window. + // At that point validation (and confirmation) will be starved, and + // download will wait forever on this missing validation event. Restart + // is safe and will buffer this block again for validation. Without the + // unconfirmable state or disorganization, no header reorganize occurs. + if (node_settings().mark_unconfirmable && + !query.set_block_unconfirmable(link)) + ec = error::validate5; } complete_block(ec, link, ctx.height, bypass); diff --git a/src/protocols/protocol_block_in_31800.cpp b/src/protocols/protocol_block_in_31800.cpp index c6709af5..ba6317df 100644 --- a/src/protocols/protocol_block_in_31800.cpp +++ b/src/protocols/protocol_block_in_31800.cpp @@ -317,7 +317,9 @@ bool protocol_block_in_31800::handle_receive_block(const code& ec, return false; } - if (!query.set_block_unconfirmable(link)) + // !mark_unconfirmable allows node to stall, preserving log. + if (node_settings().mark_unconfirmable && + !query.set_block_unconfirmable(link)) { stop(fault(error::protocol1)); return false; From e1a08540f806e9d37caaa14ef5def11d50725535 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sun, 14 Jun 2026 16:36:08 -0400 Subject: [PATCH 3/8] Rename BIND_THIS to BIND_TO. --- include/bitcoin/node/chasers/chaser.hpp | 4 ++-- include/bitcoin/node/chasers/chaser_validate.hpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/include/bitcoin/node/chasers/chaser.hpp b/include/bitcoin/node/chasers/chaser.hpp index bea55dc7..d63b6d90 100644 --- a/include/bitcoin/node/chasers/chaser.hpp +++ b/include/bitcoin/node/chasers/chaser.hpp @@ -59,14 +59,14 @@ class BCN_API chaser template auto bind(Method&& method, Args&&... args) NOEXCEPT { - return BIND_THIS(method, args); + return BIND_TO(method, args); } /// Post a method to channel strand (use POST). template auto post(Method&& method, Args&&... args) NOEXCEPT { - return boost::asio::post(strand(), BIND_THIS(method, args)); + return boost::asio::post(strand(), BIND_TO(method, args)); } /// Methods. diff --git a/include/bitcoin/node/chasers/chaser_validate.hpp b/include/bitcoin/node/chasers/chaser_validate.hpp index b403cd2e..f3b3631b 100644 --- a/include/bitcoin/node/chasers/chaser_validate.hpp +++ b/include/bitcoin/node/chasers/chaser_validate.hpp @@ -47,7 +47,7 @@ class BCN_API chaser_validate inline auto parallel(Method&& method, Args&&... args) NOEXCEPT { return boost::asio::post(validation_threadpool_.service(), - BIND_THIS(method, args)); + BIND_TO(method, args)); } typedef network::race_unity race; From b4adb7c39b3ed9ae8cc6390e508e8be3907d4725 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sun, 14 Jun 2026 16:36:57 -0400 Subject: [PATCH 4/8] Refactor get_capture() to remove lambdas. --- .../bitcoin/node/chasers/chaser_validate.hpp | 25 +++- src/chasers/chaser_validate.cpp | 128 +++++++++++------- 2 files changed, 101 insertions(+), 52 deletions(-) diff --git a/include/bitcoin/node/chasers/chaser_validate.hpp b/include/bitcoin/node/chasers/chaser_validate.hpp index f3b3631b..7c2c835f 100644 --- a/include/bitcoin/node/chasers/chaser_validate.hpp +++ b/include/bitcoin/node/chasers/chaser_validate.hpp @@ -78,8 +78,31 @@ class BCN_API chaser_validate bool stranded() const NOEXCEPT override; private: - system::chain::signatures get_capture( + 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. + void do_log(const system::chain::script& missed) NOEXCEPT; + void do_fire(missed miss, size_t count) 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; + bool do_schnorr(const system::hash_digest& digest, + const system::ec_xonly& point, const system::ec_signature& sign, + const database::header_link& link) NOEXCEPT; + 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; + bool do_threshold(const threshold_group& group, + const database::header_link& link, + const atomic_counter_ptr& id) NOEXCEPT; + void log_capture(const std::string_view& name, size_t captured, size_t missed) const NOEXCEPT; void log_captures() const NOEXCEPT; diff --git a/src/chasers/chaser_validate.cpp b/src/chasers/chaser_validate.cpp index 08596070..d8a3dd9b 100644 --- a/src/chasers/chaser_validate.cpp +++ b/src/chasers/chaser_validate.cpp @@ -397,66 +397,27 @@ chain::signatures chaser_validate::get_capture( return {}; // Group identifier for block, incremented for each multisig/threshold. - const auto id = to_shared>(); + const auto id = to_shared(); - using namespace chain; return signatures { // Default struct is disabled. .enabled = true, // Enable for a game of whack-a-mole. - .log = [&](const script& /* LOG_ONLY(missed) */) NOEXCEPT - { - ////LOGA("Sigop @ " << ctx.height << " -> " - //// << missed.to_string(chain::flags::all_rules)); - }, + .log = BIND_THIS(do_log, _1), // Update counters for missed capture. - .fire = [&](signatures::miss miss, size_t count) NOEXCEPT - { - switch (miss) - { - case signatures::miss::ecdsa: - missed_ecdsa_ += count; - break; - case signatures::miss::multisig: - missed_multisig_ += count; - break; - case signatures::miss::schnorr: - missed_schnorr_ += count; - break; - default:; - } - }, + .fire = BIND_THIS(do_fire, _1, _2), // opcode::checksig/verify - .ecdsa = [&](const hash_digest& digest, const ec_compressed& point, - const ec_signature& sign) NOEXCEPT - { - ++ecdsa_; - auto& query = archive(); - return query.set_signature(digest, point, sign, link); - }, + .ecdsa = BIND_THIS(do_ecdsa, _1, _2, _3, link), // opcode::checksigadd | opcode::checksig/verify - .schnorr = [&](const hash_digest& digest, const ec_xonly& point, - const ec_signature& sign) NOEXCEPT - { - ++schnorr_; - auto& query = archive(); - return query.set_signature(digest, point, sign, link); - }, + .schnorr = BIND_THIS(do_schnorr, _1, _2, _3, link), // opcode::checkmultisig/verify - .multisig = [&, id](const hash_digest& digest, - const ec_compresseds& points, const ec_signatures& signs) NOEXCEPT - { - BC_ASSERT(points.size() == signs.size()); - auto& query = archive(); - multisig_ += points.size(); - return query.set_signatures(digest, points, signs, (*id)++, link); - }, + .multisig = BIND_THIS(do_multisig, _1, _2, _3, link, id), // opcode::within // opcode::numequal/verify @@ -466,15 +427,80 @@ chain::signatures chaser_validate::get_capture( // opcode::lessthanorequal // opcode::greaterthanorequal // opcode::checksig (m of m) - .threshold = [&, id](const signatures::threshold_group& group) NOEXCEPT - { - threshold_ += group.entries.size(); - auto& query = archive(); - return query.set_signatures(group, (*id)++, link); - } + .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 out = archive().set_signature(digest, point, sign, link); + if (!out) fault(system::error::block_capture); + return out; +} + +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 out = archive().set_signature(digest, point, sign, link); + if (!out) fault(system::error::block_capture); + return out; +} + +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 out = archive().set_signatures(digest, points, signs, (*id)++, + link); + if (!out) fault(system::error::block_capture); + return out; +} + +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 out = archive().set_signatures(group, (*id)++, link); + if (!out) fault(system::error::block_capture); + + // False here sets signatures.fault, causing block.connect(2) to + // return error::block_capture, causing block validation resubmit. + return out; +} + void chaser_validate::log_capture(const std::string_view& name, size_t captured, size_t missed) const NOEXCEPT { From 6604e7513c9d0b9f4b365601dcf9ee3ec94e2d49 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sun, 14 Jun 2026 16:37:27 -0400 Subject: [PATCH 5/8] chaser_validate::complete_block report block on capture fault. --- src/chasers/chaser_validate.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/chasers/chaser_validate.cpp b/src/chasers/chaser_validate.cpp index d8a3dd9b..04c215b6 100644 --- a/src/chasers/chaser_validate.cpp +++ b/src/chasers/chaser_validate.cpp @@ -339,6 +339,17 @@ void chaser_validate::complete_block(const code& ec, const header_link& link, 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; + } + // INVALID BLOCK (not a fault) notify(ec, chase::unvalid, link); fire(events::block_unconfirmable, height); From 19296ba40e31fd301e28bf6e7b46639ab3c53818 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sun, 14 Jun 2026 18:48:50 -0400 Subject: [PATCH 6/8] Delint. --- src/chasers/chaser_validate.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/chasers/chaser_validate.cpp b/src/chasers/chaser_validate.cpp index 04c215b6..66b8edf9 100644 --- a/src/chasers/chaser_validate.cpp +++ b/src/chasers/chaser_validate.cpp @@ -487,6 +487,9 @@ bool chaser_validate::do_schnorr(const hash_digest& digest, return out; } +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 @@ -512,6 +515,9 @@ bool chaser_validate::do_threshold(const threshold_group& group, return out; } +BC_POP_WARNING() +BC_POP_WARNING() + void chaser_validate::log_capture(const std::string_view& name, size_t captured, size_t missed) const NOEXCEPT { From 043696c7b420377dd1ddf8a34b26f25044fd0064 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sun, 14 Jun 2026 18:49:32 -0400 Subject: [PATCH 7/8] Style. --- src/chasers/chaser_validate.cpp | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/chasers/chaser_validate.cpp b/src/chasers/chaser_validate.cpp index 66b8edf9..95741a75 100644 --- a/src/chasers/chaser_validate.cpp +++ b/src/chasers/chaser_validate.cpp @@ -472,9 +472,9 @@ bool chaser_validate::do_ecdsa(const hash_digest& digest, const header_link& link) NOEXCEPT { ++ecdsa_; - const auto out = archive().set_signature(digest, point, sign, link); - if (!out) fault(system::error::block_capture); - return out; + 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, @@ -482,9 +482,9 @@ bool chaser_validate::do_schnorr(const hash_digest& digest, const header_link& link) NOEXCEPT { ++schnorr_; - const auto out = archive().set_signature(digest, point, sign, link); - if (!out) fault(system::error::block_capture); - return out; + 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) @@ -497,22 +497,22 @@ bool chaser_validate::do_multisig(const hash_digest& digest, BC_ASSERT(points.size() == signs.size()); multisig_ += points.size(); - const auto out = archive().set_signatures(digest, points, signs, (*id)++, + const auto set = archive().set_signatures(digest, points, signs, (*id)++, link); - if (!out) fault(system::error::block_capture); - return out; + 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 out = archive().set_signatures(group, (*id)++, link); - if (!out) fault(system::error::block_capture); + 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 out; + return set; } BC_POP_WARNING() From d5638816be00de7133df131dc7c1fa7725f7675c Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sun, 14 Jun 2026 18:57:55 -0400 Subject: [PATCH 8/8] Comments. --- src/chasers/chaser_validate.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/chasers/chaser_validate.cpp b/src/chasers/chaser_validate.cpp index 95741a75..eed2c0e2 100644 --- a/src/chasers/chaser_validate.cpp +++ b/src/chasers/chaser_validate.cpp @@ -234,6 +234,7 @@ 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)) ec = error::validate4; @@ -241,11 +242,6 @@ void chaser_validate::validate_block(const header_link& link, else if ((ec = validate(bypass, *block, link, ctx))) { // !mark_unconfirmable allows node to stall, preserving log. - // Will continue to validate blocks until the end of the window. - // At that point validation (and confirmation) will be starved, and - // download will wait forever on this missing validation event. Restart - // is safe and will buffer this block again for validation. Without the - // unconfirmable state or disorganization, no header reorganize occurs. if (node_settings().mark_unconfirmable && !query.set_block_unconfirmable(link)) ec = error::validate5;