From b51a2c942ec032113f20cf107ec1f0fb55972531 Mon Sep 17 00:00:00 2001 From: josie Date: Mon, 1 Jun 2026 15:17:01 +0200 Subject: [PATCH 1/5] fix confirmed spender height lookup return a terminal height when a prevout has no confirmed spender, instead of zero. previously, unspent outputs could get a height of zero which implies they were spent by the genesis block. more specifically, outputs that are unspent because the spending transaction was not confirmed would get marked as already spent by the genesis block when attempting to broadcast the transaction. --- .../impl/query/consensus/consensus_strong.ipp | 8 ++- test/query/consensus/consensus_populate.cpp | 69 +++++++++++++++++++ test/query/consensus/consensus_strong.cpp | 65 +++++++++++++++++ 3 files changed, 139 insertions(+), 3 deletions(-) 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/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() From 50f998f9c128f36de34a6fda02df4829a2a8517d Mon Sep 17 00:00:00 2001 From: josie Date: Mon, 1 Jun 2026 18:29:31 +0200 Subject: [PATCH 2/5] add silent payment prevout summary table define the schema for the silent payments prevout summaries index. this naming was chosen to match the naming in the libsecp module PR. the name is meant to accurately represent what is stored in the table, but i am open to changing it if others feel strongly. the table is heavily inspired by the compact filter table as these two tables are conceptually similar. --- include/bitcoin/database.hpp | 2 + include/bitcoin/database/tables/names.hpp | 1 + .../tables/optionals/sp_prevout_summaries.hpp | 182 ++++++++++++++++++ include/bitcoin/database/tables/schema.hpp | 17 ++ include/bitcoin/database/tables/table.hpp | 5 +- include/bitcoin/database/tables/tables.hpp | 1 + .../database/types/sp_prevout_summaries.hpp | 52 +++++ include/bitcoin/database/types/types.hpp | 1 + 8 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 include/bitcoin/database/tables/optionals/sp_prevout_summaries.hpp create mode 100644 include/bitcoin/database/types/sp_prevout_summaries.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/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 From 7b82ba1d3452e9d5fad1e46ac69bfca1d3839bd2 Mon Sep 17 00:00:00 2001 From: josie Date: Mon, 1 Jun 2026 18:29:54 +0200 Subject: [PATCH 3/5] plumb silent payment prevout summaries through into store add optional-table settings and store lifecycle plumbing for the silent payment prevout summary table. the uncompressed setting is stored beside the table configuration because the row format is selected when the index is built and must be consistent when rows are read. the reason for adding this is a compressed table is half the storage, but there is a query time cost of doing the decompression. if a user has ample storage and wishes to optimise for speed, they can store decompressionsed prevout summaries for a roughly ~11% speedup during scanning. --- .../bitcoin/database/impl/query/extent.ipp | 20 +++++++- include/bitcoin/database/impl/store.ipp | 46 ++++++++++++++++++- include/bitcoin/database/query.hpp | 6 +++ include/bitcoin/database/settings.hpp | 5 ++ include/bitcoin/database/store.hpp | 8 ++++ src/settings.cpp | 7 ++- test/settings.cpp | 4 ++ 7 files changed, 92 insertions(+), 4 deletions(-) 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/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..e3b5cc40a 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). 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/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/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() From 55b595b73fd91ecae0844798bc4a8ca9ccc97c63 Mon Sep 17 00:00:00 2001 From: josie Date: Mon, 1 Jun 2026 18:30:12 +0200 Subject: [PATCH 4/5] index silent payment prevout summaries add query methods that build and read per-block silent payment prevout summaries. the block path mirrors compact filter construction: it uses populated prevouts, writes the optional table by block link, and indexes genesis during store initialization when the table is enabled. the query path rejects mismatched compressed/uncompressed table formats so configuration changes require an index rebuild instead of silently reading incompatible rows. --- Makefile.am | 8 +- .../query/consensus/consensus_populate.ipp | 4 +- .../database/impl/query/initialize.ipp | 1 + .../impl/query/navigate/navigate_arraymap.ipp | 10 + .../impl/query/sp_prevout_summaries.ipp | 187 ++++++++++++ include/bitcoin/database/query.hpp | 19 ++ test/query/sp_prevout_summaries.cpp | 278 ++++++++++++++++++ 7 files changed, 503 insertions(+), 4 deletions(-) create mode 100644 include/bitcoin/database/impl/query/sp_prevout_summaries.ipp create mode 100644 test/query/sp_prevout_summaries.cpp 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/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/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/query.hpp b/include/bitcoin/database/query.hpp index e3b5cc40a..16d9a179c 100644 --- a/include/bitcoin/database/query.hpp +++ b/include/bitcoin/database/query.hpp @@ -312,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; @@ -714,6 +716,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 /// ----------------------------------------------------------------------- @@ -896,6 +913,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_; }; @@ -948,6 +966,7 @@ BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) #include #include #include +#include BC_POP_WARNING() 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() From fcd10ab3913933060f3b9461e75b7b9c2f9e2869 Mon Sep 17 00:00:00 2001 From: josie Date: Mon, 1 Jun 2026 18:30:24 +0200 Subject: [PATCH 5/5] gate confirmation on silent payment prevout summaries extend validated fork selection with an optional silent-payment-index constraint. when enabled, confirmation will not advance into taproot-active candidate blocks until their prevout summaries have been written. this keeps the sp index in the same confirmation workflow as compact filters and avoids publishing confirmed blocks whose scan data is not ready. --- .../impl/query/consensus/consensus_forks.ipp | 8 +- include/bitcoin/database/query.hpp | 4 +- test/query/consensus/consensus_forks.cpp | 76 +++++++++++++++++++ 3 files changed, 85 insertions(+), 3 deletions(-) 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/query.hpp b/include/bitcoin/database/query.hpp index 16d9a179c..5c0bdec79 100644 --- a/include/bitcoin/database/query.hpp +++ b/include/bitcoin/database/query.hpp @@ -621,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; 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()