From 81780df1c6e6dae78e1e55b45558d51779f2a9fd Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sat, 3 Oct 2026 17:35:14 -0400 Subject: [PATCH 1/9] Use the stored silent start height. --- include/bitcoin/node/chasers/chaser_validate.hpp | 1 - src/chasers/chaser_validate.cpp | 1 - src/chasers/chaser_validate_parallel.cpp | 4 ++-- src/configuration.cpp | 3 ++- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/include/bitcoin/node/chasers/chaser_validate.hpp b/include/bitcoin/node/chasers/chaser_validate.hpp index eb595996..cbb8503c 100644 --- a/include/bitcoin/node/chasers/chaser_validate.hpp +++ b/include/bitcoin/node/chasers/chaser_validate.hpp @@ -154,7 +154,6 @@ class BCN_API chaser_validate // These are thread safe. const uint32_t subsidy_interval_; const uint64_t initial_subsidy_; - const size_t silent_start_height_; const size_t maximum_backlog_; const size_t maximum_height_; const uint64_t batch_target_; diff --git a/src/chasers/chaser_validate.cpp b/src/chasers/chaser_validate.cpp index 7602b4a2..3f123cde 100644 --- a/src/chasers/chaser_validate.cpp +++ b/src/chasers/chaser_validate.cpp @@ -41,7 +41,6 @@ chaser_validate::chaser_validate(full_node& node) NOEXCEPT validation_strand_(validation_threadpool_.service().get_executor()), subsidy_interval_(node.system_settings().subsidy_interval_blocks), initial_subsidy_(node.system_settings().initial_subsidy()), - silent_start_height_(node.node_settings().silent_start_height), maximum_backlog_(node.node_settings().maximum_concurrency_()), maximum_height_(node.node_settings().maximum_height_()), batch_target_(node.node_settings().batch_signatures), diff --git a/src/chasers/chaser_validate_parallel.cpp b/src/chasers/chaser_validate_parallel.cpp index 50ce45e9..b70ed1e8 100644 --- a/src/chasers/chaser_validate_parallel.cpp +++ b/src/chasers/chaser_validate_parallel.cpp @@ -144,7 +144,7 @@ code chaser_validate::complete_pooled(const header_link& link, const chain::context& ctx) NOEXCEPT { auto& query = archive(); - if (filter_ || (ctx.height >= silent_start_height_)) + if (filter_ || (ctx.height >= query.silent_start_height())) { bool batched{}, capturing{}; constexpr auto bypass = true; @@ -210,7 +210,7 @@ code chaser_validate::validate(bool& batched, bool& capturing, bool bypass, if (!query.set_filter_body(link, block)) return error::validate8; - if ((ctx.height >= silent_start_height_) && + if ((ctx.height >= query.silent_start_height()) && !query.set_silent(link, block)) return error::validate9; diff --git a/src/configuration.cpp b/src/configuration.cpp index 99aefd4a..282eb859 100644 --- a/src/configuration.cpp +++ b/src/configuration.cpp @@ -35,7 +35,8 @@ configuration::configuration(system::chain::selection context) NOEXCEPT code configuration::initialize() NOEXCEPT { - database.initialize(bitcoin, node.limited_blocks); + database.initialize(bitcoin, node.limited_blocks, + node.silent_start_height); return network.initialize(); } From 29c4df64951505637b1109acdf6cf040e93091ff Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sat, 3 Oct 2026 16:14:04 -0400 Subject: [PATCH 2/9] Index pooled transactions for silent payments. --- include/bitcoin/node/error.hpp | 1 + src/chasers/chaser_transaction.cpp | 6 ++++++ src/error.cpp | 1 + 3 files changed, 8 insertions(+) diff --git a/include/bitcoin/node/error.hpp b/include/bitcoin/node/error.hpp index d4e5b508..09cbdd9d 100644 --- a/include/bitcoin/node/error.hpp +++ b/include/bitcoin/node/error.hpp @@ -120,6 +120,7 @@ enum error_t : uint8_t transaction3, transaction4, transaction5, + transaction6, estimates_initialize, estimates_push1, estimates_push2, diff --git a/src/chasers/chaser_transaction.cpp b/src/chasers/chaser_transaction.cpp index 071b7496..7192ff54 100644 --- a/src/chasers/chaser_transaction.cpp +++ b/src/chasers/chaser_transaction.cpp @@ -191,6 +191,12 @@ void chaser_transaction::do_submit(const transactions_cptr& txs, bool test, return; } + if (!pooled && query.silent_enabled() && !query.set_silent(link, tx)) + { + handler(fault(error::transaction6), index); + return; + } + fire(events::tx_archived, to_rate(tx)); notify(error::success, chases::transaction{ link }); } diff --git a/src/error.cpp b/src/error.cpp index 6ebcb224..87de915f 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -110,6 +110,7 @@ DEFINE_ERROR_T_MESSAGE_MAP(error) { transaction3, "transaction3" }, { transaction4, "transaction4" }, { transaction5, "transaction5" }, + { transaction6, "transaction6" }, { estimates_initialize, "estimates_initialize" }, { estimates_push1, "estimates_push1" }, { estimates_push2, "estimates_push2" }, From c1d9b6148d60a8f6302978406527f4c91d06ace5 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sat, 3 Oct 2026 17:11:19 -0400 Subject: [PATCH 3/9] Notify transactions archived for compact blocks. --- src/chasers/chaser_transaction.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/chasers/chaser_transaction.cpp b/src/chasers/chaser_transaction.cpp index 7192ff54..0ec51cd2 100644 --- a/src/chasers/chaser_transaction.cpp +++ b/src/chasers/chaser_transaction.cpp @@ -310,6 +310,12 @@ code chaser_transaction::fill(database::tx_link& out, if (!pooled && !tx.is_coinbase() && !query.set_pooled(out, tx, ctx)) return fault(error::transaction2); + if (!tx.is_coinbase()) + { + fire(events::tx_archived, to_rate(tx)); + notify(error::success, chases::transaction{ out }); + } + return error::success; } From 7657e6943f3be49fae6954ec40e3ae7f605528f9 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sat, 3 Oct 2026 18:40:42 -0400 Subject: [PATCH 4/9] Index compact block transactions when archived. --- src/chasers/chaser_transaction.cpp | 34 +++++++++++++++++++----------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/chasers/chaser_transaction.cpp b/src/chasers/chaser_transaction.cpp index 0ec51cd2..9c50886d 100644 --- a/src/chasers/chaser_transaction.cpp +++ b/src/chasers/chaser_transaction.cpp @@ -239,28 +239,34 @@ void chaser_transaction::do_submit_compact(const transactions_cptr& txs, return; } - // Txs are an arbitrary subset of the block, so block checks are deferred to - // validation. Each is archived before the next, so in block order a parent - // is resolved from the store. - size_t index{}; - auto filled = links; - for (auto& fk: filled) + const auto filler = [&](auto& fk, size_t index) NOEXCEPT { - if (fk != terminal) - continue; - database::tx_link out{}; if (const auto ec = fill(out, *txs->at(index), ctx)) { const auto faulted = node::error::error_category::contains(ec) || database::error::error_category::contains(ec); handler(faulted ? ec : unconfirmable(link, ctx.height, ec), index); - return; + return false; } fk = out; - ++index; - } + return true; + }; + + // Txs are an arbitrary subset of the block, so block checks are deferred to + // validation. Each is archived before the next, so in block order a parent + // is resolved from the store. The coinbase (never a parent within its + // block) is archived last, as validation indexes only txs linked above it. + auto filled = links; + const auto deferred = filled.front() == terminal; + auto index = deferred ? one : zero; + for (auto fk = std::next(filled.begin()); fk != filled.end(); ++fk) + if (*fk == terminal && !filler(*fk, index++)) + return; + + if (deferred && !filler(filled.front(), zero)) + return; const auto strong = is_under_checkpoint(ctx.height); if (const auto ec = query.set_code(link, filled, strong)) @@ -310,6 +316,10 @@ code chaser_transaction::fill(database::tx_link& out, if (!pooled && !tx.is_coinbase() && !query.set_pooled(out, tx, ctx)) return fault(error::transaction2); + if (!pooled && !tx.is_coinbase() && query.silent_enabled() && + !query.set_silent(out, tx)) + return fault(error::transaction6); + if (!tx.is_coinbase()) { fire(events::tx_archived, to_rate(tx)); From 03900adf879c7cdbf6a58b334c3795ee38a7e00b Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sun, 4 Oct 2026 13:24:48 -0400 Subject: [PATCH 5/9] Style silent payment sources. --- src/chasers/chaser_transaction.cpp | 48 +++++++++++++++++++----------- src/configuration.cpp | 3 +- 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/src/chasers/chaser_transaction.cpp b/src/chasers/chaser_transaction.cpp index 9c50886d..2f5d280a 100644 --- a/src/chasers/chaser_transaction.cpp +++ b/src/chasers/chaser_transaction.cpp @@ -185,16 +185,19 @@ void chaser_transaction::do_submit(const transactions_cptr& txs, bool test, return; } - if (!pooled && !query.set_pooled(link, tx, pool_)) + if (!pooled) { - handler(fault(error::transaction2), index); - return; - } - - if (!pooled && query.silent_enabled() && !query.set_silent(link, tx)) - { - handler(fault(error::transaction6), index); - return; + if (!query.set_pooled(link, tx, pool_)) + { + handler(fault(error::transaction2), index); + return; + } + + if (query.silent_enabled() && !query.set_silent(link, tx)) + { + handler(fault(error::transaction6), index); + return; + } } fire(events::tx_archived, to_rate(tx)); @@ -239,7 +242,8 @@ void chaser_transaction::do_submit_compact(const transactions_cptr& txs, return; } - const auto filler = [&](auto& fk, size_t index) NOEXCEPT + using link_t = database::tx_link::integer; + const auto filler = [&](link_t& fk, size_t index) NOEXCEPT { database::tx_link out{}; if (const auto ec = fill(out, *txs->at(index), ctx)) @@ -262,9 +266,16 @@ void chaser_transaction::do_submit_compact(const transactions_cptr& txs, const auto deferred = filled.front() == terminal; auto index = deferred ? one : zero; for (auto fk = std::next(filled.begin()); fk != filled.end(); ++fk) - if (*fk == terminal && !filler(*fk, index++)) + { + if (*fk != terminal) + continue; + + if (!filler(*fk, index)) return; + ++index; + } + if (deferred && !filler(filled.front(), zero)) return; @@ -313,14 +324,17 @@ code chaser_transaction::fill(database::tx_link& out, if (const auto ec = query.set_code(out, pooled, tx)) return fault(ec); - if (!pooled && !tx.is_coinbase() && !query.set_pooled(out, tx, ctx)) - return fault(error::transaction2); + const auto coinbase = tx.is_coinbase(); + if (!pooled && !coinbase) + { + if (!query.set_pooled(out, tx, ctx)) + return fault(error::transaction2); - if (!pooled && !tx.is_coinbase() && query.silent_enabled() && - !query.set_silent(out, tx)) - return fault(error::transaction6); + if (query.silent_enabled() && !query.set_silent(out, tx)) + return fault(error::transaction6); + } - if (!tx.is_coinbase()) + if (!coinbase) { fire(events::tx_archived, to_rate(tx)); notify(error::success, chases::transaction{ out }); diff --git a/src/configuration.cpp b/src/configuration.cpp index 282eb859..a11c76fc 100644 --- a/src/configuration.cpp +++ b/src/configuration.cpp @@ -35,8 +35,7 @@ configuration::configuration(system::chain::selection context) NOEXCEPT code configuration::initialize() NOEXCEPT { - database.initialize(bitcoin, node.limited_blocks, - node.silent_start_height); + database.initialize(bitcoin, node.limited_blocks, node.silent_start_height); return network.initialize(); } From cdc1c4c1bf16083f907d9a73cf68d2b249478423 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sun, 4 Oct 2026 13:59:15 -0400 Subject: [PATCH 6/9] Index bypassed blocks at or above the silent start height. --- src/chasers/chaser_validate.cpp | 3 ++- src/chasers/chaser_validate_parallel.cpp | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/chasers/chaser_validate.cpp b/src/chasers/chaser_validate.cpp index 3f123cde..53755d99 100644 --- a/src/chasers/chaser_validate.cpp +++ b/src/chasers/chaser_validate.cpp @@ -209,13 +209,14 @@ void chaser_validate::do_bumped(height_t height) NOEXCEPT const auto bypass = is_under_checkpoint(height) || query.is_milestone(link); + const auto silent = height >= query.silent_start_height(); switch (ec.value()) { case database::error::unvalidated: case database::error::unknown_state: { - if (bypass && !filter_) + if (bypass && !filter_ && !silent) { complete_block(error::success, link, height, true); } diff --git a/src/chasers/chaser_validate_parallel.cpp b/src/chasers/chaser_validate_parallel.cpp index b70ed1e8..a2314cc4 100644 --- a/src/chasers/chaser_validate_parallel.cpp +++ b/src/chasers/chaser_validate_parallel.cpp @@ -102,7 +102,7 @@ code chaser_validate::populate(bool bypass, const chain::block& block, if (bypass) { - // Populating for filters only (no validation metadata required). + // Populating for optional indexes only (no validation metadata). block.populate(ctx); if (!query.populate_without_metadata(block)) return system::error::missing_previous_output; @@ -139,7 +139,7 @@ code chaser_validate::validate_pooled(bool& pooled, const header_link& link, error::validate11 : ec; } -// A pooled block is valid, so is produced only as required for filters. +// A pooled block is valid, so is produced only as required for indexes. code chaser_validate::complete_pooled(const header_link& link, const chain::context& ctx) NOEXCEPT { From 877b3fb82b1e39fd09f016887c0ce05061d76a6e Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sun, 4 Oct 2026 13:59:30 -0400 Subject: [PATCH 7/9] Index compact fills at or above the silent start height. --- src/chasers/chaser_transaction.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/chasers/chaser_transaction.cpp b/src/chasers/chaser_transaction.cpp index 2f5d280a..c06559c1 100644 --- a/src/chasers/chaser_transaction.cpp +++ b/src/chasers/chaser_transaction.cpp @@ -330,7 +330,8 @@ code chaser_transaction::fill(database::tx_link& out, if (!query.set_pooled(out, tx, ctx)) return fault(error::transaction2); - if (query.silent_enabled() && !query.set_silent(out, tx)) + if (ctx.height >= query.silent_start_height() && + !query.set_silent(out, tx)) return fault(error::transaction6); } From e3eabbcb9bd35fe9edd34c428cc91a751134b3aa Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sun, 4 Oct 2026 13:59:30 -0400 Subject: [PATCH 8/9] Require witness data for silent payment indexing. --- src/configuration.cpp | 4 ++++ test/configuration.cpp | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/configuration.cpp b/src/configuration.cpp index a11c76fc..8072f412 100644 --- a/src/configuration.cpp +++ b/src/configuration.cpp @@ -33,8 +33,12 @@ configuration::configuration(system::chain::selection context) NOEXCEPT { } +// Silent payment indexing requires witness data. code configuration::initialize() NOEXCEPT { + if (node.silent_start_height != max_uint32 && !node.require_witness) + return network::error::invalid_configuration; + database.initialize(bitcoin, node.limited_blocks, node.silent_start_height); return network.initialize(); } diff --git a/test/configuration.cpp b/test/configuration.cpp index 35f8ec10..afc9e95b 100644 --- a/test/configuration.cpp +++ b/test/configuration.cpp @@ -39,4 +39,20 @@ 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) +{ + 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_AUTO_TEST_CASE(configuration__initialize__silent_with_witness__success) +{ + node::configuration instance(chain::selection::mainnet); + instance.node.silent_start_height = 0; + instance.node.require_witness = true; + BOOST_REQUIRE(!instance.initialize()); +} + BOOST_AUTO_TEST_SUITE_END() From fe59c0c6229190c768389d95e9f0c73c6d7a42e7 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sun, 4 Oct 2026 15:23:12 -0400 Subject: [PATCH 9/9] Fault silent payments within the pruned bypass range. --- src/configuration.cpp | 12 ++++++++++-- test/configuration.cpp | 27 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/configuration.cpp b/src/configuration.cpp index 8072f412..42871f3a 100644 --- a/src/configuration.cpp +++ b/src/configuration.cpp @@ -33,10 +33,18 @@ configuration::configuration(system::chain::selection context) NOEXCEPT { } -// Silent payment indexing requires witness data. +// Silent payment indexing requires witness data, which a pruned store does not +// archive for checkpointed or milestoned (bypassed) blocks. code configuration::initialize() NOEXCEPT { - if (node.silent_start_height != max_uint32 && !node.require_witness) + const auto silent = node.silent_start_height != max_uint32; + if (silent && !node.require_witness) + return network::error::invalid_configuration; + + 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; database.initialize(bitcoin, node.limited_blocks, node.silent_start_height); diff --git a/test/configuration.cpp b/test/configuration.cpp index afc9e95b..e83201d8 100644 --- a/test/configuration.cpp +++ b/test/configuration.cpp @@ -55,4 +55,31 @@ 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) +{ + 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_AUTO_TEST_CASE(configuration__initialize__limited_silent_above_bypassed__success) +{ + 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(add1(bypassed)); + BOOST_REQUIRE(!instance.initialize()); +} + +BOOST_AUTO_TEST_CASE(configuration__initialize__unlimited_silent_at_bypassed__success) +{ + node::configuration instance(chain::selection::mainnet); + const auto bypassed = std::max(instance.bitcoin.top_checkpoint().height(), instance.bitcoin.milestone.height()); + instance.node.limited_blocks = false; + instance.node.silent_start_height = possible_narrow_cast(bypassed); + BOOST_REQUIRE(!instance.initialize()); +} + BOOST_AUTO_TEST_SUITE_END()