From a42882bbb115e625a56dfa94fc46abe727922432 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sat, 3 Oct 2026 14:04:28 -0400 Subject: [PATCH 1/4] Implement silent payment indexing and scan. --- .../database/impl/query/batch/silent.ipp | 63 +++++--- include/bitcoin/database/query.hpp | 8 +- test/query/batch/silent.cpp | 147 ++++++++++++++++++ 3 files changed, 191 insertions(+), 27 deletions(-) diff --git a/include/bitcoin/database/impl/query/batch/silent.ipp b/include/bitcoin/database/impl/query/batch/silent.ipp index 4cd28e520..fb8c0e11c 100644 --- a/include/bitcoin/database/impl/query/batch/silent.ipp +++ b/include/bitcoin/database/impl/query/batch/silent.ipp @@ -27,7 +27,8 @@ namespace libbitcoin { namespace database { TEMPLATE -bool CLASS::scan_silent(const stopper& cancel, const ec_secret& scan_key, +bool CLASS::scan_silent(const stopper& cancel, + const system::silent::batch::receiver& keys, const silent_handler& callback) NOEXCEPT { const auto prefix_ptr = store_.silent.prefix.get_memory(); @@ -54,13 +55,15 @@ bool CLASS::scan_silent(const stopper& cancel, const ec_secret& scan_key, // False return only implies canceled. // Callbacks invoked on caller thread if turbo is false. - silent::batch::scan(cancel, batch, scan_key, callback, store_.turbo()); + silent::batch::scan(cancel, batch, keys, callback, store_.turbo()); return !cancel; } // setters // ---------------------------------------------------------------------------- // Caller (node) controls which txs are indexed (e.g. by confirmed height). +// The coinbase is the first tx archived for a block, so txs linked below it +// were archived (and indexed) before it, as pooled or by another block. TEMPLATE bool CLASS::set_silent(const header_link& link, const block& block) NOEXCEPT @@ -74,6 +77,8 @@ bool CLASS::set_silent(const header_link& link, const block& block) NOEXCEPT if (links.size() != count) return false; + const auto first = links.front(); + stopper fail{}; std::vector it(sub1(count)); std::iota(it.begin(), it.end(), one); @@ -87,7 +92,8 @@ bool CLASS::set_silent(const header_link& link, const block& block) NOEXCEPT if (fail.load(relaxed)) return; - if (!set_silent(links.at(index), *txs->at(index))) + const auto& tx = links.at(index); + if (tx >= first && !set_silent(tx, *txs->at(index))) fail.store(true, relaxed); }); @@ -107,6 +113,8 @@ bool CLASS::set_silent(const header_link& link, if (links.size() != count) return false; + const auto first = links.front(); + stopper fail{}; std::vector it(sub1(count)); std::iota(it.begin(), it.end(), one); @@ -118,45 +126,54 @@ bool CLASS::set_silent(const header_link& link, if (fail.load(relaxed)) return; - if (!set_silent(links.at(index), txs.at(index))) + const auto& tx = links.at(index); + if (tx >= first && !set_silent(tx, txs.at(index))) fail.store(true, relaxed); }); return !fail.load(relaxed); } +// Ineligible txs have no records. TEMPLATE -bool CLASS::set_silent(const tx_link& link, - const transaction& BC_DEBUG_ONLY(tx)) NOEXCEPT +bool CLASS::set_silent(const tx_link& link, const transaction& tx) NOEXCEPT { - BC_ASSERT(!tx.is_coinbase()); - return set_silent_(link); + using namespace system::wallet; + ec_compressed summary{}; + silent_payment::scan_outputs outputs{}; + return !silent_payment::get_outputs(outputs, tx) + || !silent_payment::summarize(summary, tx) + || set_silent_(link, summary, outputs); } TEMPLATE bool CLASS::set_silent(const tx_link& link, - const transaction_view& BC_DEBUG_ONLY(tx)) NOEXCEPT + const transaction_view& tx) NOEXCEPT { - BC_ASSERT(!tx.is_coinbase()); - return set_silent_(link); + using namespace system::wallet; + ec_compressed summary{}; + silent_payment::scan_outputs outputs{}; + return !silent_payment::get_outputs(outputs, tx) + || !silent_payment::summarize(summary, tx) + || set_silent_(link, summary, outputs); } // protected TEMPLATE -bool CLASS::set_silent_(const tx_link& link) NOEXCEPT +bool CLASS::set_silent_(const tx_link& link, const ec_compressed& summary, + const system::wallet::silent_payment::scan_outputs& outputs) NOEXCEPT { if (link.is_terminal()) return false; - // Short-circuits with success on empty. - ////using namespace system::wallet; - ////silent_payment::scan_record record{}; - ////if (!silent_payment::compute_scan_record(record, tx)) - //// return true; - - // TODO: aliases for record above; - const ec_compressed key{}; - const std::vector prefixes{}; + // The prefix is ec_xonly[0..7] read as little-endian. + using namespace system; + std::vector prefixes(outputs.size()); + std::transform(outputs.cbegin(), outputs.cend(), prefixes.begin(), + [](const auto& output) NOEXCEPT + { + return unsafe_from_little_endian(output.key.data()); + }); using correlate_t = table::silent_correlate::records; using prefix_t = table::silent_prefix::put_ref; @@ -168,8 +185,6 @@ bool CLASS::set_silent_(const tx_link& link) NOEXCEPT // TODO: with additional scans as this position increases. // ======================================================================== const auto scope = get_transactor(); - - using namespace system; auto rows = possible_narrow_cast(prefixes.size()); // Allocate rows across all columns. @@ -184,7 +199,7 @@ bool CLASS::set_silent_(const tx_link& link) NOEXCEPT return store_.silent.correlate.put(fk, correlate_t{ {}, rows, link }) && store_.silent.prefix.put(fk, prefix_t{ {}, prefixes }) && - store_.silent.compressed.put(fk, compressed_t{ {}, rows, key }); + store_.silent.compressed.put(fk, compressed_t{ {}, rows, summary }); // ======================================================================== } diff --git a/include/bitcoin/database/query.hpp b/include/bitcoin/database/query.hpp index 3a7ca9e92..608e00fb6 100644 --- a/include/bitcoin/database/query.hpp +++ b/include/bitcoin/database/query.hpp @@ -671,8 +671,9 @@ class query bool set_signatures(const system::chain::schnorr_signatures& sigs, const header_link& link, bool bank) NOEXCEPT; - /// Invoke callback for each candidate match, false implies cancel. - bool scan_silent(const stopper& cancel, const ec_secret& scan_key, + /// Invoke callback for each matched tx, false implies cancel. + bool scan_silent(const stopper& cancel, + const system::silent::batch::receiver& keys, const silent_handler& callback) NOEXCEPT; /// Verify all signatures in table, false implies cancel. @@ -1039,7 +1040,8 @@ class query size_t heavy, hash_option&& interval, bool strong) NOEXCEPT; /// Set silent payment records of a non-coinbase tx. - bool set_silent_(const tx_link& link) NOEXCEPT; + bool set_silent_(const tx_link& link, const ec_compressed& summary, + const system::wallet::silent_payment::scan_outputs& outputs) NOEXCEPT; /// Pooling. /// ----------------------------------------------------------------------- diff --git a/test/query/batch/silent.cpp b/test/query/batch/silent.cpp index 97c555f28..223c8e4c9 100644 --- a/test/query/batch/silent.cpp +++ b/test/query/batch/silent.cpp @@ -22,6 +22,72 @@ BOOST_FIXTURE_TEST_SUITE(query_batch_silent_tests, test::directory_setup_fixture) +using silent_payment = system::wallet::silent_payment; +using receiver = system::silent::batch::receiver; + +constexpr system::ec_compressed summary = system::base16_array +( + "024ac253c216532e961988e2a8ce266a447c894c781e52ef6cee902361db960004" +); + +static system::chain::input to_input(const system::hash_digest& hash, + const system::data_chunk& script, + const system::data_chunk& prevout) NOEXCEPT +{ + namespace chain = system::chain; + chain::input in{ { hash, 0 }, { script, false }, {}, max_uint32 }; + in.prevout = system::to_shared(0u, + chain::script{ prevout, false }); + return in; +} + +// BIP352 send_and_receive_test_vectors.json: "Simple send: two inputs". +static system::chain::transaction simple_send() NOEXCEPT +{ + return + { + 2u, + system::chain::inputs + { + to_input( + system::base16_hash("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), + system::base16_chunk("483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5"), + system::base16_chunk("76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac")), + to_input( + system::base16_hash("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), + system::base16_chunk("48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792"), + system::base16_chunk("76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac")) + }, + system::chain::outputs + { + system::chain::output + { + 0u, + system::chain::script + { + system::base16_chunk("51203e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1"), + false + } + } + }, + 0u + }; +} + +static receiver get_keys() NOEXCEPT +{ + constexpr system::ec_secret scan = system::base16_array( + "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c"); + constexpr system::ec_secret spend = system::base16_array( + "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3"); + + system::ec_compressed point{}; + if (!system::secret_to_public(point, spend)) + return {}; + + return silent_payment{ scan, point, {} }.keys(); +} + // set_silent // ---------------------------------------------------------------------------- @@ -47,4 +113,85 @@ BOOST_AUTO_TEST_CASE(query_batch_silent__set_silent__unarchived_block__false) BOOST_REQUIRE(!query.set_silent(1, test::block_coinbase_spend_1a)); } +BOOST_AUTO_TEST_CASE(query_batch_silent__set_silent__no_taproot_output__no_records) +{ + 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_silent(1, *test::block1.transactions_ptr()->front())); + BOOST_REQUIRE_EQUAL(query.silent_records(), 0u); +} + +BOOST_AUTO_TEST_CASE(query_batch_silent__set_silent__eligible__one_record) +{ + 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_silent(42, simple_send())); + BOOST_REQUIRE_EQUAL(query.silent_records(), 1u); +} + +// scan_silent +// ---------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(query_batch_silent__scan_silent__match__expected) +{ + 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_silent(42, simple_send())); + + size_t calls{}; + uint32_t link{}; + system::ec_compressed point{}; + const stopper cancel{}; + BOOST_REQUIRE(query.scan_silent(cancel, get_keys(), + [&](const code& ec, uint32_t tx, const system::ec_compressed& tweak) NOEXCEPT + { + BOOST_REQUIRE(!ec); + link = tx; + point = tweak; + ++calls; + })); + + BOOST_REQUIRE_EQUAL(calls, 1u); + BOOST_REQUIRE_EQUAL(link, 42u); + BOOST_REQUIRE_EQUAL(point, summary); +} + +BOOST_AUTO_TEST_CASE(query_batch_silent__scan_silent__no_match__none) +{ + 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_silent(42, simple_send())); + + size_t calls{}; + const stopper cancel{}; + const receiver keys + { + .scan = system::base16_array("0000000000000000000000000000000000000000000000000000000000000001"), + .spend = get_keys().spend + }; + + BOOST_REQUIRE(query.scan_silent(cancel, keys, + [&](const code&, uint32_t, const system::ec_compressed&) NOEXCEPT + { + ++calls; + })); + BOOST_REQUIRE_EQUAL(calls, 0u); +} + BOOST_AUTO_TEST_SUITE_END() From b9b5c303c21d8f797e0c7a65fd2233a9bcf6ab28 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sat, 3 Oct 2026 14:18:04 -0400 Subject: [PATCH 2/4] Add silent range scan and written row frontier. --- .../database/impl/query/batch/silent.ipp | 64 +++++++++++++++---- include/bitcoin/database/query.hpp | 6 ++ test/query/batch/silent.cpp | 49 ++++++++++++++ 3 files changed, 105 insertions(+), 14 deletions(-) diff --git a/include/bitcoin/database/impl/query/batch/silent.ipp b/include/bitcoin/database/impl/query/batch/silent.ipp index fb8c0e11c..6f348fd0a 100644 --- a/include/bitcoin/database/impl/query/batch/silent.ipp +++ b/include/bitcoin/database/impl/query/batch/silent.ipp @@ -30,6 +30,15 @@ TEMPLATE bool CLASS::scan_silent(const stopper& cancel, const system::silent::batch::receiver& keys, const silent_handler& callback) NOEXCEPT +{ + return scan_silent(cancel, keys, zero, store_.silent.count(), callback); +} + +// Rows [first, last) must begin and end on transaction boundaries. +TEMPLATE +bool CLASS::scan_silent(const stopper& cancel, + const system::silent::batch::receiver& keys, size_t first, size_t last, + const silent_handler& callback) NOEXCEPT { const auto prefix_ptr = store_.silent.prefix.get_memory(); const auto compressed_ptr = store_.silent.compressed.get_memory(); @@ -44,13 +53,13 @@ bool CLASS::scan_silent(const stopper& cancel, const auto prefix = pointer_cast(prefix_ptr.data()); const auto compressed = pointer_cast(compressed_ptr.data()); - // Shortest column. - const auto count = store_.silent.count(); + BC_ASSERT(first <= last && last <= store_.silent.count()); + const auto count = last - first; const silent::batch batch { - .correlates = { correlate, count }, - .prefixes = { prefix, count }, - .points = { compressed, count } + .correlates = { std::next(correlate, first), count }, + .prefixes = { std::next(prefix, first), count }, + .points = { std::next(compressed, first), count } }; // False return only implies canceled. @@ -59,6 +68,25 @@ bool CLASS::scan_silent(const stopper& cancel, return !cancel; } +// Rows are allocated zero-filled and a nonzero correlate publishes the row. +TEMPLATE +size_t CLASS::get_silent_frontier(size_t first) const NOEXCEPT +{ + using namespace system; + const auto guard = store_.silent.guard(); + const auto words = pointer_cast(guard.data()); + const auto count = store_.silent.count(); + + for (auto row = first; row < count; ++row) + { + std::atomic_ref word{ *std::next(words, row) }; + if (is_zero(word.load(std::memory_order_acquire))) + return row; + } + + return count; +} + // setters // ---------------------------------------------------------------------------- // Caller (node) controls which txs are indexed (e.g. by confirmed height). @@ -175,14 +203,9 @@ bool CLASS::set_silent_(const tx_link& link, const ec_compressed& summary, return unsafe_from_little_endian(output.key.data()); }); - using correlate_t = table::silent_correlate::records; using prefix_t = table::silent_prefix::put_ref; using compressed_t = table::silent_compressed::put_ref; - // TODO: Caller must guard reads, this is writing into hot storage. This - // TODO: requires caller to chase writers and account for the last contig- - // TODO: uously populated row (for searching) and to update subscriptions - // TODO: with additional scans as this position increases. // ======================================================================== const auto scope = get_transactor(); auto rows = possible_narrow_cast(prefixes.size()); @@ -196,10 +219,23 @@ bool CLASS::set_silent_(const tx_link& link, const ec_compressed& summary, const auto guard = store_.silent.guard(); // Write values to each column in corresponding positions. - return - store_.silent.correlate.put(fk, correlate_t{ {}, rows, link }) && - store_.silent.prefix.put(fk, prefix_t{ {}, prefixes }) && - store_.silent.compressed.put(fk, compressed_t{ {}, rows, summary }); + if (!store_.silent.prefix.put(fk, prefix_t{ {}, prefixes }) || + !store_.silent.compressed.put(fk, compressed_t{ {}, rows, summary })) + return false; + + // The guard is the correlate column, published last (get_silent_frontier). + static_assert(schema::silent_correlate::minrow == sizeof(uint32_t)); + const auto words = pointer_cast(guard.data()); + const auto value = native_to_little_end( + possible_narrow_cast(link.value)); + + for (auto row = fk.value; row < fk.value + rows; ++row) + { + std::atomic_ref word{ *std::next(words, row) }; + word.store(value, std::memory_order_release); + } + + return true; // ======================================================================== } diff --git a/include/bitcoin/database/query.hpp b/include/bitcoin/database/query.hpp index 608e00fb6..50e4e6c8d 100644 --- a/include/bitcoin/database/query.hpp +++ b/include/bitcoin/database/query.hpp @@ -675,6 +675,12 @@ class query bool scan_silent(const stopper& cancel, const system::silent::batch::receiver& keys, const silent_handler& callback) NOEXCEPT; + bool scan_silent(const stopper& cancel, + const system::silent::batch::receiver& keys, size_t first, + size_t last, const silent_handler& callback) NOEXCEPT; + + /// The first silent row at or above first that is not fully written. + size_t get_silent_frontier(size_t first) const NOEXCEPT; /// Verify all signatures in table, false implies cancel. bool verify_ecdsa_signatures(const stopper& cancel, header_links&, diff --git a/test/query/batch/silent.cpp b/test/query/batch/silent.cpp index 223c8e4c9..5e38f5887 100644 --- a/test/query/batch/silent.cpp +++ b/test/query/batch/silent.cpp @@ -194,4 +194,53 @@ BOOST_AUTO_TEST_CASE(query_batch_silent__scan_silent__no_match__none) BOOST_REQUIRE_EQUAL(calls, 0u); } +BOOST_AUTO_TEST_CASE(query_batch_silent__scan_silent__range__expected) +{ + 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_silent(42, simple_send())); + BOOST_REQUIRE(query.set_silent(43, simple_send())); + + std::vector links{}; + const stopper cancel{}; + BOOST_REQUIRE(query.scan_silent(cancel, get_keys(), 1, 2, + [&](const code&, uint32_t tx, const system::ec_compressed&) NOEXCEPT + { + links.push_back(tx); + })); + BOOST_REQUIRE_EQUAL(links, std::vector{ 43 }); +} + +// get_silent_frontier +// ---------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(query_batch_silent__get_silent_frontier__empty__zero) +{ + 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_EQUAL(query.get_silent_frontier(0), 0u); +} + +BOOST_AUTO_TEST_CASE(query_batch_silent__get_silent_frontier__written__count) +{ + 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_silent(42, simple_send())); + BOOST_REQUIRE(query.set_silent(43, simple_send())); + BOOST_REQUIRE_EQUAL(query.get_silent_frontier(0), 2u); + BOOST_REQUIRE_EQUAL(query.get_silent_frontier(1), 2u); +} + BOOST_AUTO_TEST_SUITE_END() From a79c16b8ffa085fcd9848516658a093a168dc276 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sat, 3 Oct 2026 17:34:16 -0400 Subject: [PATCH 3/4] Store silent start height in the envelope. --- include/bitcoin/database/impl/query/extent.ipp | 12 ++++++++++++ include/bitcoin/database/query.hpp | 2 ++ include/bitcoin/database/settings.hpp | 3 ++- include/bitcoin/database/tables/envelope.hpp | 5 ++++- include/bitcoin/database/tables/schema.hpp | 3 ++- src/settings.cpp | 6 +++--- src/tables/envelope.cpp | 11 ++++++++--- test/tables/envelope.cpp | 17 +++++++++-------- 8 files changed, 42 insertions(+), 17 deletions(-) diff --git a/include/bitcoin/database/impl/query/extent.ipp b/include/bitcoin/database/impl/query/extent.ipp index 0be56bfcd..73eadcdb6 100644 --- a/include/bitcoin/database/impl/query/extent.ipp +++ b/include/bitcoin/database/impl/query/extent.ipp @@ -282,6 +282,18 @@ bool CLASS::filter_enabled() const NOEXCEPT return store_.filter_bk.enabled() && store_.filter_tx.enabled(); } +TEMPLATE +bool CLASS::silent_enabled() const NOEXCEPT +{ + return store_.get_envelope().silent_start_height != max_uint32; +} + +TEMPLATE +size_t CLASS::silent_start_height() const NOEXCEPT +{ + return store_.get_envelope().silent_start_height; +} + } // namespace database } // namespace libbitcoin diff --git a/include/bitcoin/database/query.hpp b/include/bitcoin/database/query.hpp index 50e4e6c8d..a0e5f833a 100644 --- a/include/bitcoin/database/query.hpp +++ b/include/bitcoin/database/query.hpp @@ -258,6 +258,8 @@ class query /// Optional/configured table state. bool address_enabled() const NOEXCEPT; bool filter_enabled() const NOEXCEPT; + bool silent_enabled() const NOEXCEPT; + size_t silent_start_height() const NOEXCEPT; size_t interval_span() const NOEXCEPT; /// Initialization (natural-keyed). diff --git a/include/bitcoin/database/settings.hpp b/include/bitcoin/database/settings.hpp index 2ec07dd26..30439b86f 100644 --- a/include/bitcoin/database/settings.hpp +++ b/include/bitcoin/database/settings.hpp @@ -39,7 +39,8 @@ struct BCD_API settings settings(system::chain::selection context) NOEXCEPT; /// Pack the creation envelope, must follow configuration parse. - void initialize(const system::settings& bitcoin, bool limited) NOEXCEPT; + void initialize(const system::settings& bitcoin, bool limited_blocks, + uint32_t silent_start) NOEXCEPT; /// Table settings. /// ----------------------------------------------------------------------- diff --git a/include/bitcoin/database/tables/envelope.hpp b/include/bitcoin/database/tables/envelope.hpp index 1cf74ee91..c1f2ba86e 100644 --- a/include/bitcoin/database/tables/envelope.hpp +++ b/include/bitcoin/database/tables/envelope.hpp @@ -36,7 +36,7 @@ struct BCD_API envelope envelope() NOEXCEPT; envelope(const system::settings& bitcoin, const settings& database, - bool limited) NOEXCEPT; + bool limited, uint32_t silent) NOEXCEPT; void set(const settings& database) NOEXCEPT; @@ -101,6 +101,9 @@ struct BCD_API envelope bool limited_blocks{}; bool provide_filters{}; + /// Node settings. + uint32_t silent_start_height{ max_uint32 }; + /// Node state. bool pooling{}; }; diff --git a/include/bitcoin/database/tables/schema.hpp b/include/bitcoin/database/tables/schema.hpp index 98e776f2b..d55f22935 100644 --- a/include/bitcoin/database/tables/schema.hpp +++ b/include/bitcoin/database/tables/schema.hpp @@ -46,7 +46,8 @@ namespace schema { /// 4.0.2.0 Header work reduced to 16 bytes, milestone (formerly merged into /// parent pk) and compact in a new header flags byte. /// 4.0.3.0 Second bank of ecdsa, schnorr, and prevalid batch tables. -constexpr std::array version{ 4, 0, 3, 0 }; +/// 4.0.4.0 Silent payment start height in the envelope. +constexpr std::array version{ 4, 0, 4, 0 }; /// Values. /// ----------------------------------------------------------------------- diff --git a/src/settings.cpp b/src/settings.cpp index b08cd9e07..feb7801ae 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -57,10 +57,10 @@ settings::settings(chain::selection context) NOEXCEPT } } -void settings::initialize(const system::settings& bitcoin, - bool limited) NOEXCEPT +void settings::initialize(const system::settings& bitcoin, bool limited_blocks, + uint32_t silent_start) NOEXCEPT { - envelope = { bitcoin, *this, limited }; + envelope = { bitcoin, *this, limited_blocks, silent_start }; } } // namespace database diff --git a/src/tables/envelope.cpp b/src/tables/envelope.cpp index 8d47ec3ec..fffd78a06 100644 --- a/src/tables/envelope.cpp +++ b/src/tables/envelope.cpp @@ -55,7 +55,7 @@ void envelope::set(const settings& database) NOEXCEPT } envelope::envelope(const system::settings& bitcoin, const settings& database, - bool limited) NOEXCEPT + bool limited, uint32_t silent) NOEXCEPT : schema(compiled), forks(bitcoin.forks), initial_subsidy_bitcoin(bitcoin.initial_subsidy_bitcoin), @@ -87,7 +87,8 @@ envelope::envelope(const system::settings& bitcoin, const settings& database, bip9_bit2_active_checkpoint(bitcoin.bip9_bit2_active_checkpoint), top_checkpoint(bitcoin.top_checkpoint()), milestone(bitcoin.milestone), - limited_blocks(limited) + limited_blocks(limited), + silent_start_height(silent) { set(database); } @@ -189,6 +190,8 @@ bool envelope::from_data(reader& source) NOEXCEPT limited_blocks = to_bool(source.read_byte()); provide_filters = to_bool(source.read_byte()); + silent_start_height = source.read_little_endian(); + pooling = to_bool(source.read_byte()); return source; } @@ -279,6 +282,8 @@ bool envelope::to_data(flipper& sink) const NOEXCEPT sink.write_byte(to_int(limited_blocks)); sink.write_byte(to_int(provide_filters)); + sink.write_little_endian(silent_start_height); + sink.write_byte(to_int(pooling)); return sink; } @@ -288,7 +293,7 @@ size_t envelope::serialized_size() const NOEXCEPT constexpr auto forks_size = 24_size; constexpr auto fixed = (4 * sizeof(uint32_t)) + sizeof(uint16_t) + (7 * sizeof(uint32_t)) + (10 * sizeof(uint8_t)) + forks_size + - sizeof(uint64_t) + (15 * sizeof(uint32_t)); + sizeof(uint64_t) + (15 * sizeof(uint32_t)) + sizeof(uint32_t); const auto checkpoint_size = [](const chain::checkpoint& in) NOEXCEPT { diff --git a/test/tables/envelope.cpp b/test/tables/envelope.cpp index ce091f627..7fb81c6f0 100644 --- a/test/tables/envelope.cpp +++ b/test/tables/envelope.cpp @@ -22,7 +22,7 @@ BOOST_AUTO_TEST_SUITE(envelope_tests) using namespace system; -constexpr auto default_size = 353_size; +constexpr auto default_size = 357_size; static data_chunk to_chunk(const envelope& instance) { @@ -52,7 +52,7 @@ BOOST_AUTO_TEST_CASE(envelope__serialized_size__default__expected) BOOST_AUTO_TEST_CASE(envelope__to_data__default__expected_version) { const envelope instance{}; - BOOST_REQUIRE_EQUAL(instance.schema, system::config::version(4, 0, 3, 0)); + BOOST_REQUIRE_EQUAL(instance.schema, system::config::version(4, 0, 4, 0)); BOOST_REQUIRE_EQUAL(to_chunk(instance).front(), schema::version.front()); } @@ -136,7 +136,7 @@ BOOST_AUTO_TEST_CASE(envelope__construct__mainnet__expected) { const system::settings bitcoin{ chain::selection::mainnet }; const database::settings configuration{ chain::selection::mainnet }; - const envelope instance{ bitcoin, configuration, false }; + const envelope instance{ bitcoin, configuration, false, 42 }; BOOST_REQUIRE_EQUAL(instance.initial_subsidy_bitcoin, 50u); BOOST_REQUIRE_EQUAL(instance.subsidy_interval_blocks, 210000u); @@ -156,7 +156,7 @@ BOOST_AUTO_TEST_CASE(envelope__construct__mainnet__unset_filter_k) { const system::settings bitcoin{ chain::selection::mainnet }; const database::settings configuration{ chain::selection::mainnet }; - const envelope instance{ bitcoin, configuration, false }; + const envelope instance{ bitcoin, configuration, false, 42 }; BOOST_REQUIRE_EQUAL(instance.header_k, 0u); BOOST_REQUIRE_EQUAL(instance.ins_k, 0u); @@ -171,7 +171,7 @@ BOOST_AUTO_TEST_CASE(envelope__from_data__mainnet__round_trip) { const system::settings bitcoin{ chain::selection::mainnet }; const database::settings configuration{ chain::selection::mainnet }; - const envelope instance{ bitcoin, configuration, false }; + const envelope instance{ bitcoin, configuration, false, 42 }; auto data = to_chunk(instance); BOOST_REQUIRE_EQUAL(data.size(), instance.serialized_size()); @@ -184,7 +184,7 @@ BOOST_AUTO_TEST_CASE(envelope__from_data__regtest__round_trip) { const system::settings bitcoin{ chain::selection::regtest }; const database::settings configuration{ chain::selection::regtest }; - const envelope instance{ bitcoin, configuration, false }; + const envelope instance{ bitcoin, configuration, false, 42 }; auto data = to_chunk(instance); BOOST_REQUIRE_EQUAL(data.size(), instance.serialized_size()); @@ -197,9 +197,10 @@ BOOST_AUTO_TEST_CASE(envelope__initialize__mainnet__expected) { const system::settings bitcoin{ chain::selection::mainnet }; database::settings configuration{ chain::selection::mainnet }; - configuration.initialize(bitcoin, false); + configuration.initialize(bitcoin, false, 42); - BOOST_REQUIRE(configuration.envelope == envelope(bitcoin, configuration, false)); + BOOST_REQUIRE(configuration.envelope == envelope(bitcoin, configuration, false, 42)); + BOOST_REQUIRE_EQUAL(configuration.envelope.silent_start_height, 42u); BOOST_REQUIRE_EQUAL(configuration.envelope.proof_of_work_limit, bitcoin.proof_of_work_limit); BOOST_REQUIRE_EQUAL(configuration.envelope.bip9_bit1_active_checkpoint.height(), 481824u); } From a24ba2bc6443a4549ced8e8b20df2385d3be7236 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sun, 4 Oct 2026 13:24:19 -0400 Subject: [PATCH 4/4] Style silent payment sources and tests. --- .../database/impl/query/batch/silent.ipp | 32 ++-- src/tables/envelope.cpp | 2 +- test/query/batch/silent.cpp | 158 +++++++++--------- 3 files changed, 96 insertions(+), 96 deletions(-) diff --git a/include/bitcoin/database/impl/query/batch/silent.ipp b/include/bitcoin/database/impl/query/batch/silent.ipp index 6f348fd0a..334342bbc 100644 --- a/include/bitcoin/database/impl/query/batch/silent.ipp +++ b/include/bitcoin/database/impl/query/batch/silent.ipp @@ -73,13 +73,14 @@ TEMPLATE size_t CLASS::get_silent_frontier(size_t first) const NOEXCEPT { using namespace system; + using word_t = table::silent_correlate::tx::integer; const auto guard = store_.silent.guard(); - const auto words = pointer_cast(guard.data()); + const auto words = pointer_cast(guard.data()); const auto count = store_.silent.count(); for (auto row = first; row < count; ++row) { - std::atomic_ref word{ *std::next(words, row) }; + std::atomic_ref word{ *std::next(words, row) }; if (is_zero(word.load(std::memory_order_acquire))) return row; } @@ -120,8 +121,8 @@ bool CLASS::set_silent(const header_link& link, const block& block) NOEXCEPT if (fail.load(relaxed)) return; - const auto& tx = links.at(index); - if (tx >= first && !set_silent(tx, *txs->at(index))) + const auto& fk = links.at(index); + if (fk >= first && !set_silent(fk, *txs->at(index))) fail.store(true, relaxed); }); @@ -154,8 +155,8 @@ bool CLASS::set_silent(const header_link& link, if (fail.load(relaxed)) return; - const auto& tx = links.at(index); - if (tx >= first && !set_silent(tx, txs.at(index))) + const auto& fk = links.at(index); + if (fk >= first && !set_silent(fk, txs.at(index))) fail.store(true, relaxed); }); @@ -196,14 +197,15 @@ bool CLASS::set_silent_(const tx_link& link, const ec_compressed& summary, // The prefix is ec_xonly[0..7] read as little-endian. using namespace system; - std::vector prefixes(outputs.size()); + using prefix_t = table::silent_prefix::integral; + std::vector prefixes(outputs.size()); std::transform(outputs.cbegin(), outputs.cend(), prefixes.begin(), [](const auto& output) NOEXCEPT { - return unsafe_from_little_endian(output.key.data()); + return unsafe_from_little_endian(output.key.data()); }); - using prefix_t = table::silent_prefix::put_ref; + using prefixes_t = table::silent_prefix::put_ref; using compressed_t = table::silent_compressed::put_ref; // ======================================================================== @@ -219,19 +221,19 @@ bool CLASS::set_silent_(const tx_link& link, const ec_compressed& summary, const auto guard = store_.silent.guard(); // Write values to each column in corresponding positions. - if (!store_.silent.prefix.put(fk, prefix_t{ {}, prefixes }) || + if (!store_.silent.prefix.put(fk, prefixes_t{ {}, prefixes }) || !store_.silent.compressed.put(fk, compressed_t{ {}, rows, summary })) return false; // The guard is the correlate column, published last (get_silent_frontier). - static_assert(schema::silent_correlate::minrow == sizeof(uint32_t)); - const auto words = pointer_cast(guard.data()); - const auto value = native_to_little_end( - possible_narrow_cast(link.value)); + using word_t = table::silent_correlate::tx::integer; + static_assert(schema::silent_correlate::minrow == sizeof(word_t)); + const auto words = pointer_cast(guard.data()); + const auto value = native_to_little_end(link.value); for (auto row = fk.value; row < fk.value + rows; ++row) { - std::atomic_ref word{ *std::next(words, row) }; + std::atomic_ref word{ *std::next(words, row) }; word.store(value, std::memory_order_release); } diff --git a/src/tables/envelope.cpp b/src/tables/envelope.cpp index fffd78a06..e3e671db1 100644 --- a/src/tables/envelope.cpp +++ b/src/tables/envelope.cpp @@ -293,7 +293,7 @@ size_t envelope::serialized_size() const NOEXCEPT constexpr auto forks_size = 24_size; constexpr auto fixed = (4 * sizeof(uint32_t)) + sizeof(uint16_t) + (7 * sizeof(uint32_t)) + (10 * sizeof(uint8_t)) + forks_size + - sizeof(uint64_t) + (15 * sizeof(uint32_t)) + sizeof(uint32_t); + sizeof(uint64_t) + (16 * sizeof(uint32_t)); const auto checkpoint_size = [](const chain::checkpoint& in) NOEXCEPT { diff --git a/test/query/batch/silent.cpp b/test/query/batch/silent.cpp index 5e38f5887..2437e2809 100644 --- a/test/query/batch/silent.cpp +++ b/test/query/batch/silent.cpp @@ -22,70 +22,64 @@ BOOST_FIXTURE_TEST_SUITE(query_batch_silent_tests, test::directory_setup_fixture) -using silent_payment = system::wallet::silent_payment; -using receiver = system::silent::batch::receiver; +using namespace system; +using silent_payment = wallet::silent_payment; +using receiver = silent::batch::receiver; +using tx_link_t = silent::batch::tx_link_t; -constexpr system::ec_compressed summary = system::base16_array +constexpr ec_compressed summary = base16_array ( "024ac253c216532e961988e2a8ce266a447c894c781e52ef6cee902361db960004" ); +constexpr ec_secret scan_secret = base16_array +( + "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" +); +constexpr ec_secret spend_secret = base16_array +( + "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3" +); -static system::chain::input to_input(const system::hash_digest& hash, - const system::data_chunk& script, - const system::data_chunk& prevout) NOEXCEPT +static chain::input to_input(const hash_digest& hash, const data_chunk& script, + const data_chunk& prevout) NOEXCEPT { - namespace chain = system::chain; chain::input in{ { hash, 0 }, { script, false }, {}, max_uint32 }; - in.prevout = system::to_shared(0u, - chain::script{ prevout, false }); + const chain::script prevout_script{ prevout, false }; + in.prevout = to_shared(0u, prevout_script); return in; } // BIP352 send_and_receive_test_vectors.json: "Simple send: two inputs". -static system::chain::transaction simple_send() NOEXCEPT +static const chain::input input0 = to_input +( + base16_hash("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), + base16_chunk("483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5"), + base16_chunk("76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac") +); +static const chain::input input1 = to_input +( + base16_hash("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), + base16_chunk("48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792"), + base16_chunk("76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac") +); +static const chain::output output0 { - return - { - 2u, - system::chain::inputs - { - to_input( - system::base16_hash("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), - system::base16_chunk("483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5"), - system::base16_chunk("76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac")), - to_input( - system::base16_hash("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), - system::base16_chunk("48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792"), - system::base16_chunk("76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac")) - }, - system::chain::outputs - { - system::chain::output - { - 0u, - system::chain::script - { - system::base16_chunk("51203e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1"), - false - } - } - }, - 0u - }; + 0u, + chain::script{ base16_chunk("51203e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1"), false } +}; + +static chain::transaction simple_send() NOEXCEPT +{ + return { 2u, { input0, input1 }, { output0 }, 0u }; } static receiver get_keys() NOEXCEPT { - constexpr system::ec_secret scan = system::base16_array( - "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c"); - constexpr system::ec_secret spend = system::base16_array( - "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3"); - - system::ec_compressed point{}; - if (!system::secret_to_public(point, spend)) + ec_compressed spend{}; + if (!secret_to_public(spend, spend_secret)) return {}; - return silent_payment{ scan, point, {} }.keys(); + return silent_payment{ scan_secret, spend, {} }.keys(); } // set_silent @@ -93,7 +87,7 @@ static receiver get_keys() NOEXCEPT BOOST_AUTO_TEST_CASE(query_batch_silent__set_silent__coinbase_only__true) { - settings settings{}; + database::settings settings{}; settings.path = TEST_DIRECTORY; test::chunk_store store{ settings }; test::query_accessor query{ store }; @@ -104,7 +98,7 @@ BOOST_AUTO_TEST_CASE(query_batch_silent__set_silent__coinbase_only__true) BOOST_AUTO_TEST_CASE(query_batch_silent__set_silent__unarchived_block__false) { - settings settings{}; + database::settings settings{}; settings.path = TEST_DIRECTORY; test::chunk_store store{ settings }; test::query_accessor query{ store }; @@ -115,7 +109,7 @@ BOOST_AUTO_TEST_CASE(query_batch_silent__set_silent__unarchived_block__false) BOOST_AUTO_TEST_CASE(query_batch_silent__set_silent__no_taproot_output__no_records) { - settings settings{}; + database::settings settings{}; settings.path = TEST_DIRECTORY; test::chunk_store store{ settings }; test::query_accessor query{ store }; @@ -127,7 +121,7 @@ BOOST_AUTO_TEST_CASE(query_batch_silent__set_silent__no_taproot_output__no_recor BOOST_AUTO_TEST_CASE(query_batch_silent__set_silent__eligible__one_record) { - settings settings{}; + database::settings settings{}; settings.path = TEST_DIRECTORY; test::chunk_store store{ settings }; test::query_accessor query{ store }; @@ -142,7 +136,7 @@ BOOST_AUTO_TEST_CASE(query_batch_silent__set_silent__eligible__one_record) BOOST_AUTO_TEST_CASE(query_batch_silent__scan_silent__match__expected) { - settings settings{}; + database::settings settings{}; settings.path = TEST_DIRECTORY; test::chunk_store store{ settings }; test::query_accessor query{ store }; @@ -150,19 +144,21 @@ BOOST_AUTO_TEST_CASE(query_batch_silent__scan_silent__match__expected) BOOST_REQUIRE(query.initialize(test::genesis)); BOOST_REQUIRE(query.set_silent(42, simple_send())); + code error{}; size_t calls{}; - uint32_t link{}; - system::ec_compressed point{}; - const stopper cancel{}; - BOOST_REQUIRE(query.scan_silent(cancel, get_keys(), - [&](const code& ec, uint32_t tx, const system::ec_compressed& tweak) NOEXCEPT - { - BOOST_REQUIRE(!ec); - link = tx; - point = tweak; - ++calls; - })); + tx_link_t link{}; + ec_compressed point{}; + const auto handler = [&](const code& ec, tx_link_t tx, const ec_compressed& key) NOEXCEPT + { + error = ec; + link = tx; + point = key; + ++calls; + }; + const stopper cancel{}; + BOOST_REQUIRE(query.scan_silent(cancel, get_keys(), handler)); + BOOST_REQUIRE(!error); BOOST_REQUIRE_EQUAL(calls, 1u); BOOST_REQUIRE_EQUAL(link, 42u); BOOST_REQUIRE_EQUAL(point, summary); @@ -170,7 +166,7 @@ BOOST_AUTO_TEST_CASE(query_batch_silent__scan_silent__match__expected) BOOST_AUTO_TEST_CASE(query_batch_silent__scan_silent__no_match__none) { - settings settings{}; + database::settings settings{}; settings.path = TEST_DIRECTORY; test::chunk_store store{ settings }; test::query_accessor query{ store }; @@ -179,24 +175,25 @@ BOOST_AUTO_TEST_CASE(query_batch_silent__scan_silent__no_match__none) BOOST_REQUIRE(query.set_silent(42, simple_send())); size_t calls{}; - const stopper cancel{}; + const auto handler = [&](const code&, tx_link_t, const ec_compressed&) NOEXCEPT + { + ++calls; + }; + const receiver keys { - .scan = system::base16_array("0000000000000000000000000000000000000000000000000000000000000001"), + .scan = base16_array("0000000000000000000000000000000000000000000000000000000000000001"), .spend = get_keys().spend }; - BOOST_REQUIRE(query.scan_silent(cancel, keys, - [&](const code&, uint32_t, const system::ec_compressed&) NOEXCEPT - { - ++calls; - })); + const stopper cancel{}; + BOOST_REQUIRE(query.scan_silent(cancel, keys, handler)); BOOST_REQUIRE_EQUAL(calls, 0u); } BOOST_AUTO_TEST_CASE(query_batch_silent__scan_silent__range__expected) { - settings settings{}; + database::settings settings{}; settings.path = TEST_DIRECTORY; test::chunk_store store{ settings }; test::query_accessor query{ store }; @@ -205,14 +202,15 @@ BOOST_AUTO_TEST_CASE(query_batch_silent__scan_silent__range__expected) BOOST_REQUIRE(query.set_silent(42, simple_send())); BOOST_REQUIRE(query.set_silent(43, simple_send())); - std::vector links{}; + std::vector links{}; + const auto handler = [&](const code&, tx_link_t tx, const ec_compressed&) NOEXCEPT + { + links.push_back(tx); + }; + const stopper cancel{}; - BOOST_REQUIRE(query.scan_silent(cancel, get_keys(), 1, 2, - [&](const code&, uint32_t tx, const system::ec_compressed&) NOEXCEPT - { - links.push_back(tx); - })); - BOOST_REQUIRE_EQUAL(links, std::vector{ 43 }); + BOOST_REQUIRE(query.scan_silent(cancel, get_keys(), 1, 2, handler)); + BOOST_REQUIRE_EQUAL(links, std::vector{ 43 }); } // get_silent_frontier @@ -220,7 +218,7 @@ BOOST_AUTO_TEST_CASE(query_batch_silent__scan_silent__range__expected) BOOST_AUTO_TEST_CASE(query_batch_silent__get_silent_frontier__empty__zero) { - settings settings{}; + database::settings settings{}; settings.path = TEST_DIRECTORY; test::chunk_store store{ settings }; test::query_accessor query{ store }; @@ -231,7 +229,7 @@ BOOST_AUTO_TEST_CASE(query_batch_silent__get_silent_frontier__empty__zero) BOOST_AUTO_TEST_CASE(query_batch_silent__get_silent_frontier__written__count) { - settings settings{}; + database::settings settings{}; settings.path = TEST_DIRECTORY; test::chunk_store store{ settings }; test::query_accessor query{ store };