From 034d07b774dba9288b24a1d0bf7cde73458c890e Mon Sep 17 00:00:00 2001 From: josie Date: Mon, 1 Jun 2026 18:29:31 +0200 Subject: [PATCH 1/4] add silent payment index table add the optional silent table for indexing and storing the prevout summaries needed for scanning. the confirmed block writer aggregates these by block, but the construction is at the transaction level. this allows us to use this for unconfirmed transactions later, once its decided how they should be stored. --- include/bitcoin/database.hpp | 2 + include/bitcoin/database/tables/names.hpp | 1 + .../database/tables/optionals/silent.hpp | 131 ++++++++++++++++++ include/bitcoin/database/tables/schema.hpp | 17 +++ include/bitcoin/database/tables/table.hpp | 5 +- include/bitcoin/database/tables/tables.hpp | 1 + include/bitcoin/database/types/silent.hpp | 46 ++++++ include/bitcoin/database/types/types.hpp | 1 + 8 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 include/bitcoin/database/tables/optionals/silent.hpp create mode 100644 include/bitcoin/database/types/silent.hpp diff --git a/include/bitcoin/database.hpp b/include/bitcoin/database.hpp index c7fbe314c..aa741ab16 100644 --- a/include/bitcoin/database.hpp +++ b/include/bitcoin/database.hpp @@ -75,10 +75,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include diff --git a/include/bitcoin/database/tables/names.hpp b/include/bitcoin/database/tables/names.hpp index b5a1c018b..b10d89b0b 100644 --- a/include/bitcoin/database/tables/names.hpp +++ b/include/bitcoin/database/tables/names.hpp @@ -68,6 +68,7 @@ namespace optionals constexpr auto address = "address"; constexpr auto filter_bk = "filter_bk"; constexpr auto filter_tx = "filter_tx"; + constexpr auto silent = "silent"; } namespace locks diff --git a/include/bitcoin/database/tables/optionals/silent.hpp b/include/bitcoin/database/tables/optionals/silent.hpp new file mode 100644 index 000000000..70f254411 --- /dev/null +++ b/include/bitcoin/database/tables/optionals/silent.hpp @@ -0,0 +1,131 @@ +/** + * 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 . + */ +#ifndef LIBBITCOIN_DATABASE_TABLES_OPTIONALS_SILENT_HPP +#define LIBBITCOIN_DATABASE_TABLES_OPTIONALS_SILENT_HPP + +#include +#include +#include +#include + +namespace libbitcoin { +namespace database { +namespace table { + +/// silent is a slab of silent payment scan records indexed by block link. +struct silent + : public array_map +{ + using array_map::arraymap; + + using tx = transaction::link; + using ix = transaction::ix; + + static link serialized_size(const database::silent& value) NOEXCEPT + { + auto size = variable_size(value.records.size()); + + for (const auto& record: value.records) + { + const auto outputs = record.outputs.size(); + size += tx::size + system::ec_compressed_size + + variable_size(outputs); + size += outputs * (ix::size + system::ec_xonly_size); + } + + return system::possible_narrow_cast(size); + } + + struct get_records + : public schema::silent + { + inline link count() const NOEXCEPT + { + return serialized_size(value); + } + + inline bool from_data(reader& source) NOEXCEPT + { + value.records.resize(source.read_variable()); + + for (auto& record: value.records) + { + const auto tx_value = + source.read_little_endian(); + record.tx = tx{ tx_value }; + record.tweak_key = source.read_forward(); + + record.outputs.resize(source.read_variable()); + + for (auto& output: record.outputs) + { + output.index = source.read_little_endian(); + output.key = source.read_forward(); + } + } + + BC_ASSERT(!source || source.get_read_position() == count()); + return source; + } + + database::silent value{}; + }; + + struct put_ref + : public schema::silent + { + inline link count() const NOEXCEPT + { + return serialized_size(value); + } + + inline bool to_data(finalizer& sink) const NOEXCEPT + { + sink.write_variable(value.records.size()); + + for (const auto& record: value.records) + { + sink.write_little_endian( + record.tx.value); + sink.write_bytes(record.tweak_key); + sink.write_variable(record.outputs.size()); + + for (const auto& output: record.outputs) + { + const auto index = + system::possible_narrow_cast( + output.index); + sink.write_little_endian(index); + sink.write_bytes(output.key); + } + } + + BC_ASSERT(!sink || sink.get_write_position() == count()); + return sink; + } + + const database::silent& value; + }; +}; + +} // namespace table +} // namespace database +} // namespace libbitcoin + +#endif diff --git a/include/bitcoin/database/tables/schema.hpp b/include/bitcoin/database/tables/schema.hpp index 9775871c9..58e9d7a14 100644 --- a/include/bitcoin/database/tables/schema.hpp +++ b/include/bitcoin/database/tables/schema.hpp @@ -50,6 +50,7 @@ constexpr size_t tx = 4; // ->tx record. constexpr size_t block = 3; // ->header record. constexpr size_t tx_slab = 5; // ->validated_tx record. constexpr size_t filter_ = 5; // ->filter record. +constexpr size_t silent_ = 5; // ->silent record. constexpr size_t doubles_ = 4; // doubles bucket (no actual keys). /// Archive tables. @@ -393,6 +394,22 @@ struct filter_tx static_assert(link::size == 5u); }; +// slab arraymap +struct silent +{ + static constexpr size_t align = false; + static constexpr size_t pk = schema::silent_; + using link = linkage; + static constexpr size_t minsize = + one; + static constexpr size_t minrow = minsize; + static constexpr size_t size = max_size_t; + static inline link count() NOEXCEPT; + static_assert(minsize == 1u); + static_assert(minrow == 1u); + static_assert(link::size == 5u); +}; + } // namespace schema } // namespace database } // namespace libbitcoin diff --git a/include/bitcoin/database/tables/table.hpp b/include/bitcoin/database/tables/table.hpp index b35b37806..61957a5f6 100644 --- a/include/bitcoin/database/tables/table.hpp +++ b/include/bitcoin/database/tables/table.hpp @@ -91,7 +91,10 @@ enum class table_t filter_bk_body, filter_tx_table, filter_tx_head, - filter_tx_body + filter_tx_body, + silent_table, + silent_head, + silent_body }; } // namespace database diff --git a/include/bitcoin/database/tables/tables.hpp b/include/bitcoin/database/tables/tables.hpp index ab005a75f..b0eaca3d2 100644 --- a/include/bitcoin/database/tables/tables.hpp +++ b/include/bitcoin/database/tables/tables.hpp @@ -39,6 +39,7 @@ #include #include #include +#include #include #include diff --git a/include/bitcoin/database/types/silent.hpp b/include/bitcoin/database/types/silent.hpp new file mode 100644 index 000000000..63b2cb4be --- /dev/null +++ b/include/bitcoin/database/types/silent.hpp @@ -0,0 +1,46 @@ +/** + * 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 . + */ +#ifndef LIBBITCOIN_DATABASE_TYPES_SILENT_HPP +#define LIBBITCOIN_DATABASE_TYPES_SILENT_HPP + +#include +#include +#include + +namespace libbitcoin { +namespace database { + +using silent_output = system::wallet::silent_payment::scan_output; + +struct BCD_API silent_record +{ + table::transaction::link tx{}; + system::ec_compressed tweak_key{}; + std_vector outputs{}; +}; + +struct BCD_API silent +{ + std_vector records{}; +}; + +} // namespace database +} // namespace libbitcoin + +#endif diff --git a/include/bitcoin/database/types/types.hpp b/include/bitcoin/database/types/types.hpp index 14d6fa215..698d7733e 100644 --- a/include/bitcoin/database/types/types.hpp +++ b/include/bitcoin/database/types/types.hpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include From 5545dbd44f7113948cf4476c35b89e0fa392f6ee Mon Sep 17 00:00:00 2001 From: josie Date: Mon, 1 Jun 2026 18:29:54 +0200 Subject: [PATCH 2/4] plumb silent payment index through store add settings, store lifecycle plumbing, extent reporting, and start-height configuration for the silent payment index. start height defaults to taproot activation, but is configurable for the personal server usecase: if the server is supporting a single wallet where the client knows the wallet birthdate, they should build the index starting from the wallet birthdate, not taproot activation. --- .../bitcoin/database/impl/query/extent.ipp | 20 +++++++++- include/bitcoin/database/impl/store.ipp | 39 ++++++++++++++++++- include/bitcoin/database/query.hpp | 6 +++ include/bitcoin/database/settings.hpp | 5 +++ include/bitcoin/database/store.hpp | 8 ++++ src/settings.cpp | 7 +++- test/query/extent.cpp | 19 +++++++++ test/settings.cpp | 4 ++ 8 files changed, 104 insertions(+), 4 deletions(-) diff --git a/include/bitcoin/database/impl/query/extent.ipp b/include/bitcoin/database/impl/query/extent.ipp index b330dab21..f2b600692 100644 --- a/include/bitcoin/database/impl/query/extent.ipp +++ b/include/bitcoin/database/impl/query/extent.ipp @@ -97,7 +97,8 @@ size_t CLASS::store_body_size() const NOEXCEPT + validated_tx_body_size() + address_body_size() + filter_bk_body_size() - + filter_tx_body_size(); + + filter_tx_body_size() + + silent_body_size(); } TEMPLATE @@ -126,7 +127,8 @@ size_t CLASS::store_head_size() const NOEXCEPT + validated_tx_head_size() + address_head_size() + filter_bk_head_size() - + filter_tx_head_size(); + + filter_tx_head_size() + + silent_head_size(); } // Sizes. @@ -150,6 +152,7 @@ DEFINE_SIZES(validated_bk) DEFINE_SIZES(validated_tx) DEFINE_SIZES(filter_bk) DEFINE_SIZES(filter_tx) +DEFINE_SIZES(silent) DEFINE_SIZES(address) // Buckets (hashmap + arraymap). @@ -167,6 +170,7 @@ DEFINE_BUCKETS(validated_bk) DEFINE_BUCKETS(validated_tx) DEFINE_BUCKETS(filter_bk) DEFINE_BUCKETS(filter_tx) +DEFINE_BUCKETS(silent) DEFINE_BUCKETS(address) // Records (arrays). @@ -255,6 +259,18 @@ bool CLASS::filter_enabled() const NOEXCEPT return store_.filter_bk.enabled() && store_.filter_tx.enabled(); } +TEMPLATE +bool CLASS::silent_enabled() const NOEXCEPT +{ + return store_.silent.enabled(); +} + +TEMPLATE +size_t CLASS::silent_start_height() const NOEXCEPT +{ + return store_.silent_start_height(); +} + } // namespace database } // namespace libbitcoin diff --git a/include/bitcoin/database/impl/store.ipp b/include/bitcoin/database/impl/store.ipp index 575859e2d..a8484a41d 100644 --- a/include/bitcoin/database/impl/store.ipp +++ b/include/bitcoin/database/impl/store.ipp @@ -125,7 +125,10 @@ const std::unordered_map CLASS::tables { table_t::filter_bk_body, "filter_bk_body" }, { table_t::filter_tx_table, "filter_tx_table" }, { table_t::filter_tx_head, "filter_tx_head" }, - { table_t::filter_tx_body, "filter_tx_body" } + { table_t::filter_tx_body, "filter_tx_body" }, + { table_t::silent_table, "silent_table" }, + { table_t::silent_head, "silent_head" }, + { table_t::silent_body, "silent_body" } }; TEMPLATE @@ -216,6 +219,10 @@ CLASS::store(const settings& config) NOEXCEPT filter_tx_body_(body(config.path, schema::optionals::filter_tx), config.filter_tx_size, config.filter_tx_rate, sequential), filter_tx(filter_tx_head_, filter_tx_body_, config.filter_tx_buckets), + silent_head_(head(config.path / schema::dir::heads, schema::optionals::silent), 1, 0, random), + silent_body_(body(config.path, schema::optionals::silent), config.silent_size, config.silent_rate, sequential), + silent(silent_head_, silent_body_, config.silent_buckets), + // Locks. // ------------------------------------------------------------------------ @@ -238,6 +245,12 @@ uint8_t CLASS::interval_depth() const NOEXCEPT return system::limit(configuration_.interval_depth); } +TEMPLATE +size_t CLASS::silent_start_height() const NOEXCEPT +{ + return configuration_.silent_start_height; +} + TEMPLATE code CLASS::create(const event_handler& handler) NOEXCEPT { @@ -309,6 +322,8 @@ code CLASS::create(const event_handler& handler) NOEXCEPT create(ec, filter_bk_body_, table_t::filter_bk_body); create(ec, filter_tx_head_, table_t::filter_tx_head); create(ec, filter_tx_body_, table_t::filter_tx_body); + create(ec, silent_head_, table_t::silent_head); + create(ec, silent_body_, table_t::silent_body); const auto populate = [&handler](code& ec, auto& storage, table_t table) NOEXCEPT @@ -345,6 +360,7 @@ code CLASS::create(const event_handler& handler) NOEXCEPT populate(ec, address, table_t::address_table); populate(ec, filter_bk, table_t::filter_bk_table); populate(ec, filter_tx, table_t::filter_tx_table); + populate(ec, silent, table_t::silent_table); if (ec) { @@ -418,6 +434,7 @@ code CLASS::open(const event_handler& handler) NOEXCEPT verify(ec, address, table_t::address_table); verify(ec, filter_bk, table_t::filter_bk_table); verify(ec, filter_tx, table_t::filter_tx_table); + verify(ec, silent, table_t::silent_table); if (ec) { @@ -534,6 +551,7 @@ code CLASS::snapshot(const event_handler& handler, bool prune) NOEXCEPT flush(ec, address_body_, table_t::address_body); flush(ec, filter_bk_body_, table_t::filter_bk_body); flush(ec, filter_tx_body_, table_t::filter_tx_body); + flush(ec, silent_body_, table_t::silent_body); if (!ec) ec = backup(handler, prune); if (!prune) transactor_mutex_.unlock(); @@ -603,6 +621,8 @@ code CLASS::reload(const event_handler& handler) NOEXCEPT reload(ec, filter_bk_body_, table_t::filter_bk_body); reload(ec, filter_tx_head_, table_t::filter_tx_head); reload(ec, filter_tx_body_, table_t::filter_tx_body); + reload(ec, silent_head_, table_t::silent_head); + reload(ec, silent_body_, table_t::silent_body); transactor_mutex_.unlock(); return ec; @@ -649,6 +669,7 @@ code CLASS::close(const event_handler& handler) NOEXCEPT close(ec, address, table_t::address_table); close(ec, filter_bk, table_t::filter_bk_table); close(ec, filter_tx, table_t::filter_tx_table); + close(ec, silent, table_t::silent_table); if (!ec) ec = unload_close(handler); @@ -721,6 +742,8 @@ code CLASS::open_load(const event_handler& handler) NOEXCEPT open(ec, filter_bk_body_, table_t::filter_bk_body); open(ec, filter_tx_head_, table_t::filter_tx_head); open(ec, filter_tx_body_, table_t::filter_tx_body); + open(ec, silent_head_, table_t::silent_head); + open(ec, silent_body_, table_t::silent_body); const auto load = [&handler](code& ec, auto& storage, table_t table) NOEXCEPT { @@ -770,6 +793,8 @@ code CLASS::open_load(const event_handler& handler) NOEXCEPT load(ec, filter_bk_body_, table_t::filter_bk_body); load(ec, filter_tx_head_, table_t::filter_tx_head); load(ec, filter_tx_body_, table_t::filter_tx_body); + load(ec, silent_head_, table_t::silent_head); + load(ec, silent_body_, table_t::silent_body); // create, open, and restore each invoke open_load. const auto dirty = header_body_.size() > schema::header::minrow; @@ -829,6 +854,8 @@ code CLASS::unload_close(const event_handler& handler) NOEXCEPT unload(ec, filter_bk_body_, table_t::filter_bk_body); unload(ec, filter_tx_head_, table_t::filter_tx_head); unload(ec, filter_tx_body_, table_t::filter_tx_body); + unload(ec, silent_head_, table_t::silent_head); + unload(ec, silent_body_, table_t::silent_body); const auto close = [&handler](code& ec, auto& storage, table_t table) NOEXCEPT { @@ -878,6 +905,8 @@ code CLASS::unload_close(const event_handler& handler) NOEXCEPT close(ec, filter_bk_body_, table_t::filter_bk_body); close(ec, filter_tx_head_, table_t::filter_tx_head); close(ec, filter_tx_body_, table_t::filter_tx_body); + close(ec, silent_head_, table_t::silent_head); + close(ec, silent_body_, table_t::silent_body); return ec; } @@ -918,6 +947,7 @@ code CLASS::backup(const event_handler& handler, bool prune) NOEXCEPT backup(ec, address, table_t::address_table); backup(ec, filter_bk, table_t::filter_bk_table); backup(ec, filter_tx, table_t::filter_tx_table); + backup(ec, silent, table_t::silent_table); if (ec) return ec; @@ -983,6 +1013,7 @@ code CLASS::dump(const path& folder, auto address_buffer = address_head_.get(); auto filter_bk_buffer = filter_bk_head_.get(); auto filter_tx_buffer = filter_tx_head_.get(); + auto silent_buffer = silent_head_.get(); if (!header_buffer) return error::unloaded_file; if (!input_buffer) return error::unloaded_file; @@ -1005,6 +1036,7 @@ code CLASS::dump(const path& folder, if (!address_buffer) return error::unloaded_file; if (!filter_bk_buffer) return error::unloaded_file; if (!filter_tx_buffer) return error::unloaded_file; + if (!silent_buffer) return error::unloaded_file; code ec{ error::success }; const auto dump = [&handler, &folder](code& ec, const auto& storage, @@ -1039,6 +1071,7 @@ code CLASS::dump(const path& folder, dump(ec, address_buffer, schema::optionals::address, table_t::address_head); dump(ec, filter_bk_buffer, schema::optionals::filter_bk, table_t::filter_bk_head); dump(ec, filter_tx_buffer, schema::optionals::filter_tx, table_t::filter_tx_head); + dump(ec, silent_buffer, schema::optionals::silent, table_t::silent_head); return ec; } @@ -1132,6 +1165,7 @@ code CLASS::restore(const event_handler& handler) NOEXCEPT restore(ec, address, table_t::address_table); restore(ec, filter_bk, table_t::filter_bk_table); restore(ec, filter_tx, table_t::filter_tx_table); + restore(ec, silent, table_t::silent_table); if (ec) /* code */ unload_close(handler); @@ -1194,6 +1228,7 @@ code CLASS::get_fault() const NOEXCEPT if ((ec = address_body_.get_fault())) return ec; if ((ec = filter_bk_body_.get_fault())) return ec; if ((ec = filter_tx_body_.get_fault())) return ec; + if ((ec = silent_body_.get_fault())) return ec; return ec; } @@ -1224,6 +1259,7 @@ size_t CLASS::get_space() const NOEXCEPT space(address_body_); space(filter_bk_body_); space(filter_tx_body_); + space(silent_body_); return total; } @@ -1258,6 +1294,7 @@ void CLASS::report(const error_handler& handler) const NOEXCEPT report(address_body_, table_t::address_body); report(filter_bk_body_, table_t::filter_bk_body); report(filter_tx_body_, table_t::filter_tx_body); + report(silent_body_, table_t::silent_body); } BC_POP_WARNING() diff --git a/include/bitcoin/database/query.hpp b/include/bitcoin/database/query.hpp index a936ce35a..b67a88d00 100644 --- a/include/bitcoin/database/query.hpp +++ b/include/bitcoin/database/query.hpp @@ -124,6 +124,7 @@ class query size_t validated_tx_head_size() const NOEXCEPT; size_t filter_bk_head_size() const NOEXCEPT; size_t filter_tx_head_size() const NOEXCEPT; + size_t silent_head_size() const NOEXCEPT; size_t address_head_size() const NOEXCEPT; /// Table body logical byte sizes. @@ -145,6 +146,7 @@ class query size_t validated_tx_body_size() const NOEXCEPT; size_t filter_bk_body_size() const NOEXCEPT; size_t filter_tx_body_size() const NOEXCEPT; + size_t silent_body_size() const NOEXCEPT; size_t address_body_size() const NOEXCEPT; /// Table (head + body) logical byte sizes. @@ -166,6 +168,7 @@ class query size_t validated_tx_size() const NOEXCEPT; size_t filter_bk_size() const NOEXCEPT; size_t filter_tx_size() const NOEXCEPT; + size_t silent_size() const NOEXCEPT; size_t address_size() const NOEXCEPT; /// Buckets (hashmap + arraymap). @@ -181,6 +184,7 @@ class query size_t validated_tx_buckets() const NOEXCEPT; size_t filter_bk_buckets() const NOEXCEPT; size_t filter_tx_buckets() const NOEXCEPT; + size_t silent_buckets() const NOEXCEPT; size_t address_buckets() const NOEXCEPT; /// Records. @@ -208,6 +212,8 @@ class query /// Optional/configured table state. bool address_enabled() const NOEXCEPT; bool filter_enabled() const NOEXCEPT; + bool silent_enabled() const NOEXCEPT; + size_t silent_start_height() const NOEXCEPT; size_t interval_span() const NOEXCEPT; /// Initialization (natural-keyed). diff --git a/include/bitcoin/database/settings.hpp b/include/bitcoin/database/settings.hpp index 749c90368..e83e0c60b 100644 --- a/include/bitcoin/database/settings.hpp +++ b/include/bitcoin/database/settings.hpp @@ -126,6 +126,11 @@ struct BCD_API settings uint32_t filter_tx_buckets; uint64_t filter_tx_size; uint16_t filter_tx_rate; + + uint32_t silent_buckets; + uint64_t silent_size; + uint16_t silent_rate; + size_t silent_start_height; }; } // namespace database diff --git a/include/bitcoin/database/store.hpp b/include/bitcoin/database/store.hpp index 762ec95b9..a808adb3b 100644 --- a/include/bitcoin/database/store.hpp +++ b/include/bitcoin/database/store.hpp @@ -60,6 +60,9 @@ class store /// Depth of electrum merkle tree interval caching. uint8_t interval_depth() const NOEXCEPT; + /// First height at which the silent payment index is required. + size_t silent_start_height() const NOEXCEPT; + /// Methods. /// ----------------------------------------------------------------------- @@ -131,6 +134,7 @@ class store table::address address; table::filter_bk filter_bk; table::filter_tx filter_tx; + table::silent silent; protected: using path = std::filesystem::path; @@ -227,6 +231,10 @@ class store Storage filter_tx_head_; Storage filter_tx_body_; + // slab + Storage silent_head_; + Storage silent_body_; + /// Locks. /// ----------------------------------------------------------------------- diff --git a/src/settings.cpp b/src/settings.cpp index b87279e1e..f493b1599 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -99,7 +99,12 @@ settings::settings() NOEXCEPT filter_tx_buckets{ 128 }, filter_tx_size{ 1 }, - filter_tx_rate{ 50 } + filter_tx_rate{ 50 }, + + silent_buckets{ 128 }, + silent_size{ 1 }, + silent_rate{ 50 }, + silent_start_height{ 0 } { } diff --git a/test/query/extent.cpp b/test/query/extent.cpp index b2f81f1ea..c2088c412 100644 --- a/test/query/extent.cpp +++ b/test/query/extent.cpp @@ -57,6 +57,7 @@ BOOST_AUTO_TEST_CASE(query_extent__body_sizes__genesis__expected) BOOST_REQUIRE_EQUAL(query.validated_tx_body_size(), zero); BOOST_REQUIRE_EQUAL(query.filter_bk_body_size(), schema::filter_bk::minrow); BOOST_REQUIRE_EQUAL(query.filter_tx_body_size(), 5u); + BOOST_REQUIRE_EQUAL(query.silent_body_size(), schema::silent::minrow); BOOST_REQUIRE_EQUAL(query.address_body_size(), schema::address::minrow); } @@ -81,6 +82,7 @@ BOOST_AUTO_TEST_CASE(query_extent__buckets__genesis__expected) BOOST_REQUIRE_EQUAL(query.validated_bk_buckets(), 128u); BOOST_REQUIRE_EQUAL(query.filter_tx_buckets(), 128u); BOOST_REQUIRE_EQUAL(query.filter_bk_buckets(), 128u); + BOOST_REQUIRE_EQUAL(query.silent_buckets(), 128u); BOOST_REQUIRE_EQUAL(query.address_buckets(), 128u); } @@ -136,6 +138,7 @@ BOOST_AUTO_TEST_CASE(query_extent__optionals_enabled__default__true) BOOST_REQUIRE(query.initialize(test::genesis)); BOOST_REQUIRE(query.address_enabled()); BOOST_REQUIRE(query.filter_enabled()); + BOOST_REQUIRE(query.silent_enabled()); } BOOST_AUTO_TEST_CASE(query_extent__address_enabled__disabled__false) @@ -149,6 +152,7 @@ BOOST_AUTO_TEST_CASE(query_extent__address_enabled__disabled__false) BOOST_REQUIRE(query.initialize(test::genesis)); BOOST_REQUIRE(!query.address_enabled()); BOOST_REQUIRE(query.filter_enabled()); + BOOST_REQUIRE(query.silent_enabled()); } BOOST_AUTO_TEST_CASE(query_extent__filter_enabled__disabled__false) @@ -162,6 +166,21 @@ BOOST_AUTO_TEST_CASE(query_extent__filter_enabled__disabled__false) BOOST_REQUIRE(query.initialize(test::genesis)); BOOST_REQUIRE(query.address_enabled()); BOOST_REQUIRE(!query.filter_enabled()); + BOOST_REQUIRE(query.silent_enabled()); +} + +BOOST_AUTO_TEST_CASE(query_extent__silent_enabled__disabled__false) +{ + settings settings{}; + settings.path = TEST_DIRECTORY; + settings.silent_buckets = 0; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE(!store.create(test::events_handler)); + BOOST_REQUIRE(query.initialize(test::genesis)); + BOOST_REQUIRE(query.address_enabled()); + BOOST_REQUIRE(query.filter_enabled()); + BOOST_REQUIRE(!query.silent_enabled()); } BOOST_AUTO_TEST_SUITE_END() diff --git a/test/settings.cpp b/test/settings.cpp index 1d35a7dde..246f37683 100644 --- a/test/settings.cpp +++ b/test/settings.cpp @@ -82,6 +82,10 @@ BOOST_AUTO_TEST_CASE(settings__construct__default__expected) BOOST_REQUIRE_EQUAL(configuration.filter_tx_buckets, 128u); BOOST_REQUIRE_EQUAL(configuration.filter_tx_size, 1u); BOOST_REQUIRE_EQUAL(configuration.filter_tx_rate, 50u); + BOOST_REQUIRE_EQUAL(configuration.silent_buckets, 128u); + BOOST_REQUIRE_EQUAL(configuration.silent_size, 1u); + BOOST_REQUIRE_EQUAL(configuration.silent_rate, 50u); + BOOST_REQUIRE_EQUAL(configuration.silent_start_height, 0u); } BOOST_AUTO_TEST_SUITE_END() From 34676708cce8341fc702496aa6ccd2d20022469a Mon Sep 17 00:00:00 2001 From: josie Date: Mon, 1 Jun 2026 18:30:12 +0200 Subject: [PATCH 3/4] index silent payment scan data build and read per-block silent payment scan records from populated prevouts. --- Makefile.am | 6 +- .../libbitcoin-database-test.vcxproj | 1 + .../libbitcoin-database-test.vcxproj.filters | 3 + .../libbitcoin-database.vcxproj | 3 + .../libbitcoin-database.vcxproj.filters | 9 + .../libbitcoin-database-test.vcxproj | 1 + .../libbitcoin-database-test.vcxproj.filters | 3 + .../libbitcoin-database.vcxproj | 3 + .../libbitcoin-database.vcxproj.filters | 9 + .../query/consensus/consensus_populate.ipp | 6 +- .../database/impl/query/initialize.ipp | 1 + .../impl/query/navigate/navigate_arraymap.ipp | 7 + .../bitcoin/database/impl/query/silent.ipp | 140 +++++++ include/bitcoin/database/query.hpp | 14 + .../database/tables/optionals/silent.hpp | 5 +- include/bitcoin/database/types/silent.hpp | 7 +- test/query/silent.cpp | 359 ++++++++++++++++++ 17 files changed, 567 insertions(+), 10 deletions(-) create mode 100644 include/bitcoin/database/impl/query/silent.ipp create mode 100644 test/query/silent.cpp diff --git a/Makefile.am b/Makefile.am index 5dcaedea9..9b5d3cf0c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -101,6 +101,7 @@ test_libbitcoin_database_test_SOURCES = \ test/query/properties_block.cpp \ test/query/properties_tx.cpp \ test/query/sequences.cpp \ + test/query/silent.cpp \ test/query/sizes.cpp \ test/query/address/address_balance.cpp \ test/query/address/address_history.cpp \ @@ -217,6 +218,7 @@ include_bitcoin_database_impl_query_HEADERS = \ include/bitcoin/database/impl/query/properties_tx.ipp \ include/bitcoin/database/impl/query/query.ipp \ include/bitcoin/database/impl/query/sequences.ipp \ + include/bitcoin/database/impl/query/silent.ipp \ include/bitcoin/database/impl/query/sizes.ipp include_bitcoin_database_impl_query_addressdir = ${includedir}/bitcoin/database/impl/query/address @@ -329,7 +331,8 @@ include_bitcoin_database_tables_optionalsdir = ${includedir}/bitcoin/database/ta include_bitcoin_database_tables_optionals_HEADERS = \ include/bitcoin/database/tables/optionals/address.hpp \ include/bitcoin/database/tables/optionals/filter_bk.hpp \ - include/bitcoin/database/tables/optionals/filter_tx.hpp + include/bitcoin/database/tables/optionals/filter_tx.hpp \ + include/bitcoin/database/tables/optionals/silent.hpp include_bitcoin_database_typesdir = ${includedir}/bitcoin/database/types include_bitcoin_database_types_HEADERS = \ @@ -337,6 +340,7 @@ include_bitcoin_database_types_HEADERS = \ include/bitcoin/database/types/header_state.hpp \ include/bitcoin/database/types/history.hpp \ include/bitcoin/database/types/position.hpp \ + include/bitcoin/database/types/silent.hpp \ include/bitcoin/database/types/span.hpp \ include/bitcoin/database/types/type.hpp \ include/bitcoin/database/types/types.hpp \ diff --git a/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj b/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj index ed7e9387b..746eefaf4 100644 --- a/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj @@ -184,6 +184,7 @@ + diff --git a/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj.filters index 4aac69976..00a3d45b2 100644 --- a/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj.filters +++ b/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj.filters @@ -228,6 +228,9 @@ src\query + + src\query + src\query diff --git a/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj b/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj index 975ce9478..855332c32 100644 --- a/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj @@ -197,6 +197,7 @@ + @@ -206,6 +207,7 @@ + @@ -260,6 +262,7 @@ + diff --git a/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj.filters index f867d21d4..cbcdd4b52 100644 --- a/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj.filters +++ b/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj.filters @@ -302,6 +302,9 @@ include\bitcoin\database\tables\optionals + + include\bitcoin\database\tables\optionals + include\bitcoin\database\tables @@ -329,6 +332,9 @@ include\bitcoin\database\types + + include\bitcoin\database\types + include\bitcoin\database\types @@ -487,6 +493,9 @@ include\bitcoin\database\impl\query + + include\bitcoin\database\impl\query + include\bitcoin\database\impl\query diff --git a/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj b/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj index fb97dc000..ece03f717 100644 --- a/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj +++ b/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj @@ -184,6 +184,7 @@ + diff --git a/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj.filters b/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj.filters index 4aac69976..00a3d45b2 100644 --- a/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj.filters +++ b/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj.filters @@ -228,6 +228,9 @@ src\query + + src\query + src\query diff --git a/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj b/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj index 0642cc7e9..f1861c7d3 100644 --- a/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj +++ b/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj @@ -197,6 +197,7 @@ + @@ -206,6 +207,7 @@ + @@ -260,6 +262,7 @@ + diff --git a/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj.filters b/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj.filters index f867d21d4..cbcdd4b52 100644 --- a/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj.filters +++ b/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj.filters @@ -302,6 +302,9 @@ include\bitcoin\database\tables\optionals + + include\bitcoin\database\tables\optionals + include\bitcoin\database\tables @@ -329,6 +332,9 @@ include\bitcoin\database\types + + include\bitcoin\database\types + include\bitcoin\database\types @@ -487,6 +493,9 @@ include\bitcoin\database\impl\query + + include\bitcoin\database\impl\query + include\bitcoin\database\impl\query diff --git a/include/bitcoin/database/impl/query/consensus/consensus_populate.ipp b/include/bitcoin/database/impl/query/consensus/consensus_populate.ipp index b4220bd65..33f13bb09 100644 --- a/include/bitcoin/database/impl/query/consensus/consensus_populate.ipp +++ b/include/bitcoin/database/impl/query/consensus/consensus_populate.ipp @@ -139,10 +139,8 @@ bool CLASS::populate_with_metadata(const input& input, // populate_without_metadata // ---------------------------------------------------------------------------- -// These are used when not performing confirmation. This also implies that -// validation is not being performed, so is used for populating prevouts for -// the purpose of computing client filters in the validation stage. So these -// are not used for in consensus but are kept here for close similarity. +// These are used outside confirmation, so validation is not being performed. +// They only populate prevouts and are kept here for close similarity. TEMPLATE bool CLASS::populate_without_metadata(const block& block) const NOEXCEPT diff --git a/include/bitcoin/database/impl/query/initialize.ipp b/include/bitcoin/database/impl/query/initialize.ipp index 3ea3748d6..decd95a44 100644 --- a/include/bitcoin/database/impl/query/initialize.ipp +++ b/include/bitcoin/database/impl/query/initialize.ipp @@ -208,6 +208,7 @@ bool CLASS::initialize(const block& genesis) NOEXCEPT // Unsafe for allocation failure, but only used in store creation. return set_filter_body(link, genesis) && set_filter_head(link) + && set_silent(link, genesis) && push_candidate(link) && push_confirmed(link, true); // ======================================================================== diff --git a/include/bitcoin/database/impl/query/navigate/navigate_arraymap.ipp b/include/bitcoin/database/impl/query/navigate/navigate_arraymap.ipp index 6e1fcb1a9..f8f6eb7b8 100644 --- a/include/bitcoin/database/impl/query/navigate/navigate_arraymap.ipp +++ b/include/bitcoin/database/impl/query/navigate/navigate_arraymap.ipp @@ -48,6 +48,13 @@ constexpr size_t CLASS::to_filter_tx(const header_link& link) const NOEXCEPT return link.is_terminal() ? table::filter_tx::link::terminal : link.value; } +TEMPLATE +constexpr size_t CLASS::to_silent(const header_link& link) const NOEXCEPT +{ + static_assert(header_link::terminal <= table::silent::link::terminal); + return link.is_terminal() ? table::silent::link::terminal : link.value; +} + TEMPLATE constexpr size_t CLASS::to_prevout(const header_link& link) const NOEXCEPT { diff --git a/include/bitcoin/database/impl/query/silent.ipp b/include/bitcoin/database/impl/query/silent.ipp new file mode 100644 index 000000000..ee1c48919 --- /dev/null +++ b/include/bitcoin/database/impl/query/silent.ipp @@ -0,0 +1,140 @@ +/** + * 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 . + */ +#ifndef LIBBITCOIN_DATABASE_QUERY_SILENT_IPP +#define LIBBITCOIN_DATABASE_QUERY_SILENT_IPP + +#include +#include +#include + +namespace libbitcoin { +namespace database { + +// silent +// ---------------------------------------------------------------------------- + +namespace payment = system::wallet::silent_payment; + +static inline bool to_silent_record(silent_record& out, + const tx_link& tx, const system::chain::transaction& transaction) NOEXCEPT +{ + payment::scan_record record{}; + if (!payment::compute_scan_record(record, transaction)) + return false; + + out = { tx, record.prevouts_summary, std::move(record.outputs) }; + return true; +} + +TEMPLATE +bool CLASS::is_silent_indexed(const header_link& link) const NOEXCEPT +{ + return store_.silent.exists(to_silent(link)); +} + +TEMPLATE +bool CLASS::get_silent(silent& out, const header_link& link) const NOEXCEPT +{ + table::silent::get_records records{}; + if (!store_.silent.at(to_silent(link), records)) + return false; + + out = std::move(records.value); + return true; +} + +TEMPLATE +bool CLASS::set_silent(silent& out, const tx_link& link, + const transaction& tx) const NOEXCEPT +{ + if (link.is_terminal()) + return false; + + if (tx.is_coinbase()) + return true; + + if (!populate_without_metadata(tx)) + return false; + + silent_record record{}; + if (!to_silent_record(record, link, tx)) + return true; + + out.records.push_back(std::move(record)); + return true; +} + +// node/validator +TEMPLATE +bool CLASS::set_silent(const header_link& link, + const block& block) NOEXCEPT +{ + if (!silent_enabled()) + return true; + + const auto height = get_height(link); + if (height.is_terminal()) + return false; + + if (height.value < silent_start_height()) + return true; + + const auto txs = to_transactions(link); + const auto& transactions = *block.transactions_ptr(); + if (txs.size() != transactions.size()) + return false; + + database::silent value{}; + value.records.reserve(transactions.size()); + + auto tx = txs.begin(); + for (const auto& transaction: transactions) + if (!set_silent(value, *tx++, *transaction)) + return false; + + return set_silent(link, value); +} + +TEMPLATE +bool CLASS::set_silent(const header_link& link, + const silent& value) NOEXCEPT +{ + if (!silent_enabled()) + return true; + + const auto height = get_height(link); + if (height.is_terminal()) + return false; + + if (height.value < silent_start_height()) + return true; + + // ======================================================================== + const auto scope = store_.get_transactor(); + + // Clean single allocation failure (e.g. disk full). + const table::silent::put_ref put{ {}, value }; + return store_.silent.put(to_silent(link), put); + // ======================================================================== +} + +} // namespace database +} // namespace libbitcoin + +#endif diff --git a/include/bitcoin/database/query.hpp b/include/bitcoin/database/query.hpp index b67a88d00..3e30c7cc0 100644 --- a/include/bitcoin/database/query.hpp +++ b/include/bitcoin/database/query.hpp @@ -312,6 +312,7 @@ class query constexpr size_t to_validated_bk(const header_link& link) const NOEXCEPT; constexpr size_t to_filter_bk(const header_link& link) const NOEXCEPT; constexpr size_t to_filter_tx(const header_link& link) const NOEXCEPT; + constexpr size_t to_silent(const header_link& link) const NOEXCEPT; constexpr size_t to_prevout(const header_link& link) const NOEXCEPT; constexpr size_t to_txs(const header_link& link) const NOEXCEPT; @@ -714,6 +715,18 @@ class query bool set_filter_head(const header_link& link, const hash_digest& head, const hash_digest& hash) NOEXCEPT; + /// Silent payment scan index. + /// ----------------------------------------------------------------------- + + bool is_silent_indexed(const header_link& link) const NOEXCEPT; + bool get_silent(silent& out, const header_link& link) const NOEXCEPT; + bool set_silent(silent& out, const tx_link& link, + const transaction& tx) const NOEXCEPT; + bool set_silent(const header_link& link, const block& block) + NOEXCEPT; + bool set_silent(const header_link& link, + const silent& value) NOEXCEPT; + protected: /// Network /// ----------------------------------------------------------------------- @@ -948,6 +961,7 @@ BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) #include #include #include +#include BC_POP_WARNING() diff --git a/include/bitcoin/database/tables/optionals/silent.hpp b/include/bitcoin/database/tables/optionals/silent.hpp index 70f254411..b719bcedd 100644 --- a/include/bitcoin/database/tables/optionals/silent.hpp +++ b/include/bitcoin/database/tables/optionals/silent.hpp @@ -69,7 +69,8 @@ struct silent const auto tx_value = source.read_little_endian(); record.tx = tx{ tx_value }; - record.tweak_key = source.read_forward(); + record.prevouts_summary = + source.read_forward(); record.outputs.resize(source.read_variable()); @@ -103,7 +104,7 @@ struct silent { sink.write_little_endian( record.tx.value); - sink.write_bytes(record.tweak_key); + sink.write_bytes(record.prevouts_summary); sink.write_variable(record.outputs.size()); for (const auto& output: record.outputs) diff --git a/include/bitcoin/database/types/silent.hpp b/include/bitcoin/database/types/silent.hpp index 63b2cb4be..a3a36ac9b 100644 --- a/include/bitcoin/database/types/silent.hpp +++ b/include/bitcoin/database/types/silent.hpp @@ -26,13 +26,14 @@ namespace libbitcoin { namespace database { -using silent_output = system::wallet::silent_payment::scan_output; +using pay_witness_taproot_output = + system::wallet::silent_payment::pay_witness_taproot_output; struct BCD_API silent_record { table::transaction::link tx{}; - system::ec_compressed tweak_key{}; - std_vector outputs{}; + system::ec_compressed prevouts_summary{}; + std_vector outputs{}; }; struct BCD_API silent diff --git a/test/query/silent.cpp b/test/query/silent.cpp new file mode 100644 index 000000000..9507df9e8 --- /dev/null +++ b/test/query/silent.cpp @@ -0,0 +1,359 @@ +/** + * 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 "../test.hpp" +#include "../mocks/blocks.hpp" +#include "../mocks/chunk_store.hpp" + +BOOST_FIXTURE_TEST_SUITE(query_silent_tests, test::directory_setup_fixture) + +using namespace system; + +constexpr auto expected_prevouts_summary = base16_array( + "024ac253c216532e961988e2a8ce266a447c894c781e52ef6cee902361db960004"); +constexpr auto expected_block_prevouts_summary = base16_array( + "0234312c771f033144f850d03442e69047e715bcffb27ceab043f5993d452584f7"); +constexpr auto expected_output = base16_array("3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1"); +constexpr auto second_output = base16_array("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"); + +static chain::transaction funding_transaction() +{ + const chain::script output_script + { + base16_chunk("76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac"), + false + }; + + return + { + 2u, + { chain::input{} }, + { { 0u, output_script } }, + 0u + }; +} + +static chain::transaction silent_spend_transaction( + const hash_digest& prevout_hash) +{ + const chain::script input_script + { + base16_chunk("483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5"), + false + }; + const chain::script output_script + { + base16_chunk("51203e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1"), + false + }; + + return + { + 2u, + { + { + { prevout_hash, 0u }, + input_script, + chain::witness{}, + max_uint32 + } + }, + { { 0u, output_script } }, + 0u + }; +} + +static chain::transaction empty_coinbase() +{ + return + { + 2u, + { chain::input{} }, + { { 0u, chain::script{} } }, + 0u + }; +} + +static chain::block make_block(const hash_digest& previous, + const chain::transactions& transactions, uint32_t nonce) +{ + return + { + { + 0x31323334, + previous, + hash_digest{ 0x1a }, + 0x41424344, + 0x51525354, + nonce + }, + transactions + }; +} + +BOOST_AUTO_TEST_CASE(query_silent__initialize__active_from_genesis__indexed) +{ + database::settings settings{}; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE(!store.create(test::events_handler)); + BOOST_REQUIRE(query.initialize(test::genesis)); + + const auto link = query.to_confirmed(0); + BOOST_REQUIRE(query.is_silent_indexed(link)); + + silent actual{}; + BOOST_REQUIRE(query.get_silent(actual, link)); + BOOST_REQUIRE(actual.records.empty()); +} + +BOOST_AUTO_TEST_CASE(query_silent__initialize__below_start_height__unindexed) +{ + database::settings settings{}; + settings.path = TEST_DIRECTORY; + settings.silent_start_height = 1; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE(!store.create(test::events_handler)); + BOOST_REQUIRE(query.initialize(test::genesis)); + + const auto link = query.to_confirmed(0); + BOOST_REQUIRE(!query.is_silent_indexed(link)); + + silent actual{}; + BOOST_REQUIRE(!query.get_silent(actual, link)); + BOOST_REQUIRE(query.set_silent(link, test::genesis)); + BOOST_REQUIRE(!query.is_silent_indexed(link)); +} + +BOOST_AUTO_TEST_CASE(query_silent__set_silent__disabled__unindexed) +{ + database::settings settings{}; + settings.path = TEST_DIRECTORY; + settings.silent_buckets = 0; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE(!store.create(test::events_handler)); + BOOST_REQUIRE(query.initialize(test::genesis)); + + const auto link = query.to_confirmed(0); + BOOST_REQUIRE(query.set_silent(link, test::genesis)); + BOOST_REQUIRE(!query.is_silent_indexed(link)); + + silent actual{}; + BOOST_REQUIRE(!query.get_silent(actual, link)); +} + +BOOST_AUTO_TEST_CASE(query_silent__set_silent__terminal_link__false) +{ + database::settings settings{}; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE(!store.create(test::events_handler)); + BOOST_REQUIRE(query.initialize(test::genesis)); + + silent value{}; + BOOST_REQUIRE(!query.set_silent(header_link::terminal, test::genesis)); + BOOST_REQUIRE(!query.set_silent(header_link::terminal, value)); +} + +BOOST_AUTO_TEST_CASE(query_silent__set_silent__records__round_trips) +{ + database::settings settings{}; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE(!store.create(test::events_handler)); + BOOST_REQUIRE(query.initialize(test::genesis)); + BOOST_REQUIRE(query.set(test::block1, context{ 0, 1, 0 }, false, false)); + + const auto link = query.to_header(test::block1.hash()); + BOOST_REQUIRE(!link.is_terminal()); + + const auto txs = query.to_transactions(link); + BOOST_REQUIRE_EQUAL(txs.size(), 1u); + + const auto tx = txs.front(); + const silent expected + { + { + { + tx, + expected_prevouts_summary, + { { 1u, expected_output }, { 2u, second_output } } + } + } + }; + + BOOST_REQUIRE(!query.is_silent_indexed(link)); + BOOST_REQUIRE(query.set_silent(link, expected)); + + silent actual{}; + BOOST_REQUIRE(query.get_silent(actual, link)); + BOOST_REQUIRE_EQUAL(actual.records.size(), 1u); + BOOST_REQUIRE(actual.records.front().tx == tx); + BOOST_REQUIRE_EQUAL(actual.records.front().prevouts_summary, + expected_prevouts_summary); + BOOST_REQUIRE_EQUAL(actual.records.front().outputs.size(), 2u); + BOOST_REQUIRE_EQUAL(actual.records.front().outputs[0].index, 1u); + BOOST_REQUIRE_EQUAL(actual.records.front().outputs[0].key, expected_output); + BOOST_REQUIRE_EQUAL(actual.records.front().outputs[1].index, 2u); + BOOST_REQUIRE_EQUAL(actual.records.front().outputs[1].key, second_output); +} + +BOOST_AUTO_TEST_CASE(query_silent__set_silent_tx__coinbase__no_record) +{ + database::settings settings{}; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE(!store.create(test::events_handler)); + BOOST_REQUIRE(query.initialize(test::genesis)); + + const auto link = query.to_confirmed(0); + const auto txs = query.to_transactions(link); + BOOST_REQUIRE_EQUAL(txs.size(), 1u); + + silent actual{}; + const auto& tx = *test::genesis.transactions_ptr()->front(); + BOOST_REQUIRE(query.set_silent(actual, txs.front(), tx)); + BOOST_REQUIRE(actual.records.empty()); +} + +BOOST_AUTO_TEST_CASE(query_silent__set_silent_tx__terminal_link__false) +{ + database::settings settings{}; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE(!store.create(test::events_handler)); + BOOST_REQUIRE(query.initialize(test::genesis)); + + silent actual{}; + BOOST_REQUIRE(!query.set_silent(actual, tx_link::terminal, empty_coinbase())); + BOOST_REQUIRE(actual.records.empty()); +} + +BOOST_AUTO_TEST_CASE(query_silent__set_silent_tx__populates_prevout__record) +{ + database::settings settings{}; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE(!store.create(test::events_handler)); + BOOST_REQUIRE(query.initialize(test::genesis)); + + const auto funding_tx = funding_transaction(); + const auto funding = make_block(test::genesis.hash(), { funding_tx }, 0x01); + BOOST_REQUIRE(query.set(funding, context{ 0, 1, 0 }, false, false)); + + const chain::transactions spend_txs + { + empty_coinbase(), + silent_spend_transaction(funding_tx.hash(false)) + }; + auto spend = make_block(funding.hash(), spend_txs, 0x02); + BOOST_REQUIRE(query.set(spend, context{ 0, 2, 0 }, false, false)); + + const auto link = query.to_header(spend.hash()); + const auto txs = query.to_transactions(link); + BOOST_REQUIRE_EQUAL(txs.size(), 2u); + const auto& spend_tx = *spend.transactions_ptr()->at(1); + BOOST_REQUIRE(!spend_tx.inputs_ptr()->front()->prevout); + + silent actual{}; + BOOST_REQUIRE(query.set_silent(actual, txs[1], spend_tx)); + BOOST_REQUIRE(spend_tx.inputs_ptr()->front()->prevout); + BOOST_REQUIRE_EQUAL(actual.records.size(), 1u); + BOOST_REQUIRE(actual.records.front().tx == txs[1]); + BOOST_REQUIRE_EQUAL(actual.records.front().prevouts_summary, + expected_block_prevouts_summary); + BOOST_REQUIRE_EQUAL(actual.records.front().outputs.size(), 1u); + BOOST_REQUIRE_EQUAL(actual.records.front().outputs.front().index, 0u); + BOOST_REQUIRE_EQUAL(actual.records.front().outputs.front().key, + expected_output); +} + +BOOST_AUTO_TEST_CASE(query_silent__set_silent_block__populates_prevouts__indexed) +{ + database::settings settings{}; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE(!store.create(test::events_handler)); + BOOST_REQUIRE(query.initialize(test::genesis)); + + const auto funding_tx = funding_transaction(); + const auto funding = make_block(test::genesis.hash(), { funding_tx }, 0x01); + BOOST_REQUIRE(query.set(funding, context{ 0, 1, 0 }, false, false)); + + const chain::transactions spend_txs + { + empty_coinbase(), + silent_spend_transaction(funding_tx.hash(false)) + }; + auto spend = make_block(funding.hash(), spend_txs, 0x02); + BOOST_REQUIRE(query.set(spend, context{ 0, 2, 0 }, false, false)); + + const auto link = query.to_header(spend.hash()); + const auto txs = query.to_transactions(link); + BOOST_REQUIRE_EQUAL(txs.size(), 2u); + const auto& spend_tx = *spend.transactions_ptr()->at(1); + BOOST_REQUIRE(!spend_tx.inputs_ptr()->front()->prevout); + + BOOST_REQUIRE(query.set_silent(link, spend)); + BOOST_REQUIRE(spend_tx.inputs_ptr()->front()->prevout); + + silent actual{}; + BOOST_REQUIRE(query.get_silent(actual, link)); + BOOST_REQUIRE_EQUAL(actual.records.size(), 1u); + BOOST_REQUIRE(actual.records.front().tx == txs[1]); + BOOST_REQUIRE_EQUAL(actual.records.front().prevouts_summary, + expected_block_prevouts_summary); + BOOST_REQUIRE_EQUAL(actual.records.front().outputs.size(), 1u); + BOOST_REQUIRE_EQUAL(actual.records.front().outputs.front().index, 0u); + BOOST_REQUIRE_EQUAL(actual.records.front().outputs.front().key, + expected_output); +} + +BOOST_AUTO_TEST_CASE(query_silent__set_silent_block__missing_prevout__false) +{ + database::settings settings{}; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE(!store.create(test::events_handler)); + BOOST_REQUIRE(query.initialize(test::genesis)); + + const chain::transactions spend_txs + { + empty_coinbase(), + silent_spend_transaction(hash_digest{ 0x42 }) + }; + auto spend = make_block(test::genesis.hash(), spend_txs, 0x03); + BOOST_REQUIRE(query.set(spend, context{ 0, 1, 0 }, false, false)); + + const auto link = query.to_header(spend.hash()); + BOOST_REQUIRE(!link.is_terminal()); + BOOST_REQUIRE(!query.set_silent(link, spend)); + BOOST_REQUIRE(!query.is_silent_indexed(link)); +} + +BOOST_AUTO_TEST_SUITE_END() From 629b78aab6c4fa15d8e7a634ee3b4dc7bf8d72ed Mon Sep 17 00:00:00 2001 From: josie Date: Mon, 1 Jun 2026 18:30:24 +0200 Subject: [PATCH 4/4] gate confirmation on silent payment index require configured optional indexes, including silent payment scan data, before validated candidates are returned for confirmation. --- .../impl/query/consensus/consensus_forks.ipp | 7 +- test/query/consensus/consensus_forks.cpp | 114 ++++++++++++++++++ 2 files changed, 119 insertions(+), 2 deletions(-) diff --git a/include/bitcoin/database/impl/query/consensus/consensus_forks.ipp b/include/bitcoin/database/impl/query/consensus/consensus_forks.ipp index 74b4e3b3a..c4d31dc64 100644 --- a/include/bitcoin/database/impl/query/consensus/consensus_forks.ipp +++ b/include/bitcoin/database/impl/query/consensus/consensus_forks.ipp @@ -124,8 +124,10 @@ header_states CLASS::get_validated_fork(size_t& fork_point, out.reserve(one); code ec{}; - // Disable filter constraint if filtering is disabled. + // Disable optional constraints when the indexes are disabled. const auto filter = filter_enabled(); + const auto silent = silent_enabled(); + const auto silent_start = silent_start_height(); /////////////////////////////////////////////////////////////////////////// std::shared_lock interlock{ candidate_reorganization_mutex_ }; @@ -134,7 +136,8 @@ header_states CLASS::get_validated_fork(size_t& fork_point, auto height = add1(fork_point); auto link = to_candidate(height); while (is_block_validated(ec, link, height, top_checkpoint) && - (!filter || is_filtered_body(link))) + (!filter || is_filtered_body(link)) && + (!silent || height < silent_start || is_silent_indexed(link))) { out.emplace_back(link, ec); link = to_candidate(++height); diff --git a/test/query/consensus/consensus_forks.cpp b/test/query/consensus/consensus_forks.cpp index 5e26226fb..29f1f8a9e 100644 --- a/test/query/consensus/consensus_forks.cpp +++ b/test/query/consensus/consensus_forks.cpp @@ -22,4 +22,118 @@ BOOST_FIXTURE_TEST_SUITE(query_consensus_tests, test::directory_setup_fixture) +static void set_validated_fork(test::query_t& query) +{ + BOOST_REQUIRE(query.initialize(test::genesis)); + BOOST_REQUIRE(query.set(test::block1, context{ 0, 1, 0 }, false, false)); + BOOST_REQUIRE(query.set(test::block2, context{ 0, 2, 0 }, false, false)); + + const auto link1 = query.to_header(test::block1.hash()); + const auto link2 = query.to_header(test::block2.hash()); + BOOST_REQUIRE(query.push_candidate(link1)); + BOOST_REQUIRE(query.push_candidate(link2)); + BOOST_REQUIRE(query.set_block_valid(link1)); + BOOST_REQUIRE(query.set_block_valid(link2)); +} + +static void set_optional_indexes(test::query_t& query, + const header_link& link, const system::chain::block& block) +{ + BOOST_REQUIRE(query.set_filter_body(link, block)); + BOOST_REQUIRE(query.set_filter_head(link)); + BOOST_REQUIRE(query.set_silent(link, block)); +} + +BOOST_AUTO_TEST_CASE(query_consensus__get_validated_fork__optional_unindexed__empty) +{ + settings settings{}; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE(!store.create(test::events_handler)); + set_validated_fork(query); + + size_t fork_point{}; + const auto fork = query.get_validated_fork(fork_point); + BOOST_REQUIRE_EQUAL(fork_point, 0u); + BOOST_REQUIRE(fork.empty()); +} + +BOOST_AUTO_TEST_CASE(query_consensus__get_validated_fork__optional_indexed__returns_validated) +{ + settings settings{}; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE(!store.create(test::events_handler)); + set_validated_fork(query); + + const auto link1 = query.to_header(test::block1.hash()); + const auto link2 = query.to_header(test::block2.hash()); + set_optional_indexes(query, link1, test::block1); + set_optional_indexes(query, link2, test::block2); + + size_t fork_point{}; + const auto fork = query.get_validated_fork(fork_point); + BOOST_REQUIRE_EQUAL(fork_point, 0u); + BOOST_REQUIRE_EQUAL(fork.size(), 2u); + BOOST_REQUIRE(fork[0].link == link1); + BOOST_REQUIRE(fork[1].link == link2); +} + +BOOST_AUTO_TEST_CASE(query_consensus__get_validated_fork__silent_below_start__not_required) +{ + settings settings{}; + settings.path = TEST_DIRECTORY; + settings.silent_start_height = 2; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE(!store.create(test::events_handler)); + set_validated_fork(query); + + const auto link1 = query.to_header(test::block1.hash()); + const auto link2 = query.to_header(test::block2.hash()); + BOOST_REQUIRE(query.set_filter_body(link1, test::block1)); + BOOST_REQUIRE(query.set_filter_head(link1)); + BOOST_REQUIRE(query.set_filter_body(link2, test::block2)); + BOOST_REQUIRE(query.set_filter_head(link2)); + + size_t fork_point{}; + auto fork = query.get_validated_fork(fork_point); + BOOST_REQUIRE_EQUAL(fork_point, 0u); + BOOST_REQUIRE_EQUAL(fork.size(), 1u); + BOOST_REQUIRE(fork.front().link == link1); + + BOOST_REQUIRE(query.set_silent(link2, test::block2)); + fork = query.get_validated_fork(fork_point); + BOOST_REQUIRE_EQUAL(fork.size(), 2u); + BOOST_REQUIRE(fork[0].link == link1); + BOOST_REQUIRE(fork[1].link == link2); +} + +BOOST_AUTO_TEST_CASE(query_consensus__get_validated_fork__silent_disabled__filter_indexed__returns_validated) +{ + settings settings{}; + settings.path = TEST_DIRECTORY; + settings.silent_buckets = 0; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE(!store.create(test::events_handler)); + set_validated_fork(query); + + const auto link1 = query.to_header(test::block1.hash()); + const auto link2 = query.to_header(test::block2.hash()); + BOOST_REQUIRE(query.set_filter_body(link1, test::block1)); + BOOST_REQUIRE(query.set_filter_head(link1)); + BOOST_REQUIRE(query.set_filter_body(link2, test::block2)); + BOOST_REQUIRE(query.set_filter_head(link2)); + + size_t fork_point{}; + const auto fork = query.get_validated_fork(fork_point); + BOOST_REQUIRE_EQUAL(fork_point, 0u); + BOOST_REQUIRE_EQUAL(fork.size(), 2u); + BOOST_REQUIRE(fork[0].link == link1); + BOOST_REQUIRE(fork[1].link == link2); +} + BOOST_AUTO_TEST_SUITE_END()