From 525fc26a84c0df87ccbfef4cda11048c4ccf9ce1 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Tue, 3 Mar 2026 20:31:49 -0500 Subject: [PATCH 01/10] Generalize electrum tests and harness. --- Makefile.am | 5 + builds/cmake/CMakeLists.txt | 5 + .../libbitcoin-server-test.vcxproj | 10 +- .../libbitcoin-server-test.vcxproj.filters | 12 + .../libbitcoin-server.vcxproj | 1 + .../libbitcoin-server.vcxproj.filters | 3 + .../server/channels/channel_electrum.hpp | 6 +- include/bitcoin/server/interfaces/types.hpp | 1 + .../server/parsers/electrum_version.hpp | 8 +- .../server/protocols/protocol_electrum.hpp | 2 +- .../protocols/protocol_electrum_version.hpp | 14 +- src/parsers/electrum_version.cpp | 75 ++++++ src/protocols/protocol_electrum_version.cpp | 82 ++----- test/endpoints/blocks.cpp | 57 +++++ test/endpoints/blocks.hpp | 58 +++++ test/endpoints/electrum.cpp | 217 ++++++------------ test/endpoints/electrum.hpp | 156 +++++++++++++ test/parsers/electrum_version.cpp | 59 +++++ 18 files changed, 539 insertions(+), 232 deletions(-) create mode 100644 src/parsers/electrum_version.cpp create mode 100644 test/endpoints/blocks.cpp create mode 100644 test/endpoints/blocks.hpp create mode 100644 test/endpoints/electrum.hpp create mode 100644 test/parsers/electrum_version.cpp diff --git a/Makefile.am b/Makefile.am index d1451823..b2f4aac9 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,9 +72,13 @@ test_libbitcoin_server_test_SOURCES = \ test/settings.cpp \ test/test.cpp \ test/test.hpp \ + test/endpoints/blocks.cpp \ + test/endpoints/blocks.hpp \ test/endpoints/electrum.cpp \ + test/endpoints/electrum.hpp \ 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 diff --git a/builds/cmake/CMakeLists.txt b/builds/cmake/CMakeLists.txt index 1c598f44..0e80e5bf 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" @@ -295,14 +296,18 @@ if (with-tests) "../../test/test.cpp" "../../test/test.hpp" "../../test/endpoints/README.md" + "../../test/endpoints/blocks.cpp" + "../../test/endpoints/blocks.hpp" "../../test/endpoints/conftest.py" "../../test/endpoints/electrum.cpp" + "../../test/endpoints/electrum.hpp" "../../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" ) 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..000b0035 100644 --- a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj @@ -119,12 +119,18 @@ - + + $(IntDir)test_endpoints_blocks.obj + + + $(IntDir)test_endpoints_electrum.obj + + @@ -133,6 +139,8 @@ + + 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..52e730c6 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 @@ -21,6 +21,9 @@ src + + src\endpoints + src\endpoints @@ -39,6 +42,9 @@ src\parsers + + src\parsers + src\parsers @@ -53,6 +59,12 @@ + + src\endpoints + + + src\endpoints + 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..d1f2b3a1 100644 --- a/include/bitcoin/server/channels/channel_electrum.hpp +++ b/include/bitcoin/server/channels/channel_electrum.hpp @@ -64,19 +64,19 @@ 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_; } private: // 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..9d7545a7 100644 --- a/include/bitcoin/server/protocols/protocol_electrum.hpp +++ b/include/bitcoin/server/protocols/protocol_electrum.hpp @@ -127,7 +127,7 @@ 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; } diff --git a/include/bitcoin/server/protocols/protocol_electrum_version.hpp b/include/bitcoin/server/protocols/protocol_electrum_version.hpp index 5a3e7334..5f32685d 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 @@ -51,18 +50,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; @@ -71,11 +70,6 @@ class BCS_API protocol_electrum_version bool set_client(const std::string& name) NOEXCEPT; 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 mostly thread safe, and used in a thread safe manner. const channel_t::ptr channel_; 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_version.cpp b/src/protocols/protocol_electrum_version.cpp index 5976811c..b404c65b 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; @@ -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/blocks.cpp b/test/endpoints/blocks.cpp new file mode 100644 index 00000000..0ebb7c37 --- /dev/null +++ b/test/endpoints/blocks.cpp @@ -0,0 +1,57 @@ +/** + * 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/[block-hash]?format=hex + +using namespace system; +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 header_t block1_data = base16_array("010000006fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000982051fd1e4ba744bbbe680e1fee14677ba1a3c3540bf7b1cdb606e857233e0e61bc6649ffff001d01e362990101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000"); +constexpr header_t block2_data = base16_array("010000004860eb18bf1b1620e37e9490fc8a427514416fd75159ab86688e9a8300000000d5fdcc541e25de1c7a5addedf24858b8bb665c9f36ef744ee42c316022c90f9bb0bc6649ffff001d08d2bd610101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d010bffffffff0100f2052a010000004341047211a824f55b505228e4c3d5194c1fcfaa15a456abdf37f9b9d97a4040afc073dee6c89064984f03385237d92167c13e236446b417ab79a0fcae412ae3316b77ac00000000"); +constexpr header_t block3_data = base16_array("01000000bddd99ccfda39da1b108ce1a5d70038d0a967bacb68b6b63065f626a0000000044f672226090d85db9a9f2fbfe5f0f9609b387af7be5b7fbb7a1767c831c9e995dbe6649ffff001d05e0ed6d0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d010effffffff0100f2052a0100000043410494b9d3e76c5b1629ecf97fff95d7a4bbdac87cc26099ada28066c6ff1eb9191223cd897194a08d0c2726c5747f1db49e8cf90e75dc3e3550ae9b30086f3cd5aaac00000000"); +constexpr header_t block4_data = base16_array("010000004944469562ae1c2c74d9a535e00b6f3e40ffbad4f2fda3895501b582000000007a06ea98cd40ba2e3288262b28638cec5337c1456aaf5eedc8e9e5a20f062bdf8cc16649ffff001d2bfee0a90101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d011affffffff0100f2052a01000000434104184f32b212815c6e522e66686324030ff7e5bf08efb21f8b00614fb7690e19131dd31304c54f37baa40db231c918106bb9fd43373e37ae31a0befc6ecaefb867ac00000000"); +constexpr header_t block5_data = base16_array("0100000085144a84488ea88d221c8bd6c059da090e88f8a2c99690ee55dbba4e00000000e11c48fecdd9e72510ca84f023370c9a38bf91ac5cae88019bee94d24528526344c36649ffff001d1d03e4770101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0120ffffffff0100f2052a0100000043410456579536d150fbce94ee62b47db2ca43af0a730a0467ba55c79e2a7ec9ce4ad297e35cdbb8e42a4643a60eef7c9abee2f5822f86b1da242d9c2301c431facfd8ac00000000"); +constexpr header_t block6_data = base16_array("01000000fc33f596f822a0a1951ffdbf2a897b095636ad871707bf5d3162729b00000000379dfb96a5ea8c81700ea4ac6b97ae9a9312b2d4301a29580e924ee6761a2520adc46649ffff001d189c4c970101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0123ffffffff0100f2052a0100000043410408ce279174b34c077c7b2043e3f3d45a588b85ef4ca466740f848ead7fb498f0a795c982552fdfa41616a7c0333a269d62108588e260fd5a48ac8e4dbf49e2bcac00000000"); +constexpr header_t block7_data = base16_array("010000008d778fdc15a2d3fb76b7122a3b5582bea4f21f5a0c693537e7a03130000000003f674005103b42f984169c7d008370967e91920a6a5d64fd51282f75bc73a68af1c66649ffff001d39a59c860101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d012bffffffff0100f2052a01000000434104a59e64c774923d003fae7491b2a7f75d6b7aa3f35606a8ff1cf06cd3317d16a41aa16928b1df1f631f31f28c7da35d4edad3603adb2338c4d4dd268f31530555ac00000000"); +constexpr header_t block8_data = base16_array("010000004494c8cf4154bdcc0720cd4a59d9c9b285e4b146d45f061d2b6c967100000000e3855ed886605b6d4a99d5fa2ef2e9b0b164e63df3c4136bebf2d0dac0f1f7a667c86649ffff001d1c4b56660101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d012cffffffff0100f2052a01000000434104cc8d85f5e7933cb18f13b97d165e1189c1fb3e9c98b0dd5446b2a1989883ff9e740a8a75da99cc59a21016caf7a7afd3e4e9e7952983e18d1ff70529d62e0ba1ac00000000"); +constexpr header_t 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{}; diff --git a/test/endpoints/blocks.hpp b/test/endpoints/blocks.hpp new file mode 100644 index 00000000..7068eae3 --- /dev/null +++ b/test/endpoints/blocks.hpp @@ -0,0 +1,58 @@ +/** + * 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" + +constexpr size_t small_header_size = 215; +using header_t = system::data_array; +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 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 header_t block1_data; +extern const header_t block2_data; +extern const header_t block3_data; +extern const header_t block4_data; +extern const header_t block5_data; +extern const header_t block6_data; +extern const header_t block7_data; +extern const header_t block8_data; +extern const header_t 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; diff --git a/test/endpoints/electrum.cpp b/test/endpoints/electrum.cpp index d124fb0d..c411acee 100644 --- a/test/endpoints/electrum.cpp +++ b/test/endpoints/electrum.cpp @@ -17,159 +17,29 @@ * along with this program. If not, see . */ #include "../test.hpp" +#include "electrum.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 +BOOST_FIXTURE_TEST_SUITE(electrum_tests, electrum_setup_fixture) + +// server.version + +BOOST_AUTO_TEST_CASE(electrum__server_version__default__expected) { - 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 }; -}; + const auto response = get(R"({"id":0,"method":"server.version","params":["foobar"]})" "\n"); + BOOST_REQUIRE_EQUAL(response.at("id").as_int64(), 0); + BOOST_REQUIRE(response.at("result").is_array()); -BOOST_FIXTURE_TEST_SUITE(electrum_tests, electrum_setup_fixture) + 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__server_version__default__1_4) +BOOST_AUTO_TEST_CASE(electrum__server_version__minimum__expected) { - const auto response = get(R"({"id":42,"method":"server.version","params":["foobar"]})" "\n"); + const auto response = get(R"({"id":42,"method":"server.version","params":["foobar","1.4"]})" "\n"); BOOST_REQUIRE_EQUAL(response.at("id").as_int64(), 42); BOOST_REQUIRE(response.at("result").is_array()); @@ -181,9 +51,56 @@ BOOST_AUTO_TEST_CASE(electrum__server_version__default__1_4) BOOST_REQUIRE_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_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.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_REQUIRE_EQUAL(response.at("id").as_int64(), 42); + BOOST_REQUIRE(response.at("error").is_object()); + + const code ec{ error::invalid_argument }; + const auto& error = response.at("error").as_object(); + BOOST_REQUIRE_EQUAL(error.size(), 2u); + BOOST_REQUIRE(error.contains("code")); + BOOST_REQUIRE_EQUAL(error.at("code").as_int64(), ec.value()); + BOOST_REQUIRE(error.contains("message")); + BOOST_REQUIRE_EQUAL(std::string(error.at("message").as_string()), ec.message()); +} + +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_REQUIRE_EQUAL(response.at("id").as_int64(), 42); + BOOST_REQUIRE(response.at("error").is_object()); + + const code ec{ error::invalid_argument }; + const auto& error = response.at("error").as_object(); + BOOST_REQUIRE_EQUAL(error.size(), 2u); + BOOST_REQUIRE(error.contains("code")); + BOOST_REQUIRE_EQUAL(error.at("code").as_int64(), ec.value()); + BOOST_REQUIRE(error.contains("message")); + BOOST_REQUIRE_EQUAL(std::string(error.at("message").as_string()), ec.message()); +} + +// blockchain.block.header + BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__genesis__expected) { - get(R"({"id":"name","method":"server.version","params":["foobar","1.4"]})" "\n"); + BOOST_REQUIRE(handshake()); + const auto expected = system::encode_base16(genesis.header().to_data()); const auto response = get(R"({"id":43,"method":"blockchain.block.header","params":[0]})" "\n"); BOOST_REQUIRE_EQUAL(response.at("id").as_int64(), 43); @@ -192,7 +109,7 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__genesis__expected) 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_REQUIRE_EQUAL(result.at("header").as_string(), expected); } BOOST_AUTO_TEST_SUITE_END() diff --git a/test/endpoints/electrum.hpp b/test/endpoints/electrum.hpp new file mode 100644 index 00000000..305fca20 --- /dev/null +++ b/test/endpoints/electrum.hpp @@ -0,0 +1,156 @@ +/** + * 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 +#include + +#define ELECTRUM_ENDPOINT "127.0.0.1:65000" + +struct electrum_setup_fixture +{ + DELETE_COPY_MOVE(electrum_setup_fixture); + + inline 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(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 = { { ELECTRUM_ENDPOINT } }; + 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()); + } + + inline ~electrum_setup_fixture() + { + socket_.close(); + server_.close(); + BOOST_REQUIRE(!store_.close([](auto, auto){})); + BOOST_REQUIRE(test::clear(test::directory)); + } + + inline const configuration& config() const NOEXCEPT + { + return config_; + } + + inline auto get(const std::string& request) + { + 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); + } + + inline bool handshake(const std::string& version="1.4", + const std::string& name="test", network::rpc::code_t id=0) + { + 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("result").is_array() || + !response.at("id").is_int64() || + response.at("id").as_int64() != id) + return false; + + // Assumes server always accept 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().network.user_agent) && + (result.at(1).as_string() == version); + } + +private: + bool setup_eight_block_store() 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); + } + + 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 }; +}; 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() From 785b2b30038e40d12a523b434b28588ad7f03e6d Mon Sep 17 00:00:00 2001 From: evoskuil Date: Tue, 3 Mar 2026 21:01:11 -0500 Subject: [PATCH 02/10] Add comprehensive electrum handshake tests. --- test/endpoints/electrum.cpp | 77 +++++++++++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 17 deletions(-) diff --git a/test/endpoints/electrum.cpp b/test/endpoints/electrum.cpp index c411acee..eeebe175 100644 --- a/test/endpoints/electrum.cpp +++ b/test/endpoints/electrum.cpp @@ -23,6 +23,8 @@ 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"); @@ -65,34 +67,75 @@ BOOST_AUTO_TEST_CASE(electrum__server_version__maximum__expected) BOOST_REQUIRE_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_REQUIRE_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_REQUIRE_EQUAL(response.at("id").as_int64(), 42); BOOST_REQUIRE(response.at("error").is_object()); - - const code ec{ error::invalid_argument }; - const auto& error = response.at("error").as_object(); - BOOST_REQUIRE_EQUAL(error.size(), 2u); - BOOST_REQUIRE(error.contains("code")); - BOOST_REQUIRE_EQUAL(error.at("code").as_int64(), ec.value()); - BOOST_REQUIRE(error.contains("message")); - BOOST_REQUIRE_EQUAL(std::string(error.at("message").as_string()), ec.message()); + BOOST_REQUIRE_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_REQUIRE_EQUAL(response.at("id").as_int64(), 42); - BOOST_REQUIRE(response.at("error").is_object()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); +} - const code ec{ error::invalid_argument }; - const auto& error = response.at("error").as_object(); - BOOST_REQUIRE_EQUAL(error.size(), 2u); - BOOST_REQUIRE(error.contains("code")); - BOOST_REQUIRE_EQUAL(error.at("code").as_int64(), ec.value()); - BOOST_REQUIRE(error.contains("message")); - BOOST_REQUIRE_EQUAL(std::string(error.at("message").as_string()), ec.message()); +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_REQUIRE_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_REQUIRE_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_REQUIRE_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_REQUIRE_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_REQUIRE(handshake(expected)); + + const auto response = get(R"({"id":42,"method":"server.version","params":["newname","1.4.2"]})" "\n"); + BOOST_REQUIRE_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_REQUIRE(handshake(expected)); + + const auto response = get(R"({"id":57,"method":"server.version","params":["foobar","invalid"]})" "\n"); + BOOST_REQUIRE_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_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); } // blockchain.block.header From ae31015c119959528cf9a36bac9947490eefb140 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Wed, 4 Mar 2026 00:46:22 -0500 Subject: [PATCH 03/10] Add block_header tests. --- Makefile.am | 12 +- builds/cmake/CMakeLists.txt | 12 +- .../libbitcoin-server-test.vcxproj | 18 +- .../libbitcoin-server-test.vcxproj.filters | 32 ++-- src/protocols/protocol_electrum.cpp | 14 +- test/endpoints/blocks.cpp | 57 ------ test/endpoints/electrum.hpp | 156 ----------------- test/protocols/blocks.cpp | 105 ++++++++++++ test/{endpoints => protocols}/blocks.hpp | 53 ++++-- test/protocols/electrum/electrum.cpp | 116 +++++++++++++ test/protocols/electrum/electrum.hpp | 49 ++++++ .../electrum/electrum_block_header.cpp | 162 ++++++++++++++++++ .../electrum/electrum_server_version.cpp} | 89 ++++------ 13 files changed, 566 insertions(+), 309 deletions(-) delete mode 100644 test/endpoints/blocks.cpp delete mode 100644 test/endpoints/electrum.hpp create mode 100644 test/protocols/blocks.cpp rename test/{endpoints => protocols}/blocks.hpp (55%) create mode 100644 test/protocols/electrum/electrum.cpp create mode 100644 test/protocols/electrum/electrum.hpp create mode 100644 test/protocols/electrum/electrum_block_header.cpp rename test/{endpoints/electrum.cpp => protocols/electrum/electrum_server_version.cpp} (55%) diff --git a/Makefile.am b/Makefile.am index b2f4aac9..3e6f67a1 100644 --- a/Makefile.am +++ b/Makefile.am @@ -72,15 +72,17 @@ test_libbitcoin_server_test_SOURCES = \ test/settings.cpp \ test/test.cpp \ test/test.hpp \ - test/endpoints/blocks.cpp \ - test/endpoints/blocks.hpp \ - test/endpoints/electrum.cpp \ - test/endpoints/electrum.hpp \ 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_version.cpp endif WITH_TESTS diff --git a/builds/cmake/CMakeLists.txt b/builds/cmake/CMakeLists.txt index 0e80e5bf..eef84119 100644 --- a/builds/cmake/CMakeLists.txt +++ b/builds/cmake/CMakeLists.txt @@ -296,11 +296,7 @@ if (with-tests) "../../test/test.cpp" "../../test/test.hpp" "../../test/endpoints/README.md" - "../../test/endpoints/blocks.cpp" - "../../test/endpoints/blocks.hpp" "../../test/endpoints/conftest.py" - "../../test/endpoints/electrum.cpp" - "../../test/endpoints/electrum.hpp" "../../test/endpoints/test_bitcoind_rpc.py" "../../test/endpoints/test_electrum.py" "../../test/endpoints/test_native.py" @@ -309,7 +305,13 @@ if (with-tests) "../../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_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 000b0035..b19bd377 100644 --- a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj @@ -119,12 +119,6 @@ - - $(IntDir)test_endpoints_blocks.obj - - - $(IntDir)test_endpoints_electrum.obj - @@ -133,14 +127,22 @@ + + $(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 52e730c6..a6a473a1 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,17 +16,17 @@ {66A0E586-2E3A-448F-0000-000000000002} + + {66A0E586-2E3A-448F-0000-000000000003} + + + {66A0E586-2E3A-448F-0000-000000000004} + src - - src\endpoints - - - src\endpoints - src @@ -51,6 +51,18 @@ src\parsers + + src\protocols + + + src\protocols\electrum + + + src\protocols\electrum + + + src\protocols\electrum + src @@ -59,11 +71,11 @@ - - src\endpoints + + src\protocols - - src\endpoints + + src\protocols\electrum src diff --git a/src/protocols/protocol_electrum.cpp b/src/protocols/protocol_electrum.cpp index 371e702f..764ea80b 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(); } diff --git a/test/endpoints/blocks.cpp b/test/endpoints/blocks.cpp deleted file mode 100644 index 0ebb7c37..00000000 --- a/test/endpoints/blocks.cpp +++ /dev/null @@ -1,57 +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 "blocks.hpp" - -// blockchain.info/rawblock/[block-hash]?format=hex - -using namespace system; -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 header_t block1_data = base16_array("010000006fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000982051fd1e4ba744bbbe680e1fee14677ba1a3c3540bf7b1cdb606e857233e0e61bc6649ffff001d01e362990101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000"); -constexpr header_t block2_data = base16_array("010000004860eb18bf1b1620e37e9490fc8a427514416fd75159ab86688e9a8300000000d5fdcc541e25de1c7a5addedf24858b8bb665c9f36ef744ee42c316022c90f9bb0bc6649ffff001d08d2bd610101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d010bffffffff0100f2052a010000004341047211a824f55b505228e4c3d5194c1fcfaa15a456abdf37f9b9d97a4040afc073dee6c89064984f03385237d92167c13e236446b417ab79a0fcae412ae3316b77ac00000000"); -constexpr header_t block3_data = base16_array("01000000bddd99ccfda39da1b108ce1a5d70038d0a967bacb68b6b63065f626a0000000044f672226090d85db9a9f2fbfe5f0f9609b387af7be5b7fbb7a1767c831c9e995dbe6649ffff001d05e0ed6d0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d010effffffff0100f2052a0100000043410494b9d3e76c5b1629ecf97fff95d7a4bbdac87cc26099ada28066c6ff1eb9191223cd897194a08d0c2726c5747f1db49e8cf90e75dc3e3550ae9b30086f3cd5aaac00000000"); -constexpr header_t block4_data = base16_array("010000004944469562ae1c2c74d9a535e00b6f3e40ffbad4f2fda3895501b582000000007a06ea98cd40ba2e3288262b28638cec5337c1456aaf5eedc8e9e5a20f062bdf8cc16649ffff001d2bfee0a90101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d011affffffff0100f2052a01000000434104184f32b212815c6e522e66686324030ff7e5bf08efb21f8b00614fb7690e19131dd31304c54f37baa40db231c918106bb9fd43373e37ae31a0befc6ecaefb867ac00000000"); -constexpr header_t block5_data = base16_array("0100000085144a84488ea88d221c8bd6c059da090e88f8a2c99690ee55dbba4e00000000e11c48fecdd9e72510ca84f023370c9a38bf91ac5cae88019bee94d24528526344c36649ffff001d1d03e4770101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0120ffffffff0100f2052a0100000043410456579536d150fbce94ee62b47db2ca43af0a730a0467ba55c79e2a7ec9ce4ad297e35cdbb8e42a4643a60eef7c9abee2f5822f86b1da242d9c2301c431facfd8ac00000000"); -constexpr header_t block6_data = base16_array("01000000fc33f596f822a0a1951ffdbf2a897b095636ad871707bf5d3162729b00000000379dfb96a5ea8c81700ea4ac6b97ae9a9312b2d4301a29580e924ee6761a2520adc46649ffff001d189c4c970101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0123ffffffff0100f2052a0100000043410408ce279174b34c077c7b2043e3f3d45a588b85ef4ca466740f848ead7fb498f0a795c982552fdfa41616a7c0333a269d62108588e260fd5a48ac8e4dbf49e2bcac00000000"); -constexpr header_t block7_data = base16_array("010000008d778fdc15a2d3fb76b7122a3b5582bea4f21f5a0c693537e7a03130000000003f674005103b42f984169c7d008370967e91920a6a5d64fd51282f75bc73a68af1c66649ffff001d39a59c860101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d012bffffffff0100f2052a01000000434104a59e64c774923d003fae7491b2a7f75d6b7aa3f35606a8ff1cf06cd3317d16a41aa16928b1df1f631f31f28c7da35d4edad3603adb2338c4d4dd268f31530555ac00000000"); -constexpr header_t block8_data = base16_array("010000004494c8cf4154bdcc0720cd4a59d9c9b285e4b146d45f061d2b6c967100000000e3855ed886605b6d4a99d5fa2ef2e9b0b164e63df3c4136bebf2d0dac0f1f7a667c86649ffff001d1c4b56660101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d012cffffffff0100f2052a01000000434104cc8d85f5e7933cb18f13b97d165e1189c1fb3e9c98b0dd5446b2a1989883ff9e740a8a75da99cc59a21016caf7a7afd3e4e9e7952983e18d1ff70529d62e0ba1ac00000000"); -constexpr header_t 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{}; diff --git a/test/endpoints/electrum.hpp b/test/endpoints/electrum.hpp deleted file mode 100644 index 305fca20..00000000 --- a/test/endpoints/electrum.hpp +++ /dev/null @@ -1,156 +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 "blocks.hpp" -#include -#include - -#define ELECTRUM_ENDPOINT "127.0.0.1:65000" - -struct electrum_setup_fixture -{ - DELETE_COPY_MOVE(electrum_setup_fixture); - - inline 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(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 = { { ELECTRUM_ENDPOINT } }; - 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()); - } - - inline ~electrum_setup_fixture() - { - socket_.close(); - server_.close(); - BOOST_REQUIRE(!store_.close([](auto, auto){})); - BOOST_REQUIRE(test::clear(test::directory)); - } - - inline const configuration& config() const NOEXCEPT - { - return config_; - } - - inline auto get(const std::string& request) - { - 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); - } - - inline bool handshake(const std::string& version="1.4", - const std::string& name="test", network::rpc::code_t id=0) - { - 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("result").is_array() || - !response.at("id").is_int64() || - response.at("id").as_int64() != id) - return false; - - // Assumes server always accept 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().network.user_agent) && - (result.at(1).as_string() == version); - } - -private: - bool setup_eight_block_store() 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); - } - - 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 }; -}; 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/endpoints/blocks.hpp b/test/protocols/blocks.hpp similarity index 55% rename from test/endpoints/blocks.hpp rename to test/protocols/blocks.hpp index 7068eae3..7b1604ee 100644 --- a/test/endpoints/blocks.hpp +++ b/test/protocols/blocks.hpp @@ -16,16 +16,20 @@ * 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" -constexpr size_t small_header_size = 215; -using header_t = system::data_array; +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; @@ -36,15 +40,38 @@ extern const system::hash_digest block7_hash; extern const system::hash_digest block8_hash; extern const system::hash_digest block9_hash; -extern const header_t block1_data; -extern const header_t block2_data; -extern const header_t block3_data; -extern const header_t block4_data; -extern const header_t block5_data; -extern const header_t block6_data; -extern const header_t block7_data; -extern const header_t block8_data; -extern const header_t block9_data; +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; @@ -56,3 +83,7 @@ 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..5dc9dc92 --- /dev/null +++ b/test/protocols/electrum/electrum.cpp @@ -0,0 +1,116 @@ +/** + * 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(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; + + electrum.binds = { { ELECTRUM_ENDPOINT } }; + 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(query_)); + + // 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::~electrum_setup_fixture() +{ + socket_.close(); + server_.close(); + BOOST_REQUIRE(!store_.close([](auto, auto){})); + BOOST_REQUIRE(test::clear(test::directory)); +} + +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{}; + read_until(socket_, stream, '\n'); + + 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("result").is_array() || + !response.at("id").is_int64() || + response.at("id").as_int64() != id) + 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().network.user_agent) && + (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..993658d9 --- /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__cp_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__cp_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/endpoints/electrum.cpp b/test/protocols/electrum/electrum_server_version.cpp similarity index 55% rename from test/endpoints/electrum.cpp rename to test/protocols/electrum/electrum_server_version.cpp index eeebe175..02be42b4 100644 --- a/test/endpoints/electrum.cpp +++ b/test/protocols/electrum/electrum_server_version.cpp @@ -16,7 +16,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -#include "../test.hpp" +#include "../../test.hpp" #include "electrum.hpp" BOOST_FIXTURE_TEST_SUITE(electrum_tests, electrum_setup_fixture) @@ -28,105 +28,105 @@ 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_REQUIRE_EQUAL(response.at("id").as_int64(), 0); - BOOST_REQUIRE(response.at("result").is_array()); + 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_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_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(), config().network.user_agent); + 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_REQUIRE_EQUAL(response.at("id").as_int64(), 42); - BOOST_REQUIRE(response.at("result").is_array()); + 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_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_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(), config().network.user_agent); + 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_REQUIRE_EQUAL(response.at("id").as_int64(), 42); - BOOST_REQUIRE(response.at("result").is_array()); + 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_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.2"); + 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(), config().network.user_agent); + 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_REQUIRE_EQUAL(response.at("result").as_array().at(1).as_string(), "1.4.2"); + 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_REQUIRE_EQUAL(response.at("id").as_int64(), 42); - BOOST_REQUIRE(response.at("error").is_object()); - BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); + 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_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); + 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_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); + 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_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); + 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_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); + 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_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); + 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_REQUIRE(handshake(expected)); + BOOST_CHECK(handshake(expected)); const auto response = get(R"({"id":42,"method":"server.version","params":["newname","1.4.2"]})" "\n"); - BOOST_REQUIRE_EQUAL(response.at("result").as_array().at(1).as_string(), expected); + 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_REQUIRE(handshake(expected)); + BOOST_CHECK(handshake(expected)); const auto response = get(R"({"id":57,"method":"server.version","params":["foobar","invalid"]})" "\n"); - BOOST_REQUIRE_EQUAL(response.at("result").as_array().at(1).as_string(), expected); + 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) @@ -135,24 +135,7 @@ BOOST_AUTO_TEST_CASE(electrum__server_version__client_name_overflow__invalid_arg 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_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); -} - -// blockchain.block.header - -BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__genesis__expected) -{ - BOOST_REQUIRE(handshake()); - const auto expected = system::encode_base16(genesis.header().to_data()); - - 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(), expected); + BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); } BOOST_AUTO_TEST_SUITE_END() From 34a261ef383ec7d5cd20e0b70c7c3647afef7b68 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Wed, 4 Mar 2026 12:03:14 -0500 Subject: [PATCH 04/10] Try/catch endpoint test read_until. --- test/protocols/electrum/electrum.cpp | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/test/protocols/electrum/electrum.cpp b/test/protocols/electrum/electrum.cpp index 5dc9dc92..0e10a695 100644 --- a/test/protocols/electrum/electrum.cpp +++ b/test/protocols/electrum/electrum.cpp @@ -35,7 +35,7 @@ electrum_setup_fixture::electrum_setup_fixture() query_{ store_ }, log_{}, server_{ query_, config_, log_ } { - BOOST_REQUIRE(test::clear(test::directory)); + BOOST_REQUIRE_MESSAGE(test::clear(test::directory), "electrum setup"); auto& database_settings = config_.database; auto& network_settings = config_.network; @@ -50,10 +50,11 @@ electrum_setup_fixture::electrum_setup_fixture() 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(!store_.create([](auto, auto){})); - BOOST_REQUIRE(setup_eight_block_store(query_)); + BOOST_REQUIRE_MESSAGE(!ec, ec.message()); + BOOST_REQUIRE_MESSAGE(setup_eight_block_store(query_), "electrum initialize"); // Run the server. std::promise running{}; @@ -63,7 +64,8 @@ electrum_setup_fixture::electrum_setup_fixture() }); // Block until server is running. - BOOST_REQUIRE(!running.get_future().get()); + ec = running.get_future().get(); + BOOST_REQUIRE_MESSAGE(!ec, ec.message()); socket_.connect(electrum.binds.back().to_endpoint()); } @@ -71,8 +73,9 @@ electrum_setup_fixture::~electrum_setup_fixture() { socket_.close(); server_.close(); - BOOST_REQUIRE(!store_.close([](auto, auto){})); - BOOST_REQUIRE(test::clear(test::directory)); + 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 @@ -84,12 +87,20 @@ boost::json::value electrum_setup_fixture::get(const std::string& request) { socket_.send(boost::asio::buffer(request)); boost::asio::streambuf stream{}; - read_until(socket_, stream, '\n'); + + try + { + boost::asio::read_until(socket_, stream, '\n'); + } + catch (const boost::system::system_error& e) + { + BOOST_WARN_MESSAGE(false, e.what()); + return {}; + } std::string response{}; std::istream response_stream{ &stream }; std::getline(response_stream, response); - return boost::json::parse(response); } From 0d75298c2298e41afe188ade255e065aefca4475 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Wed, 4 Mar 2026 17:30:10 -0500 Subject: [PATCH 05/10] Add 4 additional electrum server config settings. --- include/bitcoin/server/settings.hpp | 16 ++++++++++++++-- src/parser.cpp | 22 +++++++++++++++++++++- test/settings.cpp | 4 ++++ 3 files changed, 39 insertions(+), 3 deletions(-) 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/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) From 5cfac72ba0c7f388622ad66f4bfcb81db7a457a3 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Wed, 4 Mar 2026 17:39:03 -0500 Subject: [PATCH 06/10] Expose electrum options to protocol and channel. --- include/bitcoin/server/channels/channel_electrum.hpp | 11 ++++++++++- .../bitcoin/server/protocols/protocol_electrum.hpp | 9 +++++++++ .../server/protocols/protocol_electrum_version.hpp | 9 +++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/include/bitcoin/server/channels/channel_electrum.hpp b/include/bitcoin/server/channels/channel_electrum.hpp index d1f2b3a1..c9915a76 100644 --- a/include/bitcoin/server/channels/channel_electrum.hpp +++ b/include/bitcoin/server/channels/channel_electrum.hpp @@ -39,12 +39,13 @@ 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) @@ -74,7 +75,15 @@ class BCS_API channel_electrum 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 }; std::string name_{}; diff --git a/include/bitcoin/server/protocols/protocol_electrum.hpp b/include/bitcoin/server/protocols/protocol_electrum.hpp index 9d7545a7..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) { @@ -132,7 +133,15 @@ class BCS_API protocol_electrum 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 5f32685d..2d21a322 100644 --- a/include/bitcoin/server/protocols/protocol_electrum_version.hpp +++ b/include/bitcoin/server/protocols/protocol_electrum_version.hpp @@ -41,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) { @@ -69,7 +70,15 @@ 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: + // 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_; From 80e8fa7223c32700088c5ffb96bddedc8eeffb1f Mon Sep 17 00:00:00 2001 From: evoskuil Date: Wed, 4 Mar 2026 17:39:16 -0500 Subject: [PATCH 07/10] Style. --- test/protocols/electrum/electrum_block_header.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/protocols/electrum/electrum_block_header.cpp b/test/protocols/electrum/electrum_block_header.cpp index 993658d9..7d3d625a 100644 --- a/test/protocols/electrum/electrum_block_header.cpp +++ b/test/protocols/electrum/electrum_block_header.cpp @@ -111,7 +111,7 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__proof_example__expected) BOOST_CHECK_EQUAL(branch.at(3).as_string(), expected_branch[3]); } -BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__cp_below_height__target_overflow) +BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__checkpoint_below_height__target_overflow) { BOOST_CHECK(handshake()); @@ -127,7 +127,7 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__above_top__not_found) BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value()); } -BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__cp_above_top__not_found) +BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__checkpoint_above_top__not_found) { BOOST_CHECK(handshake()); From 2c80255c2de692cfd3d2724c70890231dd752055 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Wed, 4 Mar 2026 17:44:56 -0500 Subject: [PATCH 08/10] Integrate new electrum settings, two methods, tests. --- Makefile.am | 1 + builds/cmake/CMakeLists.txt | 1 + .../libbitcoin-server-test.vcxproj | 1 + .../libbitcoin-server-test.vcxproj.filters | 3 + src/protocols/protocol_electrum.cpp | 15 ++- src/protocols/protocol_electrum_version.cpp | 2 +- test/protocols/electrum/electrum.cpp | 13 ++- test/protocols/electrum/electrum_server.cpp | 91 +++++++++++++++++++ .../electrum/electrum_server_version.cpp | 6 +- 9 files changed, 119 insertions(+), 14 deletions(-) create mode 100644 test/protocols/electrum/electrum_server.cpp diff --git a/Makefile.am b/Makefile.am index 3e6f67a1..ec5fca00 100644 --- a/Makefile.am +++ b/Makefile.am @@ -82,6 +82,7 @@ test_libbitcoin_server_test_SOURCES = \ 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 eef84119..4183cdda 100644 --- a/builds/cmake/CMakeLists.txt +++ b/builds/cmake/CMakeLists.txt @@ -311,6 +311,7 @@ if (with-tests) "../../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 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 b19bd377..cf12bd0c 100644 --- a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj @@ -134,6 +134,7 @@ $(IntDir)test_protocols_electrum_electrum.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 a6a473a1..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 @@ -60,6 +60,9 @@ src\protocols\electrum + + src\protocols\electrum + src\protocols\electrum diff --git a/src/protocols/protocol_electrum.cpp b/src/protocols/protocol_electrum.cpp index 764ea80b..00c4a72e 100644 --- a/src/protocols/protocol_electrum.cpp +++ b/src/protocols/protocol_electrum.cpp @@ -429,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, @@ -457,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 b404c65b..cf1553b5 100644 --- a/src/protocols/protocol_electrum_version.cpp +++ b/src/protocols/protocol_electrum_version.cpp @@ -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 diff --git a/test/protocols/electrum/electrum.cpp b/test/protocols/electrum/electrum.cpp index 0e10a695..6027795d 100644 --- a/test/protocols/electrum/electrum.cpp +++ b/test/protocols/electrum/electrum.cpp @@ -44,6 +44,10 @@ electrum_setup_fixture::electrum_setup_fixture() 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; @@ -95,7 +99,7 @@ boost::json::value electrum_setup_fixture::get(const std::string& request) catch (const boost::system::system_error& e) { BOOST_WARN_MESSAGE(false, e.what()); - return {}; + return boost::json::parse(R"({"dropped":true})"); } std::string response{}; @@ -113,15 +117,14 @@ bool electrum_setup_fixture::handshake(const std::string& version, ) % id % name % version; const auto response = get(request.str()); - if (!response.at("result").is_array() || - !response.at("id").is_int64() || - response.at("id").as_int64() != id) + 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().network.user_agent) && + (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_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 index 02be42b4..3c534580 100644 --- a/test/protocols/electrum/electrum_server_version.cpp +++ b/test/protocols/electrum/electrum_server_version.cpp @@ -35,7 +35,7 @@ BOOST_AUTO_TEST_CASE(electrum__server_version__default__expected) 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(), config().network.user_agent); + BOOST_CHECK_EQUAL(result.at(0).as_string(), "server_name"); BOOST_CHECK_EQUAL(result.at(1).as_string(), "1.4"); } @@ -49,7 +49,7 @@ BOOST_AUTO_TEST_CASE(electrum__server_version__minimum__expected) 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(), config().network.user_agent); + BOOST_CHECK_EQUAL(result.at(0).as_string(), "server_name"); BOOST_CHECK_EQUAL(result.at(1).as_string(), "1.4"); } @@ -63,7 +63,7 @@ BOOST_AUTO_TEST_CASE(electrum__server_version__maximum__expected) 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(), config().network.user_agent); + BOOST_CHECK_EQUAL(result.at(0).as_string(), "server_name"); BOOST_CHECK_EQUAL(result.at(1).as_string(), "1.4.2"); } From dfd1ca649a8d06786170ee5649810f41d094a489 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Wed, 4 Mar 2026 20:35:15 -0500 Subject: [PATCH 09/10] Disable dead macro. --- include/bitcoin/server/protocols/protocol_rpc.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From f37e82add599762f1802f81215fe102f921ba1bf Mon Sep 17 00:00:00 2001 From: evoskuil Date: Wed, 4 Mar 2026 23:34:05 -0500 Subject: [PATCH 10/10] OSX TEST --- .../server/channels/channel_electrum.hpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/include/bitcoin/server/channels/channel_electrum.hpp b/include/bitcoin/server/channels/channel_electrum.hpp index c9915a76..5a470400 100644 --- a/include/bitcoin/server/channels/channel_electrum.hpp +++ b/include/bitcoin/server/channels/channel_electrum.hpp @@ -52,6 +52,24 @@ class BCS_API channel_electrum { } + 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. /// -----------------------------------------------------------------------