diff --git a/include/bitcoin/database/impl/query/address/address_history.ipp b/include/bitcoin/database/impl/query/address/address_history.ipp index ad8153fd7..75924e576 100644 --- a/include/bitcoin/database/impl/query/address/address_history.ipp +++ b/include/bitcoin/database/impl/query/address/address_history.ipp @@ -135,7 +135,8 @@ history CLASS::get_tx_history(hash_digest&& key, const tx_link& link, auto height = history::unrooted_height; auto position = history::unconfirmed_position; - const auto block = find_strong(link); + tx_link strong{}; + const auto block = find_strong(strong, link); const auto at = get_confirmed_height(block); if (at.is_terminal()) { @@ -153,7 +154,7 @@ history CLASS::get_tx_history(hash_digest&& key, const tx_link& link, else { height = at.value; - if (!get_tx_position(position, link, block)) + if (!get_tx_position(position, strong, block)) return {}; } @@ -176,7 +177,8 @@ history CLASS::get_tx_confirmed_history(hash_digest&& key, const tx_link& link, if (link.is_terminal()) return {}; - const auto block = find_strong(link); + tx_link strong{}; + const auto block = find_strong(strong, link); const auto at = get_confirmed_height(block); // Invalid with unconfirmed_position signals no fault. @@ -184,7 +186,7 @@ history CLASS::get_tx_confirmed_history(hash_digest&& key, const tx_link& link, return { .position = history::unconfirmed_position }; size_t position{}; - if (!get_tx_position(position, link, block)) + if (!get_tx_position(position, strong, block)) return {}; // Electrum uses fees only on unconfirmed (expensive). diff --git a/include/bitcoin/database/impl/query/address/address_unspent.ipp b/include/bitcoin/database/impl/query/address/address_unspent.ipp index ea5d2530b..c612e1eb9 100644 --- a/include/bitcoin/database/impl/query/address/address_unspent.ipp +++ b/include/bitcoin/database/impl/query/address/address_unspent.ipp @@ -126,12 +126,13 @@ unspent_output CLASS::get_tx_unspent(const output_link& link) const NOEXCEPT auto height = unspent_output::unused_height; auto position = unspent_output::unconfirmed_position; - const auto block = find_strong(tx); + tx_link strong{}; + const auto block = find_strong(strong, tx); const auto at = get_confirmed_height(block); if (!at.is_terminal()) { height = at.value; - if (!get_tx_position(position, tx, block)) + if (!get_tx_position(position, strong, block)) return {}; } @@ -147,7 +148,8 @@ unspent_output CLASS::get_tx_confirmed_unspent(const output_link& link) const NO return {}; const auto tx = out.parent_fk; - const auto block = find_strong(tx); + tx_link strong{}; + const auto block = find_strong(strong, tx); const auto at = get_confirmed_height(block); // Invalid with unconfirmed_position signals no fault. @@ -158,7 +160,7 @@ unspent_output CLASS::get_tx_confirmed_unspent(const output_link& link) const NO auto hash = get_tx_key(tx); const auto index = to_output_index(tx, link); if ((index == point::null_index) || (hash == system::null_hash) || - !get_tx_position(position, tx, block)) + !get_tx_position(position, strong, block)) return {}; return { { { std::move(hash), index }, out.value }, at.value, position }; diff --git a/include/bitcoin/database/impl/query/consensus/consensus_strong.ipp b/include/bitcoin/database/impl/query/consensus/consensus_strong.ipp index 733cd79d6..3ab1b7d72 100644 --- a/include/bitcoin/database/impl/query/consensus/consensus_strong.ipp +++ b/include/bitcoin/database/impl/query/consensus/consensus_strong.ipp @@ -82,21 +82,44 @@ height_link CLASS::find_strong_spender_height( TEMPLATE header_link CLASS::find_strong(const tx_link& link) const NOEXCEPT +{ + tx_link unused{}; + return find_strong(unused, link); +} + +TEMPLATE +header_link CLASS::find_strong(tx_link& out, const tx_link& link) const NOEXCEPT { // Shortcuircuit hash-based search by testing self. if (const auto fk = to_block(link); !fk.is_terminal()) + { + out = link; return fk; + } - return find_strong(get_tx_key(link)); + return find_strong(out, get_tx_key(link)); } TEMPLATE header_link CLASS::find_strong(const hash_digest& tx_hash) const NOEXCEPT { - // Find the first strong tx of the set and return its block. + tx_link unused{}; + return find_strong(unused, tx_hash); +} + +TEMPLATE +header_link CLASS::find_strong(tx_link& out, + const hash_digest& tx_hash) const NOEXCEPT +{ + // Find the first strong tx of the set and return it with its block. for (const auto& tx: to_duplicates(tx_hash)) + { if (const auto block = to_block(tx); !block.is_terminal()) + { + out = tx; return block; + } + } return {}; } diff --git a/include/bitcoin/database/impl/query/properties_tx.ipp b/include/bitcoin/database/impl/query/properties_tx.ipp index 1248ee655..e94a9fbc8 100644 --- a/include/bitcoin/database/impl/query/properties_tx.ipp +++ b/include/bitcoin/database/impl/query/properties_tx.ipp @@ -72,7 +72,9 @@ bool CLASS::get_tx_height(size_t& out, const tx_link& link) const NOEXCEPT TEMPLATE bool CLASS::get_tx_position(size_t& out, const tx_link& link) const NOEXCEPT { - return get_tx_position(out, link, find_strong(link)); + tx_link strong{}; + const auto block = find_strong(strong, link); + return get_tx_position(out, strong, block); } TEMPLATE diff --git a/include/bitcoin/database/query.hpp b/include/bitcoin/database/query.hpp index 3cc6c7551..02236a99d 100644 --- a/include/bitcoin/database/query.hpp +++ b/include/bitcoin/database/query.hpp @@ -355,6 +355,9 @@ class query tx_link find_strong_tx(const hash_digest& tx_hash) const NOEXCEPT; header_link find_strong(const tx_link& link) const NOEXCEPT; header_link find_strong(const hash_digest& tx_hash) const NOEXCEPT; + header_link find_strong(tx_link& out, const tx_link& link) const NOEXCEPT; + header_link find_strong(tx_link& out, + const hash_digest& tx_hash) const NOEXCEPT; height_link find_strong_spender_height(const point& point) const NOEXCEPT; header_link to_parent(const header_link& link) const NOEXCEPT; diff --git a/test/query/address/address_balance.cpp b/test/query/address/address_balance.cpp index 25d0a879c..9d2a14bf1 100644 --- a/test/query/address/address_balance.cpp +++ b/test/query/address/address_balance.cpp @@ -112,4 +112,37 @@ BOOST_AUTO_TEST_CASE(query_address__get_balance__block1a_address1_confirmed_bloc BOOST_REQUIRE_EQUAL(unconfirmed, 0u); } +// duplicate instances (one tx archived twice, only one instance associated) + +BOOST_AUTO_TEST_CASE(query_address__get_balance__duplicate_instances__counted_once) +{ + 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(!store.is_pooling()); + 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.push_confirmed(1, false)); + BOOST_REQUIRE(query.set(test::tx4)); + BOOST_REQUIRE(query.set(test::block_spend_1a, context{ 0, 2, 0 }, {}, false, false)); + BOOST_REQUIRE(query.set_strong(2)); + BOOST_REQUIRE(query.push_confirmed(2, false)); + BOOST_REQUIRE_EQUAL(query.to_duplicates(test::tx4.hash(false)).size(), 2u); + + // block1a output 0 is spent by tx4, whose 0x08 output to the address is unspent. + uint64_t confirmed{}; + uint64_t unconfirmed{}; + const std::atomic_bool cancel{}; + BOOST_REQUIRE_EQUAL(query.get_balance(cancel, confirmed, unconfirmed, test::block1a_address0, true), error::success); + BOOST_REQUIRE_EQUAL(confirmed, 0x08u); + BOOST_REQUIRE_EQUAL(unconfirmed, 0u); + + uint64_t balance{}; + BOOST_REQUIRE_EQUAL(query.get_confirmed_balance(cancel, balance, test::block1a_address0, true), error::success); + BOOST_REQUIRE_EQUAL(balance, 0x08u); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/query/address/address_history.cpp b/test/query/address/address_history.cpp index 0facaf0cb..3149d9ae6 100644 --- a/test/query/address/address_history.cpp +++ b/test/query/address/address_history.cpp @@ -569,4 +569,82 @@ BOOST_AUTO_TEST_CASE(query_address__get_spenders_history__confirmed__expected) BOOST_CHECK_EQUAL(histories.at(3).tx.hash(), test::tx4.hash(false)); } +// duplicate instances (one tx archived twice, only one instance associated) + +BOOST_AUTO_TEST_CASE(query_address__get_tx_history__duplicate_instances__same_entry) +{ + 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(!store.is_pooling()); + 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.push_confirmed(1, false)); + BOOST_REQUIRE(query.set(test::tx4)); + const auto loose = 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(query.push_confirmed(2, false)); + const auto stored = query.get_position_tx(query.to_header(test::block_spend_1a.hash()), 0); + BOOST_REQUIRE(stored != loose); + + const auto expected = query.get_tx_history(stored); + BOOST_REQUIRE(expected.valid()); + BOOST_REQUIRE(expected.confirmed()); + BOOST_REQUIRE_EQUAL(expected.tx.height(), 2u); + BOOST_REQUIRE_EQUAL(expected.position, 0u); + + const auto duplicate = query.get_tx_history(loose); + BOOST_REQUIRE(!duplicate.fault()); + BOOST_REQUIRE(duplicate.valid()); + BOOST_REQUIRE(duplicate == expected); + BOOST_REQUIRE_EQUAL(duplicate.tx.height(), 2u); + BOOST_REQUIRE_EQUAL(duplicate.position, 0u); + + const auto confirmed = query.get_tx_confirmed_history(loose); + BOOST_REQUIRE(!confirmed.fault()); + BOOST_REQUIRE(confirmed == expected); +} + +BOOST_AUTO_TEST_CASE(query_address__get_history__duplicate_instances__deduplicated) +{ + 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(!store.is_pooling()); + 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.push_confirmed(1, false)); + BOOST_REQUIRE(query.set(test::tx4)); + BOOST_REQUIRE(query.set(test::block_spend_1a, context{ 0, 2, 0 }, {}, false, false)); + BOOST_REQUIRE(query.set_strong(2)); + BOOST_REQUIRE(query.push_confirmed(2, false)); + BOOST_REQUIRE_EQUAL(query.to_duplicates(test::tx4.hash(false)).size(), 2u); + + histories out{}; + height_link cursor{}; + const std::atomic_bool cancel{}; + BOOST_REQUIRE_EQUAL(query.get_history(cancel, cursor, out, test::block1a_address0, max_size_t, true), error::success); + + // block1a coinbase (funds address0) and tx4 (spends it, pays address0), once each. + BOOST_REQUIRE_EQUAL(out.size(), 2u); + BOOST_REQUIRE_EQUAL(out.at(0).tx.hash(), test::block1a.transactions_ptr()->at(0)->hash(false)); + BOOST_REQUIRE_EQUAL(out.at(0).tx.height(), 1u); + BOOST_REQUIRE_EQUAL(out.at(1).tx.hash(), test::tx4.hash(false)); + BOOST_REQUIRE_EQUAL(out.at(1).tx.height(), 2u); + BOOST_REQUIRE_EQUAL(out.at(1).position, 0u); + + histories confirmed{}; + height_link confirmed_cursor{}; + BOOST_REQUIRE_EQUAL(query.get_confirmed_history(cancel, confirmed_cursor, confirmed, test::block1a_address0, max_size_t, true), error::success); + BOOST_REQUIRE_EQUAL(confirmed.size(), 2u); + BOOST_REQUIRE_EQUAL(confirmed.at(1).tx.hash(), test::tx4.hash(false)); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/query/address/address_unspent.cpp b/test/query/address/address_unspent.cpp index 3f853bae4..f58011337 100644 --- a/test/query/address/address_unspent.cpp +++ b/test/query/address/address_unspent.cpp @@ -171,4 +171,45 @@ BOOST_AUTO_TEST_CASE(query_address__get_unspent__turbo_genesis__expected) BOOST_CHECK_EQUAL(out.at(5).out.point().hash(), test::block2b.transactions_ptr()->at(0)->hash(false)); } +// duplicate instances (one tx archived twice, only one instance associated) + +BOOST_AUTO_TEST_CASE(query_address__get_unspent__duplicate_instances__deduplicated) +{ + 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(!store.is_pooling()); + 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.push_confirmed(1, false)); + BOOST_REQUIRE(query.set(test::tx4)); + BOOST_REQUIRE(query.set(test::block_spend_1a, context{ 0, 2, 0 }, {}, false, false)); + BOOST_REQUIRE(query.set_strong(2)); + BOOST_REQUIRE(query.push_confirmed(2, false)); + BOOST_REQUIRE_EQUAL(query.to_duplicates(test::tx4.hash(false)).size(), 2u); + + // block1a output 0 is spent by tx4, whose 0x08 output to the address is unspent. + unspent_outputs out{}; + const std::atomic_bool cancel{}; + BOOST_REQUIRE_EQUAL(query.get_unspent(cancel, out, test::block1a_address0, true), error::success); + BOOST_REQUIRE_EQUAL(out.size(), 1u); + BOOST_REQUIRE_EQUAL(out.at(0).out.point().hash(), test::tx4.hash(false)); + BOOST_REQUIRE_EQUAL(out.at(0).out.point().index(), 0u); + BOOST_REQUIRE_EQUAL(out.at(0).out.value(), 0x08u); + BOOST_REQUIRE_EQUAL(out.at(0).height, 2u); + BOOST_REQUIRE_EQUAL(out.at(0).position, 0u); + + unspent_outputs confirmed{}; + BOOST_REQUIRE_EQUAL(query.get_confirmed_unspent(cancel, confirmed, test::block1a_address0, true), error::success); + BOOST_REQUIRE_EQUAL(confirmed.size(), 1u); + BOOST_REQUIRE_EQUAL(confirmed.at(0).out.point().hash(), test::tx4.hash(false)); + + unspent_outputs unconfirmed{}; + BOOST_REQUIRE_EQUAL(query.get_unconfirmed_unspent(cancel, unconfirmed, test::block1a_address0, true), error::success); + BOOST_REQUIRE(unconfirmed.empty()); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/query/consensus/consensus_strong.cpp b/test/query/consensus/consensus_strong.cpp index 652992d55..2d40268df 100644 --- a/test/query/consensus/consensus_strong.cpp +++ b/test/query/consensus/consensus_strong.cpp @@ -211,4 +211,69 @@ BOOST_AUTO_TEST_CASE(query_consensus__set_strong__set_unstrong__expected) BOOST_REQUIRE(!query.is_confirmed_output(query.to_output(2, 0))); } +// duplicate instances (one tx archived twice, only one instance associated) + +BOOST_AUTO_TEST_CASE(query_consensus__duplicate_instances__navigation__expected) +{ + settings settings{}; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + + class accessor + : public test::query_accessor + { + public: + using test::query_accessor::query_accessor; + using test::query_accessor::to_block; + }; + + accessor query{ store }; + BOOST_REQUIRE(!store.create(test::events_handler)); + BOOST_REQUIRE(!store.is_pooling()); + 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.push_confirmed(1, false)); + + BOOST_REQUIRE(query.set(test::tx4)); + const auto loose = query.to_tx(test::tx4.hash(false)); + BOOST_REQUIRE(!loose.is_terminal()); + + BOOST_REQUIRE(query.set(test::block_spend_1a, context{ 0, 2, 0 }, {}, false, false)); + BOOST_REQUIRE(query.set_strong(2)); + BOOST_REQUIRE(query.push_confirmed(2, false)); + + const auto block = query.to_header(test::block_spend_1a.hash()); + const auto stored = query.get_position_tx(block, 0); + BOOST_REQUIRE(!stored.is_terminal()); + BOOST_REQUIRE(stored != loose); + BOOST_REQUIRE_EQUAL(query.get_tx_key(stored), test::tx4.hash(false)); + BOOST_REQUIRE_EQUAL(query.to_duplicates(test::tx4.hash(false)).size(), 2u); + + // The association is instance-exact. + BOOST_REQUIRE(query.to_block(loose).is_terminal()); + BOOST_REQUIRE_EQUAL(query.to_block(stored), block); + + // Strong resolution is by identity, from any instance. + BOOST_REQUIRE_EQUAL(query.find_strong(loose), block); + BOOST_REQUIRE_EQUAL(query.find_strong(stored), block); + BOOST_REQUIRE_EQUAL(query.find_strong_tx(loose), stored); + BOOST_REQUIRE_EQUAL(query.find_strong_tx(stored), stored); + BOOST_REQUIRE(query.is_confirmed_tx(loose)); + BOOST_REQUIRE(query.is_confirmed_tx(stored)); + + // Confirmed properties are of the identity, from any instance. + size_t height{}; + BOOST_REQUIRE(query.get_tx_height(height, loose)); + BOOST_REQUIRE_EQUAL(height, 2u); + BOOST_REQUIRE(query.get_tx_height(height, stored)); + BOOST_REQUIRE_EQUAL(height, 2u); + + size_t position{}; + BOOST_REQUIRE(query.get_tx_position(position, stored)); + BOOST_REQUIRE_EQUAL(position, 0u); + BOOST_REQUIRE(query.get_tx_position(position, loose)); + BOOST_REQUIRE_EQUAL(position, 0u); +} + BOOST_AUTO_TEST_SUITE_END()