From 871c5390c8191b2d3024da2b4fde04994057f50b Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sat, 11 Apr 2026 02:12:33 -0400 Subject: [PATCH 01/10] Comments. --- include/bitcoin/database/error.hpp | 1 - src/error.cpp | 1 - test/error.cpp | 9 --------- 3 files changed, 11 deletions(-) diff --git a/include/bitcoin/database/error.hpp b/include/bitcoin/database/error.hpp index ce0392996..a8e915c0f 100644 --- a/include/bitcoin/database/error.hpp +++ b/include/bitcoin/database/error.hpp @@ -45,7 +45,6 @@ enum error_t : uint8_t integrity_get_prevouts, integrity_block_confirmable1, integrity_block_confirmable2, - ////integrity_spent_duplicates, /// memory map open_open, diff --git a/src/error.cpp b/src/error.cpp index 1e2d82a15..c695a7898 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -37,7 +37,6 @@ DEFINE_ERROR_T_MESSAGE_MAP(error) { integrity_get_prevouts, "store corrupted, get_prevouts" }, { integrity_block_confirmable1, "store corrupted, block_confirmable1" }, { integrity_block_confirmable2, "store corrupted, block_confirmable2" }, - ////{ integrity_spent_duplicates, "store corrupted, spent_duplicates" }, // memory map { open_open, "opening open file" }, diff --git a/test/error.cpp b/test/error.cpp index af549976f..3bc633f51 100644 --- a/test/error.cpp +++ b/test/error.cpp @@ -97,15 +97,6 @@ BOOST_AUTO_TEST_CASE(error_t__code__integrity_block_confirmable2__true_expected_ BOOST_REQUIRE_EQUAL(ec.message(), "store corrupted, block_confirmable2"); } -////BOOST_AUTO_TEST_CASE(error_t__code__integrity_spent_duplicates__true_expected_message) -////{ -//// constexpr auto value = error::integrity_spent_duplicates; -//// const auto ec = code(value); -//// BOOST_REQUIRE(ec); -//// BOOST_REQUIRE(ec == value); -//// BOOST_REQUIRE_EQUAL(ec.message(), "store corrupted, spent_duplicates"); -////} - // memory map BOOST_AUTO_TEST_CASE(error_t__code__open_open__true_expected_message) From d35e31ce44a5aa84d49bf150b4dca7a71b94cd8f Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sat, 11 Apr 2026 18:26:48 -0400 Subject: [PATCH 02/10] Address enumerations --- .../bitcoin/database/impl/primitives/keys.ipp | 6 +- .../impl/query/address/address_balance.ipp | 8 +- .../impl/query/address/address_history.ipp | 56 +++++++- .../impl/query/address/address_outpoints.ipp | 16 +-- .../impl/query/address/address_unspent.ipp | 9 +- .../bitcoin/database/impl/query/amounts.ipp | 10 +- .../impl/query/archive/chain_reader.ipp | 2 +- .../bitcoin/database/impl/query/confirmed.ipp | 15 ++- .../impl/query/consensus/consensus_block.ipp | 4 +- .../query/consensus/consensus_compact.ipp | 2 +- .../query/consensus/consensus_prevouts.ipp | 2 +- .../impl/query/consensus/consensus_strong.ipp | 4 +- .../bitcoin/database/impl/query/extent.ipp | 4 +- .../bitcoin/database/impl/query/fee_rate.ipp | 8 +- .../impl/query/navigate/navigate_forward.ipp | 2 +- .../impl/query/navigate/navigate_reverse.ipp | 64 +++++++++- include/bitcoin/database/query.hpp | 46 +++---- .../database/tables/archives/input.hpp | 4 +- .../bitcoin/database/tables/archives/ins.hpp | 2 +- .../database/tables/archives/output.hpp | 17 +-- .../bitcoin/database/tables/archives/outs.hpp | 37 +++--- .../bitcoin/database/tables/archives/txs.hpp | 2 +- .../database/tables/caches/prevout.hpp | 4 +- test/mocks/blocks.cpp | 4 +- test/mocks/blocks.hpp | 2 + test/query/address/address_balance.cpp | 4 +- test/query/address/address_history.cpp | 16 +-- test/query/address/address_outpoints.cpp | 16 +-- test/query/address/address_unspent.cpp | 4 +- test/query/navigate/navigate_reverse.cpp | 120 +++++++++++++++++- 30 files changed, 358 insertions(+), 132 deletions(-) diff --git a/include/bitcoin/database/impl/primitives/keys.ipp b/include/bitcoin/database/impl/primitives/keys.ipp index 697d1f48e..d095aeafe 100644 --- a/include/bitcoin/database/impl/primitives/keys.ipp +++ b/include/bitcoin/database/impl/primitives/keys.ipp @@ -94,7 +94,7 @@ INLINE uint64_t hash(const Key& key) NOEXCEPT constexpr auto bytes = std::min(size(), sizeof(uint64_t)); uint64_t hash{}; - std::copy_n(key.begin(), bytes, byte_cast(hash).begin()); + std::copy_n(key.cbegin(), bytes, byte_cast(hash).begin()); return hash; } } @@ -123,7 +123,7 @@ INLINE uint64_t thumb(const Key& key) NOEXCEPT constexpr auto offset = std::min(size() - bytes, bytes); uint64_t hash{}; - const auto start = std::next(key.begin(), offset); + const auto start = std::next(key.cbegin(), offset); std::copy_n(start, bytes, byte_cast(hash).begin()); return hash; } @@ -160,7 +160,7 @@ INLINE bool compare(const Array& bytes, const Key& key) NOEXCEPT } else if constexpr (is_std_array) { - return std::equal(bytes.begin(), bytes.end(), key.begin()); + return std::equal(bytes.cbegin(), bytes.cend(), key.begin()); } } diff --git a/include/bitcoin/database/impl/query/address/address_balance.ipp b/include/bitcoin/database/impl/query/address/address_balance.ipp index c2ab63771..a594d8416 100644 --- a/include/bitcoin/database/impl/query/address/address_balance.ipp +++ b/include/bitcoin/database/impl/query/address/address_balance.ipp @@ -33,7 +33,7 @@ namespace database { // unused TEMPLATE -code CLASS::get_unconfirmed_balance(stopper& cancel, uint64_t& out, +code CLASS::get_unconfirmed_balance(const stopper& cancel, uint64_t& out, const hash_digest& key, bool turbo) const NOEXCEPT { // While duplicates are easily filtered out, conflict resolution is murky. @@ -46,7 +46,7 @@ code CLASS::get_unconfirmed_balance(stopper& cancel, uint64_t& out, // server/native TEMPLATE -code CLASS::get_confirmed_balance(stopper& cancel, uint64_t& out, +code CLASS::get_confirmed_balance(const stopper& cancel, uint64_t& out, const hash_digest& key, bool turbo) const NOEXCEPT { outpoints outs{}; @@ -57,7 +57,7 @@ code CLASS::get_confirmed_balance(stopper& cancel, uint64_t& out, } // Use of to_confirmed_unspent_outputs() provides necessary deduplication. - out = std::accumulate(outs.begin(), outs.end(), zero, + out = std::accumulate(outs.cbegin(), outs.cend(), zero, [](size_t total, const outpoint& out) NOEXCEPT { return system::ceilinged_add(total, out.value()); @@ -68,7 +68,7 @@ code CLASS::get_confirmed_balance(stopper& cancel, uint64_t& out, // server/electrum TEMPLATE -code CLASS::get_balance(stopper& cancel, uint64_t& confirmed, +code CLASS::get_balance(const stopper& cancel, uint64_t& confirmed, uint64_t& combined, const hash_digest& key, bool turbo) const NOEXCEPT { // See notes on get_unconfirmed_balance(). diff --git a/include/bitcoin/database/impl/query/address/address_history.ipp b/include/bitcoin/database/impl/query/address/address_history.ipp index 8c5b04b1d..143bcee3b 100644 --- a/include/bitcoin/database/impl/query/address/address_history.ipp +++ b/include/bitcoin/database/impl/query/address/address_history.ipp @@ -30,29 +30,73 @@ namespace database { // Address history // ---------------------------------------------------------------------------- // Canonically-sorted/deduped address history. - // root txs (height:zero) sorted before transitive (height:max) txs. // tied-height transactions sorted by base16 txid (not converted). +// All confirmed txs are root, unconfirmed may or may not not be root. +// server/electrum TEMPLATE -code CLASS::get_unconfirmed_history(stopper& , histories& , +code CLASS::get_unconfirmed_history(const stopper& , histories& , const hash_digest& , bool ) const NOEXCEPT { return {}; } +// ununsed TEMPLATE -code CLASS::get_confirmed_history(stopper& , histories& , +code CLASS::get_confirmed_history(const stopper& , histories& , const hash_digest& , bool ) const NOEXCEPT { return {}; } +// server/electrum TEMPLATE -code CLASS::get_history(stopper& , histories& , - const hash_digest& , bool ) const NOEXCEPT +code CLASS::get_history(const stopper& cancel, histories& out, + const hash_digest& key, bool /* turbo */) const NOEXCEPT { - return {}; + output_links outs{}; + if (const auto ec = to_address_outputs(cancel, outs, key)) + return ec; + + tx_links txs{}; + if (const auto ec = to_touched_txs(cancel, txs, outs)) + return ec; + + // TODO: parallel transform (requires vector), then sort/dedup. + out.clear(); + for (const auto& tx: txs) + { + if (cancel) + return error::canceled; + + // Handles terminal tx values. + auto hash = get_tx_key(tx); + if (hash == system::null_hash) + return error::integrity; + + // Possibly missing prevout. + uint64_t fee{}; + if (!get_tx_fee(fee, tx)) + return error::integrity; + + // Optimized by sharing strong across both subqueries. + size_t height{}, position{}; + if (const auto strong = find_strong(tx); !strong.is_terminal()) + { + if (!get_height(height, strong) || + !get_tx_position(position, tx, strong)) + return error::integrity; + } + else + { + height = is_confirmed_all_prevouts(tx) ? zero : max_size_t; + } + + out.insert({ { std::move(hash), height }, fee, position }); + } + + return error::success; } // turbos diff --git a/include/bitcoin/database/impl/query/address/address_outpoints.ipp b/include/bitcoin/database/impl/query/address/address_outpoints.ipp index bc6fcd4d6..f6b63619b 100644 --- a/include/bitcoin/database/impl/query/address/address_outpoints.ipp +++ b/include/bitcoin/database/impl/query/address/address_outpoints.ipp @@ -33,7 +33,7 @@ namespace database { // server/native TEMPLATE -code CLASS::get_confirmed_unspent_outputs(stopper& cancel, +code CLASS::get_confirmed_unspent_outputs(const stopper& cancel, outpoints& out, const hash_digest& key, bool turbo) const NOEXCEPT { out.clear(); @@ -52,13 +52,11 @@ code CLASS::get_confirmed_unspent_outputs(stopper& cancel, fail = outpoint.point().is_null(); return outpoint; }); - - return error::success; } // unused TEMPLATE -code CLASS::get_minimum_unspent_outputs(stopper& cancel, +code CLASS::get_minimum_unspent_outputs(const stopper& cancel, outpoints& out, const hash_digest& key, uint64_t minimum, bool turbo) const NOEXCEPT { @@ -89,13 +87,11 @@ code CLASS::get_minimum_unspent_outputs(stopper& cancel, fail = outpoint.point().is_null(); return outpoint; }); - - return error::success; } // server/native TEMPLATE -code CLASS::get_address_outputs(stopper& cancel, outpoints& out, +code CLASS::get_address_outputs(const stopper& cancel, outpoints& out, const hash_digest& key, bool turbo) const NOEXCEPT { out.clear(); @@ -111,8 +107,6 @@ code CLASS::get_address_outputs(stopper& cancel, outpoints& out, fail = outpoint.point().is_null(); return outpoint; }); - - return error::success; } // utilities @@ -121,14 +115,14 @@ code CLASS::get_address_outputs(stopper& cancel, outpoints& out, TEMPLATE template -inline code CLASS::parallel_address_transform(stopper& cancel, bool turbo, +inline code CLASS::parallel_address_transform(const stopper& cancel, bool turbo, outpoints& out, const output_links& links, Functor&& functor) NOEXCEPT { out.clear(); stopper fail{}; std::vector outpoints(links.size()); const auto policy = poolstl::execution::par_if(turbo); - std::transform(policy, links.begin(), links.end(), outpoints.begin(), + std::transform(policy, links.cbegin(), links.cend(), outpoints.begin(), [&functor, &cancel, &fail](const auto& link) NOEXCEPT { return functor(link, cancel, fail); diff --git a/include/bitcoin/database/impl/query/address/address_unspent.ipp b/include/bitcoin/database/impl/query/address/address_unspent.ipp index 2d01cac64..2a29b54b0 100644 --- a/include/bitcoin/database/impl/query/address/address_unspent.ipp +++ b/include/bitcoin/database/impl/query/address/address_unspent.ipp @@ -32,22 +32,25 @@ namespace database { // A list of all unspent output transactions in canonical order. // Unconfirmed unspent are included at end of list in consistent order. +// ununsed TEMPLATE -code CLASS::get_unconfirmed_unspent(stopper& , unspents& , +code CLASS::get_unconfirmed_unspent(const stopper& , unspents& , const hash_digest& , bool ) const NOEXCEPT { return {}; } +// ununsed TEMPLATE -code CLASS::get_confirmed_unspent(stopper& , unspents& , +code CLASS::get_confirmed_unspent(const stopper& , unspents& , const hash_digest& , bool ) const NOEXCEPT { return {}; } +// server/electrum TEMPLATE -code CLASS::get_unspent(stopper& , unspents& , +code CLASS::get_unspent(const stopper& , unspents& , const hash_digest& , bool ) const NOEXCEPT { return {}; diff --git a/include/bitcoin/database/impl/query/amounts.ipp b/include/bitcoin/database/impl/query/amounts.ipp index d0d2b885e..be1e536ce 100644 --- a/include/bitcoin/database/impl/query/amounts.ipp +++ b/include/bitcoin/database/impl/query/amounts.ipp @@ -71,7 +71,7 @@ bool CLASS::get_tx_spend(uint64_t& out, const tx_link& link) const NOEXCEPT return !links.empty() && get_outputs_total_value(out, links); } -// unused (disabled in get_tx_fees()) +// server/electrum TEMPLATE bool CLASS::get_tx_fee(uint64_t& out, const tx_link& link) const NOEXCEPT { @@ -101,11 +101,11 @@ bool CLASS::get_block_value(uint64_t& out, return false; stopper fail{}; - const auto begin = std::next(txs.tx_fks.begin()); + const auto begin = std::next(txs.tx_fks.cbegin()); constexpr auto parallel = poolstl::execution::par; constexpr auto relaxed = std::memory_order_relaxed; - out = std::transform_reduce(parallel, begin, txs.tx_fks.end(), 0_u64, + out = std::transform_reduce(parallel, begin, txs.tx_fks.cend(), 0_u64, [](uint64_t left, uint64_t right) NOEXCEPT { return system::ceilinged_add(left, right); @@ -132,11 +132,11 @@ bool CLASS::get_block_spend(uint64_t& out, return false; stopper fail{}; - const auto begin = std::next(txs.tx_fks.begin()); + const auto begin = std::next(txs.tx_fks.cbegin()); constexpr auto parallel = poolstl::execution::par; constexpr auto relaxed = std::memory_order_relaxed; - out = std::transform_reduce(parallel, begin, txs.tx_fks.end(), 0_u64, + out = std::transform_reduce(parallel, begin, txs.tx_fks.cend(), 0_u64, [](uint64_t left, uint64_t right) NOEXCEPT { return system::ceilinged_add(left, right); diff --git a/include/bitcoin/database/impl/query/archive/chain_reader.ipp b/include/bitcoin/database/impl/query/archive/chain_reader.ipp index 6845d643c..eba4ff2af 100644 --- a/include/bitcoin/database/impl/query/archive/chain_reader.ipp +++ b/include/bitcoin/database/impl/query/archive/chain_reader.ipp @@ -352,7 +352,7 @@ outpoint CLASS::get_outpoint(const output_link& link) const NOEXCEPT TEMPLATE inpoint CLASS::get_spender(const point_link& link) const NOEXCEPT { - const auto tx_fk = to_spending_tx(link); + const auto tx_fk = to_input_tx(link); if (tx_fk.is_terminal()) return {}; diff --git a/include/bitcoin/database/impl/query/confirmed.ipp b/include/bitcoin/database/impl/query/confirmed.ipp index 732c1e8ee..9f8b8214f 100644 --- a/include/bitcoin/database/impl/query/confirmed.ipp +++ b/include/bitcoin/database/impl/query/confirmed.ipp @@ -98,7 +98,7 @@ TEMPLATE bool CLASS::is_confirmed_input(const point_link& link) const NOEXCEPT { // The spend.tx is strong *and* its block is confirmed (by height). - const auto fk = to_spending_tx(link); + const auto fk = to_input_tx(link); return !fk.is_terminal() && is_confirmed_tx(fk); } @@ -115,7 +115,18 @@ bool CLASS::is_confirmed_spent_output(const output_link& link) const NOEXCEPT { // The spender is strong *and* its block is confirmed (by height). const auto ins = to_spenders(link); - return std::any_of(ins.begin(), ins.end(), [&](const auto& in) NOEXCEPT + return std::any_of(ins.cbegin(), ins.cend(), [&](const auto& in) NOEXCEPT + { + return is_confirmed_input(in); + }); +} + +TEMPLATE +bool CLASS::is_confirmed_all_prevouts(const tx_link& link) const NOEXCEPT +{ + // All prevouts of the tx's inputs are confirmed. + const auto ins = to_points(link); + return std::all_of(ins.cbegin(), ins.cend(), [&](const auto& in) NOEXCEPT { return is_confirmed_input(in); }); diff --git a/include/bitcoin/database/impl/query/consensus/consensus_block.ipp b/include/bitcoin/database/impl/query/consensus/consensus_block.ipp index 4f0d4bf48..a06c89d7c 100644 --- a/include/bitcoin/database/impl/query/consensus/consensus_block.ipp +++ b/include/bitcoin/database/impl/query/consensus/consensus_block.ipp @@ -56,7 +56,7 @@ code CLASS::block_confirmable(const header_link& link) const NOEXCEPT stopper fault{}; // Get points for each tx and the total count. - std::transform(parallel, txs.begin(), txs.end(), sets.begin(), + std::transform(parallel, txs.cbegin(), txs.cend(), sets.begin(), [this, &count, &fault](const tx_link& tx) NOEXCEPT { point_set set{}; @@ -80,7 +80,7 @@ code CLASS::block_confirmable(const header_link& link) const NOEXCEPT std::atomic consensus{}; // Checks all spends for spendability (strong, unlocked and mature). - if (std::all_of(parallel, sets.begin(), sets.end(), + if (std::all_of(parallel, sets.cbegin(), sets.cend(), [this, &ctx, &consensus](const point_set& set) NOEXCEPT { for (const auto& point: set.points) diff --git a/include/bitcoin/database/impl/query/consensus/consensus_compact.ipp b/include/bitcoin/database/impl/query/consensus/consensus_compact.ipp index 63e5b4982..1b116ed4b 100644 --- a/include/bitcoin/database/impl/query/consensus/consensus_compact.ipp +++ b/include/bitcoin/database/impl/query/consensus/consensus_compact.ipp @@ -65,7 +65,7 @@ bool CLASS::get_double_spenders(tx_links& out, if (txs.size() <= one) return true; - for (auto tx = std::next(txs.begin()); tx != txs.end(); ++tx) + for (auto tx = std::next(txs.cbegin()); tx != txs.cend(); ++tx) for (const auto& in: *(*tx)->inputs_ptr()) if (!get_double_spenders(out, in->point(), in->metadata.point_link)) return false; diff --git a/include/bitcoin/database/impl/query/consensus/consensus_prevouts.ipp b/include/bitcoin/database/impl/query/consensus/consensus_prevouts.ipp index b6a3356b1..2ca50d407 100644 --- a/include/bitcoin/database/impl/query/consensus/consensus_prevouts.ipp +++ b/include/bitcoin/database/impl/query/consensus/consensus_prevouts.ipp @@ -134,7 +134,7 @@ bool CLASS::get_doubles(tx_links& out, const block& block) const NOEXCEPT if (txs.size() <= one) return true; - for (auto tx = std::next(txs.begin()); tx != txs.end(); ++tx) + for (auto tx = std::next(txs.cbegin()); tx != txs.cend(); ++tx) for (const auto& in: *(*tx)->inputs_ptr()) if (!get_doubles(out, in->point())) return false; diff --git a/include/bitcoin/database/impl/query/consensus/consensus_strong.ipp b/include/bitcoin/database/impl/query/consensus/consensus_strong.ipp index 4a1fa2457..1c484b5c5 100644 --- a/include/bitcoin/database/impl/query/consensus/consensus_strong.ipp +++ b/include/bitcoin/database/impl/query/consensus/consensus_strong.ipp @@ -70,8 +70,8 @@ height_link CLASS::find_strong_spender_height( const point& point) const NOEXCEPT { size_t out{}; - for (const auto& sp: to_spenders(point)) - if (const auto tx = to_spending_tx(sp); get_tx_height(out, tx)) + 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) }; diff --git a/include/bitcoin/database/impl/query/extent.ipp b/include/bitcoin/database/impl/query/extent.ipp index 6139481dc..b330dab21 100644 --- a/include/bitcoin/database/impl/query/extent.ipp +++ b/include/bitcoin/database/impl/query/extent.ipp @@ -226,7 +226,7 @@ size_t CLASS::input_count(const tx_links& txs) const NOEXCEPT { constexpr auto parallel = poolstl::execution::par; const auto fn = [this](auto tx) NOEXCEPT { return input_count(tx); }; - return std::reduce(parallel, txs.begin(), txs.end(), zero, fn); + return std::reduce(parallel, txs.cbegin(), txs.cend(), zero, fn); } TEMPLATE @@ -234,7 +234,7 @@ size_t CLASS::output_count(const tx_links& txs) const NOEXCEPT { constexpr auto parallel = poolstl::execution::par; const auto fn = [this](auto tx) NOEXCEPT { return output_count(tx); }; - return std::reduce(parallel, txs.begin(), txs.end(), zero, fn); + return std::reduce(parallel, txs.cbegin(), txs.cend(), zero, fn); } TEMPLATE diff --git a/include/bitcoin/database/impl/query/fee_rate.ipp b/include/bitcoin/database/impl/query/fee_rate.ipp index 6b6c46fe7..a638b83cd 100644 --- a/include/bitcoin/database/impl/query/fee_rate.ipp +++ b/include/bitcoin/database/impl/query/fee_rate.ipp @@ -56,11 +56,11 @@ bool CLASS::get_block_fees(fee_rates& out, return false; out.resize(sub1(txs.tx_fks.size())); - const auto end = txs.tx_fks.end(); + const auto end = txs.tx_fks.cend(); auto rate = out.begin(); // Skip coinbase. - for (auto tx = std::next(txs.tx_fks.begin()); tx != end; ++tx) + for (auto tx = std::next(txs.tx_fks.cbegin()); tx != end; ++tx) if (!get_tx_fees(*rate++, *tx)) return false; @@ -68,7 +68,7 @@ bool CLASS::get_block_fees(fee_rates& out, } TEMPLATE -bool CLASS::get_branch_fees(stopper& cancel, fee_rate_sets& out, +bool CLASS::get_branch_fees(const stopper& cancel, fee_rate_sets& out, size_t start, size_t count) const NOEXCEPT { out.clear(); @@ -87,7 +87,7 @@ bool CLASS::get_branch_fees(stopper& cancel, fee_rate_sets& out, constexpr auto relaxed = std::memory_order_relaxed; // Parallel execution saves ~50%. - std::for_each(parallel, offsets.begin(), offsets.end(), + std::for_each(parallel, offsets.cbegin(), offsets.cend(), [&](const size_t& offset) NOEXCEPT { if (fail.load(relaxed)) diff --git a/include/bitcoin/database/impl/query/navigate/navigate_forward.ipp b/include/bitcoin/database/impl/query/navigate/navigate_forward.ipp index 4afe36a80..d931208e1 100644 --- a/include/bitcoin/database/impl/query/navigate/navigate_forward.ipp +++ b/include/bitcoin/database/impl/query/navigate/navigate_forward.ipp @@ -146,7 +146,7 @@ output_links CLASS::to_prevouts(const tx_links& txs) const NOEXCEPT output_links outs(ins.size()); constexpr auto parallel = poolstl::execution::par; - std::transform(parallel, ins.begin(), ins.end(), outs.begin(), + std::transform(parallel, ins.cbegin(), ins.cend(), outs.begin(), [&](const auto& spend) NOEXCEPT { return to_previous_output(spend); diff --git a/include/bitcoin/database/impl/query/navigate/navigate_reverse.ipp b/include/bitcoin/database/impl/query/navigate/navigate_reverse.ipp index 36f7038ad..3bfaf0c72 100644 --- a/include/bitcoin/database/impl/query/navigate/navigate_reverse.ipp +++ b/include/bitcoin/database/impl/query/navigate/navigate_reverse.ipp @@ -21,6 +21,7 @@ #include #include +#include #include #include @@ -44,6 +45,63 @@ header_link CLASS::to_parent(const header_link& link) const NOEXCEPT // address->outputs[receivers] // ---------------------------------------------------------------------------- +// There can be multiple spenders of the same output (due to conflicts) and +// multiple instances of an output under distinct links (due to tx dups). + +TEMPLATE +code CLASS::to_touched_txs(tx_links& out, + const output_links& outputs) const NOEXCEPT +{ + // Reserve one for each output tx and one for spending input tx (estimate). + out.clear(); + out.reserve(two * outputs.size()); + + // Orders are reversed due to expected tx_links reversal, for faster sort. + for (const auto& output: std::views::reverse(outputs)) + { + out.push_back(to_output_tx(output)); + if (out.back() == tx_link::terminal) + return error::integrity; + + for (const auto& input: std::views::reverse(to_spenders(output))) + { + out.push_back(to_input_tx(input)); + if (out.back() == tx_link::terminal) + return error::integrity; + } + } + + return {}; +} + +TEMPLATE +code CLASS::to_touched_txs(const stopper& cancel, tx_links& out, + const output_links& outputs) const NOEXCEPT +{ + // Reserve one for each output tx and one for spending input tx (estimate). + out.clear(); + out.reserve(two * outputs.size()); + + // Orders are reversed due to expected tx_links reversal, for faster sort. + for (const auto& output: std::views::reverse(outputs)) + { + if (cancel) + return error::canceled; + + out.push_back(to_output_tx(output)); + if (out.back() == tx_link::terminal) + return error::integrity; + + for (const auto& input: std::views::reverse(to_spenders(output))) + { + out.push_back(to_input_tx(input)); + if (out.back() == tx_link::terminal) + return error::integrity; + } + } + + return {}; +} TEMPLATE code CLASS::to_address_outputs(output_links& out, @@ -64,7 +122,7 @@ code CLASS::to_address_outputs(output_links& out, } TEMPLATE -code CLASS::to_address_outputs(stopper& cancel, output_links& out, +code CLASS::to_address_outputs(const stopper& cancel, output_links& out, const hash_digest& key) const NOEXCEPT { // Pushing into the vector is more efficient than precomputation of size. @@ -88,7 +146,7 @@ code CLASS::to_address_outputs(stopper& cancel, output_links& out, // ---------------------------------------------------------------------------- TEMPLATE -tx_link CLASS::to_spending_tx(const point_link& link) const NOEXCEPT +tx_link CLASS::to_input_tx(const point_link& link) const NOEXCEPT { table::ins::get_parent ins{}; if (!store_.ins.get(link, ins)) @@ -148,7 +206,7 @@ TEMPLATE point_links CLASS::to_spenders(const point& point) const NOEXCEPT { // Avoid returning spend links for coinbase inputs (not spenders). - if (point.index() == point::null_index) + if (point.is_null()) return {}; point_links points{}; diff --git a/include/bitcoin/database/query.hpp b/include/bitcoin/database/query.hpp index 1688fa4a0..01cb73099 100644 --- a/include/bitcoin/database/query.hpp +++ b/include/bitcoin/database/query.hpp @@ -246,7 +246,7 @@ class query /// put to tx (reverse navigation) tx_link to_output_tx(const output_link& link) const NOEXCEPT; tx_link to_prevout_tx(const point_link& link) const NOEXCEPT; - tx_link to_spending_tx(const point_link& link) const NOEXCEPT; + tx_link to_input_tx(const point_link& link) const NOEXCEPT; /// point to put (forward navigation) point_link to_point(const tx_link& link, @@ -311,10 +311,14 @@ class query point_link top_point(size_t bucket) const NOEXCEPT; tx_link top_tx(size_t bucket) const NOEXCEPT; - /// outputs enumeration + /// address outputs enumeration (reverse navigation) + code to_touched_txs(tx_links& out, + const output_links& outputs) const NOEXCEPT; + code to_touched_txs(const stopper& cancel, tx_links& out, + const output_links& outputs) const NOEXCEPT; code to_address_outputs(output_links& out, const hash_digest& key) const NOEXCEPT; - code to_address_outputs(stopper& cancel, output_links& out, + code to_address_outputs(const stopper& cancel, output_links& out, const hash_digest& key) const NOEXCEPT; /// Archive reads. @@ -438,7 +442,7 @@ class query /// Fee rate tuples by tx, block or branch. bool get_tx_fees(fee_rate& out, const tx_link& link) const NOEXCEPT; bool get_block_fees(fee_rates& out, const header_link& link) const NOEXCEPT; - bool get_branch_fees(stopper& cancel, fee_rate_sets& out, size_t start, + bool get_branch_fees(const stopper& cancel, fee_rate_sets& out, size_t start, size_t count) const NOEXCEPT; /// Merkle. @@ -551,6 +555,7 @@ class query bool is_confirmed_input(const point_link& link) const NOEXCEPT; bool is_confirmed_output(const output_link& link) const NOEXCEPT; bool is_confirmed_spent_output(const output_link& link) const NOEXCEPT; + bool is_confirmed_all_prevouts(const tx_link& link) const NOEXCEPT; /// Height index not used by these. bool is_strong_tx(const tx_link& link) const NOEXCEPT; @@ -612,37 +617,37 @@ class query /// ----------------------------------------------------------------------- /// Native queries (outpoints, deduped, arbitrary sort). - ////code get_unconfirmed_unspent_outputs(stopper& cancel, outpoints& out, + ////code get_unconfirmed_unspent_outputs(const stopper& cancel, outpoints& out, //// const hash_digest& key, bool turbo=false) const NOEXCEPT; - code get_confirmed_unspent_outputs(stopper& cancel, outpoints& out, + code get_confirmed_unspent_outputs(const stopper& cancel, outpoints& out, const hash_digest& key, bool turbo=false) const NOEXCEPT; - code get_minimum_unspent_outputs(stopper& cancel, outpoints& out, + code get_minimum_unspent_outputs(const stopper& cancel, outpoints& out, const hash_digest& key, uint64_t value, bool turbo=false) const NOEXCEPT; - code get_address_outputs(stopper& cancel, outpoints& out, + code get_address_outputs(const stopper& cancel, outpoints& out, const hash_digest& key, bool turbo=false) const NOEXCEPT; /// Electrum queries (histories, deduped, electrum sort). - code get_unconfirmed_history(stopper& cancel, histories& out, + code get_unconfirmed_history(const stopper& cancel, histories& out, const hash_digest& key, bool turbo=false) const NOEXCEPT; - code get_confirmed_history(stopper& cancel, histories& out, + code get_confirmed_history(const stopper& cancel, histories& out, const hash_digest& key, bool turbo=false) const NOEXCEPT; - code get_history(stopper& cancel, histories& out, + code get_history(const stopper& cancel, histories& out, const hash_digest& key, bool turbo=false) const NOEXCEPT; /// Electrum queries (unspents, deduped, electrum sort). - code get_unconfirmed_unspent(stopper& cancel, unspents& out, + code get_unconfirmed_unspent(const stopper& cancel, unspents& out, const hash_digest& key, bool turbo=false) const NOEXCEPT; - code get_confirmed_unspent(stopper& cancel, unspents& out, + code get_confirmed_unspent(const stopper& cancel, unspents& out, const hash_digest& key, bool turbo=false) const NOEXCEPT; - code get_unspent(stopper& cancel, unspents& out, + code get_unspent(const stopper& cancel, unspents& out, const hash_digest& key, bool turbo=false) const NOEXCEPT; /// Balance queries (universal, unconfirmed conflict resolution arbitrary). - code get_unconfirmed_balance(stopper& cancel, uint64_t& out, + code get_unconfirmed_balance(const stopper& cancel, uint64_t& out, const hash_digest& key, bool turbo=false) const NOEXCEPT; - code get_confirmed_balance(stopper& cancel, uint64_t& out, + code get_confirmed_balance(const stopper& cancel, uint64_t& out, const hash_digest& key, bool turbo=false) const NOEXCEPT; - code get_balance(stopper& cancel, uint64_t& confirmed, uint64_t& combined, + code get_balance(const stopper& cancel, uint64_t& confirmed, uint64_t& combined, const hash_digest& key, bool turbo=false) const NOEXCEPT; /// Filters. @@ -720,11 +725,6 @@ class query /// Consensus. /// ----------------------------------------------------------------------- - /// Called by block_confirmable (check bip30) - bool is_spent_coinbase(const tx_link& link, size_t count) const NOEXCEPT; - code spent_duplicates(const header_link& coinbase, - const context& ctx) const NOEXCEPT; - /// Called by block_confirmable (populate and check double spends). system::error::transaction_error_t spendable(const point_set::point& point, uint32_t version, const context& ctx) const NOEXCEPT; @@ -819,7 +819,7 @@ class query static inline bool push_bool(std_vector& stack, const Bool& element) NOEXCEPT; template - static inline code parallel_address_transform(stopper& cancel, bool turbo, + static inline code parallel_address_transform(const stopper& cancel, bool turbo, outpoints& out, const output_links& links, Functor&& functor) NOEXCEPT; static inline point::cptr make_point(hash_digest&& hash, uint32_t index) NOEXCEPT; diff --git a/include/bitcoin/database/tables/archives/input.hpp b/include/bitcoin/database/tables/archives/input.hpp index 3a4fb6e10..2c0b50391 100644 --- a/include/bitcoin/database/tables/archives/input.hpp +++ b/include/bitcoin/database/tables/archives/input.hpp @@ -149,7 +149,7 @@ struct input const auto& ins = *tx_.inputs_ptr(); const auto other = ins.size() * sequence_point_size; - const auto inputs = std::accumulate(ins.begin(), ins.end(), zero, + const auto inputs = std::accumulate(ins.cbegin(), ins.cend(), zero, [](size_t total, const auto& in) NOEXCEPT { // sizes cached, so this is free. @@ -163,7 +163,7 @@ struct input inline bool to_data(flipper& sink) const NOEXCEPT { const auto& ins = *tx_.inputs_ptr(); - std::for_each(ins.begin(), ins.end(), [&](const auto& in) NOEXCEPT + std::for_each(ins.cbegin(), ins.cend(), [&](const auto& in) NOEXCEPT { in->script().to_data(sink, true); in->witness().to_data(sink, true); diff --git a/include/bitcoin/database/tables/archives/ins.hpp b/include/bitcoin/database/tables/archives/ins.hpp index 834b482dd..8d00da0f0 100644 --- a/include/bitcoin/database/tables/archives/ins.hpp +++ b/include/bitcoin/database/tables/archives/ins.hpp @@ -113,7 +113,7 @@ struct ins auto in_fk = input_fk; const auto& ins = *tx_.inputs_ptr(); - std::for_each(ins.begin(), ins.end(), [&](const auto& in) NOEXCEPT + std::for_each(ins.cbegin(), ins.cend(), [&](const auto& in) NOEXCEPT { sink.write_little_endian(in->sequence()); sink.write_little_endian(in_fk); diff --git a/include/bitcoin/database/tables/archives/output.hpp b/include/bitcoin/database/tables/archives/output.hpp index db6a21d4e..5b1a64fa8 100644 --- a/include/bitcoin/database/tables/archives/output.hpp +++ b/include/bitcoin/database/tables/archives/output.hpp @@ -200,8 +200,8 @@ struct output const auto& outs = *tx_.outputs_ptr(); const auto other = outs.size() * value_parent_difference; - const auto outputs = std::accumulate(outs.begin(), outs.end(), zero, - [](size_t total, const auto& out) NOEXCEPT + const auto outputs = std::accumulate(outs.cbegin(), outs.cend(), + zero, [](size_t total, const auto& out) NOEXCEPT { // size cached, so this is free, includes sizeof(value). return total + variable_size(out->value()) + @@ -216,12 +216,13 @@ struct output inline bool to_data(flipper& sink) const NOEXCEPT { const auto& outs = *tx_.outputs_ptr(); - std::for_each(outs.begin(), outs.end(), [&](const auto& out) NOEXCEPT - { - sink.write_little_endian(parent_fk); - sink.write_variable(out->value()); - out->script().to_data(sink, true); - }); + std::for_each(outs.cbegin(), outs.cend(), + [&](const auto& out) NOEXCEPT + { + sink.write_little_endian(parent_fk); + sink.write_variable(out->value()); + out->script().to_data(sink, true); + }); BC_ASSERT(!sink || sink.get_write_position() == count()); return sink; diff --git a/include/bitcoin/database/tables/archives/outs.hpp b/include/bitcoin/database/tables/archives/outs.hpp index c2886fb75..2159f3983 100644 --- a/include/bitcoin/database/tables/archives/outs.hpp +++ b/include/bitcoin/database/tables/archives/outs.hpp @@ -48,10 +48,11 @@ struct outs inline bool from_data(reader& source) NOEXCEPT { - std::for_each(out_fks.begin(), out_fks.end(), [&](auto& fk) NOEXCEPT - { - fk = source.read_little_endian(); - }); + std::for_each(out_fks.begin(), out_fks.end(), + [&](auto& fk) NOEXCEPT + { + fk = source.read_little_endian(); + }); BC_ASSERT(!source || source.get_read_position() == count() * out::size); return source; @@ -59,10 +60,11 @@ struct outs inline bool to_data(flipper& sink) const NOEXCEPT { - std::for_each(out_fks.begin(), out_fks.end(), [&](const auto& fk) NOEXCEPT - { - sink.write_little_endian(fk); - }); + std::for_each(out_fks.cbegin(), out_fks.cend(), + [&](const auto& fk) NOEXCEPT + { + sink.write_little_endian(fk); + }); BC_ASSERT(!sink || sink.get_write_position() == count() * minrow); return sink; @@ -109,15 +111,16 @@ struct outs auto out_fk = output_fk; const auto& outs = *tx_.outputs_ptr(); - std::for_each(outs.begin(), outs.end(), [&](const auto& out) NOEXCEPT - { - sink.write_little_endian(out_fk); - - // Calculate next corresponding output fk from serialized size. - // (variable_size(value) + (value + script)) - (value - parent) - out_fk += (variable_size(out->value()) + out->serialized_size() - - value_parent_diff); - }); + std::for_each(outs.cbegin(), outs.cend(), + [&](const auto& out) NOEXCEPT + { + sink.write_little_endian(out_fk); + + // Calculate next corresponding output fk from serialized size. + // (variable_size(value) + (value + script)) - (value - parent) + out_fk += (variable_size(out->value()) + + out->serialized_size() - value_parent_diff); + }); BC_ASSERT(!sink || sink.get_write_position() == count() * minrow); return sink; diff --git a/include/bitcoin/database/tables/archives/txs.hpp b/include/bitcoin/database/tables/archives/txs.hpp index f90ab83d7..b23c08227 100644 --- a/include/bitcoin/database/tables/archives/txs.hpp +++ b/include/bitcoin/database/tables/archives/txs.hpp @@ -123,7 +123,7 @@ struct txs // tx fks const auto number = possible_narrow_cast(tx_fks.size()); sink.write_little_endian(number); - std::for_each(tx_fks.begin(), tx_fks.end(), + std::for_each(tx_fks.cbegin(), tx_fks.cend(), [&](const auto& fk) NOEXCEPT { sink.write_little_endian(fk); diff --git a/include/bitcoin/database/tables/caches/prevout.hpp b/include/bitcoin/database/tables/caches/prevout.hpp index bc90fd7ff..4afef5a48 100644 --- a/include/bitcoin/database/tables/caches/prevout.hpp +++ b/include/bitcoin/database/tables/caches/prevout.hpp @@ -100,8 +100,8 @@ struct prevout // Count is written as a tx link so the table can remain an array. sink.write_variable(number); - std::for_each(cons.begin(), cons.end(), write_conflict); - std::for_each(std::next(txs.begin()), txs.end(), write_tx); + std::for_each(cons.cbegin(), cons.cend(), write_conflict); + std::for_each(std::next(txs.cbegin()), txs.cend(), write_tx); BC_ASSERT(!sink || (sink.get_write_position() == count())); return sink; diff --git a/test/mocks/blocks.cpp b/test/mocks/blocks.cpp index d2ca7eaaa..52aebe3f2 100644 --- a/test/mocks/blocks.cpp +++ b/test/mocks/blocks.cpp @@ -25,7 +25,9 @@ using namespace system; constexpr hash_digest two_hash = from_uintx(uint256_t(two)); constexpr database::context context{ 0x01020304, 0x11121314, 0x21222324 }; -constexpr hash_digest genesis_address = base16_hash("740485f380ff6379d11ef6fe7d7cdd68aea7f8bd0d953d9fdf3531fb7d531833"); +constexpr hash_digest genesis_address = base16_hash("740485f380ff6379d11ef6fe7d7cdd68aea7f8bd0d953d9fdf3531fb7d531833"); +constexpr hash_digest block1a_address0 = base16_hash("fab04811b1d0379bf78c2a41902368c200d675788e4bff8c88ff543836e4fca1"); +constexpr hash_digest block1a_address1 = base16_hash("067bd624c5840ed0d5b65597bafcde68f07fa9d87d3b43292b3199e49a514e59"); constexpr hash_digest block0_hash = base16_hash("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"); constexpr hash_digest block1_hash = base16_hash("00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048"); diff --git a/test/mocks/blocks.hpp b/test/mocks/blocks.hpp index 219c3066b..e54f1cecc 100644 --- a/test/mocks/blocks.hpp +++ b/test/mocks/blocks.hpp @@ -31,6 +31,8 @@ const auto events_handler = [](auto, auto) {}; extern const database::context context; extern const system::hash_digest genesis_address; +extern const system::hash_digest block1a_address0; +extern const system::hash_digest block1a_address1; extern const system::hash_digest block0_hash; extern const system::hash_digest block1_hash; diff --git a/test/query/address/address_balance.cpp b/test/query/address/address_balance.cpp index dd8a2fa68..6c91812b7 100644 --- a/test/query/address/address_balance.cpp +++ b/test/query/address/address_balance.cpp @@ -36,7 +36,7 @@ BOOST_AUTO_TEST_CASE(query_address__get_balance__turbo_genesis__expected) BOOST_REQUIRE(query.initialize(test::genesis)); uint64_t combined{}; - std::atomic_bool cancel{}; + const std::atomic_bool cancel{}; BOOST_REQUIRE(!query.get_unconfirmed_balance(cancel, combined, test::genesis_address, true)); BOOST_REQUIRE_EQUAL(combined, 5000000000u); @@ -60,7 +60,7 @@ BOOST_AUTO_TEST_CASE(query_address__get_balance__genesis__expected) BOOST_REQUIRE(query.initialize(test::genesis)); uint64_t combined{}; - std::atomic_bool cancel{}; + const std::atomic_bool cancel{}; BOOST_REQUIRE(!query.get_unconfirmed_balance(cancel, combined, test::genesis_address)); BOOST_REQUIRE_EQUAL(combined, 5000000000u); diff --git a/test/query/address/address_history.cpp b/test/query/address/address_history.cpp index ecd5c228c..f19d9e864 100644 --- a/test/query/address/address_history.cpp +++ b/test/query/address/address_history.cpp @@ -36,17 +36,17 @@ BOOST_AUTO_TEST_CASE(query_address__get_history__turbo_genesis__expected) BOOST_REQUIRE(query.initialize(test::genesis)); histories out{}; - std::atomic_bool cancel{}; + const std::atomic_bool cancel{}; BOOST_REQUIRE(!query.get_unconfirmed_history(cancel, out, test::genesis_address, true)); BOOST_REQUIRE(out.empty()); out.clear(); BOOST_REQUIRE(!query.get_confirmed_history(cancel, out, test::genesis_address, true)); - BOOST_REQUIRE(out.empty()); + BOOST_CHECK_EQUAL(out.size(), 0u); out.clear(); BOOST_REQUIRE(!query.get_history(cancel, out, test::genesis_address, true)); - BOOST_REQUIRE(out.empty()); + BOOST_CHECK_EQUAL(out.size(), 1u); } BOOST_AUTO_TEST_CASE(query_address__get_history__genesis__expected) @@ -59,17 +59,17 @@ BOOST_AUTO_TEST_CASE(query_address__get_history__genesis__expected) BOOST_REQUIRE(query.initialize(test::genesis)); histories out{}; - std::atomic_bool cancel{}; + const std::atomic_bool cancel{}; BOOST_REQUIRE(!query.get_unconfirmed_history(cancel, out, test::genesis_address)); - BOOST_REQUIRE(out.empty()); + BOOST_CHECK_EQUAL(out.size(), 0u); out.clear(); - BOOST_REQUIRE(!query.get_confirmed_history(cancel, out, test::genesis_address)); - BOOST_REQUIRE(out.empty()); + BOOST_REQUIRE(!query.get_unconfirmed_history(cancel, out, test::genesis_address)); + BOOST_CHECK_EQUAL(out.size(), 0u); out.clear(); BOOST_REQUIRE(!query.get_history(cancel, out, test::genesis_address)); - BOOST_REQUIRE(out.empty()); + BOOST_CHECK_EQUAL(out.size(), 1u); } BOOST_AUTO_TEST_SUITE_END() diff --git a/test/query/address/address_outpoints.cpp b/test/query/address/address_outpoints.cpp index 517b8fa7d..499d50edd 100644 --- a/test/query/address/address_outpoints.cpp +++ b/test/query/address/address_outpoints.cpp @@ -34,7 +34,7 @@ BOOST_AUTO_TEST_CASE(query_address__get_confirmed_unspent_outputs__turbo_genesis BOOST_REQUIRE(query.initialize(test::genesis)); outpoints out{}; - std::atomic_bool cancel{}; + const std::atomic_bool cancel{}; BOOST_REQUIRE(!query.get_confirmed_unspent_outputs(cancel, out, test::genesis_address, true)); BOOST_REQUIRE_EQUAL(out.size(), 1u); BOOST_REQUIRE(*out.begin() == query.get_outpoint(query.to_output(0, 0))); @@ -50,7 +50,7 @@ BOOST_AUTO_TEST_CASE(query_address__get_confirmed_unspent_outputs__genesis__expe BOOST_REQUIRE(query.initialize(test::genesis)); outpoints out{}; - std::atomic_bool cancel{}; + const std::atomic_bool cancel{}; BOOST_REQUIRE(!query.get_confirmed_unspent_outputs(cancel, out, test::genesis_address)); BOOST_REQUIRE_EQUAL(out.size(), 1u); BOOST_REQUIRE(*out.begin() == query.get_outpoint(query.to_output(0, 0))); @@ -68,7 +68,7 @@ BOOST_AUTO_TEST_CASE(query_address__get_minimum_unspent_outputs__turbo_above__ex BOOST_REQUIRE(query.initialize(test::genesis)); outpoints out{}; - std::atomic_bool cancel{}; + const std::atomic_bool cancel{}; BOOST_REQUIRE(!query.get_minimum_unspent_outputs(cancel, out, test::genesis_address, 5000000001, true)); BOOST_REQUIRE(out.empty()); } @@ -83,7 +83,7 @@ BOOST_AUTO_TEST_CASE(query_address__get_minimum_unspent_outputs__above__excluded BOOST_REQUIRE(query.initialize(test::genesis)); outpoints out{}; - std::atomic_bool cancel{}; + const std::atomic_bool cancel{}; BOOST_REQUIRE(!query.get_minimum_unspent_outputs(cancel, out, test::genesis_address, 5000000001)); BOOST_REQUIRE(out.empty()); } @@ -98,7 +98,7 @@ BOOST_AUTO_TEST_CASE(query_address__get_minimum_unspent_outputs__at__included) BOOST_REQUIRE(query.initialize(test::genesis)); outpoints out{}; - std::atomic_bool cancel{}; + const std::atomic_bool cancel{}; BOOST_REQUIRE(!query.get_minimum_unspent_outputs(cancel, out, test::genesis_address, 5000000000)); BOOST_REQUIRE_EQUAL(out.size(), 1u); BOOST_REQUIRE(*out.begin() == query.get_outpoint(query.to_output(0, 0))); @@ -114,7 +114,7 @@ BOOST_AUTO_TEST_CASE(query_address__get_minimum_unspent_outputs__below__included BOOST_REQUIRE(query.initialize(test::genesis)); outpoints out{}; - std::atomic_bool cancel{}; + const std::atomic_bool cancel{}; BOOST_REQUIRE(!query.get_minimum_unspent_outputs(cancel, out, test::genesis_address, 0)); BOOST_REQUIRE_EQUAL(out.size(), 1u); BOOST_REQUIRE(*out.begin() == query.get_outpoint(query.to_output(0, 0))); @@ -136,7 +136,7 @@ BOOST_AUTO_TEST_CASE(query_address__get_address_outputs__turbo_genesis__expected BOOST_REQUIRE(query.initialize(test::genesis)); outpoints out{}; - std::atomic_bool cancel{}; + const std::atomic_bool cancel{}; BOOST_REQUIRE(!query.get_address_outputs(cancel, out, test::genesis_address, true)); BOOST_REQUIRE_EQUAL(out.size(), 1u); BOOST_REQUIRE(*out.begin() == query.get_outpoint(query.to_output(0, 0))); @@ -152,7 +152,7 @@ BOOST_AUTO_TEST_CASE(query_address__get_address_outputs__genesis__expected) BOOST_REQUIRE(query.initialize(test::genesis)); outpoints out{}; - std::atomic_bool cancel{}; + const std::atomic_bool cancel{}; BOOST_REQUIRE(!query.get_address_outputs(cancel, out, test::genesis_address)); BOOST_REQUIRE_EQUAL(out.size(), 1u); BOOST_REQUIRE(*out.begin() == query.get_outpoint(query.to_output(0, 0))); diff --git a/test/query/address/address_unspent.cpp b/test/query/address/address_unspent.cpp index 069527bfd..84dead70a 100644 --- a/test/query/address/address_unspent.cpp +++ b/test/query/address/address_unspent.cpp @@ -38,7 +38,7 @@ BOOST_AUTO_TEST_CASE(query_address__get_unspent__turbo_genesis__expected) BOOST_REQUIRE(query.initialize(test::genesis)); unspents out{}; - std::atomic_bool cancel{}; + const std::atomic_bool cancel{}; BOOST_REQUIRE(!query.get_unconfirmed_unspent(cancel, out, genesis_address, true)); BOOST_REQUIRE(out.empty()); @@ -61,7 +61,7 @@ BOOST_AUTO_TEST_CASE(query_address__get_unspent__genesis__expected) BOOST_REQUIRE(query.initialize(test::genesis)); unspents out{}; - std::atomic_bool cancel{}; + const std::atomic_bool cancel{}; BOOST_REQUIRE(!query.get_unconfirmed_unspent(cancel, out, genesis_address)); BOOST_REQUIRE(out.empty()); diff --git a/test/query/navigate/navigate_reverse.cpp b/test/query/navigate/navigate_reverse.cpp index d5fcb4ae6..0aea468dc 100644 --- a/test/query/navigate/navigate_reverse.cpp +++ b/test/query/navigate/navigate_reverse.cpp @@ -44,10 +44,82 @@ BOOST_AUTO_TEST_CASE(query_navigate__to_parent__always__expected) BOOST_REQUIRE_EQUAL(query.to_parent(5), header_link::terminal); } +// to_touched_txs1 + +BOOST_AUTO_TEST_CASE(query_navigate__to_touched_txs1__galways__expected) +{ + settings settings{}; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE_EQUAL(store.create(test::events_handler), error::success); + BOOST_REQUIRE(query.initialize(test::genesis)); + BOOST_REQUIRE(query.set(test::block1a, test::context, false, false)); + BOOST_REQUIRE(query.set(test::block2a, test::context, false, false)); + BOOST_REQUIRE(query.set(test::tx4)); + BOOST_REQUIRE(query.set(test::tx5)); + BOOST_REQUIRE(query.set(test::block3a, test::context, false, false)); + + output_links links{}; + BOOST_REQUIRE(!query.to_address_outputs(links, test::block1a_address1)); + + tx_links out{}; + BOOST_REQUIRE(!query.to_touched_txs(out, links)); + BOOST_REQUIRE_EQUAL(out.size(), 4u); + + // owners + BOOST_REQUIRE_EQUAL(out.at(0), 1u); // block1a (tx1) + + // spenders of (0) + BOOST_REQUIRE_EQUAL(out.at(1), 2u); // block2a (tx2/3) + BOOST_REQUIRE_EQUAL(out.at(2), 4u); // tx4 + BOOST_REQUIRE_EQUAL(out.at(3), 6u); // block3a (tx6) +} + +// to_touched_txs2 + +BOOST_AUTO_TEST_CASE(query_navigate__to_touched_txs2__always__expected) +{ + settings settings{}; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE_EQUAL(store.create(test::events_handler), error::success); + BOOST_REQUIRE(query.initialize(test::genesis)); + BOOST_REQUIRE(query.set(test::block1a, test::context, false, false)); + BOOST_REQUIRE(query.set(test::block2a, test::context, false, false)); + BOOST_REQUIRE(query.set(test::tx4)); + BOOST_REQUIRE(query.set(test::tx5)); + BOOST_REQUIRE(query.set(test::block3a, test::context, false, false)); + + output_links links{}; + const std::atomic_bool cancel{}; + BOOST_REQUIRE(!query.to_address_outputs(cancel, links, test::block1a_address0)); + + tx_links out{}; + BOOST_REQUIRE(!query.to_touched_txs(cancel, out, links)); + BOOST_REQUIRE_EQUAL(out.size(), 10u); + + // owners + BOOST_REQUIRE_EQUAL(out.at(0), 1u); // block1a (tx1) + + // spenders of (0) + BOOST_REQUIRE_EQUAL(out.at(1), 2u); // block2a (tx2/3) + BOOST_REQUIRE_EQUAL(out.at(2), 4u); // tx4 + BOOST_REQUIRE_EQUAL(out.at(3), 5u); // tx5 + BOOST_REQUIRE_EQUAL(out.at(4), 6u); // block3a (tx6) + + // owners (unspent) + BOOST_REQUIRE_EQUAL(out.at(5), 2u); // block2a (tx2) + BOOST_REQUIRE_EQUAL(out.at(6), 3u); // block2a (tx3) + BOOST_REQUIRE_EQUAL(out.at(7), 4u); // tx4 + BOOST_REQUIRE_EQUAL(out.at(8), 5u); // tx5 + BOOST_REQUIRE_EQUAL(out.at(9), 6u); // block3a (tx6) +} + // to_address_outputs1 -// to_address_outputs2 -BOOST_AUTO_TEST_CASE(query_address__to_address_outputs__genesis__expected) +BOOST_AUTO_TEST_CASE(query_navigate__to_address_outputs1__always__expected) { settings settings{}; settings.path = TEST_DIRECTORY; @@ -55,15 +127,51 @@ BOOST_AUTO_TEST_CASE(query_address__to_address_outputs__genesis__expected) test::query_accessor query{ store }; BOOST_REQUIRE_EQUAL(store.create(test::events_handler), error::success); BOOST_REQUIRE(query.initialize(test::genesis)); + BOOST_REQUIRE(query.set(test::block1a, test::context, false, false)); + BOOST_REQUIRE(query.set(test::block2a, test::context, false, false)); + BOOST_REQUIRE(query.set(test::tx4)); + BOOST_REQUIRE(query.set(test::tx5)); + BOOST_REQUIRE(query.set(test::block3a, test::context, false, false)); output_links out{}; - std::atomic_bool cancel{}; - BOOST_REQUIRE(!query.to_address_outputs(cancel, out, test::genesis_address)); + BOOST_REQUIRE(!query.to_address_outputs(out, test::block1a_address1)); + + // There is 1 instance of the `script{ { { opcode::roll } } }` output. BOOST_REQUIRE_EQUAL(out.size(), 1u); - BOOST_REQUIRE_EQUAL(out.front(), query.to_output(0, 0)); + BOOST_REQUIRE_EQUAL(out.front(), 88u); +} + +// to_address_outputs2 + +BOOST_AUTO_TEST_CASE(query_navigate__to_address_outputs2__always__expected) +{ + settings settings{}; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE_EQUAL(store.create(test::events_handler), error::success); + BOOST_REQUIRE(query.initialize(test::genesis)); + BOOST_REQUIRE(query.set(test::block1a, test::context, false, false)); + BOOST_REQUIRE(query.set(test::block2a, test::context, false, false)); + BOOST_REQUIRE(query.set(test::tx4)); + BOOST_REQUIRE(query.set(test::tx5)); + BOOST_REQUIRE(query.set(test::block3a, test::context, false, false)); + + output_links out{}; + const std::atomic_bool cancel{}; + BOOST_REQUIRE(!query.to_address_outputs(cancel, out, test::block1a_address0)); + + // There are 6 instances of the `script{ { { opcode::pick } } }` output. + BOOST_REQUIRE_EQUAL(out.size(), 6u); + BOOST_REQUIRE_EQUAL(out.at(0), 123u); + BOOST_REQUIRE_EQUAL(out.at(1), 116u); + BOOST_REQUIRE_EQUAL(out.at(2), 109u); + BOOST_REQUIRE_EQUAL(out.at(3), 102u); + BOOST_REQUIRE_EQUAL(out.at(4), 95u); + BOOST_REQUIRE_EQUAL(out.at(5), 81u); } -// to_spending_tx +// to_input_tx // to_output_tx BOOST_AUTO_TEST_CASE(query_navigate__to_output_tx__to_output__expected) From 97d0de36eb7885865af3c2013a49e35bc8b2f73f Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sat, 11 Apr 2026 23:35:45 -0400 Subject: [PATCH 03/10] Fix and test is_confirmed_all_prevouts(). --- .../bitcoin/database/impl/query/confirmed.ipp | 16 +++- test/query/confirmed.cpp | 79 +++++++++++++++++++ 2 files changed, 91 insertions(+), 4 deletions(-) diff --git a/include/bitcoin/database/impl/query/confirmed.ipp b/include/bitcoin/database/impl/query/confirmed.ipp index 9f8b8214f..182331a00 100644 --- a/include/bitcoin/database/impl/query/confirmed.ipp +++ b/include/bitcoin/database/impl/query/confirmed.ipp @@ -124,11 +124,19 @@ bool CLASS::is_confirmed_spent_output(const output_link& link) const NOEXCEPT TEMPLATE bool CLASS::is_confirmed_all_prevouts(const tx_link& link) const NOEXCEPT { - // All prevouts of the tx's inputs are confirmed. - const auto ins = to_points(link); - return std::all_of(ins.cbegin(), ins.cend(), [&](const auto& in) NOEXCEPT + // If tx is confirmed then all prevouts must be confirmed. + if (is_confirmed_tx(link)) + return true; + + // A coinbase must itself be confirmed (one input and null). + if (is_coinbase(link)) + return false; + + // All prevouts of the tx's inputs must be confirmed. + const auto outs = to_prevouts(link); + return std::all_of(outs.cbegin(), outs.cend(), [&](const auto& out) NOEXCEPT { - return is_confirmed_input(in); + return is_confirmed_output(out); }); } diff --git a/test/query/confirmed.cpp b/test/query/confirmed.cpp index 5832163e6..27367710c 100644 --- a/test/query/confirmed.cpp +++ b/test/query/confirmed.cpp @@ -692,4 +692,83 @@ BOOST_AUTO_TEST_CASE(query_confirmed__block_confirmable__unconfirmed_double_spen BOOST_REQUIRE_EQUAL(query.block_confirmable(2), error::success); } +// is_confirmed_all_prevouts + +BOOST_AUTO_TEST_CASE(query_confirmed__is_confirmed_all_prevouts__genesis__true) +{ + settings settings{}; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE_EQUAL(store.create(test::events_handler), error::success); + BOOST_REQUIRE(query.initialize(test::genesis)); + BOOST_REQUIRE(query.is_confirmed_all_prevouts(0)); +} + +BOOST_AUTO_TEST_CASE(query_confirmed__is_confirmed_all_prevouts__unconfirmed_coinbase__false) +{ + settings settings{}; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE_EQUAL(store.create(test::events_handler), error::success); + BOOST_REQUIRE(query.initialize(test::genesis)); + + // Block 1b has single null input so archived as (strong) coinbase. + BOOST_REQUIRE(query.set(test::block1b, context{ 0, 1, 0 }, false, true)); + BOOST_REQUIRE(!query.is_confirmed_all_prevouts(1)); +} + +BOOST_AUTO_TEST_CASE(query_confirmed__is_confirmed_all_prevouts__confirmed_coinbase__true) +{ + settings settings{}; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE_EQUAL(store.create(test::events_handler), error::success); + BOOST_REQUIRE(query.initialize(test::genesis)); + + // Block 1b has single null input so archived as (strong) coinbase. + BOOST_REQUIRE(query.set(test::block1b, context{ 0, 1, 0 }, false, true)); + BOOST_REQUIRE(query.push_confirmed(1, false)); + BOOST_REQUIRE(query.is_confirmed_all_prevouts(1)); +} + +BOOST_AUTO_TEST_CASE(query_confirmed__is_confirmed_all_prevouts__missing_prevouts__false) +{ + settings settings{}; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE_EQUAL(store.create(test::events_handler), error::success); + BOOST_REQUIRE(query.initialize(test::genesis)); + + // Tx1 is of block1a and consists of three inputs that do not exist. + BOOST_REQUIRE(query.set(test::block1a, context{ 0, 1, 0 }, false, true)); + BOOST_REQUIRE(!query.is_confirmed_all_prevouts(1)); +} + +BOOST_AUTO_TEST_CASE(query_confirmed__is_confirmed_all_prevouts__prevouts_confirmed__true) +{ + settings settings{}; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE_EQUAL(store.create(test::events_handler), error::success); + BOOST_REQUIRE(query.initialize(test::genesis)); + + // Block 1a has 1 tx(1) with 2 outputs. + BOOST_REQUIRE(query.set(test::block1a, context{ 0, 1, 0 }, false, true)); + + // Block 2a first tx(2) spends both block 1a outputs. + BOOST_REQUIRE(query.set(test::block2a, context{ 0, 2, 0 }, false, true)); + + // Block 1a tx(1) is strong and not confirmed. + BOOST_REQUIRE(!query.is_confirmed_all_prevouts(2)); + + // Block 1a tx(1) is strong and confirmed. + BOOST_REQUIRE(query.push_confirmed(1, false)); + BOOST_REQUIRE(query.is_confirmed_all_prevouts(2)); +} + BOOST_AUTO_TEST_SUITE_END() From cd7556d13a53cd0091de906590cba9129f7c4570 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sat, 11 Apr 2026 23:42:59 -0400 Subject: [PATCH 04/10] Add history/unspent sort_and_dedup(). --- include/bitcoin/database/types.hpp | 30 +++++++++++++------ src/types.cpp | 33 +++++++++++++++++++++ test/types.cpp | 46 ++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 8 deletions(-) diff --git a/include/bitcoin/database/types.hpp b/include/bitcoin/database/types.hpp index d97762e09..f88d72973 100644 --- a/include/bitcoin/database/types.hpp +++ b/include/bitcoin/database/types.hpp @@ -70,9 +70,13 @@ using data_chunk = system::data_chunk; /// Common system::chain aliases. /// --------------------------------------------------------------------------- -using inpoint = system::chain::point; -using outpoint = system::chain::outpoint; using checkpoint = system::chain::checkpoint; +using outpoint = system::chain::outpoint; +using inpoint = system::chain::point; + +/// Sorted and deduped. +using outpoints = std::set; +using inpoints = std::set; /// Common carriers. /// --------------------------------------------------------------------------- @@ -109,10 +113,18 @@ struct BCD_API unspent bool operator()(const unspent& a, const unspent& b) const NOEXCEPT; }; + struct equal_to + { + bool operator()(const unspent& a, const unspent& b) const NOEXCEPT; + }; + + static void sort_and_dedup(std::vector& unspent) NOEXCEPT; + outpoint tx{}; size_t height{}; size_t position{}; }; +using unspents = std::vector; struct BCD_API history { @@ -121,16 +133,18 @@ struct BCD_API history bool operator()(const history& a, const history& b) const NOEXCEPT; }; + struct equal_to + { + bool operator()(const history& a, const history& b) const NOEXCEPT; + }; + + static void sort_and_dedup(std::vector& history) NOEXCEPT; + checkpoint tx{}; uint64_t fee{}; size_t position{}; }; - -/// Sorted and deduped. -using inpoints = std::set; -using outpoints = std::set; -using unspents = std::set; -using histories = std::set; +using histories = std::vector; } // namespace database } // namespace libbitcoin diff --git a/src/types.cpp b/src/types.cpp index 101a8e63b..138bffcf6 100644 --- a/src/types.cpp +++ b/src/types.cpp @@ -18,11 +18,15 @@ */ #include +#include #include namespace libbitcoin { namespace database { +// history +// ---------------------------------------------------------------------------- + // local inline bool hash_less_than(const hash_digest& a, const hash_digest& b) NOEXCEPT { @@ -74,6 +78,22 @@ bool history::less_than::operator()(const history& a, const history& b) const NO return hash_less_than(a.tx.hash(), b.tx.hash()); } +bool history::equal_to::operator()(const history& a, const history& b) const NOEXCEPT +{ + history::less_than lesser; + return !lesser(a, b) && !lesser(b, a); +} + +void history::sort_and_dedup(std::vector& out) NOEXCEPT +{ + std::sort(out.begin(), out.end(), history::less_than{}); + auto end = std::unique(out.begin(), out.end(), history::equal_to{}); + out.erase(end, out.end()); +} + +// unspent +// ---------------------------------------------------------------------------- + bool unspent::less_than::operator()(const unspent& a, const unspent& b) const NOEXCEPT { const auto a_point = a.tx.point(); @@ -103,5 +123,18 @@ bool unspent::less_than::operator()(const unspent& a, const unspent& b) const NO return a_point < b_point; } +bool unspent::equal_to::operator()(const unspent& a, const unspent& b) const NOEXCEPT +{ + unspent::less_than lesser; + return !lesser(a, b) && !lesser(b, a); +} + +void unspent::sort_and_dedup(std::vector& out) NOEXCEPT +{ + std::sort(out.begin(), out.end(), unspent::less_than{}); + auto end = std::unique(out.begin(), out.end(), unspent::equal_to{}); + out.erase(end, out.end()); +} + } // namespace database } // namespace libbitcoin diff --git a/test/types.cpp b/test/types.cpp index 94ffa4a10..29e360875 100644 --- a/test/types.cpp +++ b/test/types.cpp @@ -146,4 +146,50 @@ BOOST_AUTO_TEST_CASE(types__history_less_than__unconfirmed_hash_identical__expec BOOST_REQUIRE(!history::less_than{}(value, value)); } +// history.sort_and_dedup() + +BOOST_AUTO_TEST_CASE(types__history_sort_and_dedup__unsorted_with_duplicates_mixed__sorted_and_deduped) +{ + constexpr auto h1 = base16_hash("0000000000000000000000000000000000000000000000000000000000000001"); + constexpr auto h2 = base16_hash("0000000000000000000000000000000000000000000000000000000000000002"); + std::vector values + { + { { h2, 0 }, 0, 0 }, // unconfirmed + { { hash_digest{}, 200 }, 0, 5 }, // confirmed + { { h1, 0 }, 0, 0 }, // unconfirmed (duplicate will be removed) + { { h1, 0 }, 0, 0 }, // unconfirmed duplicate + { { hash_digest{}, 100 }, 0, 10 }, // confirmed + { { hash_digest{}, 100 }, 0, 10 } // confirmed duplicate + }; + + history::sort_and_dedup(values); + BOOST_REQUIRE_EQUAL(values.size(), 4u); + BOOST_REQUIRE_EQUAL(values[0].tx.height(), 100u); // confirmed, lowest height + BOOST_REQUIRE_EQUAL(values[1].tx.height(), 200u); // confirmed + BOOST_REQUIRE_EQUAL(values[2].tx.height(), 0u); // unconfirmed (h1) + BOOST_REQUIRE_EQUAL(values[3].tx.height(), 0u); // unconfirmed (h2) +} + +// unspent.sort_and_dedup() + +BOOST_AUTO_TEST_CASE(types__unspent_sort_and_dedup__unsorted_with_duplicates_mixed__sorted_and_deduped) +{ + const outpoint lo{ { {}, 0 }, 0 }; + const outpoint hi{ { {}, 5 }, 0 }; + std::vector values + { + { hi, 0, 0 }, // unconfirmed + { lo, 200, 3 }, // confirmed + { lo, 100, 5 }, // confirmed + { lo, 100, 5 }, // confirmed duplicate + { hi, 0, 0 } // unconfirmed duplicate + }; + + unspent::sort_and_dedup(values); + BOOST_REQUIRE_EQUAL(values.size(), 3u); + BOOST_REQUIRE_EQUAL(values[0].height, 100u); // confirmed, lowest height + BOOST_REQUIRE_EQUAL(values[1].height, 200u); // confirmed + BOOST_REQUIRE_EQUAL(values[2].height, 0u); // unconfirmed +} + BOOST_AUTO_TEST_SUITE_END() From 42c89dd7bf98ed891bcdc75cd457d8d09293e356 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sat, 11 Apr 2026 23:43:10 -0400 Subject: [PATCH 05/10] Whitespace. --- test/mocks/blocks.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/mocks/blocks.cpp b/test/mocks/blocks.cpp index 52aebe3f2..119843b04 100644 --- a/test/mocks/blocks.cpp +++ b/test/mocks/blocks.cpp @@ -247,7 +247,7 @@ const block block1a }, input { - point{ two_hash, 0x2b }, // missing prevout + point{ two_hash, 0x2b }, // missing prevout script{ { { opcode::op_return }, { opcode::roll } } }, witness{ "[424242]" }, 0x19 // sequence From 930025bbda6ffb762c87611da0037c29678ce4d8 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sat, 11 Apr 2026 23:43:49 -0400 Subject: [PATCH 06/10] Comments. --- test/query/navigate/navigate_reverse.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/query/navigate/navigate_reverse.cpp b/test/query/navigate/navigate_reverse.cpp index 0aea468dc..b32170706 100644 --- a/test/query/navigate/navigate_reverse.cpp +++ b/test/query/navigate/navigate_reverse.cpp @@ -110,11 +110,11 @@ BOOST_AUTO_TEST_CASE(query_navigate__to_touched_txs2__always__expected) BOOST_REQUIRE_EQUAL(out.at(4), 6u); // block3a (tx6) // owners (unspent) - BOOST_REQUIRE_EQUAL(out.at(5), 2u); // block2a (tx2) + BOOST_REQUIRE_EQUAL(out.at(5), 2u); // block2a (tx2) [duplicate] BOOST_REQUIRE_EQUAL(out.at(6), 3u); // block2a (tx3) - BOOST_REQUIRE_EQUAL(out.at(7), 4u); // tx4 - BOOST_REQUIRE_EQUAL(out.at(8), 5u); // tx5 - BOOST_REQUIRE_EQUAL(out.at(9), 6u); // block3a (tx6) + BOOST_REQUIRE_EQUAL(out.at(7), 4u); // tx4 [duplicate] + BOOST_REQUIRE_EQUAL(out.at(8), 5u); // tx5 [duplicate] + BOOST_REQUIRE_EQUAL(out.at(9), 6u); // block3a (tx6) [duplicate] } // to_address_outputs1 From 518c4ed17821715b61e789dadb7be037c193991f Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sun, 12 Apr 2026 01:43:43 -0400 Subject: [PATCH 07/10] Add unspent/history excluded position filtering. --- include/bitcoin/database/types.hpp | 17 +++++++++++++++ src/types.cpp | 28 ++++++++++++++++++------ test/types.cpp | 34 ++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 6 deletions(-) diff --git a/include/bitcoin/database/types.hpp b/include/bitcoin/database/types.hpp index f88d72973..7e571bc44 100644 --- a/include/bitcoin/database/types.hpp +++ b/include/bitcoin/database/types.hpp @@ -108,6 +108,8 @@ struct span struct BCD_API unspent { + static constexpr size_t excluded_position = max_size_t; + struct less_than { bool operator()(const unspent& a, const unspent& b) const NOEXCEPT; @@ -118,6 +120,11 @@ struct BCD_API unspent bool operator()(const unspent& a, const unspent& b) const NOEXCEPT; }; + struct exclude + { + bool operator()(const unspent& element) const NOEXCEPT; + }; + static void sort_and_dedup(std::vector& unspent) NOEXCEPT; outpoint tx{}; @@ -128,6 +135,11 @@ using unspents = std::vector; struct BCD_API history { + static constexpr size_t rooted_height = zero; + static constexpr size_t unrooted_height = max_size_t; + static constexpr size_t excluded_position = max_size_t; + static constexpr size_t unconfirmed_position = zero; + struct less_than { bool operator()(const history& a, const history& b) const NOEXCEPT; @@ -138,6 +150,11 @@ struct BCD_API history bool operator()(const history& a, const history& b) const NOEXCEPT; }; + struct exclude + { + bool operator()(const history& element) const NOEXCEPT; + }; + static void sort_and_dedup(std::vector& history) NOEXCEPT; checkpoint tx{}; diff --git a/src/types.cpp b/src/types.cpp index 138bffcf6..785079175 100644 --- a/src/types.cpp +++ b/src/types.cpp @@ -84,17 +84,25 @@ bool history::equal_to::operator()(const history& a, const history& b) const NOE return !lesser(a, b) && !lesser(b, a); } +bool history::exclude::operator()(const history& element) const NOEXCEPT +{ + return element.position == max_size_t; +} + void history::sort_and_dedup(std::vector& out) NOEXCEPT { + auto excluded = std::remove_if(out.begin(), out.end(), history::exclude{}); + out.erase(excluded, out.end()); std::sort(out.begin(), out.end(), history::less_than{}); - auto end = std::unique(out.begin(), out.end(), history::equal_to{}); - out.erase(end, out.end()); + auto duplicates = std::unique(out.begin(), out.end(), history::equal_to{}); + out.erase(duplicates, out.end()); } // unspent // ---------------------------------------------------------------------------- -bool unspent::less_than::operator()(const unspent& a, const unspent& b) const NOEXCEPT +bool unspent::less_than::operator()(const unspent& a, + const unspent& b) const NOEXCEPT { const auto a_point = a.tx.point(); const auto b_point = b.tx.point(); @@ -123,17 +131,25 @@ bool unspent::less_than::operator()(const unspent& a, const unspent& b) const NO return a_point < b_point; } -bool unspent::equal_to::operator()(const unspent& a, const unspent& b) const NOEXCEPT +bool unspent::equal_to::operator()(const unspent& a, + const unspent& b) const NOEXCEPT { unspent::less_than lesser; return !lesser(a, b) && !lesser(b, a); } +bool unspent::exclude::operator()(const unspent& element) const NOEXCEPT +{ + return element.position == max_size_t; +} + void unspent::sort_and_dedup(std::vector& out) NOEXCEPT { + auto excluded = std::remove_if(out.begin(), out.end(), unspent::exclude{}); + out.erase(excluded, out.end()); std::sort(out.begin(), out.end(), unspent::less_than{}); - auto end = std::unique(out.begin(), out.end(), unspent::equal_to{}); - out.erase(end, out.end()); + auto duplicates = std::unique(out.begin(), out.end(), unspent::equal_to{}); + out.erase(duplicates, out.end()); } } // namespace database diff --git a/test/types.cpp b/test/types.cpp index 29e360875..d1bd83341 100644 --- a/test/types.cpp +++ b/test/types.cpp @@ -170,6 +170,23 @@ BOOST_AUTO_TEST_CASE(types__history_sort_and_dedup__unsorted_with_duplicates_mix BOOST_REQUIRE_EQUAL(values[3].tx.height(), 0u); // unconfirmed (h2) } +BOOST_AUTO_TEST_CASE(history__sort_and_dedup__exclusions__removes_excluded_items) +{ + std::vector items + { + history{ { {}, 1 }, 0, max_size_t }, // excluded + history{ { {}, 2 }, 0, max_size_t }, // excluded + history{ { {}, 3 }, 0, 10 }, // valid + history{ { {}, 3 }, 0, 5 }, // valid (same height, lower position) + history{ { {}, 3 }, 0, 10 } // duplicate + }; + + history::sort_and_dedup(items); + BOOST_REQUIRE_EQUAL(items.size(), 2u); + BOOST_REQUIRE_EQUAL(items[0].position, 5u); + BOOST_REQUIRE_EQUAL(items[1].position, 10u); +} + // unspent.sort_and_dedup() BOOST_AUTO_TEST_CASE(types__unspent_sort_and_dedup__unsorted_with_duplicates_mixed__sorted_and_deduped) @@ -192,4 +209,21 @@ BOOST_AUTO_TEST_CASE(types__unspent_sort_and_dedup__unsorted_with_duplicates_mix BOOST_REQUIRE_EQUAL(values[2].height, 0u); // unconfirmed } +BOOST_AUTO_TEST_CASE(unspent__sort_and_dedup__exclusions__removes_excluded_items) +{ + unspents items + { + unspent{ { {}, 1 }, 10, max_size_t }, // excluded + unspent{ { {}, 2 }, 200, max_size_t }, // excluded + unspent{ { {}, 3 }, 50, 10 }, // valid confirmed + unspent{ { {}, 4 }, 50, 5 }, // valid confirmed (same height, lower position) + unspent{ { {}, 3 }, 50, 10 } // duplicate + }; + + unspent::sort_and_dedup(items); + BOOST_REQUIRE_EQUAL(items.size(), 2u); + BOOST_REQUIRE_EQUAL(items[0].position, 5u); + BOOST_REQUIRE_EQUAL(items[1].position, 10u); +} + BOOST_AUTO_TEST_SUITE_END() From bb0cf80f89ba6e2de44597c2e6b4411463081117 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sun, 12 Apr 2026 01:44:12 -0400 Subject: [PATCH 08/10] Add find_confirmed_block(tx_link). --- include/bitcoin/database/impl/query/confirmed.ipp | 10 ++++++++++ include/bitcoin/database/query.hpp | 11 ++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/include/bitcoin/database/impl/query/confirmed.ipp b/include/bitcoin/database/impl/query/confirmed.ipp index 182331a00..3cff6b0eb 100644 --- a/include/bitcoin/database/impl/query/confirmed.ipp +++ b/include/bitcoin/database/impl/query/confirmed.ipp @@ -29,6 +29,16 @@ namespace database { // ---------------------------------------------------------------------------- // These ensure both strong and candidate/confirmed indexation. +TEMPLATE +header_link CLASS::find_confirmed_block(const tx_link& link) const NOEXCEPT +{ + const auto block = find_strong(link); + if (is_confirmed_block(block)) + return block; + + return {}; +} + TEMPLATE header_link CLASS::find_confirmed_block( const hash_digest& tx_hash) const NOEXCEPT diff --git a/include/bitcoin/database/query.hpp b/include/bitcoin/database/query.hpp index 01cb73099..83b39e9a4 100644 --- a/include/bitcoin/database/query.hpp +++ b/include/bitcoin/database/query.hpp @@ -267,6 +267,7 @@ class query /// find confirmed objects (reverse navigation) header_link find_confirmed_block(const hash_digest& tx_hash) const NOEXCEPT; + header_link find_confirmed_block(const tx_link& link) const NOEXCEPT; point_link find_confirmed_spender(const point& prevout) const NOEXCEPT; /// output to spenders (reverse navigation) @@ -816,12 +817,16 @@ class query // Chain objects. template - static inline bool push_bool(std_vector& stack, + static bool push_bool(std_vector& stack, const Bool& element) NOEXCEPT; template - static inline code parallel_address_transform(const stopper& cancel, bool turbo, + static code parallel_outpoint_transform(const stopper& cancel, bool turbo, outpoints& out, const output_links& links, Functor&& functor) NOEXCEPT; - static inline point::cptr make_point(hash_digest&& hash, + template + static code parallel_history_transform(const stopper& cancel, bool turbo, + histories& out, const tx_links& links, Functor&& functor) NOEXCEPT; + + static point::cptr make_point(hash_digest&& hash, uint32_t index) NOEXCEPT; // Not thread safe. From 7168ac7923404a6bcb9c492732b82083c3c8d80e Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sun, 12 Apr 2026 02:01:35 -0400 Subject: [PATCH 09/10] Complete address_history impl (sans tests). --- .../impl/query/address/address_history.ipp | 175 +++++++++++++----- .../impl/query/address/address_outpoints.ipp | 50 +++-- .../impl/query/archive/chain_reader.ipp | 2 +- test/query/address/address_history.cpp | 2 +- test/types.cpp | 48 ++--- 5 files changed, 190 insertions(+), 87 deletions(-) diff --git a/include/bitcoin/database/impl/query/address/address_history.ipp b/include/bitcoin/database/impl/query/address/address_history.ipp index 143bcee3b..575aa5ce9 100644 --- a/include/bitcoin/database/impl/query/address/address_history.ipp +++ b/include/bitcoin/database/impl/query/address/address_history.ipp @@ -32,77 +32,168 @@ namespace database { // Canonically-sorted/deduped address history. // root txs (height:zero) sorted before transitive (height:max) txs. // tied-height transactions sorted by base16 txid (not converted). -// All confirmed txs are root, unconfirmed may or may not not be root. +// All confirmed txs are root, unconfirmed may or may not be root. // server/electrum TEMPLATE -code CLASS::get_unconfirmed_history(const stopper& , histories& , - const hash_digest& , bool ) const NOEXCEPT +code CLASS::get_unconfirmed_history(const stopper& cancel, histories& out, + const hash_digest& key, bool turbo) const NOEXCEPT { - return {}; + output_links outs{}; + if (const auto ec = to_address_outputs(cancel, outs, key)) + return ec; + + tx_links txs{}; + if (const auto ec = to_touched_txs(cancel, txs, outs)) + return ec; + + out.clear(); + out.resize(txs.size()); + return parallel_history_transform(cancel, turbo, out, txs, + [this](const auto& link, auto& cancel, auto& fail) NOEXCEPT -> history + { + if (cancel) + return {}; + + if (const auto block = find_strong(link); is_confirmed_block(block)) + { + constexpr auto excluded = history::excluded_position; + return { system::chain::checkpoint{}, {}, excluded }; + } + else + { + uint64_t fee{}; + auto hash = get_tx_key(link); + if (hash == system::null_hash || !get_tx_fee(fee, link)) + { + fail = true; + } + + const auto height = is_confirmed_all_prevouts(link) ? + history::rooted_height : history::unrooted_height; + + constexpr auto unconfirmed = history::unconfirmed_position; + return { { std::move(hash), height }, fee, unconfirmed }; + } + }); } // ununsed TEMPLATE -code CLASS::get_confirmed_history(const stopper& , histories& , - const hash_digest& , bool ) const NOEXCEPT +code CLASS::get_confirmed_history(const stopper& cancel, histories& out, + const hash_digest& key, bool turbo) const NOEXCEPT { - return {}; + output_links outs{}; + if (const auto ec = to_address_outputs(cancel, outs, key)) + return ec; + + tx_links txs{}; + if (const auto ec = to_touched_txs(cancel, txs, outs)) + return ec; + + out.clear(); + out.resize(txs.size()); + return parallel_history_transform(cancel, turbo, out, txs, + [this](const auto& link, auto& cancel, auto& fail) NOEXCEPT->history + { + if (cancel) + return {}; + + if (const auto block = find_strong(link); is_confirmed_block(block)) + { + uint64_t fee{}; + size_t height{}, position{}; + auto hash = get_tx_key(link); + if (hash == system::null_hash || !get_tx_fee(fee, link) || + !get_height(height, block) || + !get_tx_position(position, link, block)) + { + fail = true; + } + + return { { std::move(hash), height }, fee, position }; + } + else + { + constexpr auto exclude = history::excluded_position; + return { system::chain::checkpoint{}, {}, exclude }; + } + }); } // server/electrum TEMPLATE code CLASS::get_history(const stopper& cancel, histories& out, - const hash_digest& key, bool /* turbo */) const NOEXCEPT + const hash_digest& key, bool turbo) const NOEXCEPT { output_links outs{}; if (const auto ec = to_address_outputs(cancel, outs, key)) return ec; - tx_links txs{}; - if (const auto ec = to_touched_txs(cancel, txs, outs)) + tx_links links{}; + if (const auto ec = to_touched_txs(cancel, links, outs)) return ec; - // TODO: parallel transform (requires vector), then sort/dedup. out.clear(); - for (const auto& tx: txs) - { - if (cancel) - return error::canceled; - - // Handles terminal tx values. - auto hash = get_tx_key(tx); - if (hash == system::null_hash) - return error::integrity; - - // Possibly missing prevout. - uint64_t fee{}; - if (!get_tx_fee(fee, tx)) - return error::integrity; - - // Optimized by sharing strong across both subqueries. - size_t height{}, position{}; - if (const auto strong = find_strong(tx); !strong.is_terminal()) + out.resize(links.size()); + return parallel_history_transform(cancel, turbo, out, links, + [this](const auto& link, auto& cancel, auto& fail) NOEXCEPT -> history { - if (!get_height(height, strong) || - !get_tx_position(position, tx, strong)) - return error::integrity; - } - else + if (cancel) + return {}; + + uint64_t fee{}; + auto hash = get_tx_key(link); + if (hash == system::null_hash || !get_tx_fee(fee, link)) + fail = true; + + size_t height{}, position{}; + if (const auto block = find_strong(link); is_confirmed_block(block)) + { + if (!get_height(height, block) || + !get_tx_position(position, link, block)) + fail = true; + } + else + { + height = is_confirmed_all_prevouts(link) ? + history::rooted_height : history::unrooted_height; + } + + return { { std::move(hash), height }, fee, position }; + }); +} + +// utilities +// ---------------------------------------------------------------------------- +// private/static + +TEMPLATE +template +code CLASS::parallel_history_transform(const stopper& cancel, bool turbo, + histories& out, const tx_links& txs, Functor&& functor) NOEXCEPT +{ + const auto policy = poolstl::execution::par_if(turbo); + stopper fail{}; + + out.clear(); + out.resize(txs.size()); + std::transform(policy, txs.cbegin(), txs.cend(), out.begin(), + [&functor, &cancel, &fail](const auto& tx) NOEXCEPT { - height = is_confirmed_all_prevouts(tx) ? zero : max_size_t; - } + return functor(tx, cancel, fail); + }); + + if (fail) + return error::integrity; - out.insert({ { std::move(hash), height }, fee, position }); - } + if (cancel) + return error::canceled; + history::sort_and_dedup(out); return error::success; } -// turbos -// ---------------------------------------------------------------------------- -// protected - } // namespace database } // namespace libbitcoin diff --git a/include/bitcoin/database/impl/query/address/address_outpoints.ipp b/include/bitcoin/database/impl/query/address/address_outpoints.ipp index f6b63619b..9782231ec 100644 --- a/include/bitcoin/database/impl/query/address/address_outpoints.ipp +++ b/include/bitcoin/database/impl/query/address/address_outpoints.ipp @@ -41,15 +41,17 @@ code CLASS::get_confirmed_unspent_outputs(const stopper& cancel, if (const code ec = to_address_outputs(cancel, links, key)) return ec; - return parallel_address_transform(cancel, turbo, out, links, - [this](const auto& link, auto& cancel, auto& fail) NOEXCEPT + return parallel_outpoint_transform(cancel, turbo, out, links, + [this](const auto& link, auto& cancel, auto& fail) NOEXCEPT -> outpoint { // !is_confirmed_unspent must be filtered out. if (cancel || fail || !is_confirmed_unspent(link)) - return outpoint{}; + return {}; + + const auto outpoint = get_outpoint(link); + if (outpoint.point().is_null()) + fail = true; - auto outpoint = get_outpoint(link); - fail = outpoint.point().is_null(); return outpoint; }); } @@ -65,26 +67,29 @@ code CLASS::get_minimum_unspent_outputs(const stopper& cancel, if (const code ec = to_address_outputs(cancel, links, key)) return ec; - return parallel_address_transform(cancel, turbo, out, links, + return parallel_outpoint_transform(cancel, turbo, out, links, [this, minimum](const auto& link, auto& cancel, auto& fail) NOEXCEPT + -> outpoint { // !is_confirmed_unspent must be filtered out. if (cancel || fail || !is_confirmed_unspent(link)) - return outpoint{}; + return {}; uint64_t value{}; if (!get_value(value, link)) { fail = true; - return outpoint{}; + return {}; } // Must be filtered out. if (value < minimum) - return outpoint{}; + return {}; + + const auto outpoint = get_outpoint(link); + if (outpoint.point().is_null()) + fail = true; - auto outpoint = get_outpoint(link); - fail = outpoint.point().is_null(); return outpoint; }); } @@ -99,12 +104,16 @@ code CLASS::get_address_outputs(const stopper& cancel, outpoints& out, if (const code ec = to_address_outputs(cancel, links, key)) return ec; - return parallel_address_transform(cancel, turbo, out, links, - [this](const auto& link, auto& cancel, auto& fail) NOEXCEPT + return parallel_outpoint_transform(cancel, turbo, out, links, + [this](const auto& link, auto& cancel, auto& fail) NOEXCEPT -> outpoint { - if (cancel || fail) return outpoint{}; - auto outpoint = get_outpoint(link); - fail = outpoint.point().is_null(); + if (cancel || fail) + return {}; + + const auto outpoint = get_outpoint(link); + if (outpoint.point().is_null()) + fail = true; + return outpoint; }); } @@ -115,13 +124,15 @@ code CLASS::get_address_outputs(const stopper& cancel, outpoints& out, TEMPLATE template -inline code CLASS::parallel_address_transform(const stopper& cancel, bool turbo, +code CLASS::parallel_outpoint_transform(const stopper& cancel, bool turbo, outpoints& out, const output_links& links, Functor&& functor) NOEXCEPT { - out.clear(); + const auto policy = poolstl::execution::par_if(turbo); stopper fail{}; + + out.clear(); std::vector outpoints(links.size()); - const auto policy = poolstl::execution::par_if(turbo); + std::transform(policy, links.cbegin(), links.cend(), outpoints.begin(), [&functor, &cancel, &fail](const auto& link) NOEXCEPT { @@ -134,6 +145,7 @@ inline code CLASS::parallel_address_transform(const stopper& cancel, bool turbo, if (cancel) return error::canceled; + // TODO: change outpoints to vector and avoid copy. for (auto& outpoint: outpoints) { if (cancel) diff --git a/include/bitcoin/database/impl/query/archive/chain_reader.ipp b/include/bitcoin/database/impl/query/archive/chain_reader.ipp index eba4ff2af..2ed1a49fe 100644 --- a/include/bitcoin/database/impl/query/archive/chain_reader.ipp +++ b/include/bitcoin/database/impl/query/archive/chain_reader.ipp @@ -381,7 +381,7 @@ inpoints CLASS::get_spenders(const point& point) const NOEXCEPT TEMPLATE template -inline bool CLASS::push_bool(std_vector& stack, +bool CLASS::push_bool(std_vector& stack, const Bool& element) NOEXCEPT { if (!element) diff --git a/test/query/address/address_history.cpp b/test/query/address/address_history.cpp index f19d9e864..a72b164f4 100644 --- a/test/query/address/address_history.cpp +++ b/test/query/address/address_history.cpp @@ -42,7 +42,7 @@ BOOST_AUTO_TEST_CASE(query_address__get_history__turbo_genesis__expected) out.clear(); BOOST_REQUIRE(!query.get_confirmed_history(cancel, out, test::genesis_address, true)); - BOOST_CHECK_EQUAL(out.size(), 0u); + BOOST_CHECK_EQUAL(out.size(), 1u); out.clear(); BOOST_REQUIRE(!query.get_history(cancel, out, test::genesis_address, true)); diff --git a/test/types.cpp b/test/types.cpp index d1bd83341..c640a9720 100644 --- a/test/types.cpp +++ b/test/types.cpp @@ -154,12 +154,12 @@ BOOST_AUTO_TEST_CASE(types__history_sort_and_dedup__unsorted_with_duplicates_mix constexpr auto h2 = base16_hash("0000000000000000000000000000000000000000000000000000000000000002"); std::vector values { - { { h2, 0 }, 0, 0 }, // unconfirmed - { { hash_digest{}, 200 }, 0, 5 }, // confirmed - { { h1, 0 }, 0, 0 }, // unconfirmed (duplicate will be removed) - { { h1, 0 }, 0, 0 }, // unconfirmed duplicate - { { hash_digest{}, 100 }, 0, 10 }, // confirmed - { { hash_digest{}, 100 }, 0, 10 } // confirmed duplicate + { { h2, 0 }, 0, 0 }, // unconfirmed + { { hash_digest{}, 200 }, 0, 5 }, // confirmed + { { h1, 0 }, 0, 0 }, // unconfirmed (duplicate will be removed) + { { h1, 0 }, 0, 0 }, // unconfirmed duplicate + { { hash_digest{}, 100 }, 0, 10 }, // confirmed + { { hash_digest{}, 100 }, 0, 10 } // confirmed duplicate }; history::sort_and_dedup(values); @@ -174,11 +174,11 @@ BOOST_AUTO_TEST_CASE(history__sort_and_dedup__exclusions__removes_excluded_items { std::vector items { - history{ { {}, 1 }, 0, max_size_t }, // excluded - history{ { {}, 2 }, 0, max_size_t }, // excluded - history{ { {}, 3 }, 0, 10 }, // valid - history{ { {}, 3 }, 0, 5 }, // valid (same height, lower position) - history{ { {}, 3 }, 0, 10 } // duplicate + history{ { hash_digest{}, 1 }, 0, max_size_t }, // excluded + history{ { hash_digest{}, 2 }, 0, max_size_t }, // excluded + history{ { hash_digest{}, 3 }, 0, 10 }, // valid + history{ { hash_digest{}, 3 }, 0, 5 }, // valid (same height, lower position) + history{ { hash_digest{}, 3 }, 0, 10 } // duplicate }; history::sort_and_dedup(items); @@ -195,29 +195,29 @@ BOOST_AUTO_TEST_CASE(types__unspent_sort_and_dedup__unsorted_with_duplicates_mix const outpoint hi{ { {}, 5 }, 0 }; std::vector values { - { hi, 0, 0 }, // unconfirmed - { lo, 200, 3 }, // confirmed - { lo, 100, 5 }, // confirmed - { lo, 100, 5 }, // confirmed duplicate - { hi, 0, 0 } // unconfirmed duplicate + { hi, 0, 0 }, // unconfirmed + { lo, 200, 3 }, // confirmed + { lo, 100, 5 }, // confirmed + { lo, 100, 5 }, // confirmed duplicate + { hi, 0, 0 } // unconfirmed duplicate }; unspent::sort_and_dedup(values); BOOST_REQUIRE_EQUAL(values.size(), 3u); - BOOST_REQUIRE_EQUAL(values[0].height, 100u); // confirmed, lowest height - BOOST_REQUIRE_EQUAL(values[1].height, 200u); // confirmed - BOOST_REQUIRE_EQUAL(values[2].height, 0u); // unconfirmed + BOOST_REQUIRE_EQUAL(values[0].height, 100u); // confirmed, lowest height + BOOST_REQUIRE_EQUAL(values[1].height, 200u); // confirmed + BOOST_REQUIRE_EQUAL(values[2].height, 0u); // unconfirmed } BOOST_AUTO_TEST_CASE(unspent__sort_and_dedup__exclusions__removes_excluded_items) { unspents items { - unspent{ { {}, 1 }, 10, max_size_t }, // excluded - unspent{ { {}, 2 }, 200, max_size_t }, // excluded - unspent{ { {}, 3 }, 50, 10 }, // valid confirmed - unspent{ { {}, 4 }, 50, 5 }, // valid confirmed (same height, lower position) - unspent{ { {}, 3 }, 50, 10 } // duplicate + unspent{ { {}, 1 }, 10, max_size_t }, // excluded + unspent{ { {}, 2 }, 200, max_size_t }, // excluded + unspent{ { {}, 3 }, 50, 10 }, // valid confirmed + unspent{ { {}, 4 }, 50, 5 }, // valid confirmed (same height, lower position) + unspent{ { {}, 3 }, 50, 10 } // duplicate }; unspent::sort_and_dedup(items); From d8f86b317ea2ee780315793efaeff4e746cb9a32 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sun, 12 Apr 2026 02:11:20 -0400 Subject: [PATCH 10/10] Style. --- include/bitcoin/database/impl/query/address/address_history.ipp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/bitcoin/database/impl/query/address/address_history.ipp b/include/bitcoin/database/impl/query/address/address_history.ipp index 575aa5ce9..8907d42f5 100644 --- a/include/bitcoin/database/impl/query/address/address_history.ipp +++ b/include/bitcoin/database/impl/query/address/address_history.ipp @@ -94,7 +94,7 @@ code CLASS::get_confirmed_history(const stopper& cancel, histories& out, out.clear(); out.resize(txs.size()); return parallel_history_transform(cancel, turbo, out, txs, - [this](const auto& link, auto& cancel, auto& fail) NOEXCEPT->history + [this](const auto& link, auto& cancel, auto& fail) NOEXCEPT -> history { if (cancel) return {};