diff --git a/Makefile.am b/Makefile.am
index d1451823..ec5fca00 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -43,6 +43,7 @@ src_libbitcoin_server_la_SOURCES = \
src/settings.cpp \
src/parsers/bitcoind_query.cpp \
src/parsers/bitcoind_target.cpp \
+ src/parsers/electrum_version.cpp \
src/parsers/native_query.cpp \
src/parsers/native_target.cpp \
src/protocols/protocol_bitcoind_rest.cpp \
@@ -71,11 +72,18 @@ test_libbitcoin_server_test_SOURCES = \
test/settings.cpp \
test/test.cpp \
test/test.hpp \
- test/endpoints/electrum.cpp \
test/parsers/bitcoind_query.cpp \
test/parsers/bitcoind_target.cpp \
+ test/parsers/electrum_version.cpp \
test/parsers/native_query.cpp \
- test/parsers/native_target.cpp
+ test/parsers/native_target.cpp \
+ test/protocols/blocks.cpp \
+ 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_server.cpp \
+ test/protocols/electrum/electrum_server_version.cpp
endif WITH_TESTS
diff --git a/builds/cmake/CMakeLists.txt b/builds/cmake/CMakeLists.txt
index 1c598f44..4183cdda 100644
--- a/builds/cmake/CMakeLists.txt
+++ b/builds/cmake/CMakeLists.txt
@@ -242,6 +242,7 @@ add_library( ${CANONICAL_LIB_NAME}
"../../src/settings.cpp"
"../../src/parsers/bitcoind_query.cpp"
"../../src/parsers/bitcoind_target.cpp"
+ "../../src/parsers/electrum_version.cpp"
"../../src/parsers/native_query.cpp"
"../../src/parsers/native_target.cpp"
"../../src/protocols/protocol_bitcoind_rest.cpp"
@@ -296,15 +297,22 @@ if (with-tests)
"../../test/test.hpp"
"../../test/endpoints/README.md"
"../../test/endpoints/conftest.py"
- "../../test/endpoints/electrum.cpp"
"../../test/endpoints/test_bitcoind_rpc.py"
"../../test/endpoints/test_electrum.py"
"../../test/endpoints/test_native.py"
"../../test/endpoints/utils.py"
"../../test/parsers/bitcoind_query.cpp"
"../../test/parsers/bitcoind_target.cpp"
+ "../../test/parsers/electrum_version.cpp"
"../../test/parsers/native_query.cpp"
- "../../test/parsers/native_target.cpp" )
+ "../../test/parsers/native_target.cpp"
+ "../../test/protocols/blocks.cpp"
+ "../../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_server.cpp"
+ "../../test/protocols/electrum/electrum_server_version.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 1941434b..cf12bd0c 100644
--- a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj
+++ b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj
@@ -119,20 +119,31 @@
-
+
+
+ $(IntDir)test_protocols_blocks.obj
+
+
+ $(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 3805853e..8ae34192 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
@@ -16,14 +16,17 @@
{66A0E586-2E3A-448F-0000-000000000002}
+
+ {66A0E586-2E3A-448F-0000-000000000003}
+
+
+ {66A0E586-2E3A-448F-0000-000000000004}
+
src
-
- src\endpoints
-
src
@@ -39,12 +42,30 @@
src\parsers
+
+ src\parsers
+
src\parsers
src\parsers
+
+ src\protocols
+
+
+ src\protocols\electrum
+
+
+ src\protocols\electrum
+
+
+ src\protocols\electrum
+
+
+ src\protocols\electrum
+
src
@@ -53,6 +74,12 @@
+
+ src\protocols
+
+
+ src\protocols\electrum
+
src
diff --git a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj
index 5e97c485..577d1b32 100644
--- a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj
+++ b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj
@@ -127,6 +127,7 @@
+
diff --git a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters
index 1bdec80b..ae7d511c 100644
--- a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters
+++ b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters
@@ -69,6 +69,9 @@
src\parsers
+
+ src\parsers
+
src\parsers
diff --git a/include/bitcoin/server/channels/channel_electrum.hpp b/include/bitcoin/server/channels/channel_electrum.hpp
index 79038c1f..5a470400 100644
--- a/include/bitcoin/server/channels/channel_electrum.hpp
+++ b/include/bitcoin/server/channels/channel_electrum.hpp
@@ -39,18 +39,37 @@ class BCS_API channel_electrum
public:
typedef std::shared_ptr ptr;
using interface_t = interface::electrum;
- using options_t = typename network::channel_rpc::options_t;
+ using options_t = settings::electrum_server;
inline channel_electrum(const network::logger& log,
const network::socket::ptr& socket, uint64_t identifier,
const node::configuration& config, const options_t& options) NOEXCEPT
: server::channel(log, socket, identifier, config),
+ options_(options),
network::channel_rpc(log, socket, identifier,
config.network, options),
network::tracker(log)
{
}
+ void stop(const code& ec) NOEXCEPT override
+ {
+ std::cerr << "[STOP TRIGGERED] value=" << ec.value()
+ << " message=\"" << ec.message() << "\""
+ << " category=" << ec.category().name() << std::endl;
+
+ network::channel_rpc::stop(ec);
+ }
+
+ void stopping(const code& ec) NOEXCEPT override
+ {
+ std::cerr << "[STOPPING - ASYNC] value=" << ec.value()
+ << " message=\"" << ec.message() << "\""
+ << " category=" << ec.category().name() << std::endl;
+
+ network::channel_rpc::stopping(ec);
+ }
+
/// Properties.
/// -----------------------------------------------------------------------
@@ -64,19 +83,27 @@ class BCS_API channel_electrum
return name_;
}
- inline void set_version(electrum_version version) NOEXCEPT
+ inline void set_version(electrum::version version) NOEXCEPT
{
version_ = version;
}
- inline electrum_version version() const NOEXCEPT
+ inline electrum::version version() const NOEXCEPT
{
return version_;
}
+ inline const options_t& options() const NOEXCEPT
+ {
+ return options_;
+ }
+
private:
+ // This is thread safe.
+ const options_t& options_;
+
// These are protected by strand.
- electrum_version version_{ electrum_version::v0_0 };
+ electrum::version version_{ electrum::version::v0_0 };
std::string name_{};
};
diff --git a/include/bitcoin/server/interfaces/types.hpp b/include/bitcoin/server/interfaces/types.hpp
index 717e2241..459410e9 100644
--- a/include/bitcoin/server/interfaces/types.hpp
+++ b/include/bitcoin/server/interfaces/types.hpp
@@ -46,6 +46,7 @@ using object_t = network::rpc::object_t;
using array_t = network::rpc::array_t;
using value_t = network::rpc::value_t;
using null_t = network::rpc::null_t;
+using code_t = network::rpc::code_t;
namespace empty { constexpr auto array = network::rpc::empty::array; };
namespace empty { constexpr auto object = network::rpc::empty::object; };
diff --git a/include/bitcoin/server/parsers/electrum_version.hpp b/include/bitcoin/server/parsers/electrum_version.hpp
index 63ec3def..5ac26112 100644
--- a/include/bitcoin/server/parsers/electrum_version.hpp
+++ b/include/bitcoin/server/parsers/electrum_version.hpp
@@ -19,12 +19,14 @@
#ifndef LIBBITCOIN_SERVER_PARSERS_ELECTRUM_VERSION_HPP
#define LIBBITCOIN_SERVER_PARSERS_ELECTRUM_VERSION_HPP
+#include
#include
namespace libbitcoin {
namespace server {
+namespace electrum {
-enum class electrum_version
+enum class version
{
/// Invalid version.
v0_0,
@@ -66,6 +68,10 @@ enum class electrum_version
v1_6
};
+std::string_view version_to_string(version value) NOEXCEPT;
+version version_from_string(const std::string_view& value) NOEXCEPT;
+
+} // namespace electrum
} // namespace server
} // namespace libbitcoin
diff --git a/include/bitcoin/server/protocols/protocol_electrum.hpp b/include/bitcoin/server/protocols/protocol_electrum.hpp
index 8449ecc6..a66d830e 100644
--- a/include/bitcoin/server/protocols/protocol_electrum.hpp
+++ b/include/bitcoin/server/protocols/protocol_electrum.hpp
@@ -42,6 +42,7 @@ class BCS_API protocol_electrum
const network::channel::ptr& channel,
const options_t& options) NOEXCEPT
: protocol_rpc(session, channel, options),
+ options_(options),
channel_(std::dynamic_pointer_cast(channel)),
network::tracker(session->log)
{
@@ -127,12 +128,20 @@ 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
+ inline bool is_version(electrum::version version) const NOEXCEPT
{
return channel_->version() >= version;
}
+ inline const options_t& options() const NOEXCEPT
+ {
+ return options_;
+ }
+
private:
+ // This is thread safe.
+ const options_t& options_;
+
// This is mostly thread safe, and used in a thread safe manner.
const channel_t::ptr channel_;
};
diff --git a/include/bitcoin/server/protocols/protocol_electrum_version.hpp b/include/bitcoin/server/protocols/protocol_electrum_version.hpp
index 5a3e7334..2d21a322 100644
--- a/include/bitcoin/server/protocols/protocol_electrum_version.hpp
+++ b/include/bitcoin/server/protocols/protocol_electrum_version.hpp
@@ -20,7 +20,6 @@
#define LIBBITCOIN_SERVER_PROTOCOLS_PROTOCOL_ELECTRUM_VERSION_HPP
#include
-#include
#include
#include
#include
@@ -42,6 +41,7 @@ class BCS_API protocol_electrum_version
const network::channel::ptr& channel,
const options_t& options) NOEXCEPT
: protocol_rpc(session, channel, options),
+ options_(options),
channel_(std::dynamic_pointer_cast(channel)),
network::tracker(session->log)
{
@@ -51,18 +51,18 @@ class BCS_API protocol_electrum_version
virtual void finished(const code& ec, const code& shake) NOEXCEPT;
protected:
- static constexpr electrum_version minimum = electrum_version::v1_4;
- static constexpr electrum_version maximum = electrum_version::v1_4_2;
+ static constexpr electrum::version minimum = electrum::version::v1_4;
+ static constexpr electrum::version maximum = electrum::version::v1_4_2;
static constexpr size_t max_client_name_length = 1024;
void handle_server_version(const code& ec,
rpc_interface::server_version, const std::string& client_name,
const interface::value_t& protocol_version) NOEXCEPT;
- electrum_version version() const NOEXCEPT;
+ electrum::version version() const NOEXCEPT;
std::string_view negotiated_version() const NOEXCEPT;
bool set_version(const interface::value_t& version) NOEXCEPT;
- bool get_versions(electrum_version& min, electrum_version& max,
+ bool get_versions(electrum::version& min, electrum::version& max,
const interface::value_t& version) NOEXCEPT;
std::string_view server_name() const NOEXCEPT;
@@ -70,11 +70,14 @@ class BCS_API protocol_electrum_version
std::string escape_client(const std::string& in) NOEXCEPT;
bool set_client(const std::string& name) NOEXCEPT;
+ inline const options_t& options() const NOEXCEPT
+ {
+ return options_;
+ }
+
private:
- static std::string_view version_to_string(
- electrum_version version) NOEXCEPT;
- static electrum_version version_from_string(
- const std::string_view& version) NOEXCEPT;
+ // This is thread safe.
+ const options_t& options_;
// This is mostly thread safe, and used in a thread safe manner.
const channel_t::ptr channel_;
diff --git a/include/bitcoin/server/protocols/protocol_rpc.hpp b/include/bitcoin/server/protocols/protocol_rpc.hpp
index 4d740536..9d988295 100644
--- a/include/bitcoin/server/protocols/protocol_rpc.hpp
+++ b/include/bitcoin/server/protocols/protocol_rpc.hpp
@@ -49,8 +49,8 @@ class BCS_API protocol_rpc
};
#define SUBSCRIBE_RPC(...) SUBSCRIBE_CHANNEL(void, __VA_ARGS__)
-#define SEND_RPC(message, size_hint, method, ...) \
- send(message, size_hint, &CLASS::method, __VA_ARGS__)
+////#define SEND_RPC(message, size_hint, method, ...) \
+//// send(message, size_hint, &CLASS::method, __VA_ARGS__)
} // namespace server
} // namespace libbitcoin
diff --git a/include/bitcoin/server/settings.hpp b/include/bitcoin/server/settings.hpp
index d63b5daf..6acc9d64 100644
--- a/include/bitcoin/server/settings.hpp
+++ b/include/bitcoin/server/settings.hpp
@@ -90,9 +90,21 @@ class BCS_API settings
using base = network::settings::tls_server;
using base::base;
- // Maximum number of headers the server will return in single request.
- // Recommended to be multiple of difficulty retarget period, e.g. 2016.
+ /// Maximum number of headers the server will return in single request.
+ /// Recommended to be a multiple of the difficulty retarget period.
uint32_t maximum_headers{ 10 * 2016 };
+
+ /// Maximum cumulative number of address subscriptions per channel.
+ uint32_t maximum_subscriptions{ 1'000'000 };
+
+ /// Arbitrary server name returned by server.version.
+ std::string server_name{ BC_USER_AGENT };
+
+ /// Arbitrary string returned by server.donation_address.
+ std::string donation_address{};
+
+ /// Arbitrary string returned by server.banner.
+ std::string banner_message{};
};
/// html (http/s) document server settings (has directory/default).
diff --git a/src/parser.cpp b/src/parser.cpp
index b1ed9285..18c352f1 100644
--- a/src/parser.cpp
+++ b/src/parser.cpp
@@ -1132,7 +1132,27 @@ options_metadata parser::load_settings() THROWS
(
"electrum.maximum_headers",
value(&configured.server.electrum.maximum_headers),
- "The maximum allowed header request cound, defaults to '20160'."
+ "The maximum allowed headers returned per request, defaults to '20160'."
+ )
+ (
+ "electrum.maximum_subscriptions",
+ value(&configured.server.electrum.maximum_subscriptions),
+ "The maximum allowed address subscriptions per channel, defaults to '1000000'."
+ )
+ (
+ "electrum.server_name",
+ value(&configured.server.electrum.server_name),
+ "String returned by server.version, defaults to '" BC_USER_AGENT "'."
+ )
+ (
+ "electrum.donation_address",
+ value(&configured.server.electrum.donation_address),
+ "String returned by server.donation_address, defaults to empty."
+ )
+ (
+ "electrum.banner_message",
+ value(&configured.server.electrum.banner_message),
+ "String returned by server.banner, defaults to empty."
)
/* [stratum_v1] */
diff --git a/src/parsers/electrum_version.cpp b/src/parsers/electrum_version.cpp
new file mode 100644
index 00000000..b7994842
--- /dev/null
+++ b/src/parsers/electrum_version.cpp
@@ -0,0 +1,75 @@
+/**
+ * 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
+
+#include
+
+namespace libbitcoin {
+namespace server {
+namespace electrum {
+
+std::string_view version_to_string(version value) NOEXCEPT
+{
+ static const std::unordered_map map
+ {
+ { version::v0_0, "0.0" },
+ { version::v0_6, "0.6" },
+ { version::v0_8, "0.8" },
+ { version::v0_9, "0.9" },
+ { version::v0_10, "0.10" },
+ { version::v1_0, "1.0" },
+ { version::v1_1, "1.1" },
+ { version::v1_2, "1.2" },
+ { version::v1_3, "1.3" },
+ { version::v1_4, "1.4" },
+ { version::v1_4_1, "1.4.1" },
+ { version::v1_4_2, "1.4.2" },
+ { version::v1_6, "1.6" }
+ };
+
+ const auto it = map.find(value);
+ return it != map.end() ? it->second : "0.0";
+}
+
+version version_from_string( const std::string_view& value) NOEXCEPT
+{
+ static const std::unordered_map map
+ {
+ { "0.0", version::v0_0 },
+ { "0.6", version::v0_6 },
+ { "0.8", version::v0_8 },
+ { "0.9", version::v0_9 },
+ { "0.10", version::v0_10 },
+ { "1.0", version::v1_0 },
+ { "1.1", version::v1_1 },
+ { "1.2", version::v1_2 },
+ { "1.3", version::v1_3 },
+ { "1.4", version::v1_4 },
+ { "1.4.1", version::v1_4_1 },
+ { "1.4.2", version::v1_4_2 },
+ { "1.6", version::v1_6 }
+ };
+
+ const auto it = map.find(value);
+ return it != map.end() ? it->second : version::v0_0;
+}
+
+} // namespace electrum
+} // namespace server
+} // namespace libbitcoin
diff --git a/src/protocols/protocol_electrum.cpp b/src/protocols/protocol_electrum.cpp
index 371e702f..00c4a72e 100644
--- a/src/protocols/protocol_electrum.cpp
+++ b/src/protocols/protocol_electrum.cpp
@@ -182,6 +182,7 @@ void protocol_electrum::blockchain_block_headers(size_t starting,
const auto prove = !is_zero(quantity) && !is_zero(waypoint);
const auto target = starting + sub1(quantity);
const auto& query = archive();
+ const auto top = query.get_top_confirmed();
using namespace system;
// The documented requirement: `start_height + (count - 1) <= cp_height` is
@@ -191,16 +192,21 @@ void protocol_electrum::blockchain_block_headers(size_t starting,
send_code(error::argument_overflow);
return;
}
- else if (prove && target > waypoint)
+ else if (starting > top)
{
- send_code(error::target_overflow);
+ send_code(error::not_found);
return;
}
- else if (prove && waypoint > query.get_top_confirmed())
+ else if (prove && waypoint > top)
{
send_code(error::not_found);
return;
}
+ else if (prove && target > waypoint)
+ {
+ send_code(error::target_overflow);
+ return;
+ }
// Recommended to be at least one difficulty retarget period, e.g. 2016.
// The maximum number of headers the server will return in single request.
@@ -236,7 +242,7 @@ void protocol_electrum::blockchain_block_headers(size_t starting,
result["count"] = uint64_t{ headers.size() };
result["headers"] = std::move(headers);
}
- else
+ else if (!headers.empty())
{
result["header"] = headers.front();
}
@@ -423,14 +429,16 @@ void protocol_electrum::handle_server_banner(const code& ec,
if (stopped(ec))
return;
- send_result(network_settings().user_agent, 70, BIND(complete, _1));
+ send_result({ options().banner_message }, 42, BIND(complete, _1));
}
void protocol_electrum::handle_server_donation_address(const code& ec,
rpc_interface::server_donation_address) NOEXCEPT
{
- if (stopped(ec)) return;
- send_code(error::not_implemented);
+ if (stopped(ec))
+ return;
+
+ send_result({ options().donation_address }, 42, BIND(complete, _1));
}
void protocol_electrum::handle_server_features(const code& ec,
@@ -451,8 +459,11 @@ void protocol_electrum::handle_server_peers_subscribe(const code& ec,
void protocol_electrum::handle_server_ping(const code& ec,
rpc_interface::server_ping) NOEXCEPT
{
- if (stopped(ec)) return;
- send_code(error::not_implemented);
+ if (stopped(ec))
+ return;
+
+ // Any receive, including ping, resets the base channel inactivity timer.
+ send_result({ null_t{} }, 42, BIND(complete, _1));
}
// Handlers (mempool).
diff --git a/src/protocols/protocol_electrum_version.cpp b/src/protocols/protocol_electrum_version.cpp
index 5976811c..cf1553b5 100644
--- a/src/protocols/protocol_electrum_version.cpp
+++ b/src/protocols/protocol_electrum_version.cpp
@@ -29,9 +29,9 @@ namespace libbitcoin {
namespace server {
#define CLASS protocol_electrum_version
-
+
using namespace network;
-using namespace interface;
+using namespace network::rpc;
using namespace std::placeholders;
BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT)
@@ -88,7 +88,7 @@ void protocol_electrum_version::handle_server_version(const code& ec,
return;
// v0_0 implies version has not been set (first call).
- if ((channel_->version() == electrum_version::v0_0) &&
+ if ((channel_->version() == electrum::version::v0_0) &&
(!set_client(client_name) || !set_version(protocol_version)))
{
const auto reason = error::invalid_argument;
@@ -115,7 +115,7 @@ void protocol_electrum_version::handle_server_version(const code& ec,
std::string_view protocol_electrum_version::server_name() const NOEXCEPT
{
- return network_settings().user_agent;
+ return options().server_name;
}
std::string_view protocol_electrum_version::client_name() const NOEXCEPT
@@ -152,13 +152,13 @@ std::string protocol_electrum_version::escape_client(
std::string_view protocol_electrum_version::negotiated_version() const NOEXCEPT
{
- return version_to_string(channel_->version());
+ return electrum::version_to_string(channel_->version());
}
bool protocol_electrum_version::set_version(const value_t& version) NOEXCEPT
{
- electrum_version client_min{};
- electrum_version client_max{};
+ electrum::version client_min{};
+ electrum::version client_max{};
if (!get_versions(client_min, client_max, version))
return false;
@@ -168,14 +168,14 @@ bool protocol_electrum_version::set_version(const value_t& version) NOEXCEPT
return false;
LOGA("Electrum [" << opposite() << "] version ("
- << version_to_string(client_max) << ") " << client_name());
+ << electrum::version_to_string(client_max) << ") " << client_name());
channel_->set_version(upper);
return true;
}
-bool protocol_electrum_version::get_versions(electrum_version& min,
- electrum_version& max, const interface::value_t& version) NOEXCEPT
+bool protocol_electrum_version::get_versions(electrum::version& min,
+ electrum::version& max, const interface::value_t& version) NOEXCEPT
{
// Optional value_t can be string_t or array_t of two string_t.
const auto& value = version.value();
@@ -184,7 +184,7 @@ bool protocol_electrum_version::get_versions(electrum_version& min,
if (std::holds_alternative(value))
{
// An interface default can't be set for optional.
- max = min = electrum_version::v1_4;
+ max = min = electrum::version::v1_4;
return true;
}
@@ -192,8 +192,8 @@ bool protocol_electrum_version::get_versions(electrum_version& min,
if (std::holds_alternative(value))
{
// A single value implies minimum is the same as maximum.
- max = min = version_from_string(std::get(value));
- return min != electrum_version::v0_0;
+ max = min = electrum::version_from_string(std::get(value));
+ return min != electrum::version::v0_0;
}
// Two versions.
@@ -210,65 +210,15 @@ bool protocol_electrum_version::get_versions(electrum_version& min,
!std::holds_alternative(max_version))
return false;
- min = version_from_string(std::get(min_version));
- max = version_from_string(std::get(max_version));
- return min != electrum_version::v0_0
- && max != electrum_version::v0_0;
+ min = electrum::version_from_string(std::get(min_version));
+ max = electrum::version_from_string(std::get(max_version));
+ return min != electrum::version::v0_0
+ && max != electrum::version::v0_0;
}
return false;
}
-// private/static
-std::string_view protocol_electrum_version::version_to_string(
- electrum_version version) NOEXCEPT
-{
- static const std::unordered_map map
- {
- { electrum_version::v0_0, "0.0" },
- { electrum_version::v0_6, "0.6" },
- { electrum_version::v0_8, "0.8" },
- { electrum_version::v0_9, "0.9" },
- { electrum_version::v0_10, "0.10" },
- { electrum_version::v1_0, "1.0" },
- { electrum_version::v1_1, "1.1" },
- { electrum_version::v1_2, "1.2" },
- { electrum_version::v1_3, "1.3" },
- { electrum_version::v1_4, "1.4" },
- { electrum_version::v1_4_1, "1.4.1" },
- { electrum_version::v1_4_2, "1.4.2" },
- { electrum_version::v1_6, "1.6" }
- };
-
- const auto it = map.find(version);
- return it != map.end() ? it->second : "0.0";
-}
-
-// private/static
-electrum_version protocol_electrum_version::version_from_string(
- const std::string_view& version) NOEXCEPT
-{
- static const std::unordered_map map
- {
- { "0.0", electrum_version::v0_0 },
- { "0.6", electrum_version::v0_6 },
- { "0.8", electrum_version::v0_8 },
- { "0.9", electrum_version::v0_9 },
- { "0.10", electrum_version::v0_10 },
- { "1.0", electrum_version::v1_0 },
- { "1.1", electrum_version::v1_1 },
- { "1.2", electrum_version::v1_2 },
- { "1.3", electrum_version::v1_3 },
- { "1.4", electrum_version::v1_4 },
- { "1.4.1", electrum_version::v1_4_1 },
- { "1.4.2", electrum_version::v1_4_2 },
- { "1.6", electrum_version::v1_6 }
- };
-
- const auto it = map.find(version);
- return it != map.end() ? it->second : electrum_version::v0_0;
-}
-
BC_POP_WARNING()
BC_POP_WARNING()
BC_POP_WARNING()
diff --git a/test/endpoints/electrum.cpp b/test/endpoints/electrum.cpp
deleted file mode 100644
index d124fb0d..00000000
--- a/test/endpoints/electrum.cpp
+++ /dev/null
@@ -1,198 +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
-
-using namespace system;
-
-constexpr auto block1_hash = base16_hash("00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048");
-constexpr auto block2_hash = base16_hash("000000006a625f06636b8bb6ac7b960a8d03705d1ace08b1a19da3fdcc99ddbd");
-constexpr auto block3_hash = base16_hash("0000000082b5015589a3fdf2d4baff403e6f0be035a5d9742c1cae6295464449");
-constexpr auto block4_hash = base16_hash("000000004ebadb55ee9096c9a2f8880e09da59c0d68b1c228da88e48844a1485");
-constexpr auto block5_hash = base16_hash("000000009b7262315dbf071787ad3656097b892abffd1f95a1a022f896f533fc");
-constexpr auto block6_hash = base16_hash("000000003031a0e73735690c5a1ff2a4be82553b2a12b776fbd3a215dc8f778d");
-constexpr auto block7_hash = base16_hash("0000000071966c2b1d065fd446b1e485b2c9d9594acd2007ccbd5441cfc89444");
-constexpr auto block8_hash = base16_hash("00000000408c48f847aa786c2268fc3e6ec2af68e8468a34a28c61b7f1de0dc6");
-
-// blockchain.info/rawblock/[block-hash]?format=hex
-constexpr auto block1_data = base16_array("010000006fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000982051fd1e4ba744bbbe680e1fee14677ba1a3c3540bf7b1cdb606e857233e0e61bc6649ffff001d01e362990101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000");
-constexpr auto block2_data = base16_array("010000004860eb18bf1b1620e37e9490fc8a427514416fd75159ab86688e9a8300000000d5fdcc541e25de1c7a5addedf24858b8bb665c9f36ef744ee42c316022c90f9bb0bc6649ffff001d08d2bd610101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d010bffffffff0100f2052a010000004341047211a824f55b505228e4c3d5194c1fcfaa15a456abdf37f9b9d97a4040afc073dee6c89064984f03385237d92167c13e236446b417ab79a0fcae412ae3316b77ac00000000");
-constexpr auto block3_data = base16_array("01000000bddd99ccfda39da1b108ce1a5d70038d0a967bacb68b6b63065f626a0000000044f672226090d85db9a9f2fbfe5f0f9609b387af7be5b7fbb7a1767c831c9e995dbe6649ffff001d05e0ed6d0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d010effffffff0100f2052a0100000043410494b9d3e76c5b1629ecf97fff95d7a4bbdac87cc26099ada28066c6ff1eb9191223cd897194a08d0c2726c5747f1db49e8cf90e75dc3e3550ae9b30086f3cd5aaac00000000");
-constexpr auto block4_data = base16_array("010000004944469562ae1c2c74d9a535e00b6f3e40ffbad4f2fda3895501b582000000007a06ea98cd40ba2e3288262b28638cec5337c1456aaf5eedc8e9e5a20f062bdf8cc16649ffff001d2bfee0a90101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d011affffffff0100f2052a01000000434104184f32b212815c6e522e66686324030ff7e5bf08efb21f8b00614fb7690e19131dd31304c54f37baa40db231c918106bb9fd43373e37ae31a0befc6ecaefb867ac00000000");
-constexpr auto block5_data = base16_array("0100000085144a84488ea88d221c8bd6c059da090e88f8a2c99690ee55dbba4e00000000e11c48fecdd9e72510ca84f023370c9a38bf91ac5cae88019bee94d24528526344c36649ffff001d1d03e4770101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0120ffffffff0100f2052a0100000043410456579536d150fbce94ee62b47db2ca43af0a730a0467ba55c79e2a7ec9ce4ad297e35cdbb8e42a4643a60eef7c9abee2f5822f86b1da242d9c2301c431facfd8ac00000000");
-constexpr auto block6_data = base16_array("01000000fc33f596f822a0a1951ffdbf2a897b095636ad871707bf5d3162729b00000000379dfb96a5ea8c81700ea4ac6b97ae9a9312b2d4301a29580e924ee6761a2520adc46649ffff001d189c4c970101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0123ffffffff0100f2052a0100000043410408ce279174b34c077c7b2043e3f3d45a588b85ef4ca466740f848ead7fb498f0a795c982552fdfa41616a7c0333a269d62108588e260fd5a48ac8e4dbf49e2bcac00000000");
-constexpr auto block7_data = base16_array("010000008d778fdc15a2d3fb76b7122a3b5582bea4f21f5a0c693537e7a03130000000003f674005103b42f984169c7d008370967e91920a6a5d64fd51282f75bc73a68af1c66649ffff001d39a59c860101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d012bffffffff0100f2052a01000000434104a59e64c774923d003fae7491b2a7f75d6b7aa3f35606a8ff1cf06cd3317d16a41aa16928b1df1f631f31f28c7da35d4edad3603adb2338c4d4dd268f31530555ac00000000");
-constexpr auto block8_data = base16_array("010000004494c8cf4154bdcc0720cd4a59d9c9b285e4b146d45f061d2b6c967100000000e3855ed886605b6d4a99d5fa2ef2e9b0b164e63df3c4136bebf2d0dac0f1f7a667c86649ffff001d1c4b56660101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d012cffffffff0100f2052a01000000434104cc8d85f5e7933cb18f13b97d165e1189c1fb3e9c98b0dd5446b2a1989883ff9e740a8a75da99cc59a21016caf7a7afd3e4e9e7952983e18d1ff70529d62e0ba1ac00000000");
-
-static const auto genesis = system::settings{ chain::selection::mainnet }.genesis_block;
-static const chain::block block1{ block1_data, true };
-static const chain::block block2{ block2_data, true };
-static const chain::block block3{ block3_data, true };
-static const chain::block block4{ block4_data, true };
-static const chain::block block5{ block5_data, true };
-static const chain::block block6{ block6_data, true };
-static const chain::block block7{ block7_data, true };
-static const chain::block block8{ block8_data, true };
-
-static const server::settings::embedded_pages admin{};
-static const server::settings::embedded_pages native{};
-
-struct electrum_setup_fixture
-{
- using context_t = database::context;
- using store_t = database::store;
- using query_t = database::query>;
-
- DELETE_COPY_MOVE(electrum_setup_fixture);
-
- electrum_setup_fixture() NOEXCEPT
- : config_{ chain::selection::mainnet, native, admin },
- store_
- {
- [&]() NOEXCEPT -> const database::settings&
- {
- config_.database.path = TEST_DIRECTORY;
- return config_.database;
- }()
- },
- query_{ store_ }, log_{},
- server_{ query_, config_, log_ }
- {
- BOOST_REQUIRE(test::clear(test::directory));
-
- auto& database_settings = config_.database;
- auto& network_settings = config_.network;
- auto& node_settings = config_.node;
- auto& server_settings = config_.server;
- auto& electrum = server_settings.electrum;
-
- // >>>>>>>>>>>>>>> REQUIRES LOCALHOST TCP PORT 65000. <<<<<<<<<<<<<<<
- electrum.binds = { { "127.0.0.1:65000" } };
- electrum.maximum_headers = 5;
- electrum.connections = 1;
- database_settings.interval_depth = 2;
- node_settings.delay_inbound = false;
- network_settings.inbound.connections = 0;
- network_settings.outbound.connections = 0;
-
- // Create and populate the store.
- BOOST_REQUIRE(!store_.create([](auto, auto){}));
- BOOST_REQUIRE(setup_eight_block_store());
-
- // Run the server.
- std::promise running{};
- server_.run([&](const code& ec) NOEXCEPT
- {
- running.set_value(ec);
- });
-
- // Block until server is running.
- BOOST_REQUIRE(!running.get_future().get());
- socket_.connect(electrum.binds.back().to_endpoint());
- }
-
- ~electrum_setup_fixture() NOEXCEPT
- {
- socket_.close();
- server_.close();
- BOOST_REQUIRE(!store_.close([](auto, auto){}));
- BOOST_REQUIRE(test::clear(test::directory));
- }
-
- const configuration& config() const NOEXCEPT
- {
- return config_;
- }
-
- auto get(const std::string& request) NOEXCEPT
- {
- socket_.send(boost::asio::buffer(request));
- boost::asio::streambuf stream{};
- read_until(socket_, stream, '\n');
-
- std::string response{};
- std::istream response_stream{ &stream };
- std::getline(response_stream, response);
-
- return boost::json::parse(response);
- }
-
-private:
- bool setup_eight_block_store() NOEXCEPT
- {
- return query_.initialize(genesis) &&
- query_.set(block1, context_t{ 0, 1, 0 }, false, false) &&
- query_.set(block2, context_t{ 0, 2, 0 }, false, false) &&
- query_.set(block3, context_t{ 0, 3, 0 }, false, false) &&
- query_.set(block4, context_t{ 0, 4, 0 }, false, false) &&
- query_.set(block5, context_t{ 0, 5, 0 }, false, false) &&
- query_.set(block6, context_t{ 0, 6, 0 }, false, false) &&
- query_.set(block7, context_t{ 0, 7, 0 }, false, false) &&
- query_.set(block8, context_t{ 0, 8, 0 }, false, false) &&
- query_.push_confirmed(query_.to_header(block1_hash), false) &&
- query_.push_confirmed(query_.to_header(block2_hash), false) &&
- query_.push_confirmed(query_.to_header(block3_hash), false) &&
- query_.push_confirmed(query_.to_header(block4_hash), false) &&
- query_.push_confirmed(query_.to_header(block5_hash), false) &&
- query_.push_confirmed(query_.to_header(block6_hash), false) &&
- query_.push_confirmed(query_.to_header(block7_hash), false) &&
- query_.push_confirmed(query_.to_header(block8_hash), false);
- }
-
- configuration config_;
- store_t store_;
- query_t query_;
- network::logger log_;
- server::server_node server_;
- boost::asio::io_context io{};
- boost::asio::ip::tcp::socket socket_{ io };
-};
-
-BOOST_FIXTURE_TEST_SUITE(electrum_tests, electrum_setup_fixture)
-
-BOOST_AUTO_TEST_CASE(electrum__server_version__default__1_4)
-{
- const auto response = get(R"({"id":42,"method":"server.version","params":["foobar"]})" "\n");
- BOOST_REQUIRE_EQUAL(response.at("id").as_int64(), 42);
- BOOST_REQUIRE(response.at("result").is_array());
-
- const auto& result = response.at("result").as_array();
- BOOST_REQUIRE_EQUAL(result.size(), 2u);
- BOOST_REQUIRE(result.at(0).is_string());
- BOOST_REQUIRE(result.at(1).is_string());
- BOOST_REQUIRE_EQUAL(result.at(0).as_string(), config().network.user_agent);
- BOOST_REQUIRE_EQUAL(result.at(1).as_string(), "1.4");
-}
-
-BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__genesis__expected)
-{
- get(R"({"id":"name","method":"server.version","params":["foobar","1.4"]})" "\n");
-
- const auto response = get(R"({"id":43,"method":"blockchain.block.header","params":[0]})" "\n");
- BOOST_REQUIRE_EQUAL(response.at("id").as_int64(), 43);
- BOOST_REQUIRE(response.at("result").is_object());
-
- const auto& result = response.at("result").as_object();
- BOOST_REQUIRE_EQUAL(result.size(), 1u);
- BOOST_REQUIRE(result.at("header").is_string());
- BOOST_REQUIRE_EQUAL(result.at("header").as_string(), encode_base16(genesis.header().to_data()));
-}
-
-BOOST_AUTO_TEST_SUITE_END()
diff --git a/test/parsers/electrum_version.cpp b/test/parsers/electrum_version.cpp
new file mode 100644
index 00000000..74091357
--- /dev/null
+++ b/test/parsers/electrum_version.cpp
@@ -0,0 +1,59 @@
+/**
+ * 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"
+
+BOOST_AUTO_TEST_SUITE(electrum_version_tests)
+
+using namespace electrum;
+
+BOOST_AUTO_TEST_CASE(electrum_version__version_to_string__all__expected)
+{
+ BOOST_REQUIRE_EQUAL(version_to_string(version::v0_0), "0.0");
+ BOOST_REQUIRE_EQUAL(version_to_string(version::v0_6), "0.6");
+ BOOST_REQUIRE_EQUAL(version_to_string(version::v0_8), "0.8");
+ BOOST_REQUIRE_EQUAL(version_to_string(version::v0_9), "0.9");
+ BOOST_REQUIRE_EQUAL(version_to_string(version::v0_10), "0.10");
+ BOOST_REQUIRE_EQUAL(version_to_string(version::v1_0), "1.0");
+ BOOST_REQUIRE_EQUAL(version_to_string(version::v1_1), "1.1");
+ BOOST_REQUIRE_EQUAL(version_to_string(version::v1_2), "1.2");
+ BOOST_REQUIRE_EQUAL(version_to_string(version::v1_3), "1.3");
+ BOOST_REQUIRE_EQUAL(version_to_string(version::v1_4), "1.4");
+ BOOST_REQUIRE_EQUAL(version_to_string(version::v1_4_1), "1.4.1");
+ BOOST_REQUIRE_EQUAL(version_to_string(version::v1_4_2), "1.4.2");
+ BOOST_REQUIRE_EQUAL(version_to_string(version::v1_6), "1.6");
+}
+
+BOOST_AUTO_TEST_CASE(electrum_version__version_from_string__all__expected)
+{
+ BOOST_REQUIRE(version_from_string("0.0") == version::v0_0);
+ BOOST_REQUIRE(version_from_string("0.6") == version::v0_6);
+ BOOST_REQUIRE(version_from_string("0.8") == version::v0_8);
+ BOOST_REQUIRE(version_from_string("0.9") == version::v0_9);
+ BOOST_REQUIRE(version_from_string("0.10") == version::v0_10);
+ BOOST_REQUIRE(version_from_string("1.0") == version::v1_0);
+ BOOST_REQUIRE(version_from_string("1.1") == version::v1_1);
+ BOOST_REQUIRE(version_from_string("1.2") == version::v1_2);
+ BOOST_REQUIRE(version_from_string("1.3") == version::v1_3);
+ BOOST_REQUIRE(version_from_string("1.4") == version::v1_4);
+ BOOST_REQUIRE(version_from_string("1.4.1") == version::v1_4_1);
+ BOOST_REQUIRE(version_from_string("1.4.2") == version::v1_4_2);
+ BOOST_REQUIRE(version_from_string("1.6") == version::v1_6);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/test/protocols/blocks.cpp b/test/protocols/blocks.cpp
new file mode 100644
index 00000000..eadeb40b
--- /dev/null
+++ b/test/protocols/blocks.cpp
@@ -0,0 +1,105 @@
+/**
+ * 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 "blocks.hpp"
+
+// blockchain.info/rawblock/[hash]?format=hex
+
+using namespace system;
+constexpr hash_digest block0_hash = base16_hash("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f");
+constexpr hash_digest block1_hash = base16_hash("00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048");
+constexpr hash_digest block2_hash = base16_hash("000000006a625f06636b8bb6ac7b960a8d03705d1ace08b1a19da3fdcc99ddbd");
+constexpr hash_digest block3_hash = base16_hash("0000000082b5015589a3fdf2d4baff403e6f0be035a5d9742c1cae6295464449");
+constexpr hash_digest block4_hash = base16_hash("000000004ebadb55ee9096c9a2f8880e09da59c0d68b1c228da88e48844a1485");
+constexpr hash_digest block5_hash = base16_hash("000000009b7262315dbf071787ad3656097b892abffd1f95a1a022f896f533fc");
+constexpr hash_digest block6_hash = base16_hash("000000003031a0e73735690c5a1ff2a4be82553b2a12b776fbd3a215dc8f778d");
+constexpr hash_digest block7_hash = base16_hash("0000000071966c2b1d065fd446b1e485b2c9d9594acd2007ccbd5441cfc89444");
+constexpr hash_digest block8_hash = base16_hash("00000000408c48f847aa786c2268fc3e6ec2af68e8468a34a28c61b7f1de0dc6");
+constexpr hash_digest block9_hash = base16_hash("000000008d9dc510f23c2657fc4f67bea30078cc05a90eb89e84cc475c080805");
+
+constexpr hash_digest root01 = sha256::double_hash(block0_hash, block1_hash);
+constexpr hash_digest root23 = sha256::double_hash(block2_hash, block3_hash);
+constexpr hash_digest root03 = sha256::double_hash(root01, root23);
+constexpr hash_digest root45 = sha256::double_hash(block4_hash, block5_hash);
+constexpr hash_digest root67 = sha256::double_hash(block6_hash, block7_hash);
+constexpr hash_digest root47 = sha256::double_hash(root45, root67);
+constexpr hash_digest root07 = sha256::double_hash(root03, root47);
+constexpr hash_digest root82 = sha256::double_hash(block8_hash, block8_hash);
+constexpr hash_digest root84 = sha256::double_hash(root82, root82);
+constexpr hash_digest root88 = sha256::double_hash(root84, root84);
+constexpr hash_digest root08 = sha256::double_hash(root07, root88);
+
+constexpr header_data header0_data = base16_array("0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c");
+constexpr header_data header1_data = base16_array("010000006fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000982051fd1e4ba744bbbe680e1fee14677ba1a3c3540bf7b1cdb606e857233e0e61bc6649ffff001d01e36299");
+constexpr header_data header2_data = base16_array("010000004860eb18bf1b1620e37e9490fc8a427514416fd75159ab86688e9a8300000000d5fdcc541e25de1c7a5addedf24858b8bb665c9f36ef744ee42c316022c90f9bb0bc6649ffff001d08d2bd61");
+constexpr header_data header3_data = base16_array("01000000bddd99ccfda39da1b108ce1a5d70038d0a967bacb68b6b63065f626a0000000044f672226090d85db9a9f2fbfe5f0f9609b387af7be5b7fbb7a1767c831c9e995dbe6649ffff001d05e0ed6d");
+constexpr header_data header4_data = base16_array("010000004944469562ae1c2c74d9a535e00b6f3e40ffbad4f2fda3895501b582000000007a06ea98cd40ba2e3288262b28638cec5337c1456aaf5eedc8e9e5a20f062bdf8cc16649ffff001d2bfee0a9");
+constexpr header_data header5_data = base16_array("0100000085144a84488ea88d221c8bd6c059da090e88f8a2c99690ee55dbba4e00000000e11c48fecdd9e72510ca84f023370c9a38bf91ac5cae88019bee94d24528526344c36649ffff001d1d03e477");
+constexpr header_data header6_data = base16_array("01000000fc33f596f822a0a1951ffdbf2a897b095636ad871707bf5d3162729b00000000379dfb96a5ea8c81700ea4ac6b97ae9a9312b2d4301a29580e924ee6761a2520adc46649ffff001d189c4c97");
+constexpr header_data header7_data = base16_array("010000008d778fdc15a2d3fb76b7122a3b5582bea4f21f5a0c693537e7a03130000000003f674005103b42f984169c7d008370967e91920a6a5d64fd51282f75bc73a68af1c66649ffff001d39a59c86");
+constexpr header_data header8_data = base16_array("010000004494c8cf4154bdcc0720cd4a59d9c9b285e4b146d45f061d2b6c967100000000e3855ed886605b6d4a99d5fa2ef2e9b0b164e63df3c4136bebf2d0dac0f1f7a667c86649ffff001d1c4b5666");
+constexpr header_data header9_data = base16_array("01000000c60ddef1b7618ca2348a46e868afc26e3efc68226c78aa47f8488c4000000000c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd37047fca6649ffff001d28404f53");
+
+constexpr block_data block1_data = base16_array("010000006fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000982051fd1e4ba744bbbe680e1fee14677ba1a3c3540bf7b1cdb606e857233e0e61bc6649ffff001d01e362990101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000");
+constexpr block_data block2_data = base16_array("010000004860eb18bf1b1620e37e9490fc8a427514416fd75159ab86688e9a8300000000d5fdcc541e25de1c7a5addedf24858b8bb665c9f36ef744ee42c316022c90f9bb0bc6649ffff001d08d2bd610101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d010bffffffff0100f2052a010000004341047211a824f55b505228e4c3d5194c1fcfaa15a456abdf37f9b9d97a4040afc073dee6c89064984f03385237d92167c13e236446b417ab79a0fcae412ae3316b77ac00000000");
+constexpr block_data block3_data = base16_array("01000000bddd99ccfda39da1b108ce1a5d70038d0a967bacb68b6b63065f626a0000000044f672226090d85db9a9f2fbfe5f0f9609b387af7be5b7fbb7a1767c831c9e995dbe6649ffff001d05e0ed6d0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d010effffffff0100f2052a0100000043410494b9d3e76c5b1629ecf97fff95d7a4bbdac87cc26099ada28066c6ff1eb9191223cd897194a08d0c2726c5747f1db49e8cf90e75dc3e3550ae9b30086f3cd5aaac00000000");
+constexpr block_data block4_data = base16_array("010000004944469562ae1c2c74d9a535e00b6f3e40ffbad4f2fda3895501b582000000007a06ea98cd40ba2e3288262b28638cec5337c1456aaf5eedc8e9e5a20f062bdf8cc16649ffff001d2bfee0a90101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d011affffffff0100f2052a01000000434104184f32b212815c6e522e66686324030ff7e5bf08efb21f8b00614fb7690e19131dd31304c54f37baa40db231c918106bb9fd43373e37ae31a0befc6ecaefb867ac00000000");
+constexpr block_data block5_data = base16_array("0100000085144a84488ea88d221c8bd6c059da090e88f8a2c99690ee55dbba4e00000000e11c48fecdd9e72510ca84f023370c9a38bf91ac5cae88019bee94d24528526344c36649ffff001d1d03e4770101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0120ffffffff0100f2052a0100000043410456579536d150fbce94ee62b47db2ca43af0a730a0467ba55c79e2a7ec9ce4ad297e35cdbb8e42a4643a60eef7c9abee2f5822f86b1da242d9c2301c431facfd8ac00000000");
+constexpr block_data block6_data = base16_array("01000000fc33f596f822a0a1951ffdbf2a897b095636ad871707bf5d3162729b00000000379dfb96a5ea8c81700ea4ac6b97ae9a9312b2d4301a29580e924ee6761a2520adc46649ffff001d189c4c970101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0123ffffffff0100f2052a0100000043410408ce279174b34c077c7b2043e3f3d45a588b85ef4ca466740f848ead7fb498f0a795c982552fdfa41616a7c0333a269d62108588e260fd5a48ac8e4dbf49e2bcac00000000");
+constexpr block_data block7_data = base16_array("010000008d778fdc15a2d3fb76b7122a3b5582bea4f21f5a0c693537e7a03130000000003f674005103b42f984169c7d008370967e91920a6a5d64fd51282f75bc73a68af1c66649ffff001d39a59c860101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d012bffffffff0100f2052a01000000434104a59e64c774923d003fae7491b2a7f75d6b7aa3f35606a8ff1cf06cd3317d16a41aa16928b1df1f631f31f28c7da35d4edad3603adb2338c4d4dd268f31530555ac00000000");
+constexpr block_data block8_data = base16_array("010000004494c8cf4154bdcc0720cd4a59d9c9b285e4b146d45f061d2b6c967100000000e3855ed886605b6d4a99d5fa2ef2e9b0b164e63df3c4136bebf2d0dac0f1f7a667c86649ffff001d1c4b56660101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d012cffffffff0100f2052a01000000434104cc8d85f5e7933cb18f13b97d165e1189c1fb3e9c98b0dd5446b2a1989883ff9e740a8a75da99cc59a21016caf7a7afd3e4e9e7952983e18d1ff70529d62e0ba1ac00000000");
+constexpr block_data block9_data = base16_array("01000000c60ddef1b7618ca2348a46e868afc26e3efc68226c78aa47f8488c4000000000c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd37047fca6649ffff001d28404f530101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0134ffffffff0100f2052a0100000043410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac00000000");
+
+const chain::block genesis = system::settings{ chain::selection::mainnet }.genesis_block;
+const chain::block block1{ block1_data, true };
+const chain::block block2{ block2_data, true };
+const chain::block block3{ block3_data, true };
+const chain::block block4{ block4_data, true };
+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 server::settings::embedded_pages admin{};
+const server::settings::embedded_pages native{};
+
+bool setup_eight_block_store(query_t& query) NOEXCEPT
+{
+ using namespace database;
+ return query.initialize(genesis) &&
+ query.set(block1, context{ 0, 1, 0 }, false, false) &&
+ query.set(block2, context{ 0, 2, 0 }, false, false) &&
+ query.set(block3, context{ 0, 3, 0 }, false, false) &&
+ query.set(block4, context{ 0, 4, 0 }, false, false) &&
+ query.set(block5, context{ 0, 5, 0 }, false, false) &&
+ query.set(block6, context{ 0, 6, 0 }, false, false) &&
+ query.set(block7, context{ 0, 7, 0 }, false, false) &&
+ query.set(block8, context{ 0, 8, 0 }, false, false) &&
+ query.set(block9, context{ 0, 9, 0 }, false, false) &&
+ query.push_confirmed(query.to_header(block1_hash), false) &&
+ query.push_confirmed(query.to_header(block2_hash), false) &&
+ query.push_confirmed(query.to_header(block3_hash), false) &&
+ query.push_confirmed(query.to_header(block4_hash), false) &&
+ query.push_confirmed(query.to_header(block5_hash), false) &&
+ query.push_confirmed(query.to_header(block6_hash), false) &&
+ query.push_confirmed(query.to_header(block7_hash), false) &&
+ query.push_confirmed(query.to_header(block8_hash), false) &&
+ query.push_confirmed(query.to_header(block9_hash), false);
+}
diff --git a/test/protocols/blocks.hpp b/test/protocols/blocks.hpp
new file mode 100644
index 00000000..7b1604ee
--- /dev/null
+++ b/test/protocols/blocks.hpp
@@ -0,0 +1,89 @@
+/**
+ * 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 .
+ */
+#ifndef LIBBITCOIN_SERVER_TEST_PROTOCOLS_BLOCKS
+#define LIBBITCOIN_SERVER_TEST_PROTOCOLS_BLOCKS
+
+#include "../test.hpp"
+
+using block_data = system::data_array<215>;
+using header_data = system::data_array<80>;
+using store_t = database::store;
+using query_t = database::query>;
+
+extern const server::settings::embedded_pages admin;
+extern const server::settings::embedded_pages native;
+
+extern const system::hash_digest block0_hash;
+extern const system::hash_digest block1_hash;
+extern const system::hash_digest block2_hash;
+extern const system::hash_digest block3_hash;
+extern const system::hash_digest block4_hash;
+extern const system::hash_digest block5_hash;
+extern const system::hash_digest block6_hash;
+extern const system::hash_digest block7_hash;
+extern const system::hash_digest block8_hash;
+extern const system::hash_digest block9_hash;
+
+extern const system::hash_digest root01;
+extern const system::hash_digest root23;
+extern const system::hash_digest root03;
+extern const system::hash_digest root45;
+extern const system::hash_digest root67;
+extern const system::hash_digest root47;
+extern const system::hash_digest root07;
+extern const system::hash_digest root82;
+extern const system::hash_digest root84;
+extern const system::hash_digest root88;
+extern const system::hash_digest root08;
+
+extern const header_data header0_data;
+extern const header_data header1_data;
+extern const header_data header2_data;
+extern const header_data header3_data;
+extern const header_data header4_data;
+extern const header_data header5_data;
+extern const header_data header6_data;
+extern const header_data header7_data;
+extern const header_data header8_data;
+extern const header_data header9_data;
+
+extern const block_data block1_data;
+extern const block_data block2_data;
+extern const block_data block3_data;
+extern const block_data block4_data;
+extern const block_data block5_data;
+extern const block_data block6_data;
+extern const block_data block7_data;
+extern const block_data block8_data;
+extern const block_data block9_data;
+
+extern const system::chain::block genesis;
+extern const system::chain::block block1;
+extern const system::chain::block block2;
+extern const system::chain::block block3;
+extern const system::chain::block block4;
+extern const system::chain::block block5;
+extern const system::chain::block block6;
+extern const system::chain::block block7;
+extern const system::chain::block block8;
+extern const system::chain::block block9;
+
+bool setup_eight_block_store(query_t& query) NOEXCEPT;
+
+#endif
diff --git a/test/protocols/electrum/electrum.cpp b/test/protocols/electrum/electrum.cpp
new file mode 100644
index 00000000..6027795d
--- /dev/null
+++ b/test/protocols/electrum/electrum.cpp
@@ -0,0 +1,130 @@
+/**
+ * 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 "../blocks.hpp"
+#include "electrum.hpp"
+#include
+#include
+
+electrum_setup_fixture::electrum_setup_fixture()
+ : config_{ system::chain::selection::mainnet, native, admin },
+ store_
+ {
+ [&]() NOEXCEPT -> const database::settings&
+ {
+ config_.database.path = TEST_DIRECTORY;
+ return config_.database;
+ }()
+ },
+ query_{ store_ }, log_{},
+ server_{ query_, config_, log_ }
+{
+ BOOST_REQUIRE_MESSAGE(test::clear(test::directory), "electrum setup");
+
+ auto& database_settings = config_.database;
+ auto& network_settings = config_.network;
+ auto& node_settings = config_.node;
+ auto& server_settings = config_.server;
+ auto& electrum = server_settings.electrum;
+
+ electrum.binds = { { ELECTRUM_ENDPOINT } };
+ electrum.server_name = "server_name";
+ electrum.banner_message = "banner_message";
+ electrum.donation_address = "donation_address";
+ electrum.maximum_subscriptions = 3;
+ electrum.maximum_headers = 5;
+ electrum.connections = 1;
+ database_settings.interval_depth = 2;
+ node_settings.delay_inbound = false;
+ network_settings.inbound.connections = 0;
+ network_settings.outbound.connections = 0;
+ auto ec = store_.create([](auto, auto) {});
+
+ // Create and populate the store.
+ BOOST_REQUIRE_MESSAGE(!ec, ec.message());
+ BOOST_REQUIRE_MESSAGE(setup_eight_block_store(query_), "electrum initialize");
+
+ // Run the server.
+ std::promise running{};
+ server_.run([&](const code& ec) NOEXCEPT
+ {
+ running.set_value(ec);
+ });
+
+ // Block until server is running.
+ ec = running.get_future().get();
+ BOOST_REQUIRE_MESSAGE(!ec, ec.message());
+ socket_.connect(electrum.binds.back().to_endpoint());
+}
+
+electrum_setup_fixture::~electrum_setup_fixture()
+{
+ socket_.close();
+ server_.close();
+ const auto ec = store_.close([](auto, auto){});
+ BOOST_WARN_MESSAGE(!ec, ec.message());
+ BOOST_WARN_MESSAGE(test::clear(test::directory), "electrum cleanup");
+}
+
+const configuration& electrum_setup_fixture::config() const NOEXCEPT
+{
+ return config_;
+}
+
+boost::json::value electrum_setup_fixture::get(const std::string& request)
+{
+ socket_.send(boost::asio::buffer(request));
+ boost::asio::streambuf stream{};
+
+ try
+ {
+ boost::asio::read_until(socket_, stream, '\n');
+ }
+ catch (const boost::system::system_error& e)
+ {
+ BOOST_WARN_MESSAGE(false, e.what());
+ return boost::json::parse(R"({"dropped":true})");
+ }
+
+ std::string response{};
+ std::istream response_stream{ &stream };
+ std::getline(response_stream, response);
+ return boost::json::parse(response);
+}
+
+bool electrum_setup_fixture::handshake(const std::string& version,
+ const std::string& name, network::rpc::code_t id)
+{
+ const auto request = boost::format
+ (
+ R"({"id":%1%,"method":"server.version","params":["%2%","%3%"]})" "\n"
+ ) % id % name % version;
+
+ const auto response = get(request.str());
+ if (!response.at("id").is_int64() || response.at("id").as_int64() != id ||
+ !response.at("result").is_array())
+ return false;
+
+ // Assumes server always accepts proposed version.
+ const auto& result = response.at("result").as_array();
+ return (result.size() == two) &&
+ (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.hpp b/test/protocols/electrum/electrum.hpp
new file mode 100644
index 00000000..aa8b3b3e
--- /dev/null
+++ b/test/protocols/electrum/electrum.hpp
@@ -0,0 +1,49 @@
+/**
+ * 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 .
+ */
+#ifndef LIBBITCOIN_SERVER_TEST_PROTOCOLS_ELECTRUM_ELECTRUM
+#define LIBBITCOIN_SERVER_TEST_PROTOCOLS_ELECTRUM_ELECTRUM
+
+#include "../../test.hpp"
+#include "../blocks.hpp"
+
+#define ELECTRUM_ENDPOINT "127.0.0.1:65000"
+
+struct electrum_setup_fixture
+{
+ DELETE_COPY_MOVE(electrum_setup_fixture);
+
+ electrum_setup_fixture();
+ ~electrum_setup_fixture();
+
+ const configuration& config() const NOEXCEPT;
+ boost::json::value get(const std::string& request);
+ bool handshake(const std::string& version="1.4",
+ const std::string& name="test", network::rpc::code_t id=0);
+
+private:
+ configuration config_;
+ store_t store_;
+ query_t query_;
+ network::logger log_;
+ server::server_node server_;
+ boost::asio::io_context io{};
+ boost::asio::ip::tcp::socket socket_{ io };
+};
+
+#endif
diff --git a/test/protocols/electrum/electrum_block_header.cpp b/test/protocols/electrum/electrum_block_header.cpp
new file mode 100644
index 00000000..7d3d625a
--- /dev/null
+++ b/test/protocols/electrum/electrum_block_header.cpp
@@ -0,0 +1,162 @@
+/**
+ * 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_server.cpp b/test/protocols/electrum/electrum_server.cpp
new file mode 100644
index 00000000..6b196486
--- /dev/null
+++ b/test/protocols/electrum/electrum_server.cpp
@@ -0,0 +1,91 @@
+/**
+ * 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)
+
+// server.banner
+
+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__server_banner__jsonrpc_unspecified_no_aparams__dropped)
+{
+ BOOST_CHECK(handshake());
+
+ // params[] required in json 1.0, server drops connection for invalid json-rpc.
+ const auto response = get(R"({"id":42,"method":"server.banner"})" "\n");
+ BOOST_CHECK(response.at("dropped").as_bool());
+}
+
+BOOST_AUTO_TEST_CASE(electrum__server_banner__jsonrpc_unspecified_named_aparams__dropped)
+{
+ BOOST_CHECK(handshake());
+
+ // params{} disallowed in json 1.0, server drops connection for invalid json-rpc.
+ const auto response = get(R"({"id":42,"method":"server.banner","params":{}})" "\n");
+ BOOST_CHECK(response.at("dropped").as_bool());
+}
+
+BOOST_AUTO_TEST_CASE(electrum__server_banner__jsonrpc_unspecified_empty_params__expected)
+{
+ BOOST_CHECK(handshake());
+
+ const auto response = get(R"({"id":42,"method":"server.banner","params":[]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("result").as_string(), "banner_message");
+}
+
+BOOST_AUTO_TEST_CASE(electrum__server_banner__jsonrpc_1__expected)
+{
+ BOOST_CHECK(handshake());
+
+ const auto response = get(R"({"jsonrpc":"1.0","id":42,"method":"server.banner","params":[]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("result").as_string(), "banner_message");
+}
+
+BOOST_AUTO_TEST_CASE(electrum__server_banner__jsonrpc_2__expected)
+{
+ BOOST_CHECK(handshake());
+
+ const auto response = get(R"({"jsonrpc":"2.0","id":42,"method":"server.banner"})" "\n");
+ BOOST_CHECK_EQUAL(response.at("result").as_string(), "banner_message");
+}
+
+// server.donation_address
+
+BOOST_AUTO_TEST_CASE(electrum__server_donation_address__jsonrpc_1__expected)
+{
+ BOOST_CHECK(handshake());
+
+ const auto response = get(R"({"jsonrpc":"1.0","id":43,"method":"server.donation_address","params":[]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("result").as_string(), "donation_address");
+}
+
+BOOST_AUTO_TEST_CASE(electrum__server_donation_address__jsonrpc_2__expected)
+{
+ BOOST_CHECK(handshake());
+
+ const auto response = get(R"({"jsonrpc":"2.0","id":43,"method":"server.donation_address"})" "\n");
+ BOOST_CHECK_EQUAL(response.at("result").as_string(), "donation_address");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/test/protocols/electrum/electrum_server_version.cpp b/test/protocols/electrum/electrum_server_version.cpp
new file mode 100644
index 00000000..3c534580
--- /dev/null
+++ b/test/protocols/electrum/electrum_server_version.cpp
@@ -0,0 +1,141 @@
+/**
+ * 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)
+
+// server.version
+
+static const code invalid_argument{ error::invalid_argument };
+
+BOOST_AUTO_TEST_CASE(electrum__server_version__default__expected)
+{
+ const auto response = get(R"({"id":0,"method":"server.version","params":["foobar"]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("id").as_int64(), 0);
+ BOOST_CHECK(response.at("result").is_array());
+
+ const auto& result = response.at("result").as_array();
+ BOOST_CHECK_EQUAL(result.size(), 2u);
+ BOOST_CHECK(result.at(0).is_string());
+ BOOST_CHECK(result.at(1).is_string());
+ BOOST_CHECK_EQUAL(result.at(0).as_string(), "server_name");
+ BOOST_CHECK_EQUAL(result.at(1).as_string(), "1.4");
+}
+
+BOOST_AUTO_TEST_CASE(electrum__server_version__minimum__expected)
+{
+ const auto response = get(R"({"id":42,"method":"server.version","params":["foobar","1.4"]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("id").as_int64(), 42);
+ BOOST_CHECK(response.at("result").is_array());
+
+ const auto& result = response.at("result").as_array();
+ BOOST_CHECK_EQUAL(result.size(), 2u);
+ BOOST_CHECK(result.at(0).is_string());
+ BOOST_CHECK(result.at(1).is_string());
+ BOOST_CHECK_EQUAL(result.at(0).as_string(), "server_name");
+ BOOST_CHECK_EQUAL(result.at(1).as_string(), "1.4");
+}
+
+BOOST_AUTO_TEST_CASE(electrum__server_version__maximum__expected)
+{
+ const auto response = get(R"({"id":42,"method":"server.version","params":["foobar","1.4.2"]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("id").as_int64(), 42);
+ BOOST_CHECK(response.at("result").is_array());
+
+ const auto& result = response.at("result").as_array();
+ BOOST_CHECK_EQUAL(result.size(), 2u);
+ BOOST_CHECK(result.at(0).is_string());
+ BOOST_CHECK(result.at(1).is_string());
+ BOOST_CHECK_EQUAL(result.at(0).as_string(), "server_name");
+ BOOST_CHECK_EQUAL(result.at(1).as_string(), "1.4.2");
+}
+
+BOOST_AUTO_TEST_CASE(electrum__server_version__valid_range__expected)
+{
+ const auto response = get(R"({"id":42,"method":"server.version","params":["foobar",["1.4","1.4.2"]]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("result").as_array().at(1).as_string(), "1.4.2");
+}
+
+BOOST_AUTO_TEST_CASE(electrum__server_version__invalid__invalid_argument)
+{
+ const auto response = get(R"({"id":42,"method":"server.version","params":["foobar","42"]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("id").as_int64(), 42);
+ BOOST_CHECK(response.at("error").is_object());
+ BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value());
+}
+
+BOOST_AUTO_TEST_CASE(electrum__server_version__below_minimum__invalid_argument)
+{
+ const auto response = get(R"({"id":42,"method":"server.version","params":["foobar","1.3"]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value());
+}
+
+BOOST_AUTO_TEST_CASE(electrum__server_version__array_min_exceeds_max__invalid_argument)
+{
+ const auto response = get(R"({"id":42,"method":"server.version","params":["foobar",["1.4.2","1.4"]]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value());
+}
+
+BOOST_AUTO_TEST_CASE(electrum__server_version__array_wrong_size__invalid_argument)
+{
+ const auto response = get(R"({"id":52,"method":"server.version","params":["foobar",["1.4","1.4.2","extra"]]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value());
+}
+
+BOOST_AUTO_TEST_CASE(electrum__server_version__not_strings__invalid_argument)
+{
+ const auto response = get(R"({"id":42,"method":"server.version","params":["foobar",[1.4,1.4]]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value());
+}
+
+BOOST_AUTO_TEST_CASE(electrum__server_version__non_string__invalid_argument)
+{
+ const auto response = get(R"({"id":42,"method":"server.version","params":["foobar",1.4]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value());
+}
+
+BOOST_AUTO_TEST_CASE(electrum__server_version__subsequent_call__returns_negotiated)
+{
+ const auto expected = "1.4";
+ BOOST_CHECK(handshake(expected));
+
+ const auto response = get(R"({"id":42,"method":"server.version","params":["newname","1.4.2"]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("result").as_array().at(1).as_string(), expected);
+}
+
+BOOST_AUTO_TEST_CASE(electrum__server_version__subsequent_call_with_invalid_params__success)
+{
+ const auto expected = "1.4";
+ BOOST_CHECK(handshake(expected));
+
+ const auto response = get(R"({"id":57,"method":"server.version","params":["foobar","invalid"]})" "\n");
+ BOOST_CHECK_EQUAL(response.at("result").as_array().at(1).as_string(), expected);
+}
+
+BOOST_AUTO_TEST_CASE(electrum__server_version__client_name_overflow__invalid_argument)
+{
+ // Exceeds max_client_name_length (protected).
+ const std::string name(1025, 'a');
+ const auto request = boost::format(R"({"id":42,"method":"server.version","params":["%1%","1.4"]})" "\n") % name;
+ const auto response = get(request.str());
+ BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value());
+}
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/test/settings.cpp b/test/settings.cpp
index 1192f8aa..45e1e935 100644
--- a/test/settings.cpp
+++ b/test/settings.cpp
@@ -233,6 +233,10 @@ BOOST_AUTO_TEST_CASE(server__electrum_server__defaults__expected)
// electrum_server
BOOST_REQUIRE_EQUAL(server.maximum_headers, 10u * 2016u);
+ BOOST_REQUIRE_EQUAL(server.maximum_subscriptions, 1'000'000u);
+ BOOST_REQUIRE_EQUAL(server.server_name, BC_USER_AGENT);
+ BOOST_REQUIRE(server.donation_address.empty());
+ BOOST_REQUIRE(server.banner_message.empty());
}
BOOST_AUTO_TEST_CASE(server__stratum_v1_server__defaults__expected)