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.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/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/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/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/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/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..3e30c7cc0 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).
@@ -306,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;
@@ -708,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
/// -----------------------------------------------------------------------
@@ -942,6 +961,7 @@ BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT)
#include
#include
#include
+#include
BC_POP_WARNING()
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/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..b719bcedd
--- /dev/null
+++ b/include/bitcoin/database/tables/optionals/silent.hpp
@@ -0,0 +1,132 @@
+/**
+ * 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.prevouts_summary =
+ 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.prevouts_summary);
+ 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..a3a36ac9b
--- /dev/null
+++ b/include/bitcoin/database/types/silent.hpp
@@ -0,0 +1,47 @@
+/**
+ * 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 pay_witness_taproot_output =
+ system::wallet::silent_payment::pay_witness_taproot_output;
+
+struct BCD_API silent_record
+{
+ table::transaction::link tx{};
+ system::ec_compressed prevouts_summary{};
+ 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
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/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()
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/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()
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()