From 7792cfcef4b42f393db64818cb086d710991901a Mon Sep 17 00:00:00 2001 From: josie Date: Mon, 1 Jun 2026 15:17:01 +0200 Subject: [PATCH] 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. also check the computed block link when short-circuiting find_strong(tx_link), so unconfirmed duplicate tx links fall back to hash-based strong lookup instead of returning a terminal block link. --- .../impl/query/consensus/consensus_strong.ipp | 6 +- test/query/confirmed.cpp | 83 +++++++++++++++++++ 2 files changed, 86 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 c78f9674c..4545c3b64 100644 --- a/include/bitcoin/database/impl/query/consensus/consensus_strong.ipp +++ b/include/bitcoin/database/impl/query/consensus/consensus_strong.ipp @@ -72,9 +72,9 @@ height_link CLASS::find_strong_spender_height( size_t out{}; for (const auto& in: to_spenders(point)) 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) @@ -84,7 +84,7 @@ TEMPLATE header_link CLASS::find_strong(const tx_link& link) const NOEXCEPT { // Shortcuircuit hash-based search by testing self. - if (const auto fk = to_block(link); !link.is_terminal()) + if (const auto fk = to_block(link); !fk.is_terminal()) return fk; return find_strong(get_tx_key(link)); diff --git a/test/query/confirmed.cpp b/test/query/confirmed.cpp index 96a54282c..9c9396d82 100644 --- a/test/query/confirmed.cpp +++ b/test/query/confirmed.cpp @@ -144,6 +144,89 @@ BOOST_AUTO_TEST_CASE(query_confirmed__is_confirmed_tx__confirm__expected) BOOST_REQUIRE(query.is_confirmed_tx(2)); } +BOOST_AUTO_TEST_CASE(query_confirmed__find_strong__unconfirmed_duplicate__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(test::block1a, context{ 0, 1, 0 }, false, false)); + BOOST_REQUIRE(query.set_strong(1)); + + BOOST_REQUIRE(query.set(test::tx4)); + const auto unconfirmed = query.to_tx(test::tx4.hash(false)); + + BOOST_REQUIRE(query.set(test::block_spend_1a, context{ 0, 2, 0 }, false, false)); + BOOST_REQUIRE(query.set_strong(2)); + + BOOST_REQUIRE_EQUAL(query.find_strong(unconfirmed), + query.to_header(test::block_spend_1a.hash())); +} + +BOOST_AUTO_TEST_CASE(query_confirmed__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_confirmed__find_strong_spender_height__unconfirmed__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_confirmed__find_strong_spender_height__confirmed__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(test::block1a, context{ 0, 1, 0 }, false, false)); + BOOST_REQUIRE(query.set_strong(1)); + BOOST_REQUIRE(query.set(test::block_spend_1a, 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_CASE(query_confirmed__is_confirmed_input__genesis__true) { settings settings{};