diff --git a/Makefile.am b/Makefile.am index 5dcaedea9..8bc3904c3 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/sp_prevout_summaries.cpp \ test/query/sizes.cpp \ test/query/address/address_balance.cpp \ test/query/address/address_history.cpp \ @@ -217,7 +218,8 @@ 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/sizes.ipp + include/bitcoin/database/impl/query/sizes.ipp \ + include/bitcoin/database/impl/query/sp_prevout_summaries.ipp include_bitcoin_database_impl_query_addressdir = ${includedir}/bitcoin/database/impl/query/address include_bitcoin_database_impl_query_address_HEADERS = \ @@ -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/sp_prevout_summaries.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/sp_prevout_summaries.hpp \ include/bitcoin/database/types/span.hpp \ include/bitcoin/database/types/type.hpp \ include/bitcoin/database/types/types.hpp \ diff --git a/include/bitcoin/database.hpp b/include/bitcoin/database.hpp index c7fbe314c..c8c193ad5 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 c70a8b410..98c901ca0 100644 --- a/include/bitcoin/database/impl/query/consensus/consensus_forks.ipp +++ b/include/bitcoin/database/impl/query/consensus/consensus_forks.ipp @@ -117,7 +117,8 @@ header_links CLASS::get_confirmed_fork(const header_link& fork) const NOEXCEPT // node/confirmer TEMPLATE header_states CLASS::get_validated_fork(size_t& fork_point, - size_t top_checkpoint, bool filter) const NOEXCEPT + size_t top_checkpoint, bool filter, bool sp_prevout_summaries, + size_t sp_prevout_summaries_activation) const NOEXCEPT { // Reservation may limit allocation to most common scenario. header_states out{}; @@ -126,6 +127,7 @@ header_states CLASS::get_validated_fork(size_t& fork_point, // Disable filter constraint if filtering is disabled. filter &= filter_enabled(); + sp_prevout_summaries &= sp_prevout_summaries_enabled(); /////////////////////////////////////////////////////////////////////////// std::shared_lock interlock{ candidate_reorganization_mutex_ }; @@ -134,7 +136,9 @@ 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)) && + (!sp_prevout_summaries || height < sp_prevout_summaries_activation || + is_sp_prevout_summaries_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..ca0c3b8e3 100644 --- a/include/bitcoin/database/impl/query/consensus/consensus_populate.ipp +++ b/include/bitcoin/database/impl/query/consensus/consensus_populate.ipp @@ -141,8 +141,8 @@ bool CLASS::populate_with_metadata(const input& input, // ---------------------------------------------------------------------------- // 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. +// the purpose of computing optional block indexes. So these are not used in +// consensus but 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/consensus/consensus_strong.ipp b/include/bitcoin/database/impl/query/consensus/consensus_strong.ipp index 1c484b5c5..fc9cc8d4c 100644 --- a/include/bitcoin/database/impl/query/consensus/consensus_strong.ipp +++ b/include/bitcoin/database/impl/query/consensus/consensus_strong.ipp @@ -69,12 +69,14 @@ TEMPLATE height_link CLASS::find_strong_spender_height( const point& point) const NOEXCEPT { - size_t out{}; for (const auto& in: to_spenders(point)) + { + size_t out{}; if (const auto tx = to_input_tx(in); get_tx_height(out, tx)) - break; + return { system::possible_narrow_cast(out) }; + } - return { system::possible_narrow_cast(out) }; + return {}; } // find_strong (block) diff --git a/include/bitcoin/database/impl/query/extent.ipp b/include/bitcoin/database/impl/query/extent.ipp index b330dab21..968c63282 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() + + sp_prevout_summaries_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() + + sp_prevout_summaries_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(sp_prevout_summaries) 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(sp_prevout_summaries) 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::sp_prevout_summaries_enabled() const NOEXCEPT +{ + return store_.sp_prevout_summaries.enabled(); +} + +TEMPLATE +bool CLASS::sp_prevout_summaries_uncompressed() const NOEXCEPT +{ + return store_.sp_prevout_summaries_uncompressed(); +} + } // namespace database } // namespace libbitcoin diff --git a/include/bitcoin/database/impl/query/initialize.ipp b/include/bitcoin/database/impl/query/initialize.ipp index 3ea3748d6..93364ca3c 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_sp_prevout_summaries(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..351bc3f91 100644 --- a/include/bitcoin/database/impl/query/navigate/navigate_arraymap.ipp +++ b/include/bitcoin/database/impl/query/navigate/navigate_arraymap.ipp @@ -48,6 +48,16 @@ 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_sp_prevout_summaries(const header_link& link) const + NOEXCEPT +{ + static_assert(header_link::terminal <= + table::sp_prevout_summaries::link::terminal); + return link.is_terminal() ? table::sp_prevout_summaries::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/sp_prevout_summaries.ipp b/include/bitcoin/database/impl/query/sp_prevout_summaries.ipp new file mode 100644 index 000000000..32db9e3bc --- /dev/null +++ b/include/bitcoin/database/impl/query/sp_prevout_summaries.ipp @@ -0,0 +1,187 @@ +/** + * 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_SP_PREVOUT_SUMMARIES_IPP +#define LIBBITCOIN_DATABASE_QUERY_SP_PREVOUT_SUMMARIES_IPP + +#include +#include +#include + +namespace libbitcoin { +namespace database { + +// sp_prevout_summaries +// ---------------------------------------------------------------------------- + +TEMPLATE +bool CLASS::is_sp_prevout_summaries_indexed(const header_link& link) const + NOEXCEPT +{ + const auto uncompressed = sp_prevout_summaries_uncompressed(); + table::sp_prevout_summaries::get_format summaries{ {}, uncompressed }; + return store_.sp_prevout_summaries.at(to_sp_prevout_summaries(link), + summaries); +} + +TEMPLATE +bool CLASS::get_sp_prevout_summaries(sp_prevout_summaries& out, + const header_link& link) const NOEXCEPT +{ + const auto uncompressed = sp_prevout_summaries_uncompressed(); + table::sp_prevout_summaries::get_summaries summaries{ {}, uncompressed }; + if (!store_.sp_prevout_summaries.at(to_sp_prevout_summaries(link), + summaries)) + return false; + + out = std::move(summaries.summaries); + return true; +} + +// node/validator +TEMPLATE +bool CLASS::set_sp_prevout_summaries(const header_link& link, + const block& block) NOEXCEPT +{ + using namespace system::chain; + if (!sp_prevout_summaries_enabled()) + return true; + + const auto txs = to_transactions(link); + const auto& transactions = *block.transactions_ptr(); + if (txs.size() != transactions.size()) + return false; + + database::sp_prevout_summaries summaries{}; + summaries.format = sp_prevout_summaries_uncompressed() ? + sp_prevout_summaries::uncompressed : sp_prevout_summaries::compressed; + summaries.records.reserve(transactions.size()); + + for (size_t index{}; index < transactions.size(); ++index) + { + silent_payment::record record{}; + if (!silent_payment::compute_scan_record(record, *transactions[index])) + continue; + + sp_prevout_summary entry + { + txs[index], + record.tweak_key, + {}, + std::move(record.outputs) + }; + + if (summaries.format == sp_prevout_summaries::uncompressed && + !system::decompress(entry.tweak_point, entry.tweak_key)) + return false; + + summaries.records.push_back(std::move(entry)); + } + + return set_sp_prevout_summaries(link, summaries); +} + +TEMPLATE +bool CLASS::set_sp_prevout_summaries(const header_link& link, + const sp_prevout_summaries& summaries) NOEXCEPT +{ + if (!sp_prevout_summaries_enabled()) + return true; + + const auto uncompressed = sp_prevout_summaries_uncompressed(); + if (uncompressed && summaries.format != sp_prevout_summaries::uncompressed) + { + auto copy = summaries; + copy.format = sp_prevout_summaries::uncompressed; + for (auto& record: copy.records) + if (!system::decompress(record.tweak_point, record.tweak_key)) + return false; + + // ==================================================================== + const auto scope = store_.get_transactor(); + + return store_.sp_prevout_summaries.put(to_sp_prevout_summaries(link), + table::sp_prevout_summaries::put_ref + { + {}, + true, + copy + }); + // ==================================================================== + } + + // ======================================================================== + const auto scope = store_.get_transactor(); + + // Clean single allocation failure (e.g. disk full). + return store_.sp_prevout_summaries.put(to_sp_prevout_summaries(link), + table::sp_prevout_summaries::put_ref + { + {}, + uncompressed, + summaries + }); + // ======================================================================== +} + +TEMPLATE +bool CLASS::get_sp_prevout_summaries_tip(size_t& out) const NOEXCEPT +{ + const auto tip = sp_prevout_summaries_tip_.load(std::memory_order_relaxed); + if (tip == max_size_t) + return false; + + out = tip; + return true; +} + +TEMPLATE +void CLASS::set_sp_prevout_summaries_tip(size_t height) NOEXCEPT +{ + sp_prevout_summaries_tip_.store(height, std::memory_order_relaxed); +} + +TEMPLATE +size_t CLASS::recover_sp_prevout_summaries_tip(size_t activation) NOEXCEPT +{ + const auto top = get_top_confirmed(); + auto tip = max_size_t; + + if (sp_prevout_summaries_enabled() && top >= activation) + { + for (auto height = activation; height <= top; ++height) + { + const auto link = to_confirmed(height); + if (link.is_terminal() || !is_sp_prevout_summaries_indexed(link)) + break; + + tip = height; + } + } + + if (tip == max_size_t && !is_zero(activation)) + tip = sub1(activation); + + set_sp_prevout_summaries_tip(tip); + return tip; +} + +} // namespace database +} // namespace libbitcoin + +#endif diff --git a/include/bitcoin/database/impl/store.ipp b/include/bitcoin/database/impl/store.ipp index 575859e2d..a5115ffa4 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::sp_prevout_summaries_table, "sp_prevout_summaries_table" }, + { table_t::sp_prevout_summaries_head, "sp_prevout_summaries_head" }, + { table_t::sp_prevout_summaries_body, "sp_prevout_summaries_body" } }; TEMPLATE @@ -216,6 +219,15 @@ 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), + sp_prevout_summaries_head_(head(config.path / schema::dir::heads, + schema::optionals::sp_prevout_summaries), 1, 0, random), + sp_prevout_summaries_body_(body(config.path, + schema::optionals::sp_prevout_summaries), + config.sp_prevout_summaries_size, config.sp_prevout_summaries_rate, + sequential), + sp_prevout_summaries(sp_prevout_summaries_head_, + sp_prevout_summaries_body_, config.sp_prevout_summaries_buckets), + // Locks. // ------------------------------------------------------------------------ @@ -238,6 +250,12 @@ uint8_t CLASS::interval_depth() const NOEXCEPT return system::limit(configuration_.interval_depth); } +TEMPLATE +bool CLASS::sp_prevout_summaries_uncompressed() const NOEXCEPT +{ + return configuration_.sp_prevout_summaries_uncompressed; +} + TEMPLATE code CLASS::create(const event_handler& handler) NOEXCEPT { @@ -309,6 +327,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, sp_prevout_summaries_head_, table_t::sp_prevout_summaries_head); + create(ec, sp_prevout_summaries_body_, table_t::sp_prevout_summaries_body); const auto populate = [&handler](code& ec, auto& storage, table_t table) NOEXCEPT @@ -345,6 +365,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, sp_prevout_summaries, table_t::sp_prevout_summaries_table); if (ec) { @@ -418,6 +439,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, sp_prevout_summaries, table_t::sp_prevout_summaries_table); if (ec) { @@ -534,6 +556,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, sp_prevout_summaries_body_, table_t::sp_prevout_summaries_body); if (!ec) ec = backup(handler, prune); if (!prune) transactor_mutex_.unlock(); @@ -603,6 +626,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, sp_prevout_summaries_head_, table_t::sp_prevout_summaries_head); + reload(ec, sp_prevout_summaries_body_, table_t::sp_prevout_summaries_body); transactor_mutex_.unlock(); return ec; @@ -649,6 +674,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, sp_prevout_summaries, table_t::sp_prevout_summaries_table); if (!ec) ec = unload_close(handler); @@ -721,6 +747,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, sp_prevout_summaries_head_, table_t::sp_prevout_summaries_head); + open(ec, sp_prevout_summaries_body_, table_t::sp_prevout_summaries_body); const auto load = [&handler](code& ec, auto& storage, table_t table) NOEXCEPT { @@ -770,6 +798,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, sp_prevout_summaries_head_, table_t::sp_prevout_summaries_head); + load(ec, sp_prevout_summaries_body_, table_t::sp_prevout_summaries_body); // create, open, and restore each invoke open_load. const auto dirty = header_body_.size() > schema::header::minrow; @@ -829,6 +859,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, sp_prevout_summaries_head_, table_t::sp_prevout_summaries_head); + unload(ec, sp_prevout_summaries_body_, table_t::sp_prevout_summaries_body); const auto close = [&handler](code& ec, auto& storage, table_t table) NOEXCEPT { @@ -878,6 +910,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, sp_prevout_summaries_head_, table_t::sp_prevout_summaries_head); + close(ec, sp_prevout_summaries_body_, table_t::sp_prevout_summaries_body); return ec; } @@ -918,6 +952,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, sp_prevout_summaries, table_t::sp_prevout_summaries_table); if (ec) return ec; @@ -983,6 +1018,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 sp_prevout_summaries_buffer = sp_prevout_summaries_head_.get(); if (!header_buffer) return error::unloaded_file; if (!input_buffer) return error::unloaded_file; @@ -1005,6 +1041,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 (!sp_prevout_summaries_buffer) return error::unloaded_file; code ec{ error::success }; const auto dump = [&handler, &folder](code& ec, const auto& storage, @@ -1039,6 +1076,9 @@ 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, sp_prevout_summaries_buffer, + schema::optionals::sp_prevout_summaries, + table_t::sp_prevout_summaries_head); return ec; } @@ -1132,6 +1172,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, sp_prevout_summaries, table_t::sp_prevout_summaries_table); if (ec) /* code */ unload_close(handler); @@ -1194,6 +1235,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 = sp_prevout_summaries_body_.get_fault())) return ec; return ec; } @@ -1224,6 +1266,7 @@ size_t CLASS::get_space() const NOEXCEPT space(address_body_); space(filter_bk_body_); space(filter_tx_body_); + space(sp_prevout_summaries_body_); return total; } @@ -1258,6 +1301,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(sp_prevout_summaries_body_, table_t::sp_prevout_summaries_body); } BC_POP_WARNING() diff --git a/include/bitcoin/database/query.hpp b/include/bitcoin/database/query.hpp index 18a708b35..5c0bdec79 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 sp_prevout_summaries_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 sp_prevout_summaries_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 sp_prevout_summaries_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 sp_prevout_summaries_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 sp_prevout_summaries_enabled() const NOEXCEPT; + bool sp_prevout_summaries_uncompressed() const NOEXCEPT; size_t interval_span() const NOEXCEPT; /// Initialization (natural-keyed). @@ -306,6 +312,8 @@ 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_sp_prevout_summaries(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; @@ -613,7 +621,9 @@ class query header_links get_confirmed_fork(const header_link& fork) const NOEXCEPT; header_links get_candidate_fork(size_t& fork_point) const NOEXCEPT; header_states get_validated_fork(size_t& fork_point, - size_t top_checkpoint=zero, bool filter=false) const NOEXCEPT; + size_t top_checkpoint=zero, bool filter=false, + bool sp_prevout_summaries=false, + size_t sp_prevout_summaries_activation=zero) const NOEXCEPT; bool initialize(const block& genesis) NOEXCEPT; bool push_candidate(const header_link& link) NOEXCEPT; @@ -708,6 +718,21 @@ class query bool set_filter_head(const header_link& link, const hash_digest& head, const hash_digest& hash) NOEXCEPT; + /// Silent payment prevout summaries. + /// ----------------------------------------------------------------------- + + bool is_sp_prevout_summaries_indexed(const header_link& link) const + NOEXCEPT; + bool get_sp_prevout_summaries(sp_prevout_summaries& out, + const header_link& link) const NOEXCEPT; + bool set_sp_prevout_summaries(const header_link& link, const block& block) + NOEXCEPT; + bool set_sp_prevout_summaries(const header_link& link, + const sp_prevout_summaries& summaries) NOEXCEPT; + bool get_sp_prevout_summaries_tip(size_t& out) const NOEXCEPT; + void set_sp_prevout_summaries_tip(size_t height) NOEXCEPT; + size_t recover_sp_prevout_summaries_tip(size_t activation) NOEXCEPT; + protected: /// Network /// ----------------------------------------------------------------------- @@ -890,6 +915,7 @@ class query mutable std::shared_mutex candidate_reorganization_mutex_{}; mutable std::shared_mutex confirmed_reorganization_mutex_{}; mutable std::atomic span_{}; + mutable std::atomic sp_prevout_summaries_tip_{ max_size_t }; Store& store_; }; @@ -942,6 +968,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..1a844e769 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 sp_prevout_summaries_buckets; + uint64_t sp_prevout_summaries_size; + uint16_t sp_prevout_summaries_rate; + bool sp_prevout_summaries_uncompressed; }; } // namespace database diff --git a/include/bitcoin/database/store.hpp b/include/bitcoin/database/store.hpp index 762ec95b9..a5416c056 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; + /// True if sp_prevout_summaries stores tweak points uncompressed. + bool sp_prevout_summaries_uncompressed() const NOEXCEPT; + /// Methods. /// ----------------------------------------------------------------------- @@ -131,6 +134,7 @@ class store table::address address; table::filter_bk filter_bk; table::filter_tx filter_tx; + table::sp_prevout_summaries sp_prevout_summaries; protected: using path = std::filesystem::path; @@ -227,6 +231,10 @@ class store Storage filter_tx_head_; Storage filter_tx_body_; + // slab + Storage sp_prevout_summaries_head_; + Storage sp_prevout_summaries_body_; + /// Locks. /// ----------------------------------------------------------------------- diff --git a/include/bitcoin/database/tables/names.hpp b/include/bitcoin/database/tables/names.hpp index b5a1c018b..78dbdde84 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 sp_prevout_summaries = "sp_prevout_summaries"; } namespace locks diff --git a/include/bitcoin/database/tables/optionals/sp_prevout_summaries.hpp b/include/bitcoin/database/tables/optionals/sp_prevout_summaries.hpp new file mode 100644 index 000000000..4714b40e9 --- /dev/null +++ b/include/bitcoin/database/tables/optionals/sp_prevout_summaries.hpp @@ -0,0 +1,182 @@ +/** + * 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_SP_PREVOUT_SUMMARIES_HPP +#define LIBBITCOIN_DATABASE_TABLES_OPTIONALS_SP_PREVOUT_SUMMARIES_HPP + +#include +#include +#include +#include + +namespace libbitcoin { +namespace database { +namespace table { + +/// sp_prevout_summaries is a slab of silent payment prevout summaries indexed +/// by block link. +struct sp_prevout_summaries + : public array_map +{ + using array_map::arraymap; + + using tx = transaction::link; + using ix = transaction::ix; + + static constexpr uint8_t to_format(bool uncompressed) NOEXCEPT + { + return uncompressed ? database::sp_prevout_summaries::uncompressed : + database::sp_prevout_summaries::compressed; + } + + static constexpr size_t key_size(bool uncompressed) NOEXCEPT + { + return uncompressed ? system::ec_uncompressed_size : + system::ec_compressed_size; + } + + static link serialized_size( + const database::sp_prevout_summaries& summaries, + bool uncompressed) NOEXCEPT + { + size_t size = sizeof(summaries.format) + variable_size( + summaries.records.size()); + + for (const auto& record: summaries.records) + { + size += tx::size + key_size(uncompressed) + + variable_size(record.outputs.size()); + size += record.outputs.size() * + (ix::size + system::ec_xonly_size); + } + + return system::possible_narrow_cast(size); + } + + struct get_summaries + : public schema::sp_prevout_summaries + { + inline link count() const NOEXCEPT + { + return serialized_size(summaries, uncompressed); + } + + inline bool from_data(reader& source) NOEXCEPT + { + summaries.format = source.read_byte(); + if (summaries.format != to_format(uncompressed)) + return false; + + summaries.records.resize(source.read_variable()); + + for (auto& record: summaries.records) + { + record.tx = tx{ source.read_little_endian() }; + + if (uncompressed) + { + record.tweak_point = source.read_forward< + system::ec_uncompressed_size>(); + if (!system::compress(record.tweak_key, + record.tweak_point)) + return false; + } + else + { + record.tweak_key = source.read_forward< + system::ec_compressed_size>(); + } + + 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; + } + + const bool uncompressed{}; + database::sp_prevout_summaries summaries{}; + }; + + struct get_format + : public schema::sp_prevout_summaries + { + inline bool from_data(reader& source) NOEXCEPT + { + format = source.read_byte(); + return source && format == to_format(uncompressed); + } + + const bool uncompressed{}; + uint8_t format{}; + }; + + struct put_ref + : public schema::sp_prevout_summaries + { + inline link count() const NOEXCEPT + { + return serialized_size(summaries, uncompressed); + } + + inline bool to_data(finalizer& sink) const NOEXCEPT + { + sink.write_byte(to_format(uncompressed)); + sink.write_variable(summaries.records.size()); + + for (const auto& record: summaries.records) + { + sink.write_little_endian( + record.tx.value); + if (uncompressed) + sink.write_bytes(record.tweak_point); + else + sink.write_bytes(record.tweak_key); + sink.write_variable(record.outputs.size()); + + for (const auto& output: record.outputs) + { + sink.write_little_endian( + system::possible_narrow_cast( + output.index)); + sink.write_bytes(output.key); + } + } + + BC_ASSERT(!sink || sink.get_write_position() == count()); + return sink; + } + + const bool uncompressed{}; + const database::sp_prevout_summaries& summaries; + }; +}; + +} // 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..5ed508fe8 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 sp_prevout_summaries_ = 5; // ->sp_prevout_summaries 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 sp_prevout_summaries +{ + static constexpr size_t align = false; + static constexpr size_t pk = schema::sp_prevout_summaries_; + 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..c58f4b55e 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, + sp_prevout_summaries_table, + sp_prevout_summaries_head, + sp_prevout_summaries_body }; } // namespace database diff --git a/include/bitcoin/database/tables/tables.hpp b/include/bitcoin/database/tables/tables.hpp index ab005a75f..34fa90883 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/sp_prevout_summaries.hpp b/include/bitcoin/database/types/sp_prevout_summaries.hpp new file mode 100644 index 000000000..b8f7a9cc4 --- /dev/null +++ b/include/bitcoin/database/types/sp_prevout_summaries.hpp @@ -0,0 +1,52 @@ +/** + * 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_SP_PREVOUT_SUMMARIES_HPP +#define LIBBITCOIN_DATABASE_TYPES_SP_PREVOUT_SUMMARIES_HPP + +#include +#include +#include + +namespace libbitcoin { +namespace database { + +using sp_prevout_summary_output = system::chain::silent_payment::output; + +struct BCD_API sp_prevout_summary +{ + table::transaction::link tx{}; + system::ec_compressed tweak_key{}; + system::ec_uncompressed tweak_point{}; + std_vector outputs{}; +}; + +struct BCD_API sp_prevout_summaries +{ + static constexpr uint8_t compressed = 1; + static constexpr uint8_t uncompressed = 2; + static constexpr uint8_t version = compressed; + + uint8_t format{ version }; + 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..9cf42cfb2 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..86a5ddb80 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 }, + + sp_prevout_summaries_buckets{ 128 }, + sp_prevout_summaries_size{ 1 }, + sp_prevout_summaries_rate{ 50 }, + sp_prevout_summaries_uncompressed{ false } { } diff --git a/test/query/consensus/consensus_forks.cpp b/test/query/consensus/consensus_forks.cpp index 5e26226fb..53228e42e 100644 --- a/test/query/consensus/consensus_forks.cpp +++ b/test/query/consensus/consensus_forks.cpp @@ -22,4 +22,80 @@ 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)); +} + +BOOST_AUTO_TEST_CASE(query_consensus__get_validated_fork__sp_prevout_summaries_not_requested__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); + + 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 == query.to_header(test::block1.hash())); + BOOST_REQUIRE(fork[1].link == query.to_header(test::block2.hash())); +} + +BOOST_AUTO_TEST_CASE(query_consensus__get_validated_fork__sp_prevout_summaries_unindexed__stops_at_activation) +{ + 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()); + + size_t fork_point{}; + auto fork = query.get_validated_fork(fork_point, 0, false, true, 1); + BOOST_REQUIRE_EQUAL(fork_point, 0u); + BOOST_REQUIRE(fork.empty()); + + BOOST_REQUIRE(query.set_sp_prevout_summaries(link1, test::block1)); + fork = query.get_validated_fork(fork_point, 0, false, true, 1); + BOOST_REQUIRE_EQUAL(fork.size(), 1u); + BOOST_REQUIRE(fork[0].link == link1); + + BOOST_REQUIRE(query.set_sp_prevout_summaries(link2, test::block2)); + fork = query.get_validated_fork(fork_point, 0, false, true, 1); + 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__sp_prevout_summaries_before_activation__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); + + size_t fork_point{}; + const auto fork = query.get_validated_fork(fork_point, 0, false, true, 2); + BOOST_REQUIRE_EQUAL(fork_point, 0u); + BOOST_REQUIRE_EQUAL(fork.size(), 1u); + BOOST_REQUIRE(fork.front().link == query.to_header(test::block1.hash())); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/query/consensus/consensus_populate.cpp b/test/query/consensus/consensus_populate.cpp index 5e26226fb..54e7307d3 100644 --- a/test/query/consensus/consensus_populate.cpp +++ b/test/query/consensus/consensus_populate.cpp @@ -22,4 +22,73 @@ BOOST_FIXTURE_TEST_SUITE(query_consensus_tests, test::directory_setup_fixture) +BOOST_AUTO_TEST_CASE(query_consensus__populate_with_metadata__unspent_prevout__not_double_spend) +{ + 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::block1a, context{ 0, 1, 0 }, false, false)); + BOOST_REQUIRE(query.set_strong(1)); + + const system::chain::transaction tx{ test::tx5.to_data(true), true }; + BOOST_REQUIRE(query.populate_with_metadata(tx, true)); + + const auto& metadata = tx.inputs_ptr()->front()->metadata; + BOOST_REQUIRE_EQUAL(metadata.prevout_height, 1u); + BOOST_REQUIRE_EQUAL(metadata.spender_height, max_uint32); + BOOST_REQUIRE_EQUAL(tx.confirm({ 0, 0, 0, 2, 0, 0 }), + system::error::transaction_success); +} + +BOOST_AUTO_TEST_CASE(query_consensus__populate_with_metadata__unconfirmed_spender__not_double_spend) +{ + 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::block1a, context{ 0, 1, 0 }, false, false)); + BOOST_REQUIRE(query.set_strong(1)); + BOOST_REQUIRE(query.set(test::tx5)); + + const system::chain::transaction tx{ test::tx5.to_data(true), true }; + BOOST_REQUIRE(query.populate_with_metadata(tx, true)); + + const auto& metadata = tx.inputs_ptr()->front()->metadata; + BOOST_REQUIRE_EQUAL(metadata.prevout_height, 1u); + BOOST_REQUIRE_EQUAL(metadata.spender_height, max_uint32); + BOOST_REQUIRE_EQUAL(tx.confirm({ 0, 0, 0, 2, 0, 0 }), + system::error::transaction_success); +} + +BOOST_AUTO_TEST_CASE(query_consensus__populate_with_metadata__strong_spender__double_spend) +{ + 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::block1a, context{ 0, 1, 0 }, false, false)); + BOOST_REQUIRE(query.set_strong(1)); + BOOST_REQUIRE(query.set(test::block2a, context{ 0, 2, 0 }, false, false)); + BOOST_REQUIRE(query.set_strong(2)); + + const system::chain::transaction tx{ test::tx5.to_data(true), true }; + BOOST_REQUIRE(query.populate_with_metadata(tx, true)); + + const auto& metadata = tx.inputs_ptr()->front()->metadata; + BOOST_REQUIRE_EQUAL(metadata.prevout_height, 1u); + BOOST_REQUIRE_EQUAL(metadata.spender_height, 2u); + BOOST_REQUIRE_EQUAL(tx.confirm({ 0, 0, 0, 3, 0, 0 }), + system::error::confirmed_double_spend); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/query/consensus/consensus_strong.cpp b/test/query/consensus/consensus_strong.cpp index 5e26226fb..36a8f2df7 100644 --- a/test/query/consensus/consensus_strong.cpp +++ b/test/query/consensus/consensus_strong.cpp @@ -22,4 +22,69 @@ BOOST_FIXTURE_TEST_SUITE(query_consensus_tests, test::directory_setup_fixture) +BOOST_AUTO_TEST_CASE(query_consensus__find_strong_spender_height__unspent__terminal) +{ + 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::block1a, context{ 0, 1, 0 }, false, false)); + BOOST_REQUIRE(query.set_strong(1)); + + const system::chain::point point + { + test::block1a.transactions_ptr()->front()->hash(false), 0 + }; + + BOOST_REQUIRE(query.find_strong_spender_height(point).is_terminal()); +} + +BOOST_AUTO_TEST_CASE(query_consensus__find_strong_spender_height__unconfirmed_spender__terminal) +{ + 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::block1a, context{ 0, 1, 0 }, false, false)); + BOOST_REQUIRE(query.set_strong(1)); + BOOST_REQUIRE(query.set(test::tx5)); + + const system::chain::point point + { + test::block1a.transactions_ptr()->front()->hash(false), 0 + }; + + BOOST_REQUIRE(query.find_strong_spender_height(point).is_terminal()); +} + +BOOST_AUTO_TEST_CASE(query_consensus__find_strong_spender_height__strong_spender__expected_height) +{ + 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::block1a, context{ 0, 1, 0 }, false, false)); + BOOST_REQUIRE(query.set_strong(1)); + BOOST_REQUIRE(query.set(test::block2a, context{ 0, 2, 0 }, false, false)); + BOOST_REQUIRE(query.set_strong(2)); + + const system::chain::point point + { + test::block1a.transactions_ptr()->front()->hash(false), 0 + }; + const auto height = query.find_strong_spender_height(point); + + BOOST_REQUIRE(!height.is_terminal()); + BOOST_REQUIRE_EQUAL(height.value, 2u); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/query/sp_prevout_summaries.cpp b/test/query/sp_prevout_summaries.cpp new file mode 100644 index 000000000..da3f28f77 --- /dev/null +++ b/test/query/sp_prevout_summaries.cpp @@ -0,0 +1,278 @@ +/** + * 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_sp_prevout_summaries_tests, test::directory_setup_fixture) + +using namespace system; +using namespace system::chain; + +static const std::string expected_tweak +{ + "024ac253c216532e961988e2a8ce266a447c894c781e52ef6cee902361db960004" +}; + +static const std::string expected_output +{ + "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1" +}; + +static data_chunk chunk(const std::string_view& text) +{ + data_chunk out{}; + BOOST_REQUIRE(decode_base16(out, text)); + return out; +} + +static hash_digest hash(const std::string_view& text) +{ + hash_digest out{}; + BOOST_REQUIRE(decode_hash(out, text)); + return out; +} + +template +static data_array array(const std::string_view& text) +{ + data_array out{}; + BOOST_REQUIRE(decode_base16(out, text)); + return out; +} + +static script raw_script(const std::string_view& text) +{ + return { chunk(text), false }; +} + +static output taproot_output(const std::string_view& key) +{ + return { 0u, raw_script(std::string{ "5120" }.append(key)) }; +} + +BOOST_AUTO_TEST_CASE(query_sp_prevout_summaries__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_sp_prevout_summaries_indexed(link)); + BOOST_REQUIRE_EQUAL(query.recover_sp_prevout_summaries_tip(0), 0u); + + size_t tip{}; + BOOST_REQUIRE(query.get_sp_prevout_summaries_tip(tip)); + BOOST_REQUIRE_EQUAL(tip, 0u); + + sp_prevout_summaries summaries{}; + BOOST_REQUIRE(query.get_sp_prevout_summaries(summaries, link)); + BOOST_REQUIRE(summaries.records.empty()); +} + +static transaction prevout_tx(const std::string_view& txid, + const std::string_view& output_script) +{ + transaction tx + { + 1u, + { { {}, raw_script("00"), max_uint32 } }, + { { 0u, raw_script(output_script) } }, + 0u + }; + tx.set_nominal_hash(hash(txid)); + return tx; +} + +BOOST_AUTO_TEST_CASE(query_sp_prevout_summaries__set_sp_prevout_summaries__bip352_vector__expected) +{ + 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 prevout0 = prevout_tx( + "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac"); + const auto prevout1 = prevout_tx( + "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac"); + BOOST_REQUIRE(query.set(prevout0)); + BOOST_REQUIRE(query.set(prevout1)); + + transaction vector_tx + { + 2u, + { + { + { + hash("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), + 0u + }, + raw_script( + "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e798" + "1878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871" + "d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040" + "e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5"), + {}, + max_uint32 + }, + { + { + hash("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), + 0u + }, + raw_script( + "48304602210086783ded73e961037e77d49d9deee4edc2b23136e972" + "8d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f713" + "4d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111" + "699b15d046319febe77f8de5286e9e512703cdee1bf3be3792"), + {}, + max_uint32 + } + }, + { taproot_output(expected_output) }, + 0u + }; + + const auto header = to_shared( + chain::header + { + 1u, + test::genesis.hash(), + bitcoin_hash(vector_tx.to_data(true)), + 0u, + 0u, + 0u + }); + const auto transactions = to_shared( + transaction_cptrs + { + test::genesis.transactions_ptr()->front(), + to_shared(vector_tx) + }); + const block vector_block{ header, transactions }; + BOOST_REQUIRE(query.set(vector_block, database::context{ 0, 1, 0 }, + false, false)); + const auto link = query.to_header(vector_block.hash()); + BOOST_REQUIRE(!link.is_terminal()); + + BOOST_REQUIRE(query.populate_without_metadata(vector_block)); + BOOST_REQUIRE(query.set_sp_prevout_summaries(link, vector_block)); + + sp_prevout_summaries summaries{}; + BOOST_REQUIRE(query.get_sp_prevout_summaries(summaries, link)); + BOOST_REQUIRE_EQUAL(summaries.format, sp_prevout_summaries::compressed); + BOOST_REQUIRE_EQUAL(summaries.records.size(), 1u); + + const auto tx = query.get_tx_key(summaries.records.front().tx); + BOOST_REQUIRE_EQUAL(tx, vector_tx.hash(false)); + BOOST_REQUIRE_EQUAL(summaries.records.front().tweak_key, + array(expected_tweak)); + BOOST_REQUIRE_EQUAL(summaries.records.front().outputs.size(), 1u); + BOOST_REQUIRE_EQUAL(summaries.records.front().outputs.front().index, 0u); + BOOST_REQUIRE_EQUAL(summaries.records.front().outputs.front().key, + array(expected_output)); +} + +BOOST_AUTO_TEST_CASE(query_sp_prevout_summaries__set_sp_prevout_summaries_uncompressed__expected) +{ + database::settings settings{}; + settings.path = TEST_DIRECTORY; + settings.sp_prevout_summaries_uncompressed = true; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE(!store.create(test::events_handler)); + BOOST_REQUIRE(query.initialize(test::genesis)); + + ec_compressed tweak_key{}; + ec_uncompressed tweak_point{}; + BOOST_REQUIRE(decode_base16(tweak_key, expected_tweak)); + BOOST_REQUIRE(decompress(tweak_point, tweak_key)); + + const auto link = query.to_confirmed(0); + const auto tx = query.to_transaction(link, 0); + BOOST_REQUIRE(query.set_sp_prevout_summaries(link, sp_prevout_summaries + { + sp_prevout_summaries::compressed, + { + { + tx, + tweak_key, + tweak_point, + {} + } + } + })); + + sp_prevout_summaries summaries{}; + BOOST_REQUIRE(query.get_sp_prevout_summaries(summaries, link)); + BOOST_REQUIRE_EQUAL(summaries.format, sp_prevout_summaries::uncompressed); + BOOST_REQUIRE_EQUAL(summaries.records.size(), 1u); + BOOST_REQUIRE_EQUAL(summaries.records.front().tx, tx); + BOOST_REQUIRE_EQUAL(summaries.records.front().tweak_key, tweak_key); + BOOST_REQUIRE_EQUAL(summaries.records.front().tweak_point, tweak_point); + BOOST_REQUIRE(summaries.records.front().outputs.empty()); +} + +BOOST_AUTO_TEST_CASE(query_sp_prevout_summaries__get_format__format_mismatch__false) +{ + test::chunk_storage head{}; + test::chunk_storage body{}; + table::sp_prevout_summaries table + { + head, + body, + table::sp_prevout_summaries::link{ 128 } + }; + BOOST_REQUIRE(table.create()); + + const sp_prevout_summaries summaries + { + sp_prevout_summaries::compressed, + { + { + {}, + array(expected_tweak), + {}, + {} + } + } + }; + + BOOST_REQUIRE(table.put(0, table::sp_prevout_summaries::put_ref + { + {}, + false, + summaries + })); + + table::sp_prevout_summaries::get_format compressed{ {}, false }; + BOOST_REQUIRE(table.at(0, compressed)); + + table::sp_prevout_summaries::get_format uncompressed{ {}, true }; + BOOST_REQUIRE(!table.at(0, uncompressed)); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/settings.cpp b/test/settings.cpp index 1d35a7dde..e569d1386 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.sp_prevout_summaries_buckets, 128u); + BOOST_REQUIRE_EQUAL(configuration.sp_prevout_summaries_size, 1u); + BOOST_REQUIRE_EQUAL(configuration.sp_prevout_summaries_rate, 50u); + BOOST_REQUIRE_EQUAL(configuration.sp_prevout_summaries_uncompressed, false); } BOOST_AUTO_TEST_SUITE_END()