From 62ba1a46b0927fae6b64847288615688fbdf9352 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Mon, 12 Oct 2026 23:14:51 -0400 Subject: [PATCH 1/3] Add outs and wtxid key queries. --- .../impl/query/navigate/navigate_natural.ipp | 20 +++++++++++++++++++ include/bitcoin/database/query.hpp | 2 ++ .../database/tables/archives/output.hpp | 20 +++++++++++++++++++ test/query/navigate/navigate_natural.cpp | 13 ++++++++++++ 4 files changed, 55 insertions(+) diff --git a/include/bitcoin/database/impl/query/navigate/navigate_natural.ipp b/include/bitcoin/database/impl/query/navigate/navigate_natural.ipp index 3e3dd86d0..5a6c4b3bc 100644 --- a/include/bitcoin/database/impl/query/navigate/navigate_natural.ipp +++ b/include/bitcoin/database/impl/query/navigate/navigate_natural.ipp @@ -118,6 +118,26 @@ inline ins_key CLASS::get_point_key(const ins_link& link) const NOEXCEPT return point.key; } +TEMPLATE +inline hash_digest CLASS::get_outs_key(const outs_link& link) const NOEXCEPT +{ + table::outs::get_output put{}; + if (!store_.outs.puts.get_raw(link, put)) + return {}; + + table::output::get_script_hash output{}; + if (!store_.output.raw(store_.output.get_memory(), put.out_fk, output)) + return {}; + + return output.hash; +} + +TEMPLATE +inline hash_digest CLASS::get_wtxid_key(const pool_link& link) const NOEXCEPT +{ + return get_pooled_wtxid(link); +} + TEMPLATE inline hash_digest CLASS::get_point_hash(const ins_link& link) const NOEXCEPT { diff --git a/include/bitcoin/database/query.hpp b/include/bitcoin/database/query.hpp index 08d0e9c27..20905d6a4 100644 --- a/include/bitcoin/database/query.hpp +++ b/include/bitcoin/database/query.hpp @@ -454,6 +454,8 @@ class query inline hash_digest get_tx_key(const tx_link& link) const NOEXCEPT; hash_digest get_wtxid(const tx_link& link) const NOEXCEPT; inline ins_key get_point_key(const ins_link& link) const NOEXCEPT; + inline hash_digest get_outs_key(const outs_link& link) const NOEXCEPT; + inline hash_digest get_wtxid_key(const pool_link& link) const NOEXCEPT; inline hash_digest get_point_hash(const ins_link& link) const NOEXCEPT; /// False position implies not confirmed (or fault). diff --git a/include/bitcoin/database/tables/archives/output.hpp b/include/bitcoin/database/tables/archives/output.hpp index 372e0b669..cb4e7f12c 100644 --- a/include/bitcoin/database/tables/archives/output.hpp +++ b/include/bitcoin/database/tables/archives/output.hpp @@ -146,6 +146,26 @@ struct output bool match{}; }; + /// Unstreamed, hashes the script in place. + struct get_script_hash + : public schema::output + { + inline bool from_data(memory::iterator start) NOEXCEPT + { + using namespace system; + + // Skip parent fk and value, and read the script size. + const auto* position = std::next(start, tx::size); + unsafe_from_variable(position); + const auto script_size = unsafe_from_variable(position); + const auto bytes = possible_narrow_cast(script_size); + hash = accumulator::hash(bytes, position); + return true; + } + + system::hash_digest hash{}; + }; + /// Unstreamed, writes full output directly to the sink. struct write_script : public schema::output diff --git a/test/query/navigate/navigate_natural.cpp b/test/query/navigate/navigate_natural.cpp index e1421b152..67cbe905c 100644 --- a/test/query/navigate/navigate_natural.cpp +++ b/test/query/navigate/navigate_natural.cpp @@ -223,6 +223,19 @@ BOOST_AUTO_TEST_CASE(query_navigate__get_point_key__always__expected) BOOST_CHECK_EQUAL(query.get_point_hash(4), system::one_hash); } +// get_outs_key + +BOOST_AUTO_TEST_CASE(query_navigate__get_outs_key__genesis__expected) +{ + settings settings{}; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_CHECK(!store.create(test::events_handler)); + BOOST_CHECK(query.initialize(test::genesis)); + BOOST_CHECK_EQUAL(query.get_outs_key(0), test::genesis.transactions_ptr()->front()->outputs_ptr()->front()->script().hash()); +} + // get_tx_key BOOST_AUTO_TEST_CASE(query_navigate__get_tx_key__always__expected) From 51c18cbe85ad684ed6d4f14efb649f5bb77df075 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 13 Oct 2026 00:14:22 -0400 Subject: [PATCH 2/3] Report headmap counts as buckets. --- include/bitcoin/database/impl/query/extent.ipp | 15 +++++++++++---- include/bitcoin/database/query.hpp | 8 ++++---- test/query/extent.cpp | 6 +++--- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/include/bitcoin/database/impl/query/extent.ipp b/include/bitcoin/database/impl/query/extent.ipp index 618c824a4..61146bc7e 100644 --- a/include/bitcoin/database/impl/query/extent.ipp +++ b/include/bitcoin/database/impl/query/extent.ipp @@ -46,6 +46,13 @@ size_t CLASS::name##_buckets() const NOEXCEPT \ return store_.name.buckets(); \ } +#define DEFINE_HEADMAP_BUCKETS(name) \ +TEMPLATE \ +size_t CLASS::name##_buckets() const NOEXCEPT \ +{ \ + return store_.name.count(); \ +} + #define DEFINE_RECORDS(name) \ TEMPLATE \ size_t CLASS::name##_records() const NOEXCEPT \ @@ -194,7 +201,7 @@ DEFINE_SIZES(wtxid) DEFINE_SIZES(filter_bk) DEFINE_SIZES(filter_tx) -// Buckets (hashmap + arraymap). +// Buckets (hashmap + arraymap + headmap). // ---------------------------------------------------------------------------- DEFINE_BUCKETS(header) @@ -203,8 +210,11 @@ DEFINE_BUCKETS(outs) DEFINE_BUCKETS(txs) DEFINE_BUCKETS(tx) +DEFINE_HEADMAP_BUCKETS(candidate) +DEFINE_HEADMAP_BUCKETS(confirmed) DEFINE_BUCKETS(strong) DEFINE_BUCKETS(scan) +DEFINE_HEADMAP_BUCKETS(state) DEFINE_BUCKETS(duplicate) DEFINE_BUCKETS(prevout) DEFINE_BUCKETS(pool) @@ -220,8 +230,6 @@ DEFINE_RECORDS(ins) DEFINE_RECORDS(outs) DEFINE_RECORDS(tx) -DEFINE_RECORDS(candidate) -DEFINE_RECORDS(confirmed) DEFINE_RECORDS(strong) DEFINE_RECORDS(scan) DEFINE_RECORDS(scan_prefix) @@ -238,7 +246,6 @@ DEFINE_RECORDS(prevalid0) DEFINE_RECORDS(prevalid1) DEFINE_RECORDS(presilent0) DEFINE_RECORDS(presilent1) -DEFINE_RECORDS(state) DEFINE_RECORDS(pool) DEFINE_RECORDS(spends) DEFINE_RECORDS(wtxid) diff --git a/include/bitcoin/database/query.hpp b/include/bitcoin/database/query.hpp index 20905d6a4..d71015ff5 100644 --- a/include/bitcoin/database/query.hpp +++ b/include/bitcoin/database/query.hpp @@ -238,15 +238,18 @@ class query size_t filter_bk_size() const NOEXCEPT; size_t filter_tx_size() const NOEXCEPT; - /// Buckets (hashmap + arraymap). + /// Buckets (hashmap + arraymap + headmap). size_t header_buckets() const NOEXCEPT; size_t ins_buckets() const NOEXCEPT; size_t outs_buckets() const NOEXCEPT; size_t txs_buckets() const NOEXCEPT; size_t tx_buckets() const NOEXCEPT; + size_t candidate_buckets() const NOEXCEPT; + size_t confirmed_buckets() const NOEXCEPT; size_t strong_buckets() const NOEXCEPT; size_t scan_buckets() const NOEXCEPT; + size_t state_buckets() const NOEXCEPT; size_t duplicate_buckets() const NOEXCEPT; size_t prevout_buckets() const NOEXCEPT; size_t pool_buckets() const NOEXCEPT; @@ -260,8 +263,6 @@ class query size_t outs_records() const NOEXCEPT; size_t tx_records() const NOEXCEPT; - size_t candidate_records() const NOEXCEPT; - size_t confirmed_records() const NOEXCEPT; size_t strong_records() const NOEXCEPT; size_t scan_records() const NOEXCEPT; size_t scan_prefix_records() const NOEXCEPT; @@ -278,7 +279,6 @@ class query size_t prevalid1_records() const NOEXCEPT; size_t presilent0_records() const NOEXCEPT; size_t presilent1_records() const NOEXCEPT; - size_t state_records() const NOEXCEPT; size_t pool_records() const NOEXCEPT; size_t spends_records() const NOEXCEPT; size_t wtxid_records() const NOEXCEPT; diff --git a/test/query/extent.cpp b/test/query/extent.cpp index 1d1bbfa8c..a5fc0ca2a 100644 --- a/test/query/extent.cpp +++ b/test/query/extent.cpp @@ -96,7 +96,10 @@ BOOST_AUTO_TEST_CASE(query_extent__buckets__genesis__expected) BOOST_REQUIRE_EQUAL(query.txs_buckets(), 128u); BOOST_REQUIRE_EQUAL(query.tx_buckets(), 128u); + BOOST_REQUIRE_EQUAL(query.candidate_buckets(), one); + BOOST_REQUIRE_EQUAL(query.confirmed_buckets(), one); BOOST_REQUIRE_EQUAL(query.strong_buckets(), 128u); + BOOST_REQUIRE_EQUAL(query.state_buckets(), one); BOOST_REQUIRE_EQUAL(query.duplicate_buckets(), 128u); BOOST_REQUIRE_EQUAL(query.prevout_buckets(), 128); BOOST_REQUIRE_EQUAL(query.pool_buckets(), 128u); @@ -119,8 +122,6 @@ BOOST_AUTO_TEST_CASE(query_extent__records__genesis__expected) BOOST_REQUIRE_EQUAL(query.outs_records(), one); BOOST_REQUIRE_EQUAL(query.tx_records(), one); - BOOST_REQUIRE_EQUAL(query.candidate_records(), one); - BOOST_REQUIRE_EQUAL(query.confirmed_records(), one); BOOST_REQUIRE_EQUAL(query.strong_records(), one); BOOST_REQUIRE_EQUAL(query.scan_records(), zero); BOOST_REQUIRE_EQUAL(query.ecdsa0_records(), zero); @@ -128,7 +129,6 @@ BOOST_AUTO_TEST_CASE(query_extent__records__genesis__expected) BOOST_REQUIRE_EQUAL(query.duplicate_records(), zero); BOOST_REQUIRE_EQUAL(query.prevalid0_records(), zero); BOOST_REQUIRE_EQUAL(query.presilent0_records(), zero); - BOOST_REQUIRE_EQUAL(query.state_records(), one); BOOST_REQUIRE_EQUAL(query.pool_records(), zero); BOOST_REQUIRE_EQUAL(query.spends_records(), zero); BOOST_REQUIRE_EQUAL(query.wtxid_records(), zero); From 0925f045475f0df01bb073c4234e0beedd12d90b Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Mon, 12 Oct 2026 23:28:04 -0400 Subject: [PATCH 3/3] Add is_confirmable_spent. --- include/bitcoin/database/impl/query/pool.ipp | 13 +++++++ include/bitcoin/database/query.hpp | 3 ++ test/query/pool.cpp | 40 ++++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/include/bitcoin/database/impl/query/pool.ipp b/include/bitcoin/database/impl/query/pool.ipp index 50b7d2b68..a3d690037 100644 --- a/include/bitcoin/database/impl/query/pool.ipp +++ b/include/bitcoin/database/impl/query/pool.ipp @@ -58,6 +58,19 @@ bool CLASS::is_confirmable_tx(const tx_link& link) const NOEXCEPT return is_confirmable_tx(memo, link); } +TEMPLATE +bool CLASS::is_confirmable_spent(const output_link& link) const NOEXCEPT +{ + for (const auto& in: to_spenders(link)) + { + const auto tx = to_input_tx(in); + if (is_pooled(tx) && is_confirmable_tx(tx)) + return true; + } + + return false; +} + TEMPLATE code CLASS::get_pooled(pooled_tx& out, const tx_link& link, const context& ctx) const NOEXCEPT diff --git a/include/bitcoin/database/query.hpp b/include/bitcoin/database/query.hpp index d71015ff5..6f431cd4c 100644 --- a/include/bitcoin/database/query.hpp +++ b/include/bitcoin/database/query.hpp @@ -708,6 +708,9 @@ class query /// Unconfirmed, and not conflicted by a confirmed tx (or via ancestry). bool is_confirmable_tx(const tx_link& link) const NOEXCEPT; + /// Spent by a pooled and confirmable tx. + bool is_confirmable_spent(const output_link& link) const NOEXCEPT; + /// Confirmable tx links, from cursor toward end, at most limit. code get_confirmable_txs(pool_link& cursor, tx_links& out, const pool_link& end, size_t limit) const NOEXCEPT; diff --git a/test/query/pool.cpp b/test/query/pool.cpp index 5a2aed044..7b37677ba 100644 --- a/test/query/pool.cpp +++ b/test/query/pool.cpp @@ -144,6 +144,46 @@ BOOST_AUTO_TEST_CASE(query_pool__get_pooled__without_bip113__unvalidated) BOOST_REQUIRE_EQUAL(query.get_pooled(pooled, 1, context{ 0, 8, 9 }), error::unvalidated); } +// is_confirmable_spent + +BOOST_AUTO_TEST_CASE(query_pool__is_confirmable_spent__pooled_spender__true) +{ + 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)); + store.set_pooling(); + + const auto parent = to_spend(genesis_coinbase(), 2u); + const auto child = to_spend(parent.hash(false), 1u); + BOOST_REQUIRE(archive_pool(query, parent)); + BOOST_REQUIRE(!query.is_confirmable_spent(query.to_output(parent.hash(false), 0))); + + BOOST_REQUIRE(archive_pool(query, child)); + BOOST_REQUIRE(query.is_confirmable_spent(query.to_output(parent.hash(false), 0))); + BOOST_REQUIRE(!query.is_confirmable_spent(query.to_output(child.hash(false), 0))); +} + +BOOST_AUTO_TEST_CASE(query_pool__is_confirmable_spent__conflicted_spender__false) +{ + 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)); + store.set_pooling(); + + const auto parent = to_spend(genesis_coinbase(), 2u); + const auto child = to_spend(parent.hash(false), 1u); + BOOST_REQUIRE(archive_pool(query, parent)); + BOOST_REQUIRE(archive_pool(query, child)); + BOOST_REQUIRE(!archive_confirm(query, to_spend(genesis_coinbase(), 3u)).is_terminal()); + BOOST_REQUIRE(!query.is_confirmable_spent(query.to_output(parent.hash(false), 0))); +} + // get_pooled_time BOOST_AUTO_TEST_CASE(query_pool__get_pooled_time__pooled__expected)