From 7de4f78a6b5e5bd0a413211f7a6ee0da72ab0088 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Thu, 15 Oct 2026 16:30:04 -0400 Subject: [PATCH 1/3] Index bypassed blocks from views without materializing. --- .../bitcoin/node/chasers/chaser_validate.hpp | 12 +- src/chasers/chaser_validate_parallel.cpp | 162 +++++++++--------- 2 files changed, 87 insertions(+), 87 deletions(-) diff --git a/include/bitcoin/node/chasers/chaser_validate.hpp b/include/bitcoin/node/chasers/chaser_validate.hpp index c275d5de..86dbe2ca 100644 --- a/include/bitcoin/node/chasers/chaser_validate.hpp +++ b/include/bitcoin/node/chasers/chaser_validate.hpp @@ -67,16 +67,18 @@ class BCN_API chaser_validate virtual void post_scan(const header_link& link, bool bypass) NOEXCEPT; virtual void validate_block(const header_link& link, bool bypass) NOEXCEPT; virtual void scan_block(const header_link& link, bool bypass) NOEXCEPT; - virtual code validate(bool& batched, bool& capturing, bool bypass, + virtual code validate(bool& batched, bool& capturing, const system::chain::block& block, const header_link& link, const system::chain::context& ctx) NOEXCEPT; - virtual code populate(bool bypass, const system::chain::block& block, + virtual code populate(const system::chain::block& block, const system::chain::context& ctx) NOEXCEPT; - virtual code validate_silent(const header_link& link, bool bank) NOEXCEPT; + virtual code validate_view(const header_link& link, bool filter, + bool silent, bool bank) NOEXCEPT; + virtual code complete_view(const header_link& link, bool filter, + bool silent, bool bank) NOEXCEPT; virtual code validate_pooled(bool& pooled, const header_link& link, const system::chain::context& ctx) NOEXCEPT; - virtual code complete_pooled(const header_link& link, - const system::chain::context& ctx) NOEXCEPT; + virtual code complete_pooled(const header_link& link) NOEXCEPT; virtual void complete_block(const code& ec, const header_link& link, size_t height, bool bypass, bool batched=false, bool capturing=false) NOEXCEPT; diff --git a/src/chasers/chaser_validate_parallel.cpp b/src/chasers/chaser_validate_parallel.cpp index 070fd10d..38ba4d43 100644 --- a/src/chasers/chaser_validate_parallel.cpp +++ b/src/chasers/chaser_validate_parallel.cpp @@ -76,29 +76,28 @@ void chaser_validate::validate_block(const header_link& link, } else if (pooled) { - ec = complete_pooled(link, ctx); + ec = complete_pooled(link); } - else if (bypass && !filter_ && query.is_silent(link, ctx.height)) + else if (bypass) { - ec = validate_silent(link, is_silent_capturing(link)); + const auto silent = query.is_silent(link, ctx.height); + ec = complete_view(link, filter_, silent, silent && + is_silent_capturing(link)); } else { - // TODO: implement allocator parameter resulting in full allocation to - // shared_ptr, to optimize deallocate (12% of milestone/filter). const auto block = query.get_block(link, node_witness_); if (!block) { ec = error::validate2; } - else if ((ec = populate(bypass, *block, ctx))) + else if ((ec = populate(*block, ctx))) { if (!query.set_block_unconfirmable(link)) ec = error::validate4; } - else if ((ec = validate(batched, capturing, bypass, *block, link, - ctx))) + else if ((ec = validate(batched, capturing, *block, link, ctx))) { if (!query.set_block_unconfirmable(link)) ec = error::validate5; @@ -132,30 +131,18 @@ void chaser_validate::validate_block(const header_link& link, // helpers // ---------------------------------------------------------------------------- -code chaser_validate::populate(bool bypass, const chain::block& block, +code chaser_validate::populate(const chain::block& block, const chain::context& ctx) NOEXCEPT { - const auto& query = archive(); + // Internal maturity and time locks are verified here because they are + // the only necessary confirmation checks for internal spends. + if (const auto ec = block.populate(ctx)) + return ec; - if (bypass) - { - // Populating for optional indexes only (no validation metadata). - block.populate(ctx); - if (!query.populate_without_metadata(block)) - return system::error::missing_previous_output; - } - else - { - // Internal maturity and time locks are verified here because they are - // the only necessary confirmation checks for internal spends. - if (const auto ec = block.populate(ctx)) - return ec; + // Metadata identifies internal spends allowing confirmation bypass. + if (!archive().populate_with_metadata(block)) + return system::error::missing_previous_output; - // Metadata identifies internal spends allowing confirmation bypass. - if (!query.populate_with_metadata(block)) - return system::error::missing_previous_output; - } - return error::success; } @@ -168,36 +155,57 @@ void chaser_validate::scan_block(const header_link& link, size_t height{}; const auto ec = archive().get_height(height, link) ? - validate_silent(link, false) : error::validate3; + complete_view(link, false, true, false) : error::validate3; --validate_backlog_; complete_block(ec, link, height, bypass); } -// Bypassed without client filters, a block is read only for its silent -// payment records, so it is not materialized. -code chaser_validate::validate_silent(const header_link& link, - bool bank) NOEXCEPT +// A bypassed block is read only for its optional indexes, so it is not +// materialized. A filter requires all prevouts, otherwise only those of the +// silent payment eligible txs are populated. +code chaser_validate::validate_view(const header_link& link, bool filter, + bool silent, bool bank) NOEXCEPT { auto& query = archive(); - auto wire = query.get_wire_block(link, node_witness_); - chain::view::block block{ std::move(wire), node_witness_ }; + const auto witness = node_witness_ && silent; + auto wire = query.get_wire_block(link, witness); + chain::view::block block{ std::move(wire), witness }; if (!block.is_valid()) return error::validate2; - // Only the prevouts of silent payment eligible txs are populated. data_chunk prevouts{}; std::vector selected{}; - if (!query.get_silent_prevouts(prevouts, selected, link, block) || + if (!(filter ? + query.get_block_prevouts(prevouts, selected, link, block) : + query.get_silent_prevouts(prevouts, selected, link, block)) || !block.populate(std::move(prevouts), selected)) - return query.set_block_unconfirmable(link) ? - code{ system::error::missing_previous_output } : error::validate4; + return system::error::missing_previous_output; + + if (filter && !query.set_filter_body(link, block)) + return error::validate8; - if (bank ? !commit_silent_batch(link, block) : - !query.set_silent(link, block)) + // Banked records are set when the bank drains. + if (silent && (bank ? !commit_silent_batch(link, block) : + !query.set_silent(link, block))) return error::validate9; + return error::success; +} + +code chaser_validate::complete_view(const header_link& link, bool filter, + bool silent, bool bank) NOEXCEPT +{ + auto& query = archive(); + const auto ec = validate_view(link, filter, silent, bank); + if (ec == system::error::missing_previous_output) + return query.set_block_unconfirmable(link) ? ec : error::validate4; + + if (ec) + return ec; + + // Valid must be set after set_filter_body. return query.set_block_valid(link) ? error::success : error::validate10; } @@ -219,23 +227,16 @@ code chaser_validate::validate_pooled(bool& pooled, const header_link& link, } // A pooled block is valid and SP indexed, so produce only for client filters. -code chaser_validate::complete_pooled(const header_link& link, - const chain::context& ctx) NOEXCEPT +code chaser_validate::complete_pooled(const header_link& link) NOEXCEPT { auto& query = archive(); if (filter_) { - bool batched{}, capturing{}; - constexpr auto bypass = true; - const auto block = query.get_block(link, node_witness_); - if (!block) - return error::validate2; - - if (populate(bypass, *block, ctx)) + const auto ec = validate_view(link, true, false, false); + if (ec == system::error::missing_previous_output) return error::validate11; - if (const auto ec = validate(batched, capturing, bypass, *block, link, - ctx)) + if (ec) return ec; } @@ -246,48 +247,45 @@ code chaser_validate::complete_pooled(const header_link& link, return query.set_block_valid(link) ? error::success : error::validate10; } -code chaser_validate::validate(bool& batched, bool& capturing, bool bypass, +code chaser_validate::validate(bool& batched, bool& capturing, const chain::block& block, const header_link& link, const chain::context& ctx) NOEXCEPT { auto& query = archive(); - if (!bypass) - { - code ec{}; - if (((ec = block.check(false))) || ((ec = block.check(ctx, false)))) - return ec; + code ec{}; + 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.accept(ctx, subsidy_interval_, initial_subsidy_))) + return ec; - // Initialize signature capture (appends to this thread's accumulators). - const auto capture = get_capture(link); - capturing = capture.enabled; + // Initialize signature capture (appends to this thread's accumulators). + const auto capture = get_capture(link); + capturing = capture.enabled; - ec = block.connect(ctx, capture); + ec = block.connect(ctx, capture); - // At least one signature batch was attempted (batch completion). - batched = capture.batched; + // At least one signature batch was attempted (batch completion). + batched = capture.batched; - // Commit (or discard) the captured signatures, marking the block prevalid - // contingent on batch verification. The commit epoch is mutually - // exclusive with batch verification (turnstile). - if (capturing) - { - if (!ec && batched) - ec = commit_capture(batched, link); - else - clear_capture(); - } + // Commit (or discard) the captured signatures, marking the block prevalid + // contingent on batch verification. The commit epoch is mutually + // exclusive with batch verification (turnstile). + if (capturing) + { + if (!ec && batched) + ec = commit_capture(batched, link); + else + clear_capture(); + } - if (ec) - return ec; + if (ec) + return ec; - // Prevouts optimize confirmation. - if (!query.set_prevouts(link, block)) - return error::validate7; - } + // Prevouts optimize confirmation. + if (!query.set_prevouts(link, block)) + return error::validate7; if (!query.set_filter_body(link, block)) return error::validate8; From e69557f1db8179758cf5f5a47a76f6c20c5b958f Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Thu, 15 Oct 2026 23:24:20 -0400 Subject: [PATCH 2/3] Add configuration error codes. --- include/bitcoin/node/error.hpp | 7 ++++++- src/error.cpp | 7 ++++++- test/error.cpp | 29 +++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/include/bitcoin/node/error.hpp b/include/bitcoin/node/error.hpp index 82e099f6..3eb685be 100644 --- a/include/bitcoin/node/error.hpp +++ b/include/bitcoin/node/error.hpp @@ -137,7 +137,12 @@ enum error_t : uint8_t batch5, batch6, batch7, - batch8 + batch8, + + /// configuration + silent_unwitnessed, + silent_pruned, + filter_pruned }; // No current need for error_code equivalence mapping. diff --git a/src/error.cpp b/src/error.cpp index 48b0a508..295b46e0 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -127,7 +127,12 @@ DEFINE_ERROR_T_MESSAGE_MAP(error) { batch5, "batch5" }, { batch6, "batch6" }, { batch7, "batch7" }, - { batch8, "batch8" } + { batch8, "batch8" }, + + // configuration + { silent_unwitnessed, "silent payments require witness" }, + { silent_pruned, "silent payments start in pruned blocks" }, + { filter_pruned, "client filters require unpruned blocks" } }; DEFINE_ERROR_T_CATEGORY(error, "node", "node code") diff --git a/test/error.cpp b/test/error.cpp index 3254da24..5e4e7bf8 100644 --- a/test/error.cpp +++ b/test/error.cpp @@ -337,4 +337,33 @@ BOOST_AUTO_TEST_CASE(error_t__code__batch1__true_expected_message) // TODO: batch2-... +// configuration + +BOOST_AUTO_TEST_CASE(error_t__code__silent_unwitnessed__true_expected_message) +{ + constexpr auto value = error::silent_unwitnessed; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "silent payments require witness"); +} + +BOOST_AUTO_TEST_CASE(error_t__code__silent_pruned__true_expected_message) +{ + constexpr auto value = error::silent_pruned; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "silent payments start in pruned blocks"); +} + +BOOST_AUTO_TEST_CASE(error_t__code__filter_pruned__true_expected_message) +{ + constexpr auto value = error::filter_pruned; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "client filters require unpruned blocks"); +} + BOOST_AUTO_TEST_SUITE_END() From aa33a5630f953b08b151139ad464c168e13892d5 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Thu, 15 Oct 2026 23:24:20 -0400 Subject: [PATCH 3/3] Reject client filters with pruning, report each config failure. --- src/configuration.cpp | 9 +++++++-- test/configuration.cpp | 37 +++++++++++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/configuration.cpp b/src/configuration.cpp index 046310a1..963b06de 100644 --- a/src/configuration.cpp +++ b/src/configuration.cpp @@ -39,13 +39,18 @@ code configuration::initialize() NOEXCEPT { const auto silent = node.silent_start_height != max_uint32; if (silent && !node.require_witness) - return network::error::invalid_configuration; + return error::silent_unwitnessed; const auto checkpoint = bitcoin.top_checkpoint().height(); const auto milestone = bitcoin.milestone.height(); const auto bypassed = std::max(checkpoint, milestone); if (silent && node.limited_blocks && node.silent_start_height <= bypassed) - return network::error::invalid_configuration; + return error::silent_pruned; + + const auto filter = to_bool(database.filter_bk.buckets) && + to_bool(database.filter_tx.buckets); + if (filter && node.limited_blocks) + return error::filter_pruned; // Batch tables are unused when their batching does not run. if (!node.batch_verify_()) diff --git a/test/configuration.cpp b/test/configuration.cpp index bd4134ca..886a7ef1 100644 --- a/test/configuration.cpp +++ b/test/configuration.cpp @@ -39,12 +39,12 @@ BOOST_AUTO_TEST_CASE(configuration__initialize__mainnet__success_peer_identifier BOOST_REQUIRE_EQUAL(instance.network.peer.identifier, 0xd9b4bef9_u32); } -BOOST_AUTO_TEST_CASE(configuration__initialize__silent_without_witness__invalid_configuration) +BOOST_AUTO_TEST_CASE(configuration__initialize__silent_without_witness__silent_unwitnessed) { node::configuration instance(chain::selection::mainnet); instance.node.silent_start_height = 0; instance.node.require_witness = false; - BOOST_REQUIRE_EQUAL(instance.initialize(), network::error::invalid_configuration); + BOOST_REQUIRE_EQUAL(instance.initialize(), node::error::silent_unwitnessed); } BOOST_AUTO_TEST_CASE(configuration__initialize__silent_with_witness__success) @@ -55,13 +55,13 @@ BOOST_AUTO_TEST_CASE(configuration__initialize__silent_with_witness__success) BOOST_REQUIRE(!instance.initialize()); } -BOOST_AUTO_TEST_CASE(configuration__initialize__limited_silent_at_bypassed__invalid_configuration) +BOOST_AUTO_TEST_CASE(configuration__initialize__limited_silent_at_bypassed__silent_pruned) { node::configuration instance(chain::selection::mainnet); const auto bypassed = std::max(instance.bitcoin.top_checkpoint().height(), instance.bitcoin.milestone.height()); instance.node.limited_blocks = true; instance.node.silent_start_height = possible_narrow_cast(bypassed); - BOOST_REQUIRE_EQUAL(instance.initialize(), network::error::invalid_configuration); + BOOST_REQUIRE_EQUAL(instance.initialize(), node::error::silent_pruned); } BOOST_AUTO_TEST_CASE(configuration__initialize__limited_silent_above_bypassed__success) @@ -70,6 +70,8 @@ BOOST_AUTO_TEST_CASE(configuration__initialize__limited_silent_above_bypassed__s const auto bypassed = std::max(instance.bitcoin.top_checkpoint().height(), instance.bitcoin.milestone.height()); instance.node.limited_blocks = true; instance.node.silent_start_height = possible_narrow_cast(add1(bypassed)); + instance.database.filter_bk.buckets = 0; + instance.database.filter_tx.buckets = 0; BOOST_REQUIRE(!instance.initialize()); } @@ -82,6 +84,33 @@ BOOST_AUTO_TEST_CASE(configuration__initialize__unlimited_silent_at_bypassed__su BOOST_REQUIRE(!instance.initialize()); } +BOOST_AUTO_TEST_CASE(configuration__initialize__limited_filter__filter_pruned) +{ + node::configuration instance(chain::selection::mainnet); + instance.node.limited_blocks = true; + instance.database.filter_bk.buckets = 1; + instance.database.filter_tx.buckets = 1; + BOOST_REQUIRE_EQUAL(instance.initialize(), node::error::filter_pruned); +} + +BOOST_AUTO_TEST_CASE(configuration__initialize__limited_partial_filter__success) +{ + node::configuration instance(chain::selection::mainnet); + instance.node.limited_blocks = true; + instance.database.filter_bk.buckets = 1; + instance.database.filter_tx.buckets = 0; + BOOST_REQUIRE(!instance.initialize()); +} + +BOOST_AUTO_TEST_CASE(configuration__initialize__unlimited_filter__success) +{ + node::configuration instance(chain::selection::mainnet); + instance.node.limited_blocks = false; + instance.database.filter_bk.buckets = 1; + instance.database.filter_tx.buckets = 1; + BOOST_REQUIRE(!instance.initialize()); +} + BOOST_AUTO_TEST_CASE(configuration__initialize__cpu_batching_without_gpu__tables_sized) { node::configuration instance(chain::selection::mainnet);