From 0d3ba59f8ad77c6d19fcb7be50234960a757729b Mon Sep 17 00:00:00 2001 From: evoskuil Date: Mon, 20 Apr 2026 14:13:29 -0400 Subject: [PATCH 1/5] Rename source file and add stub test file. --- Makefile.am | 3 +- .../libbitcoin-server-test.vcxproj | 1 + .../libbitcoin-server-test.vcxproj.filters | 3 ++ .../libbitcoin-server.vcxproj | 2 +- .../libbitcoin-server.vcxproj.filters | 6 ++-- ...be.cpp => protocol_electrum_subscribe.cpp} | 0 .../protocols/electrum/electrum_addresses.cpp | 2 -- .../electrum/electrum_scriptpubkey.cpp | 3 -- .../protocols/electrum/electrum_subscribe.cpp | 36 +++++++++++++++++++ 9 files changed, 46 insertions(+), 10 deletions(-) rename src/protocols/electrum/{protocol_electrum_scripthash_subscribe.cpp => protocol_electrum_subscribe.cpp} (100%) create mode 100644 test/protocols/electrum/electrum_subscribe.cpp diff --git a/Makefile.am b/Makefile.am index 68dbee8c..84bc77d2 100644 --- a/Makefile.am +++ b/Makefile.am @@ -57,9 +57,9 @@ src_libbitcoin_server_la_SOURCES = \ src/protocols/electrum/protocol_electrum_mempool.cpp \ src/protocols/electrum/protocol_electrum_outpoints.cpp \ src/protocols/electrum/protocol_electrum_scripthash.cpp \ - src/protocols/electrum/protocol_electrum_scripthash_subscribe.cpp \ src/protocols/electrum/protocol_electrum_scriptpubkey.cpp \ src/protocols/electrum/protocol_electrum_server.cpp \ + src/protocols/electrum/protocol_electrum_subscribe.cpp \ src/protocols/electrum/protocol_electrum_transactions.cpp \ src/protocols/electrum/protocol_electrum_version.cpp \ src/protocols/native/protocol_native.cpp \ @@ -108,6 +108,7 @@ test_libbitcoin_server_test_SOURCES = \ test/protocols/electrum/electrum_scripthash.cpp \ test/protocols/electrum/electrum_scriptpubkey.cpp \ test/protocols/electrum/electrum_server.cpp \ + test/protocols/electrum/electrum_subscribe.cpp \ test/protocols/electrum/electrum_transactions.cpp \ test/protocols/electrum/electrum_version.cpp \ test/protocols/native/native.cpp \ diff --git a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj index a464a72f..7d0c9501 100644 --- a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj @@ -148,6 +148,7 @@ + $(IntDir)test_protocols_electrum_electrum_version.obj diff --git a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters index 899f4c72..e517cdae 100644 --- a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters +++ b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters @@ -93,6 +93,9 @@ src\protocols\electrum + + src\protocols\electrum + src\protocols\electrum diff --git a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj index 66994154..e46bf8db 100644 --- a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj @@ -139,9 +139,9 @@ - + diff --git a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters index 138baa7e..75181587 100644 --- a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters +++ b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters @@ -117,15 +117,15 @@ src\protocols\electrum - - src\protocols\electrum - src\protocols\electrum src\protocols\electrum + + src\protocols\electrum + src\protocols\electrum diff --git a/src/protocols/electrum/protocol_electrum_scripthash_subscribe.cpp b/src/protocols/electrum/protocol_electrum_subscribe.cpp similarity index 100% rename from src/protocols/electrum/protocol_electrum_scripthash_subscribe.cpp rename to src/protocols/electrum/protocol_electrum_subscribe.cpp diff --git a/test/protocols/electrum/electrum_addresses.cpp b/test/protocols/electrum/electrum_addresses.cpp index 9ed9b915..2342aeff 100644 --- a/test/protocols/electrum/electrum_addresses.cpp +++ b/test/protocols/electrum/electrum_addresses.cpp @@ -384,6 +384,4 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_address_list_unspent__confirmed_and_un BOOST_REQUIRE(point12_0 < point11_1); } -// blockchain.address.subscribe - BOOST_AUTO_TEST_SUITE_END() diff --git a/test/protocols/electrum/electrum_scriptpubkey.cpp b/test/protocols/electrum/electrum_scriptpubkey.cpp index 36780f1f..ce26bfa8 100644 --- a/test/protocols/electrum/electrum_scriptpubkey.cpp +++ b/test/protocols/electrum/electrum_scriptpubkey.cpp @@ -427,7 +427,4 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_scriptpubkey_list_unspent__confirmed_a BOOST_REQUIRE(point12_0 < point11_1); } -// blockchain.scriptpubkey.subscribe -// blockchain.scriptpubkey.unsubscribe - BOOST_AUTO_TEST_SUITE_END() diff --git a/test/protocols/electrum/electrum_subscribe.cpp b/test/protocols/electrum/electrum_subscribe.cpp new file mode 100644 index 00000000..9426998e --- /dev/null +++ b/test/protocols/electrum/electrum_subscribe.cpp @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS) + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#include "../../test.hpp" +#include "electrum.hpp" + +////using namespace system; +////static const code not_found{ server::error::not_found }; +////static const code wrong_version{ server::error::wrong_version }; +////static const code not_implemented{ server::error::not_implemented }; +////static const code invalid_argument{ server::error::invalid_argument }; + +BOOST_FIXTURE_TEST_SUITE(electrum_tests, electrum_ten_block_setup_fixture) + +// blockchain.address.subscribe +// blockchain.scripthash.subscribe +// blockchain.scripthash.unsubscribe +// blockchain.scriptpubkey.subscribe +// blockchain.scriptpubkey.unsubscribe + +BOOST_AUTO_TEST_SUITE_END() From 810a7468c0243fa7836f3b0c34c03655287c0c04 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Mon, 20 Apr 2026 18:05:12 -0400 Subject: [PATCH 2/5] Style, delint. --- src/protocols/native/protocol_native_address.cpp | 6 +++++- src/protocols/native/protocol_native_block.cpp | 4 ++++ src/protocols/native/protocol_native_input.cpp | 10 ++++++++-- src/protocols/native/protocol_native_output.cpp | 8 +++++++- src/protocols/native/protocol_native_tx.cpp | 6 ++++++ 5 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/protocols/native/protocol_native_address.cpp b/src/protocols/native/protocol_native_address.cpp index 899b84d2..64fdf8ad 100644 --- a/src/protocols/native/protocol_native_address.cpp +++ b/src/protocols/native/protocol_native_address.cpp @@ -24,9 +24,11 @@ namespace libbitcoin { namespace server { +#define CLASS protocol_native + using namespace system; -#define CLASS protocol_native +BC_PUSH_WARNING(NO_INCOMPLETE_SWITCH) // handle_get_address // ---------------------------------------------------------------------------- @@ -231,5 +233,7 @@ void protocol_native::complete_get_address_balance(const code& ec, send_not_found(); } +BC_POP_WARNING() + } // namespace server } // namespace libbitcoin diff --git a/src/protocols/native/protocol_native_block.cpp b/src/protocols/native/protocol_native_block.cpp index 743a37ae..52828be8 100644 --- a/src/protocols/native/protocol_native_block.cpp +++ b/src/protocols/native/protocol_native_block.cpp @@ -30,6 +30,8 @@ namespace server { using namespace system; using namespace network::messages::peer; +BC_PUSH_WARNING(NO_INCOMPLETE_SWITCH) + bool protocol_native::handle_get_top(const code& ec, interface::top, uint8_t, uint8_t media) NOEXCEPT { @@ -484,5 +486,7 @@ bool protocol_native::handle_get_block_tx(const code& ec, interface::block_tx, return true; } +BC_POP_WARNING() + } // namespace server } // namespace libbitcoin diff --git a/src/protocols/native/protocol_native_input.cpp b/src/protocols/native/protocol_native_input.cpp index b92629d0..7df46b11 100644 --- a/src/protocols/native/protocol_native_input.cpp +++ b/src/protocols/native/protocol_native_input.cpp @@ -26,6 +26,9 @@ namespace server { using namespace system; +BC_PUSH_WARNING(NO_INCOMPLETE_SWITCH) +BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) + bool protocol_native::handle_get_inputs(const code& ec, interface::inputs, uint8_t, uint8_t media, const hash_cptr& hash, bool witness) NOEXCEPT { @@ -94,7 +97,7 @@ bool protocol_native::handle_get_input(const code& ec, interface::input, // Json input serialization includes witness. send_json(value_from(input), two * input->serialized_size(witness)); - return true; + return true; } } @@ -155,7 +158,7 @@ bool protocol_native::handle_get_input_witness(const code& ec, case json: send_json(value_from(witness), two * witness->serialized_size(false)); - return true; + return true; } } @@ -163,5 +166,8 @@ bool protocol_native::handle_get_input_witness(const code& ec, return true; } +BC_POP_WARNING() +BC_POP_WARNING() + } // namespace server } // namespace libbitcoin diff --git a/src/protocols/native/protocol_native_output.cpp b/src/protocols/native/protocol_native_output.cpp index ce871600..2fc3f188 100644 --- a/src/protocols/native/protocol_native_output.cpp +++ b/src/protocols/native/protocol_native_output.cpp @@ -26,6 +26,9 @@ namespace server { using namespace system; +BC_PUSH_WARNING(NO_INCOMPLETE_SWITCH) +BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) + bool protocol_native::handle_get_outputs(const code& ec, interface::outputs, uint8_t, uint8_t media, const hash_cptr& hash) NOEXCEPT { @@ -90,7 +93,7 @@ bool protocol_native::handle_get_output(const code& ec, interface::output, return true; case json: send_json(value_from(output), two * size); - return true; + return true; } } @@ -194,5 +197,8 @@ bool protocol_native::handle_get_output_spenders(const code& ec, return true; } +BC_POP_WARNING() +BC_POP_WARNING() + } // namespace server } // namespace libbitcoin diff --git a/src/protocols/native/protocol_native_tx.cpp b/src/protocols/native/protocol_native_tx.cpp index 20d3857d..af3de2a8 100644 --- a/src/protocols/native/protocol_native_tx.cpp +++ b/src/protocols/native/protocol_native_tx.cpp @@ -26,6 +26,9 @@ namespace server { using namespace system; +BC_PUSH_WARNING(NO_INCOMPLETE_SWITCH) +BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) + bool protocol_native::handle_get_tx(const code& ec, interface::tx, uint8_t, uint8_t media, const hash_cptr& hash, bool witness) NOEXCEPT { @@ -167,5 +170,8 @@ bool protocol_native::handle_get_tx_details(const code& ec, return true; } +BC_POP_WARNING() +BC_POP_WARNING() + } // namespace server } // namespace libbitcoin From 64c39cd76c02d92990c9ef7fc5eef4c4b571776f Mon Sep 17 00:00:00 2001 From: evoskuil Date: Mon, 20 Apr 2026 18:06:25 -0400 Subject: [PATCH 3/5] Electrum handle node::chase::transaction, move reorg fn. --- src/protocols/electrum/protocol_electrum.cpp | 19 +++++++++++++++++-- .../electrum/protocol_electrum_subscribe.cpp | 16 ---------------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/protocols/electrum/protocol_electrum.cpp b/src/protocols/electrum/protocol_electrum.cpp index e6290a10..8ccbb31d 100644 --- a/src/protocols/electrum/protocol_electrum.cpp +++ b/src/protocols/electrum/protocol_electrum.cpp @@ -136,10 +136,9 @@ bool protocol_electrum::handle_event(const code&, node::chase event_, if (stopped()) return false; - // TODO: collapse three atomics this into a single enumeration. switch (event_) { - ////case node::chase::transaction: + case node::chase::transaction: case node::chase::organized: { if (subscribed_height_.load(relaxed)) @@ -185,6 +184,22 @@ bool protocol_electrum::handle_event(const code&, node::chase event_, return true; } +// regress +// ---------------------------------------------------------------------------- + +// The chain has been reduced in height, clear all midstate cache and cursors. +void protocol_electrum::do_reorganized(node::header_t) NOEXCEPT +{ + BC_ASSERT(notification_strand_.running_in_this_thread()); + + for (auto& [key, sub]: address_subscriptions_) + { + // writer.flush resets hash accumulator, sub.type remains unchanged. + sub.state.writer.flush(); + sub.cursor = {}; + } +} + BC_POP_WARNING() } // namespace server diff --git a/src/protocols/electrum/protocol_electrum_subscribe.cpp b/src/protocols/electrum/protocol_electrum_subscribe.cpp index bf386d1e..9c055e5e 100644 --- a/src/protocols/electrum/protocol_electrum_subscribe.cpp +++ b/src/protocols/electrum/protocol_electrum_subscribe.cpp @@ -227,22 +227,6 @@ void protocol_electrum::scripthash_notify(const hash_digest& status, }, 128, BIND(handle_send, _1)); } -// regress -// ---------------------------------------------------------------------------- - -// The chain has been reduced in height, clear all midstate cache and cursors. -void protocol_electrum::do_reorganized(node::header_t) NOEXCEPT -{ - BC_ASSERT(notification_strand_.running_in_this_thread()); - - for (auto& [key, sub]: address_subscriptions_) - { - // writer.flush resets hash accumulator, sub.type remains unchanged. - sub.state.writer.flush(); - sub.cursor = {}; - } -} - // utility // ---------------------------------------------------------------------------- // private From 171f5208e261b53a764059d744b802afd487871c Mon Sep 17 00:00:00 2001 From: evoskuil Date: Tue, 21 Apr 2026 00:24:32 -0400 Subject: [PATCH 4/5] Implement outpoint notification differencing (fix). --- .../server/protocols/protocol_electrum.hpp | 91 ++++--- .../electrum/protocol_electrum_outpoints.cpp | 228 ++++++++++-------- .../electrum/protocol_electrum_subscribe.cpp | 9 +- 3 files changed, 185 insertions(+), 143 deletions(-) diff --git a/include/bitcoin/server/protocols/protocol_electrum.hpp b/include/bitcoin/server/protocols/protocol_electrum.hpp index e8d96b2c..32291e86 100644 --- a/include/bitcoin/server/protocols/protocol_electrum.hpp +++ b/include/bitcoin/server/protocols/protocol_electrum.hpp @@ -21,7 +21,6 @@ #include #include -#include #include #include #include @@ -206,12 +205,40 @@ class BCS_API protocol_electrum rpc_interface::mempool_get_info) NOEXCEPT; protected: + using point = system::chain::point; + using hash_digest = system::hash_digest; + using history = database::history; using unspents = database::unspents; using histories = database::histories; - using hash_digest = system::hash_digest; enum class notify_t { address, scripthash, scriptpubkey }; - typedef std::function status_handler; + + // Status hash optimization (~200 bytes). + struct midstate + { + hash_digest status{}; + system::stream::out::fast stream{ status }; + system::hash::sha256::fast writer{ stream }; + }; + + // Subscription to address/scripthash/scruptpubkey. + struct address_subscription + { + notify_t type{}; + midstate state{}; + database::address_link cursor{}; + }; + + // Subscription to outpoint. + struct outpoint_subscription + { + database::history outpoint{}; + database::histories spenders{}; + + bool operator==(const outpoint_subscription& other) const NOEXCEPT + { + return outpoint == other.outpoint && spenders == other.spenders; + } + }; /// Common implementation for block_header/s. void blockchain_block_headers(size_t starting, size_t quantity, @@ -259,18 +286,22 @@ class BCS_API protocol_electrum void scripthash_notify(const hash_digest& status, const hash_digest& hash, notify_t type) NOEXCEPT; + code get_scripthash_history(hash_digest& status, address_subscription& sub, + const hash_digest& hash, size_t limit=max_size_t) NOEXCEPT; + /// Outpoint. /// ----------------------------------------------------------------------- - void do_outpoint_subscribe(const system::chain::point& prevout, - const std::string& hint) NOEXCEPT; + void do_outpoint_subscribe(const point& prevout) NOEXCEPT; void complete_outpoint_subscribe(const code& ec, - const system::chain::point& prevout, - const std::string& hint) NOEXCEPT; - void do_outpoint_unsubscribe(const system::chain::point& prevout) NOEXCEPT; + const outpoint_subscription& sub, const point& prevout) NOEXCEPT; + void do_outpoint_unsubscribe(const point& prevout) NOEXCEPT; void complete_outpoint_unsubscribe(bool found) NOEXCEPT; void outpoint_notify(const std::unique_ptr& status, - const system::chain::point& prevout) NOEXCEPT; + const point& prevout) NOEXCEPT; + + bool get_outpoint_history(outpoint_subscription& sub, + const point& prevout) const NOEXCEPT; /// Utilities. /// ----------------------------------------------------------------------- @@ -296,22 +327,6 @@ class BCS_API protocol_electrum BIND_SAFE(BIND_SHARED(method, args))); } - // Status hash optimization (~200 bytes). - struct midstate - { - hash_digest status{}; - system::stream::out::fast stream{ status }; - system::hash::sha256::fast writer{ stream }; - }; - - // Subscription to address/scripthash/scruptpubkey. - struct subscription - { - notify_t type{}; - midstate state{}; - database::address_link cursor{}; - }; - // Aliases. using array_t = network::rpc::array_t; using object_t = network::rpc::object_t; @@ -319,21 +334,21 @@ class BCS_API protocol_electrum static constexpr electrum::version minimum = version_t::minimum; static constexpr electrum::version maximum = version_t::maximum; - // Status utilities. - code get_scripthash_status(hash_digest& out, subscription& sub, - const hash_digest& hash, size_t limit=max_size_t) NOEXCEPT; - bool get_outpoint_statuses(std::vector& out, - const system::chain::point& prevout) const NOEXCEPT; - bool get_outpoint_status(interface::object_t& out, - const system::chain::point& prevout) const NOEXCEPT; - bool send_outpoint_status(const system::chain::point& prevout, - const std::string& hint) NOEXCEPT; - // Transformations. static array_t transform(const unspents& unspents) NOEXCEPT; static array_t transform(const histories& histories) NOEXCEPT; static hash_digest to_status(const histories& histories) NOEXCEPT; static std::string to_method_name(notify_t type) NOEXCEPT; + static bool is_valid_hint(const std::string& hint) NOEXCEPT; + static object_t to_outpoint_status(size_t output_height) NOEXCEPT; + static object_t to_outpoint_status(size_t output_height, + const history& history) NOEXCEPT; + static object_t to_outpoint_status( + const outpoint_subscription& sub) NOEXCEPT; + std::unique_ptr make_status( + size_t output_height) const NOEXCEPT; + std::unique_ptr make_status(size_t output_height, + const history& history) const NOEXCEPT; // Compute server.features.hosts value from config. object_t self_hosts() const NOEXCEPT; @@ -367,8 +382,8 @@ class BCS_API protocol_electrum network::asio::strand notification_strand_; // These are protected by notification strand. - std::set outpoint_subscriptions_{}; - std::map address_subscriptions_{}; + std::map outpoint_subscriptions_{}; + std::map address_subscriptions_{}; }; } // namespace server diff --git a/src/protocols/electrum/protocol_electrum_outpoints.cpp b/src/protocols/electrum/protocol_electrum_outpoints.cpp index c8d7e14d..d61cd3fc 100644 --- a/src/protocols/electrum/protocol_electrum_outpoints.cpp +++ b/src/protocols/electrum/protocol_electrum_outpoints.cpp @@ -18,6 +18,7 @@ */ #include +#include #include #include #include @@ -28,14 +29,13 @@ namespace server { #define CLASS protocol_electrum using namespace system; -using namespace system::chain; using namespace network::rpc; using namespace std::placeholders; constexpr auto relaxed = std::memory_order_relaxed; BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) BC_PUSH_WARNING(SMART_PTR_NOT_NEEDED) -BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR) +BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_UNIQUE_PTR) void protocol_electrum::handle_blockchain_utxo_get_address(const code& ec, rpc_interface::blockchain_utxo_get_address, const std::string& tx_hash, @@ -104,13 +104,38 @@ void protocol_electrum::handle_blockchain_outpoint_get_status(const code& ec, uint32_t index{}; hash_digest hash{}; - if (!to_integer(index, txout_idx) || !decode_hash(hash, tx_hash)) + if (!to_integer(index, txout_idx) || !decode_hash(hash, tx_hash) || + !is_valid_hint(spk_hint)) { send_code(error::invalid_argument); return; } - send_outpoint_status({ hash, index }, spk_hint); + outpoint_subscription sub{}; + if (!get_outpoint_history(sub, { hash, index })) + { + send_code(error::not_found); + return; + } + + // Sends first spender only. + send_result(to_outpoint_status(sub), 128, BIND(complete, _1)); +} + +bool protocol_electrum::get_outpoint_history(outpoint_subscription& out, + const point& prevout) const NOEXCEPT +{ + const auto& query = archive(); + out.outpoint = query.get_tx_history(prevout.hash()); + if (!out.outpoint.valid()) + { + out.spenders.clear(); + return false; + } + + // TODO: this could be accept stopping_ but it's a small query. + out.spenders = query.get_spenders_history(prevout); + return true; } // subscribe @@ -133,46 +158,43 @@ void protocol_electrum::handle_blockchain_outpoint_subscribe(const code& ec, uint32_t index{}; hash_digest hash{}; - if (!to_integer(index, txout_idx) || !decode_hash(hash, tx_hash)) + if (!to_integer(index, txout_idx) || !decode_hash(hash, tx_hash) || + !is_valid_hint(spk_hint)) { send_code(error::invalid_argument); return; } - // Outpoint status is not long-running so the notification strand is only - // used to guard the notifications set. No need for the monitor, as - // do_outpoint_subscribe() is trivial and pointless to cancel. - ////monitor(true); - NOTIFY(do_outpoint_subscribe, point{ hash, index }, spk_hint); + monitor(true); + NOTIFY(do_outpoint_subscribe, point{ hash, index }); } -void protocol_electrum::do_outpoint_subscribe(const point& prevout, - const std::string& hint) NOEXCEPT +void protocol_electrum::do_outpoint_subscribe(const point& prevout) NOEXCEPT { // Cancellability is preserved because not on channel strand. BC_ASSERT(notification_strand_.running_in_this_thread()); - code ec{}; + outpoint_subscription sub{}; + code ec{ error::subscription_limit }; if (outpoint_subscriptions_.size() < options_.maximum_subscriptions) { - // Subscription response is idempotent. - outpoint_subscriptions_.insert(prevout); + ec = get_outpoint_history(sub, prevout) ? + error::success : ec = error::not_found; + + outpoint_subscriptions_.emplace(prevout, sub); subscribed_outpoint_.store(true, relaxed); } - else - { - ec = error::subscription_limit; - } - POST(complete_outpoint_subscribe, ec, prevout, hint); + // All current subscribers are cached and forwarded. + POST(complete_outpoint_subscribe, ec, std::move(sub), prevout); } void protocol_electrum::complete_outpoint_subscribe(const code& ec, - const point& prevout, const std::string& hint) NOEXCEPT + const outpoint_subscription& sub, const point& prevout) NOEXCEPT { BC_ASSERT(stranded()); - ////monitor(false); + monitor(false); if (stopped()) return; @@ -182,7 +204,21 @@ void protocol_electrum::complete_outpoint_subscribe(const code& ec, return; } - send_outpoint_status(prevout, hint); + // Send first spender only. + send_result(to_outpoint_status(sub), 128, BIND(complete, _1)); + + // Send here vs. completion handler because asio will queue it up + // behind the send anyway, and this prevents another sub copy. + // Send remaining spenders as notifications. + if (!sub.spenders.empty()) + { + const auto height = sub.outpoint.tx.height(); + for (auto spender = std::next(sub.spenders.begin()); + spender != sub.spenders.end(); ++spender) + { + POST(outpoint_notify, make_status(height, *spender), prevout); + } + } } // unsubscribe @@ -239,25 +275,48 @@ void protocol_electrum::do_outpoint(node::header_t) NOEXCEPT // Cancellability is preserved because not on channel strand. BC_ASSERT(notification_strand_.running_in_this_thread()); - for (const auto& prevout: outpoint_subscriptions_) + for (auto& subscription: outpoint_subscriptions_) { - if (stopping_) return; - std::vector statuses{}; - if (!get_outpoint_statuses(statuses, prevout)) + if (stopping_) + return; + + auto& sub = subscription.second; + const auto& prevout = subscription.first; + + outpoint_subscription res{}; + if (!get_outpoint_history(res, prevout)) { LOGV("Electrum::do_outpoint, outpoint not found."); + continue; + } + + // There is no change. + if (sub == res) continue; + + const auto height = sub.outpoint.tx.height(); + if (!sub.outpoint.valid() || sub.outpoint.tx.height() != height) + { + // Outpoint changed (or newly found), send all current spenders. + if (res.spenders.empty()) + { + POST(outpoint_notify, make_status(height), prevout); + } + else for (const auto& spender: res.spenders) + { + POST(outpoint_notify, make_status(height, spender), prevout); + } } else { - // There can be more than one spender for a given output, so - // instead of picking one arbitrarily, send all independently. - for (auto& status: statuses) + // Outpoint unchanged, send only new or changed spenders. + for (const auto& spender: difference(res.spenders, sub.spenders)) { - // Asio-buffered message (small, not under caller control). - auto ptr = std::make_unique(std::move(status)); - POST(outpoint_notify, std::move(ptr), prevout); + POST(outpoint_notify, make_status(height, spender), prevout); } } + + // Update subscription state. + sub = std::move(res); } } @@ -275,100 +334,67 @@ void protocol_electrum::outpoint_notify(const std::unique_ptr& status, // utility // ---------------------------------------------------------------------------- -// private -bool protocol_electrum::get_outpoint_statuses(std::vector& out, - const point& prevout) const NOEXCEPT +object_t protocol_electrum::to_outpoint_status(size_t output_height) NOEXCEPT { - BC_ASSERT(notification_strand_.running_in_this_thread()); - - const auto& query = archive(); - const auto history = query.get_tx_history(prevout.hash()); - if (!history.tx.is_valid()) - return false; - - out.clear(); - const auto height = to_unsigned(history.tx.height()); - const auto ins = query.get_spenders_history(prevout); - - // No spenders, just return unspent singleton. - if (ins.empty()) + return { - out.push_back({ { "height", height } }); - return true; - } + { "height", to_unsigned(output_height) } + }; +} - // One or more spenders, return all. - out.resize(ins.size()); - std::ranges::transform(ins, out.begin(), [height](const auto& in) NOEXCEPT +object_t protocol_electrum::to_outpoint_status(size_t output_height, + const history& history) NOEXCEPT +{ + return { - return object_t - { - { "height", height }, - { "spender_txhash", encode_hash(in.tx.hash()) }, - { "spender_height", to_unsigned(in.tx.height()) } - }; - }); - - return true; + { "height", to_unsigned(output_height) }, + { "spender_txhash", encode_hash(history.tx.hash()) }, + { "spender_height", to_unsigned(history.tx.height()) } + }; } -bool protocol_electrum::get_outpoint_status(object_t& out, - const point& prevout) const NOEXCEPT +// Converts only the first (if any). +object_t protocol_electrum::to_outpoint_status( + const outpoint_subscription& sub) NOEXCEPT { - BC_ASSERT(stranded()); - - const auto& query = archive(); - const auto history = query.get_tx_history(prevout.hash()); - if (!history.tx.is_valid()) - return false; - - // Zero or more spenders, return the first if multiple. - out = { { "height", to_unsigned(history.tx.height()) } }; - if (const auto ins = query.get_spenders_history(prevout); !ins.empty()) - { - out["spender_txhash"] = encode_hash(ins.front().tx.hash()); - out["spender_height"] = to_unsigned(ins.front().tx.height()); - } + BC_ASSERT(sub.outpoint.valid()); - return true; + const auto output_height = sub.outpoint.tx.height(); + return sub.spenders.empty() ? + to_outpoint_status(output_height) : + to_outpoint_status(output_height, sub.spenders.front()); } -bool protocol_electrum::send_outpoint_status(const point& prevout, - const std::string& hint) NOEXCEPT +bool protocol_electrum::is_valid_hint(const std::string& hint) NOEXCEPT { - BC_ASSERT(stranded()); - - // This is parsed for correctness but is not used. - // Script is advisory, and should match output script. if (!hint.empty()) { data_chunk bytes{}; if (!decode_base16(bytes, hint)) - { - send_code(error::invalid_argument); return false; - } chain::script script{ std::move(bytes), false }; if (!script.is_valid()) - { - send_code(error::invalid_argument); return false; - } - } - - object_t status{}; - if (!get_outpoint_status(status, prevout)) - { - send_code(error::not_found); - return false; } - send_result(std::move(status), 128, BIND(complete, _1)); return true; } +std::unique_ptr protocol_electrum::make_status( + size_t output_height) const NOEXCEPT +{ + return std::make_unique(to_outpoint_status(output_height)); +} + +std::unique_ptr protocol_electrum::make_status( + size_t output_height, const history& history) const NOEXCEPT +{ + return std::make_unique(to_outpoint_status(output_height, + history)); +} + BC_POP_WARNING() BC_POP_WARNING() BC_POP_WARNING() diff --git a/src/protocols/electrum/protocol_electrum_subscribe.cpp b/src/protocols/electrum/protocol_electrum_subscribe.cpp index 9c055e5e..a7d92682 100644 --- a/src/protocols/electrum/protocol_electrum_subscribe.cpp +++ b/src/protocols/electrum/protocol_electrum_subscribe.cpp @@ -96,7 +96,7 @@ void protocol_electrum::do_scripthash_subscribe(const hash_digest& hash, // Subscription response is idempotent. auto& at = *address_subscriptions_.try_emplace(hash, type, mid{}).first; - ec = get_scripthash_status(status, at.second, at.first, limit); + ec = get_scripthash_history(status, at.second, at.first, limit); subscribed_address_.store(true, relaxed); } else @@ -202,7 +202,7 @@ void protocol_electrum::do_scripthash(node::header_t) NOEXCEPT for (auto& [key, sub]: address_subscriptions_) { hash_digest status{}; - if (const auto ec = get_scripthash_status(status, sub, key)) + if (const auto ec = get_scripthash_history(status, sub, key)) { if (ec == database::error::canceled) return; LOGF("Electrum::do_scripthash, " << ec.message()); @@ -268,8 +268,9 @@ hash_digest protocol_electrum::to_status(const histories& histories) NOEXCEPT return out.status; } -code protocol_electrum::get_scripthash_status(hash_digest& out, - subscription& /* sub */, const hash_digest& hash, size_t /* limit */) NOEXCEPT +code protocol_electrum::get_scripthash_history(hash_digest& out, + address_subscription& /* sub */, const hash_digest& hash, + size_t /* limit */) NOEXCEPT { // TODO: use cursors and midstate to optimize succesive queries. // TODO: limit first pass query depth. From 1be0e573a0fe818e89e9c9b64211d16ccf6c039b Mon Sep 17 00:00:00 2001 From: evoskuil Date: Tue, 21 Apr 2026 01:30:15 -0400 Subject: [PATCH 5/5] Comments, style. --- src/protocols/electrum/protocol_electrum.cpp | 3 ++- .../electrum/protocol_electrum_outpoints.cpp | 19 +++++++++---------- .../electrum/protocol_electrum_subscribe.cpp | 9 ++------- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/protocols/electrum/protocol_electrum.cpp b/src/protocols/electrum/protocol_electrum.cpp index 8ccbb31d..70a1e327 100644 --- a/src/protocols/electrum/protocol_electrum.cpp +++ b/src/protocols/electrum/protocol_electrum.cpp @@ -184,8 +184,9 @@ bool protocol_electrum::handle_event(const code&, node::chase event_, return true; } -// regress +// reorganization // ---------------------------------------------------------------------------- +// outpoint subscriptions do not require modification. // The chain has been reduced in height, clear all midstate cache and cursors. void protocol_electrum::do_reorganized(node::header_t) NOEXCEPT diff --git a/src/protocols/electrum/protocol_electrum_outpoints.cpp b/src/protocols/electrum/protocol_electrum_outpoints.cpp index d61cd3fc..073a08a6 100644 --- a/src/protocols/electrum/protocol_electrum_outpoints.cpp +++ b/src/protocols/electrum/protocol_electrum_outpoints.cpp @@ -133,7 +133,6 @@ bool protocol_electrum::get_outpoint_history(outpoint_subscription& out, return false; } - // TODO: this could be accept stopping_ but it's a small query. out.spenders = query.get_spenders_history(prevout); return true; } @@ -169,6 +168,7 @@ void protocol_electrum::handle_blockchain_outpoint_subscribe(const code& ec, NOTIFY(do_outpoint_subscribe, point{ hash, index }); } +// Subscription response is idempotent. void protocol_electrum::do_outpoint_subscribe(const point& prevout) NOEXCEPT { // Cancellability is preserved because not on channel strand. @@ -180,7 +180,6 @@ void protocol_electrum::do_outpoint_subscribe(const point& prevout) NOEXCEPT { ec = get_outpoint_history(sub, prevout) ? error::success : ec = error::not_found; - outpoint_subscriptions_.emplace(prevout, sub); subscribed_outpoint_.store(true, relaxed); } @@ -207,9 +206,9 @@ void protocol_electrum::complete_outpoint_subscribe(const code& ec, // Send first spender only. send_result(to_outpoint_status(sub), 128, BIND(complete, _1)); + // Send remaining spenders as notifications. // Send here vs. completion handler because asio will queue it up // behind the send anyway, and this prevents another sub copy. - // Send remaining spenders as notifications. if (!sub.spenders.empty()) { const auto height = sub.outpoint.tx.height(); @@ -283,25 +282,25 @@ void protocol_electrum::do_outpoint(node::header_t) NOEXCEPT auto& sub = subscription.second; const auto& prevout = subscription.first; - outpoint_subscription res{}; - if (!get_outpoint_history(res, prevout)) + outpoint_subscription result{}; + if (!get_outpoint_history(result, prevout)) { LOGV("Electrum::do_outpoint, outpoint not found."); continue; } // There is no change. - if (sub == res) continue; + if (sub == result) continue; const auto height = sub.outpoint.tx.height(); if (!sub.outpoint.valid() || sub.outpoint.tx.height() != height) { // Outpoint changed (or newly found), send all current spenders. - if (res.spenders.empty()) + if (result.spenders.empty()) { POST(outpoint_notify, make_status(height), prevout); } - else for (const auto& spender: res.spenders) + else for (const auto& spender: result.spenders) { POST(outpoint_notify, make_status(height, spender), prevout); } @@ -309,14 +308,14 @@ void protocol_electrum::do_outpoint(node::header_t) NOEXCEPT else { // Outpoint unchanged, send only new or changed spenders. - for (const auto& spender: difference(res.spenders, sub.spenders)) + for (const auto& spender: difference(result.spenders, sub.spenders)) { POST(outpoint_notify, make_status(height, spender), prevout); } } // Update subscription state. - sub = std::move(res); + sub = std::move(result); } } diff --git a/src/protocols/electrum/protocol_electrum_subscribe.cpp b/src/protocols/electrum/protocol_electrum_subscribe.cpp index a7d92682..0bcf684c 100644 --- a/src/protocols/electrum/protocol_electrum_subscribe.cpp +++ b/src/protocols/electrum/protocol_electrum_subscribe.cpp @@ -81,28 +81,23 @@ void protocol_electrum::scripthash_subscribe(const hash_digest& hash, NOTIFY(do_scripthash_subscribe, hash, type); } +// Subscription response is idempotent. void protocol_electrum::do_scripthash_subscribe(const hash_digest& hash, notify_t type) NOEXCEPT { // Cancellability is preserved because not on channel strand. BC_ASSERT(notification_strand_.running_in_this_thread()); - code ec{}; hash_digest status{}; + code ec{ error::subscription_limit }; if (address_subscriptions_.size() < options_.maximum_subscriptions) { using mid = midstate; const auto limit = options().maximum_history; - - // Subscription response is idempotent. auto& at = *address_subscriptions_.try_emplace(hash, type, mid{}).first; ec = get_scripthash_history(status, at.second, at.first, limit); subscribed_address_.store(true, relaxed); } - else - { - ec = error::subscription_limit; - } POST(complete_scripthash_subscribe, ec, hash, status); }