diff --git a/Makefile.am b/Makefile.am
index ec5fca00..7fddf4a6 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -81,9 +81,12 @@ test_libbitcoin_server_test_SOURCES = \
test/protocols/blocks.hpp \
test/protocols/electrum/electrum.cpp \
test/protocols/electrum/electrum.hpp \
- test/protocols/electrum/electrum_block_header.cpp \
+ test/protocols/electrum/electrum_addresses.cpp \
+ test/protocols/electrum/electrum_fees.cpp \
+ test/protocols/electrum/electrum_headers.cpp \
test/protocols/electrum/electrum_server.cpp \
- test/protocols/electrum/electrum_server_version.cpp
+ test/protocols/electrum/electrum_server_version.cpp \
+ test/protocols/electrum/electrum_transactions.cpp
endif WITH_TESTS
diff --git a/builds/cmake/CMakeLists.txt b/builds/cmake/CMakeLists.txt
index 4183cdda..7e4e4f7b 100644
--- a/builds/cmake/CMakeLists.txt
+++ b/builds/cmake/CMakeLists.txt
@@ -310,9 +310,12 @@ if (with-tests)
"../../test/protocols/blocks.hpp"
"../../test/protocols/electrum/electrum.cpp"
"../../test/protocols/electrum/electrum.hpp"
- "../../test/protocols/electrum/electrum_block_header.cpp"
+ "../../test/protocols/electrum/electrum_addresses.cpp"
+ "../../test/protocols/electrum/electrum_fees.cpp"
+ "../../test/protocols/electrum/electrum_headers.cpp"
"../../test/protocols/electrum/electrum_server.cpp"
- "../../test/protocols/electrum/electrum_server_version.cpp" )
+ "../../test/protocols/electrum/electrum_server_version.cpp"
+ "../../test/protocols/electrum/electrum_transactions.cpp" )
add_test( NAME libbitcoin-server-test COMMAND libbitcoin-server-test
--run_test=*
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 cf12bd0c..a5b4ee32 100644
--- a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj
+++ b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj
@@ -133,9 +133,12 @@
$(IntDir)test_protocols_electrum_electrum.obj
-
+
+
+
+
$(IntDir)test_test.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 8ae34192..ebe5bf8e 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
@@ -57,7 +57,13 @@
src\protocols\electrum
-
+
+ src\protocols\electrum
+
+
+ src\protocols\electrum
+
+
src\protocols\electrum
@@ -66,6 +72,9 @@
src\protocols\electrum
+
+ src\protocols\electrum
+
src
diff --git a/include/bitcoin/server/channels/channel_electrum.hpp b/include/bitcoin/server/channels/channel_electrum.hpp
index c9915a76..c42f77c2 100644
--- a/include/bitcoin/server/channels/channel_electrum.hpp
+++ b/include/bitcoin/server/channels/channel_electrum.hpp
@@ -65,12 +65,12 @@ class BCS_API channel_electrum
return name_;
}
- inline void set_version(electrum::version version) NOEXCEPT
+ inline void set_version(server::electrum::version version) NOEXCEPT
{
version_ = version;
}
- inline electrum::version version() const NOEXCEPT
+ inline server::electrum::version version() const NOEXCEPT
{
return version_;
}
@@ -85,7 +85,7 @@ class BCS_API channel_electrum
const options_t& options_;
// These are protected by strand.
- electrum::version version_{ electrum::version::v0_0 };
+ server::electrum::version version_{ server::electrum::version::v0_0 };
std::string name_{};
};
diff --git a/include/bitcoin/server/protocols/protocol_electrum.hpp b/include/bitcoin/server/protocols/protocol_electrum.hpp
index a66d830e..65b30789 100644
--- a/include/bitcoin/server/protocols/protocol_electrum.hpp
+++ b/include/bitcoin/server/protocols/protocol_electrum.hpp
@@ -128,7 +128,10 @@ class BCS_API protocol_electrum
void blockchain_block_headers(size_t starting, size_t quantity,
size_t waypoint, bool multiplicity) NOEXCEPT;
- inline bool is_version(electrum::version version) const NOEXCEPT
+ /// Notify client of new header.
+ void do_header(node::header_t link) NOEXCEPT;
+
+ inline bool is_version(server::electrum::version version) const NOEXCEPT
{
return channel_->version() >= version;
}
@@ -139,8 +142,9 @@ class BCS_API protocol_electrum
}
private:
- // This is thread safe.
+ // These are thread safe.
const options_t& options_;
+ std::atomic_bool subscribed_{};
// This is mostly thread safe, and used in a thread safe manner.
const channel_t::ptr channel_;
diff --git a/src/protocols/protocol_electrum.cpp b/src/protocols/protocol_electrum.cpp
index 00c4a72e..4df677ee 100644
--- a/src/protocols/protocol_electrum.cpp
+++ b/src/protocols/protocol_electrum.cpp
@@ -94,7 +94,7 @@ void protocol_electrum::stopping(const code& ec) NOEXCEPT
// ----------------------------------------------------------------------------
bool protocol_electrum::handle_event(const code&, node::chase event_,
- node::event_value) NOEXCEPT
+ node::event_value value) NOEXCEPT
{
// Do not pass ec to stopped as it is not a call status.
if (stopped())
@@ -102,8 +102,14 @@ bool protocol_electrum::handle_event(const code&, node::chase event_,
switch (event_)
{
- case node::chase::suspend:
+ case node::chase::organized:
{
+ if (subscribed_.load(std::memory_order_relaxed))
+ {
+ BC_ASSERT(std::holds_alternative(value));
+ POST(do_header, std::get(value));
+ }
+
break;
}
default:
@@ -297,19 +303,39 @@ void protocol_electrum::handle_blockchain_headers_subscribe(const code& ec,
return;
}
- // TODO: signal header subscription.
+ subscribed_.store(true, std::memory_order_relaxed);
+ send_result(
+ {
+ object_t
+ {
+ { "height", uint64_t{ top } },
+ { "hex", to_hex(*header, chain::header::serialized_size()) }
+ }
+ }, 256, BIND(complete, _1));
+}
- // TODO: idempotent subscribe to chase::organized via session/chaser/node.
- // TODO: upon notification send just the header notified by the link.
- // TODO: it is client responsibility to deal with reorgs and race gaps.
- send_result(value_t
+void protocol_electrum::do_header(node::header_t link) NOEXCEPT
+{
+ BC_ASSERT(stranded());
+
+ const auto& query = archive();
+ const auto height = query.get_height(link);
+ const auto header = query.get_header(link);
+
+ if (height.is_terminal() || !header)
+ {
+ LOGF("Electrum::do_header, object not found (" << link << ").");
+ return;
+ }
+
+ send_notification("blockchain.headers.subscribe",
+ {
+ object_t
{
- object_t
- {
- { "height", uint64_t{ top } },
- { "hex", to_hex(*header, chain::header::serialized_size()) }
- }
- }, 256, BIND(complete, _1));
+ { "height", height.value },
+ { "hex", to_hex(*header, chain::header::serialized_size()) }
+ }
+ }, 100, BIND(complete, _1));
}
void protocol_electrum::handle_blockchain_estimate_fee(const code& ec,
@@ -381,6 +407,8 @@ void protocol_electrum::handle_blockchain_scripthash_unsubscribe(const code& ec,
send_code(error::not_implemented);
}
+// TODO: requires tx pool in order to validate against unconfirmed txs.
+// TODO: requires that p2p channels subscribe to transaction broadcast.
void protocol_electrum::handle_blockchain_transaction_broadcast(const code& ec,
rpc_interface::blockchain_transaction_broadcast,
const std::string& ) NOEXCEPT
@@ -477,14 +505,14 @@ void protocol_electrum::handle_mempool_get_fee_histogram(const code& ec,
// TODO: requires tx pool metadata graph.
send_result(value_t
+ {
+ array_t
{
- array_t
- {
- array_t{ 1, 1024 },
- array_t{ 2, 2048 },
- array_t{ 4, 4096 }
- }
- }, 256, BIND(complete, _1));
+ array_t{ 1, 1024 },
+ array_t{ 2, 2048 },
+ array_t{ 4, 4096 }
+ }
+ }, 256, BIND(complete, _1));
}
BC_POP_WARNING()
diff --git a/test/protocols/blocks.cpp b/test/protocols/blocks.cpp
index eadeb40b..6a6a7fcd 100644
--- a/test/protocols/blocks.cpp
+++ b/test/protocols/blocks.cpp
@@ -75,7 +75,7 @@ const chain::block block5{ block5_data, true };
const chain::block block6{ block6_data, true };
const chain::block block7{ block7_data, true };
const chain::block block8{ block8_data, true };
-const chain::block block9{ block8_data, true };
+const chain::block block9{ block9_data, true };
const server::settings::embedded_pages admin{};
const server::settings::embedded_pages native{};
diff --git a/test/protocols/electrum/electrum.cpp b/test/protocols/electrum/electrum.cpp
index a1850f4e..dfbad074 100644
--- a/test/protocols/electrum/electrum.cpp
+++ b/test/protocols/electrum/electrum.cpp
@@ -52,6 +52,7 @@ electrum_setup_fixture::electrum_setup_fixture()
electrum.connections = 1;
database_settings.interval_depth = 2;
node_settings.delay_inbound = false;
+ node_settings.minimum_fee_rate = 99.0;
network_settings.inbound.connections = 0;
network_settings.outbound.connections = 0;
auto ec = store_.create([](auto, auto) {});
@@ -127,4 +128,4 @@ bool electrum_setup_fixture::handshake(const std::string& version,
(result.at(0).is_string() && result.at(1).is_string()) &&
(result.at(0).as_string() == config().server.electrum.server_name) &&
(result.at(1).as_string() == version);
-}
\ No newline at end of file
+}
diff --git a/test/protocols/electrum/electrum_addresses.cpp b/test/protocols/electrum/electrum_addresses.cpp
new file mode 100644
index 00000000..3a32de00
--- /dev/null
+++ b/test/protocols/electrum/electrum_addresses.cpp
@@ -0,0 +1,24 @@
+/**
+ * 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"
+
+BOOST_FIXTURE_TEST_SUITE(electrum_tests, electrum_setup_fixture)
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/test/protocols/electrum/electrum_block_header.cpp b/test/protocols/electrum/electrum_block_header.cpp
deleted file mode 100644
index 7d3d625a..00000000
--- a/test/protocols/electrum/electrum_block_header.cpp
+++ /dev/null
@@ -1,162 +0,0 @@
-/**
- * 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"
-
-BOOST_FIXTURE_TEST_SUITE(electrum_tests, electrum_setup_fixture)
-
-// blockchain.block.header
-
-using namespace system;
-static const code not_found{ server::error::not_found };
-static const code target_overflow{ server::error::target_overflow };
-static const code invalid_argument{ server::error::invalid_argument };
-
-BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__genesis_no_checkpoint__expected_no_proof)
-{
- BOOST_CHECK(handshake());
-
- const auto response = get(R"({"id":43,"method":"blockchain.block.header","params":[0]})" "\n");
- BOOST_CHECK_EQUAL(response.at("result").as_object().at("header").as_string(), encode_base16(header0_data));
-}
-
-BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__block1_no_checkpoint__expected_no_proof)
-{
- BOOST_CHECK(handshake());
-
- const auto response = get(R"({"id":44,"method":"blockchain.block.header","params":[1]})" "\n");
- BOOST_CHECK_EQUAL(response.at("result").as_object().at("header").as_string(), encode_base16(header1_data));
-}
-
-BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__genesis_zero_checkpoint__expected_no_proof)
-{
- BOOST_CHECK(handshake());
-
- const auto response = get(R"({"id":45,"method":"blockchain.block.header","params":[0,0]})" "\n");
- const auto& result = response.at("result").as_object();
- BOOST_CHECK_EQUAL(result.at("header").as_string(), encode_base16(header0_data));
-}
-
-BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__proof_self_block1__expected)
-{
- BOOST_CHECK(handshake());
- const auto expected_header = encode_base16(header1_data);
- const auto expected_root = encode_hash(merkle_root(
- {
- block0_hash,
- block1_hash
- }));
-
- const auto response = get(R"({"id":46,"method":"blockchain.block.header","params":[1,1]})" "\n");
- const auto& result = response.at("result").as_object();
- BOOST_CHECK_EQUAL(result.at("header").as_string(), expected_header);
- BOOST_CHECK_EQUAL(result.at("root").as_string(), expected_root);
-
- const auto& branch = result.at("branch").as_array();
- BOOST_CHECK_EQUAL(branch.size(), 1u);
- BOOST_CHECK_EQUAL(branch.at(0).as_string(), encode_hash(block0_hash));
-}
-
-BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__proof_example__expected)
-{
- BOOST_CHECK(handshake());
-
- const auto expected_root = encode_hash(merkle_root(
- {
- block0_hash,
- block1_hash,
- block2_hash,
- block3_hash,
- block4_hash,
- block5_hash,
- block6_hash,
- block7_hash,
- block8_hash
- }));
-
- const string_list expected_branch
- {
- encode_hash(block4_hash),
- encode_hash(root67),
- encode_hash(root03),
- encode_hash(root88)
- };
-
- const auto response = get(R"({"id":50,"method":"blockchain.block.header","params":[5,8]})" "\n");
- const auto& result = response.at("result").as_object();
- BOOST_CHECK_EQUAL(result.at("header").as_string(), encode_base16(header5_data));
- BOOST_CHECK_EQUAL(result.at("root").as_string(), expected_root);
-
- const auto& branch = result.at("branch").as_array();
- BOOST_CHECK_EQUAL(branch.size(), expected_branch.size());
- BOOST_CHECK_EQUAL(branch.at(0).as_string(), expected_branch[0]);
- BOOST_CHECK_EQUAL(branch.at(1).as_string(), expected_branch[1]);
- BOOST_CHECK_EQUAL(branch.at(2).as_string(), expected_branch[2]);
- BOOST_CHECK_EQUAL(branch.at(3).as_string(), expected_branch[3]);
-}
-
-BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__checkpoint_below_height__target_overflow)
-{
- BOOST_CHECK(handshake());
-
- const auto response = get(R"({"id":51,"method":"blockchain.block.header","params":[2,1]})" "\n");
- BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), target_overflow.value());
-}
-
-BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__above_top__not_found)
-{
- BOOST_CHECK(handshake());
-
- const auto response = get(R"({"id":52,"method":"blockchain.block.header","params":[10]})" "\n");
- BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value());
-}
-
-BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__checkpoint_above_top__not_found)
-{
- BOOST_CHECK(handshake());
-
- const auto response = get(R"({"id":53,"method":"blockchain.block.header","params":[1,10]})" "\n");
- BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value());
-}
-
-BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__negative_height__invalid_argument)
-{
- BOOST_CHECK(handshake());
-
- const auto response = get(R"({"id":54,"method":"blockchain.block.header","params":[-1]})" "\n");
- BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value());
-}
-
-BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__fractional_height__invalid_argument)
-{
- BOOST_CHECK(handshake());
-
- const auto response = get(R"({"id":55,"method":"blockchain.block.header","params":[1.5]})" "\n");
- BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value());
-}
-
-BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__over_top_height__not_found)
-{
- BOOST_CHECK(handshake());
-
- const auto response = get(R"({"id":56,"method":"blockchain.block.header","params":[4294967296]})" "\n");
- BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value());
-}
-
-BOOST_AUTO_TEST_SUITE_END()
diff --git a/test/protocols/electrum/electrum_fees.cpp b/test/protocols/electrum/electrum_fees.cpp
new file mode 100644
index 00000000..bdc2622d
--- /dev/null
+++ b/test/protocols/electrum/electrum_fees.cpp
@@ -0,0 +1,37 @@
+/**
+ * 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"
+
+BOOST_FIXTURE_TEST_SUITE(electrum_tests, electrum_setup_fixture)
+
+// blockchain.relay_fee
+
+BOOST_AUTO_TEST_CASE(electrum__blockchain_relay_fee__default__expected)
+{
+ BOOST_CHECK(handshake());
+
+ constexpr auto expected = 99.0;
+ const auto response = get(R"({"id":90,"method":"blockchain.relayfee","params":[]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("id").as_int64(), 90);
+ BOOST_CHECK(response.at("result").is_number());
+ BOOST_CHECK_EQUAL(response.at("result").as_double(), expected);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/test/protocols/electrum/electrum_headers.cpp b/test/protocols/electrum/electrum_headers.cpp
new file mode 100644
index 00000000..3247c379
--- /dev/null
+++ b/test/protocols/electrum/electrum_headers.cpp
@@ -0,0 +1,385 @@
+/**
+ * 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"
+
+BOOST_FIXTURE_TEST_SUITE(electrum_tests, electrum_setup_fixture)
+
+using namespace system;
+static const code not_found{ server::error::not_found };
+static const code target_overflow{ server::error::target_overflow };
+static const code invalid_argument{ server::error::invalid_argument };
+
+// blockchain.block.header
+
+BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__genesis_no_checkpoint__expected_no_proof)
+{
+ BOOST_CHECK(handshake());
+
+ const auto response = get(R"({"id":43,"method":"blockchain.block.header","params":[0]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("result").as_object().at("header").as_string(), encode_base16(header0_data));
+}
+
+BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__block1_no_checkpoint__expected_no_proof)
+{
+ BOOST_CHECK(handshake());
+
+ const auto response = get(R"({"id":44,"method":"blockchain.block.header","params":[1]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("result").as_object().at("header").as_string(), encode_base16(header1_data));
+}
+
+BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__genesis_zero_checkpoint__expected_no_proof)
+{
+ BOOST_CHECK(handshake());
+
+ const auto response = get(R"({"id":45,"method":"blockchain.block.header","params":[0,0]})" "\n");
+ const auto& result = response.at("result").as_object();
+ BOOST_CHECK_EQUAL(result.at("header").as_string(), encode_base16(header0_data));
+}
+
+BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__proof_self_block1__expected)
+{
+ BOOST_CHECK(handshake());
+ const auto expected_header = encode_base16(header1_data);
+ const auto expected_root = encode_hash(merkle_root(
+ {
+ block0_hash,
+ block1_hash
+ }));
+
+ const auto response = get(R"({"id":46,"method":"blockchain.block.header","params":[1,1]})" "\n");
+ const auto& result = response.at("result").as_object();
+ BOOST_CHECK_EQUAL(result.at("header").as_string(), expected_header);
+ BOOST_CHECK_EQUAL(result.at("root").as_string(), expected_root);
+
+ const auto& branch = result.at("branch").as_array();
+ BOOST_CHECK_EQUAL(branch.size(), 1u);
+ BOOST_CHECK_EQUAL(branch.at(0).as_string(), encode_hash(block0_hash));
+}
+
+BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__proof_example__expected)
+{
+ BOOST_CHECK(handshake());
+
+ const auto expected_root = encode_hash(merkle_root(
+ {
+ block0_hash,
+ block1_hash,
+ block2_hash,
+ block3_hash,
+ block4_hash,
+ block5_hash,
+ block6_hash,
+ block7_hash,
+ block8_hash
+ }));
+
+ const string_list expected_branch
+ {
+ encode_hash(block4_hash),
+ encode_hash(root67),
+ encode_hash(root03),
+ encode_hash(root88)
+ };
+
+ const auto response = get(R"({"id":50,"method":"blockchain.block.header","params":[5,8]})" "\n");
+ const auto& result = response.at("result").as_object();
+ BOOST_CHECK_EQUAL(result.at("header").as_string(), encode_base16(header5_data));
+ BOOST_CHECK_EQUAL(result.at("root").as_string(), expected_root);
+
+ const auto& branch = result.at("branch").as_array();
+ BOOST_CHECK_EQUAL(branch.size(), expected_branch.size());
+ BOOST_CHECK_EQUAL(branch.at(0).as_string(), expected_branch[0]);
+ BOOST_CHECK_EQUAL(branch.at(1).as_string(), expected_branch[1]);
+ BOOST_CHECK_EQUAL(branch.at(2).as_string(), expected_branch[2]);
+ BOOST_CHECK_EQUAL(branch.at(3).as_string(), expected_branch[3]);
+}
+
+BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__checkpoint_below_height__target_overflow)
+{
+ BOOST_CHECK(handshake());
+
+ const auto response = get(R"({"id":51,"method":"blockchain.block.header","params":[2,1]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), target_overflow.value());
+}
+
+BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__above_top__not_found)
+{
+ BOOST_CHECK(handshake());
+
+ const auto response = get(R"({"id":52,"method":"blockchain.block.header","params":[10]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value());
+}
+
+BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__checkpoint_above_top__not_found)
+{
+ BOOST_CHECK(handshake());
+
+ const auto response = get(R"({"id":53,"method":"blockchain.block.header","params":[1,10]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value());
+}
+
+BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__negative_height__invalid_argument)
+{
+ BOOST_CHECK(handshake());
+
+ const auto response = get(R"({"id":54,"method":"blockchain.block.header","params":[-1]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value());
+}
+
+BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__fractional_height__invalid_argument)
+{
+ BOOST_CHECK(handshake());
+
+ const auto response = get(R"({"id":55,"method":"blockchain.block.header","params":[1.5]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value());
+}
+
+BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__over_top_height__not_found)
+{
+ BOOST_CHECK(handshake());
+
+ const auto response = get(R"({"id":56,"method":"blockchain.block.header","params":[4294967296]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value());
+}
+
+// blockchain.block.headers
+
+BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__genesis_count1_no_checkpoint__expected)
+{
+ BOOST_CHECK(handshake());
+
+ const auto response = get(R"({"id":60,"method":"blockchain.block.headers","params":[0,1]})" "\n");
+ const auto& result = response.at("result").as_object();
+ BOOST_CHECK_EQUAL(result.at("max").as_int64(), 5);
+ BOOST_CHECK_EQUAL(result.at("count").as_int64(), 1);
+ BOOST_CHECK(result.at("headers").is_array());
+ BOOST_CHECK_EQUAL(result.at("headers").as_array().size(), 1u);
+ BOOST_CHECK_EQUAL(result.at("headers").as_array().at(0).as_string(), encode_base16(header0_data));
+}
+
+BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__block1to3_no_checkpoint__expected)
+{
+ BOOST_CHECK(handshake());
+
+ const auto response = get(R"({"id":61,"method":"blockchain.block.headers","params":[1,3]})" "\n");
+ const auto& result = response.at("result").as_object();
+ BOOST_CHECK_EQUAL(result.at("max").as_int64(), 5);
+ BOOST_CHECK_EQUAL(result.at("count").as_int64(), 3);
+ BOOST_CHECK(result.at("headers").is_array());
+
+ const auto& headers = result.at("headers").as_array();
+ BOOST_CHECK_EQUAL(headers.size(), 3u);
+ BOOST_CHECK_EQUAL(headers.at(0).as_string(), encode_base16(header1_data));
+ BOOST_CHECK_EQUAL(headers.at(1).as_string(), encode_base16(header2_data));
+ BOOST_CHECK_EQUAL(headers.at(2).as_string(), encode_base16(header3_data));
+}
+
+BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__count_exceeds_max__capped)
+{
+ BOOST_CHECK(handshake());
+
+ const auto response = get(R"({"id":62,"method":"blockchain.block.headers","params":[0,10]})" "\n");
+ const auto& result = response.at("result").as_object();
+ BOOST_CHECK_EQUAL(result.at("max").as_int64(), 5);
+ BOOST_CHECK_EQUAL(result.at("count").as_int64(), 5);
+ BOOST_CHECK_EQUAL(result.at("headers").as_array().size(), 5u);
+}
+
+BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__count_zero__empty_headers)
+{
+ BOOST_CHECK(handshake());
+
+ const auto response = get(R"({"id":63,"method":"blockchain.block.headers","params":[5,0]})" "\n");
+ const auto& result = response.at("result").as_object();
+ BOOST_CHECK_EQUAL(result.at("count").as_int64(), 0);
+ BOOST_CHECK(result.at("headers").as_array().empty());
+}
+
+BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__proof_no_offset__expected)
+{
+ BOOST_CHECK(handshake());
+
+ const auto expected_root = encode_hash(merkle_root(
+ {
+ block0_hash,
+ block1_hash,
+ block2_hash,
+ block3_hash,
+ block4_hash,
+ block5_hash,
+ block6_hash,
+ block7_hash,
+ block8_hash
+ }));
+
+ const string_list expected_branch
+ {
+ encode_hash(block4_hash),
+ encode_hash(root67),
+ encode_hash(root03),
+ encode_hash(root88)
+ };
+
+ const auto response = get(R"({"id":64,"method":"blockchain.block.headers","params":[5,1,8]})" "\n");
+ const auto& result = response.at("result").as_object();
+ BOOST_CHECK_EQUAL(result.at("max").as_int64(), 5);
+ BOOST_CHECK_EQUAL(result.at("count").as_int64(), 1);
+ BOOST_CHECK_EQUAL(result.at("headers").as_array().size(), 1u);
+ BOOST_CHECK_EQUAL(result.at("root").as_string(), expected_root);
+
+ const auto& branch = result.at("branch").as_array();
+ BOOST_CHECK_EQUAL(branch.size(), expected_branch.size());
+ BOOST_CHECK_EQUAL(branch.at(0).as_string(), expected_branch[0]);
+ BOOST_CHECK_EQUAL(branch.at(1).as_string(), expected_branch[1]);
+ BOOST_CHECK_EQUAL(branch.at(2).as_string(), expected_branch[2]);
+ BOOST_CHECK_EQUAL(branch.at(3).as_string(), expected_branch[3]);
+}
+
+BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__proof_offset__expected)
+{
+ BOOST_CHECK(handshake());
+
+ const auto expected_root = encode_hash(merkle_root(
+ {
+ block0_hash,
+ block1_hash,
+ block2_hash,
+ block3_hash,
+ block4_hash,
+ block5_hash,
+ block6_hash,
+ block7_hash,
+ block8_hash
+ }));
+
+ const string_list expected_branch
+ {
+ encode_hash(block6_hash),
+ encode_hash(root45),
+ encode_hash(root03),
+ encode_hash(root88)
+ };
+
+ const auto response = get(R"({"id":64,"method":"blockchain.block.headers","params":[5,3,8]})" "\n");
+ const auto& result = response.at("result").as_object();
+ BOOST_CHECK_EQUAL(result.at("max").as_int64(), 5);
+ BOOST_CHECK_EQUAL(result.at("count").as_int64(), 3);
+ BOOST_CHECK_EQUAL(result.at("headers").as_array().size(), 3u);
+ BOOST_CHECK_EQUAL(result.at("root").as_string(), expected_root);
+
+ const auto& branch = result.at("branch").as_array();
+ BOOST_CHECK_EQUAL(branch.size(), expected_branch.size());
+ BOOST_CHECK_EQUAL(branch.at(0).as_string(), expected_branch[0]);
+ BOOST_CHECK_EQUAL(branch.at(1).as_string(), expected_branch[1]);
+ BOOST_CHECK_EQUAL(branch.at(2).as_string(), expected_branch[2]);
+ BOOST_CHECK_EQUAL(branch.at(3).as_string(), expected_branch[3]);
+}
+
+BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__start_above_top__not_found)
+{
+ BOOST_CHECK(handshake());
+
+ const auto response = get(R"({"id":65,"method":"blockchain.block.headers","params":[10,1]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value());
+}
+
+BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__target_exceeds_waypoint__target_overflow)
+{
+ BOOST_CHECK(handshake());
+
+ const auto response = get(R"({"id":66,"method":"blockchain.block.headers","params":[2,3,1]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), target_overflow.value());
+}
+
+BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__waypoint_above_top__not_found)
+{
+ BOOST_CHECK(handshake());
+
+ const auto response = get(R"({"id":67,"method":"blockchain.block.headers","params":[0,1,10]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value());
+}
+
+BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__negative_start__invalid_argument)
+{
+ BOOST_CHECK(handshake());
+
+ const auto response = get(R"({"id":68,"method":"blockchain.block.headers","params":[-1,1]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value());
+}
+
+BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__fractional_count__invalid_argument)
+{
+ BOOST_CHECK(handshake());
+
+ const auto response = get(R"({"id":69,"method":"blockchain.block.headers","params":[0,1.5]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value());
+}
+
+BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__start_plus_count_huge__not_found)
+{
+ BOOST_CHECK(handshake());
+
+ // argument_overflow is not actually reachable via json due to its integer limits.
+ const auto response = get(R"({"id":70,"method":"blockchain.block.headers","params":[9007199254740991,2]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value());
+}
+
+// blockchain.headers.subscribe
+
+BOOST_AUTO_TEST_CASE(electrum__blockchain_headers_subscribe__default__expected)
+{
+ BOOST_CHECK(handshake());
+
+ const auto response = get(R"({"id":80,"method":"blockchain.headers.subscribe","params":[]})" "\n");
+ const auto& result = response.at("result").as_object();
+ BOOST_CHECK_EQUAL(result.at("height").as_int64(), 9);
+ BOOST_CHECK_EQUAL(result.at("hex").as_string(), system::encode_base16(header9_data));
+}
+
+BOOST_AUTO_TEST_CASE(electrum__blockchain_headers_subscribe__jsonrpc_2__expected)
+{
+ BOOST_CHECK(handshake());
+
+ const auto response = get(R"({"jsonrpc":"2.0","id":81,"method":"blockchain.headers.subscribe"})" "\n");
+ const auto& result = response.at("result").as_object();
+ BOOST_CHECK_EQUAL(result.at("height").as_int64(), 9);
+ BOOST_CHECK_EQUAL(result.at("hex").as_string(), system::encode_base16(header9_data));
+}
+
+BOOST_AUTO_TEST_CASE(electrum__blockchain_headers_subscribe__id_preserved__expected)
+{
+ BOOST_CHECK(handshake());
+
+ const auto response = get(R"({"id":123,"method":"blockchain.headers.subscribe","params":[]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("id").as_int64(), 123);
+ BOOST_CHECK(response.at("result").is_object());
+}
+
+BOOST_AUTO_TEST_CASE(electrum__blockchain_headers_subscribe__empty_params__expected)
+{
+ BOOST_CHECK(handshake());
+
+ const auto response = get(R"({"id":82,"method":"blockchain.headers.subscribe","params":[]})" "\n");
+ const auto& result = response.at("result").as_object();
+ BOOST_CHECK_EQUAL(result.at("height").as_int64(), 9);
+ BOOST_CHECK_EQUAL(result.at("hex").as_string(), system::encode_base16(header9_data));
+}
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/test/protocols/electrum/electrum_transactions.cpp b/test/protocols/electrum/electrum_transactions.cpp
new file mode 100644
index 00000000..3a32de00
--- /dev/null
+++ b/test/protocols/electrum/electrum_transactions.cpp
@@ -0,0 +1,24 @@
+/**
+ * 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"
+
+BOOST_FIXTURE_TEST_SUITE(electrum_tests, electrum_setup_fixture)
+
+BOOST_AUTO_TEST_SUITE_END()