From 9fe6a89b51207d102236b3dd4cb6717625d0a5d3 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 15 Sep 2026 02:37:57 -0400 Subject: [PATCH] Add flag to populate_with_metadata() for unconfirmed tx. --- .../bitcoin/database/impl/query/confirmed.ipp | 10 +++++-- .../query/consensus/consensus_populate.ipp | 26 ++++++++++------- include/bitcoin/database/query.hpp | 12 +++++--- test/query/archive/chain_reader.cpp | 28 +++++++++++++++++++ 4 files changed, 60 insertions(+), 16 deletions(-) diff --git a/include/bitcoin/database/impl/query/confirmed.ipp b/include/bitcoin/database/impl/query/confirmed.ipp index 887d18255..baf82b4a2 100644 --- a/include/bitcoin/database/impl/query/confirmed.ipp +++ b/include/bitcoin/database/impl/query/confirmed.ipp @@ -161,9 +161,15 @@ bool CLASS::is_unconfirmed_spent(const output_link& link) const NOEXCEPT TEMPLATE bool CLASS::is_spent(const output_link& link) const NOEXCEPT { - // *Any* tx spends the output. Note that this could even be a tx that is in + return is_spent(get_outpoint(link).point()); +} + +TEMPLATE +bool CLASS::is_spent(const point& prevout) const NOEXCEPT +{ + // *Any* tx spends the point. Note that this could even be a tx that is in // conflict with another long-confirmed tx, or a valid tx in invalid block. - return store_.ins.exists(get_outpoint(link).point()); + return store_.ins.exists(prevout); } } // namespace database diff --git a/include/bitcoin/database/impl/query/consensus/consensus_populate.ipp b/include/bitcoin/database/impl/query/consensus/consensus_populate.ipp index b0397a724..3c78b2150 100644 --- a/include/bitcoin/database/impl/query/consensus/consensus_populate.ipp +++ b/include/bitcoin/database/impl/query/consensus/consensus_populate.ipp @@ -49,31 +49,31 @@ bool CLASS::populate_with_metadata(const block& block, } TEMPLATE -bool CLASS::populate_with_metadata(const transaction& tx, - bool chain) const NOEXCEPT +bool CLASS::populate_with_metadata(const transaction& tx, bool chain, + bool pool) const NOEXCEPT { // This override makes the public method safe for coinbase calling. - return tx.is_coinbase() || populate_with_metadata_(tx, chain); + return tx.is_coinbase() || populate_with_metadata_(tx, chain, pool); } // protected TEMPLATE -bool CLASS::populate_with_metadata_(const transaction& tx, - bool chain) const NOEXCEPT +bool CLASS::populate_with_metadata_(const transaction& tx, bool chain, + bool pool) const NOEXCEPT { BC_ASSERT(!tx.is_coinbase()); const auto& ins = tx.inputs_ptr(); return std::all_of(ins->begin(), ins->end(), - [this, chain](const auto& in) NOEXCEPT + [this, chain, pool](const auto& in) NOEXCEPT { - return this->populate_with_metadata(*in, chain); + return this->populate_with_metadata(*in, chain, pool); }); } TEMPLATE -bool CLASS::populate_with_metadata(const input& input, - bool chain) const NOEXCEPT +bool CLASS::populate_with_metadata(const input& input, bool chain, + bool pool) const NOEXCEPT { // Null point would return nullptr and be interpreted as missing. BC_ASSERT(!input.point().is_null()); @@ -119,7 +119,13 @@ bool CLASS::populate_with_metadata(const input& input, metadata.median_time_past = max_uint32; } - if (const auto height = find_strong_spender_height(input.point()); + if (pool) + { + // Any spender (including duplicate tx) implies spent. + const auto spent = is_spent(input.point()); + metadata.spender_height = spent ? 0_u32 : max_uint32; + } + else if (const auto height = find_strong_spender_height(input.point()); !height.is_terminal()) { // Confirmed spender found at height. diff --git a/include/bitcoin/database/query.hpp b/include/bitcoin/database/query.hpp index 304843cff..6d56079ae 100644 --- a/include/bitcoin/database/query.hpp +++ b/include/bitcoin/database/query.hpp @@ -468,9 +468,12 @@ class query bool populate_without_metadata(const transaction& tx) const NOEXCEPT; /// False implies missing prevouts, node input.metadata is populated. - bool populate_with_metadata(const input& input, bool chain=false) const NOEXCEPT; + /// Pool implies free tx, where any conflict implies is spent at height 0. + bool populate_with_metadata(const input& input, bool chain=false, + bool pool=false) const NOEXCEPT; bool populate_with_metadata(const block& block, bool chain=false) const NOEXCEPT; - bool populate_with_metadata(const transaction& tx, bool chain=false) const NOEXCEPT; + bool populate_with_metadata(const transaction& tx, bool chain=false, + bool pool=false) const NOEXCEPT; /// Fees. /// ----------------------------------------------------------------------- @@ -645,6 +648,7 @@ class query bool is_unconfirmed_spent(const output_link& link) const NOEXCEPT; bool is_confirmed_spent(const output_link& link) const NOEXCEPT; bool is_spent(const output_link& link) const NOEXCEPT; + bool is_spent(const point& prevout) const NOEXCEPT; /// Height index not used by these. bool is_strong_tx(const tx_link& link) const NOEXCEPT; @@ -923,8 +927,8 @@ class query const header_link& link, size_t height) const NOEXCEPT; /// Bypasses and only asserts coinbase guard (internal use). - bool populate_with_metadata_(const transaction& tx, - bool chain) const NOEXCEPT; + bool populate_with_metadata_(const transaction& tx, bool chain, + bool pool) const NOEXCEPT; /// merkle /// ----------------------------------------------------------------------- diff --git a/test/query/archive/chain_reader.cpp b/test/query/archive/chain_reader.cpp index 1cfd0e764..c337631d0 100644 --- a/test/query/archive/chain_reader.cpp +++ b/test/query/archive/chain_reader.cpp @@ -852,6 +852,34 @@ BOOST_AUTO_TEST_CASE(query_chain_reader__populate_with_metadata__metadata__expec BOOST_CHECK_EQUAL(tx4.inputs_ptr()->at(1)->metadata.parent_tx, 1u); } +// A free tx cannot conflict, so any spender of its prevout implies spent. +BOOST_AUTO_TEST_CASE(query_chain_reader__populate_with_metadata__pool_spender__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(query.set(test::block1a, test::context, false, false)); + + // tx5 spends the first output of the first tx of block1a, as does tx4. + const auto& unspent = clean_(test::tx5); + BOOST_CHECK(query.populate_with_metadata(unspent, true, true)); + BOOST_CHECK_EQUAL(unspent.inputs_ptr()->at(0)->metadata.spender_height, max_uint32); + + BOOST_CHECK(query.set(test::tx4)); + + const auto& spent = clean_(test::tx5); + BOOST_CHECK(query.populate_with_metadata(spent, true, true)); + BOOST_CHECK_EQUAL(spent.inputs_ptr()->at(0)->metadata.spender_height, 0u); + + // The spender is unassociated, so it is not a confirmed spender. + const auto& chained = clean_(test::tx5); + BOOST_CHECK(query.populate_with_metadata(chained, true, false)); + BOOST_CHECK_EQUAL(chained.inputs_ptr()->at(0)->metadata.spender_height, max_uint32); +} + // populate_without_metadata // ----------------------------------------------------------------------------