From 7061c6f18404e61a1ac8cd8add17f017dc767795 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Tue, 31 Mar 2026 00:49:49 -0400 Subject: [PATCH 01/12] Implement and test handle_blockchain_transaction_get_merkle. --- src/protocols/protocol_electrum.cpp | 57 +++++++++++++-- .../electrum/electrum_transactions.cpp | 72 +++++++++++++++++++ 2 files changed, 125 insertions(+), 4 deletions(-) diff --git a/src/protocols/protocol_electrum.cpp b/src/protocols/protocol_electrum.cpp index 976bc4ba..41b27785 100644 --- a/src/protocols/protocol_electrum.cpp +++ b/src/protocols/protocol_electrum.cpp @@ -518,11 +518,60 @@ void protocol_electrum::handle_blockchain_transaction_get(const code& ec, } void protocol_electrum::handle_blockchain_transaction_get_merkle(const code& ec, - rpc_interface::blockchain_transaction_get_merkle, const std::string& , - double ) NOEXCEPT + rpc_interface::blockchain_transaction_get_merkle, const std::string& tx_hash, + double height) NOEXCEPT { - if (stopped(ec)) return; - send_code(error::not_implemented); + using namespace system; + if (stopped(ec)) + return; + + hash_digest hash{}; + size_t block_height{}; + if (!to_integer(block_height, height) || !decode_hash(hash, tx_hash)) + { + send_code(error::invalid_argument); + return; + } + + const auto& query = archive(); + const auto block_link = query.to_confirmed(block_height); + if (block_link.is_terminal()) + { + send_code(error::not_found); + return; + } + + auto hashes = query.get_tx_keys(block_link); + if (hashes.empty()) + { + send_code(error::server_error); + return; + } + + const auto index = find_position(hashes, hash); + if (is_negative(index)) + { + send_code(error::not_found); + return; + } + + using namespace chain; + const auto position = to_unsigned(index); + const auto proof = block::merkle_branch(index, std::move(hashes)); + + array_t branch(proof.size()); + std::ranges::transform(proof, branch.begin(), + [](const auto& hash) NOEXCEPT{ return encode_hash(hash); }); + + send_result( + { + object_t + { + { "merkle", std::move(branch) }, + { "block_height", block_height }, + { "pos", position } + } + }, two * hash_size * add1(branch.size()), BIND(complete, _1)); } void protocol_electrum::handle_blockchain_transaction_id_from_pos(const code& ec, diff --git a/test/protocols/electrum/electrum_transactions.cpp b/test/protocols/electrum/electrum_transactions.cpp index 32054631..090c96ce 100644 --- a/test/protocols/electrum/electrum_transactions.cpp +++ b/test/protocols/electrum/electrum_transactions.cpp @@ -151,6 +151,78 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get__genesis_coinbase_verb // blockchain.transaction.get_merkle +BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get_merkle__empty_hash__invalid_argument) +{ + BOOST_CHECK(handshake()); + + const auto response = get(R"({"id":100,"method":"blockchain.transaction.get_merkle","params":["",0]})" "\n"); + BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get_merkle__invalid_hash_encoding__invalid_argument) +{ + BOOST_CHECK(handshake()); + + const auto response = get(R"({"id":101,"method":"blockchain.transaction.get_merkle","params":["deadbeef",0]})" "\n"); + BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get_merkle__nonexistent_height__not_found) +{ + BOOST_CHECK(handshake()); + + const auto bogus = "0000000000000000000000000000000000000000000000000000000000000042"; + const auto request = R"({"id":102,"method":"blockchain.transaction.get_merkle","params":["%1%",999]})" "\n"; + const auto response = get((boost::format(request) % bogus).str()); + BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value()); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get_merkle__tx_not_in_block__not_found) +{ + BOOST_CHECK(handshake()); + + const auto bogus = "0000000000000000000000000000000000000000000000000000000000000042"; + const auto request = R"({"id":103,"method":"blockchain.transaction.get_merkle","params":["%1%",0]})" "\n"; + const auto response = get((boost::format(request) % bogus).str()); + BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value()); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get_merkle__genesis_coinbase__expected) +{ + BOOST_CHECK(handshake()); + + const auto& coinbase = *genesis.transactions_ptr()->front(); + const auto tx_hash = encode_hash(coinbase.hash(false)); + const auto request = R"({"id":104,"method":"blockchain.transaction.get_merkle","params":["%1%",0]})" "\n"; + const auto response = get((boost::format(request) % tx_hash).str()); + const auto& result = response.at("result").as_object(); + BOOST_CHECK_EQUAL(result.at("block_height").as_int64(), 0); + BOOST_CHECK_EQUAL(result.at("pos").as_int64(), 0); + BOOST_CHECK(result.at("merkle").as_array().empty()); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get_merkle__missing_param__dropped) +{ + BOOST_CHECK(handshake()); + + const auto& coinbase = *genesis.transactions_ptr()->front(); + const auto tx_hash = encode_hash(coinbase.hash(false)); + const auto request = R"({"id":105,"method":"blockchain.transaction.get_merkle","params":["%1%"]})" "\n"; + const auto response = get((boost::format(request) % tx_hash).str()); + BOOST_CHECK(response.at("dropped").as_bool()); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get_merkle__extra_param__dropped) +{ + BOOST_CHECK(handshake()); + + const auto& coinbase = *genesis.transactions_ptr()->front(); + const auto tx_hash = encode_hash(coinbase.hash(false)); + const auto request = R"({"id":106,"method":"blockchain.transaction.get_merkle","params":["%1%",0,"extra"]})" "\n"; + const auto response = get((boost::format(request) % tx_hash).str()); + BOOST_CHECK(response.at("dropped").as_bool()); +} + // blockchain.transaction.id_from_pos BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_id_from_pos__genesis_coinbase_default__expected) From 0b8de24915ac6a2692279cc1f200f911fcecb9b7 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Tue, 31 Mar 2026 01:50:00 -0400 Subject: [PATCH 02/12] Add a multi-tx test block, expose electrum fixture query_. --- test/protocols/blocks.cpp | 107 +++++++++++++++++++++++++++ test/protocols/blocks.hpp | 1 + test/protocols/electrum/electrum.cpp | 7 +- test/protocols/electrum/electrum.hpp | 5 +- 4 files changed, 112 insertions(+), 8 deletions(-) diff --git a/test/protocols/blocks.cpp b/test/protocols/blocks.cpp index 55022ac8..19e1d32f 100644 --- a/test/protocols/blocks.cpp +++ b/test/protocols/blocks.cpp @@ -103,3 +103,110 @@ bool setup_ten_block_store(query_t& query) NOEXCEPT query.push_confirmed(query.to_header(block8_hash), false) && query.push_confirmed(query.to_header(block9_hash), false); } + +using namespace chain; +const chain::block bogus_block10 +{ + header + { + 0x31323334, + block9_hash, + one_hash, + 0x41424344, + 0x51525354, + 0x61626364 + }, + transactions + { + transaction + { + 0x01, + inputs + { + input + { + point{}, + script{}, + witness{}, + 0x02 + }, + input + { + point{}, + script{}, + witness{}, + 0x03 + } + }, + outputs + { + output + { + 0x04, + script{} + } + }, + 0x05 + }, + transaction + { + 0x06, + inputs + { + input + { + point{}, + script{}, + witness{}, + 0x07 + }, + input + { + point{}, + script{}, + witness{}, + 0x08 + } + }, + outputs + { + output + { + 0x09, + script{} + } + }, + 0x0a + }, + transaction + { + 0x0b, + inputs + { + input + { + point{}, + script{}, + witness{}, + 0x0c + }, + input + { + point{}, + script{}, + witness{}, + 0x0d + } + }, + outputs + { + output + { + 0x0e, + script{} + } + }, + 0x0f + } + } +}; \ No newline at end of file diff --git a/test/protocols/blocks.hpp b/test/protocols/blocks.hpp index 794c4b4d..2d232a42 100644 --- a/test/protocols/blocks.hpp +++ b/test/protocols/blocks.hpp @@ -83,6 +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; +extern const system::chain::block bogus_block10; bool setup_ten_block_store(query_t& query) NOEXCEPT; diff --git a/test/protocols/electrum/electrum.cpp b/test/protocols/electrum/electrum.cpp index cb053027..5b284598 100644 --- a/test/protocols/electrum/electrum.cpp +++ b/test/protocols/electrum/electrum.cpp @@ -83,11 +83,6 @@ electrum_setup_fixture::~electrum_setup_fixture() BOOST_WARN_MESSAGE(test::clear(test::directory), "electrum cleanup"); } -const configuration& electrum_setup_fixture::config() const NOEXCEPT -{ - return config_; -} - boost::json::value electrum_setup_fixture::get(const std::string& request) { socket_.send(boost::asio::buffer(request)); @@ -126,6 +121,6 @@ bool electrum_setup_fixture::handshake(const std::string& version, const auto& result = response.at("result").as_array(); return (result.size() == two) && (result.at(0).is_string() && result.at(1).is_string()) && - (result.at(0).as_string() == config().server.electrum.server_name) && + (result.at(0).as_string() == config_.server.electrum.server_name) && (result.at(1).as_string() == version); } diff --git a/test/protocols/electrum/electrum.hpp b/test/protocols/electrum/electrum.hpp index aa8b3b3e..0a80b644 100644 --- a/test/protocols/electrum/electrum.hpp +++ b/test/protocols/electrum/electrum.hpp @@ -31,15 +31,16 @@ struct 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: +protected: configuration config_; store_t store_; query_t query_; + +private: network::logger log_; server::server_node server_; boost::asio::io_context io{}; From 36701f92ddc6f3d6bb02d50d702d9ef455d99c0e Mon Sep 17 00:00:00 2001 From: evoskuil Date: Tue, 31 Mar 2026 01:50:20 -0400 Subject: [PATCH 03/12] Add blockchain_transaction_get_merkle__mutiple_txs_block test. --- .../electrum/electrum_transactions.cpp | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/protocols/electrum/electrum_transactions.cpp b/test/protocols/electrum/electrum_transactions.cpp index 090c96ce..01442f08 100644 --- a/test/protocols/electrum/electrum_transactions.cpp +++ b/test/protocols/electrum/electrum_transactions.cpp @@ -201,6 +201,34 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get_merkle__genesis_coinba BOOST_CHECK(result.at("merkle").as_array().empty()); } +BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get_merkle__mutiple_txs_block__expected) +{ + BOOST_CHECK(handshake()); + + const auto& txs = *bogus_block10.transactions_ptr(); + const auto& tx0 = *txs.at(0); + const auto& tx1 = *txs.at(1); + const auto& tx2 = *txs.at(2); + const auto tx0_hash = tx0.hash(false); + const auto tx1_hash = tx1.hash(false); + const auto tx2_hash = tx2.hash(false); + + // Add a multi-tx block. + query_.set(bogus_block10, database::context{ 0, 10, 0 }, false, false); + query_.push_confirmed(query_.to_header(bogus_block10.hash()), false); + + const auto request = R"({"id":104,"method":"blockchain.transaction.get_merkle","params":["%1%",10]})" "\n"; + const auto response = get((boost::format(request) % encode_hash(tx1_hash)).str()); + const auto& result = response.at("result").as_object(); + BOOST_CHECK_EQUAL(result.at("block_height").as_int64(), 10); + BOOST_CHECK_EQUAL(result.at("pos").as_int64(), 1); + + const auto& merkle = result.at("merkle").as_array(); + BOOST_CHECK_EQUAL(merkle.size(), 2u); + BOOST_CHECK_EQUAL(merkle.at(0).as_string(), encode_hash(tx0_hash)); + BOOST_CHECK_EQUAL(merkle.at(1).as_string(), encode_hash(bitcoin_hash(tx2_hash, tx2_hash))); +} + BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get_merkle__missing_param__dropped) { BOOST_CHECK(handshake()); From b4154094c77e34654dcf721ddcd1a90be78f28c3 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Tue, 31 Mar 2026 14:34:34 -0400 Subject: [PATCH 04/12] Add electrum ping tests. --- test/protocols/electrum/electrum_server.cpp | 32 +++++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/test/protocols/electrum/electrum_server.cpp b/test/protocols/electrum/electrum_server.cpp index 6b196486..e7cb4edc 100644 --- a/test/protocols/electrum/electrum_server.cpp +++ b/test/protocols/electrum/electrum_server.cpp @@ -21,13 +21,13 @@ 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 }; +// server.banner + BOOST_AUTO_TEST_CASE(electrum__server_banner__jsonrpc_unspecified_no_aparams__dropped) { BOOST_CHECK(handshake()); @@ -37,7 +37,7 @@ BOOST_AUTO_TEST_CASE(electrum__server_banner__jsonrpc_unspecified_no_aparams__dr BOOST_CHECK(response.at("dropped").as_bool()); } -BOOST_AUTO_TEST_CASE(electrum__server_banner__jsonrpc_unspecified_named_aparams__dropped) +BOOST_AUTO_TEST_CASE(electrum__server_banner__jsonrpc_unspecified_named_params__dropped) { BOOST_CHECK(handshake()); @@ -88,4 +88,30 @@ BOOST_AUTO_TEST_CASE(electrum__server_donation_address__jsonrpc_2__expected) BOOST_CHECK_EQUAL(response.at("result").as_string(), "donation_address"); } +// server.ping + +BOOST_AUTO_TEST_CASE(electrum__server_ping__null) +{ + BOOST_CHECK(handshake()); + + const auto response = get(R"({"id":200,"method":"server.ping","params":[]})" "\n"); + BOOST_CHECK(response.at("result").is_null()); +} + +BOOST_AUTO_TEST_CASE(electrum__server_ping__jsonrpc_unspecified_no_aparams__dropped) +{ + BOOST_CHECK(handshake()); + + const auto response = get(R"({"id":201,"method":"server.ping"})" "\n"); + BOOST_CHECK(response.at("dropped").as_bool()); +} + +BOOST_AUTO_TEST_CASE(electrum__server_ping__extra_param__dropped) +{ + BOOST_CHECK(handshake()); + + const auto response = get(R"({"id":202,"method":"server.ping","params":["extra"]})" "\n"); + BOOST_CHECK(response.at("dropped").as_bool()); +} + BOOST_AUTO_TEST_SUITE_END() From f38a07870ef2240b912cbac64997a31a4b8df346 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Tue, 31 Mar 2026 15:37:16 -0400 Subject: [PATCH 05/12] Comments. --- test/protocols/electrum/electrum_addresses.cpp | 7 +++++++ test/protocols/electrum/electrum_fees.cpp | 4 ++++ test/protocols/electrum/electrum_server.cpp | 5 +++++ 3 files changed, 16 insertions(+) diff --git a/test/protocols/electrum/electrum_addresses.cpp b/test/protocols/electrum/electrum_addresses.cpp index 3a32de00..bf4371f0 100644 --- a/test/protocols/electrum/electrum_addresses.cpp +++ b/test/protocols/electrum/electrum_addresses.cpp @@ -21,4 +21,11 @@ BOOST_FIXTURE_TEST_SUITE(electrum_tests, electrum_setup_fixture) +// blockchain.scripthash.get_balance +// blockchain.scripthash.get_history +// blockchain.scripthash.get_mempool +// blockchain.scripthash.list_unspent +// blockchain.scripthash.subscribe +// blockchain.scripthash.unsubscribe + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/protocols/electrum/electrum_fees.cpp b/test/protocols/electrum/electrum_fees.cpp index bdc2622d..c76a91f7 100644 --- a/test/protocols/electrum/electrum_fees.cpp +++ b/test/protocols/electrum/electrum_fees.cpp @@ -21,6 +21,8 @@ BOOST_FIXTURE_TEST_SUITE(electrum_tests, electrum_setup_fixture) +// blockchain.estimatefee + // blockchain.relay_fee BOOST_AUTO_TEST_CASE(electrum__blockchain_relay_fee__default__expected) @@ -34,4 +36,6 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_relay_fee__default__expected) BOOST_CHECK_EQUAL(response.at("result").as_double(), expected); } +// get_fee_histogram + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/protocols/electrum/electrum_server.cpp b/test/protocols/electrum/electrum_server.cpp index e7cb4edc..95302397 100644 --- a/test/protocols/electrum/electrum_server.cpp +++ b/test/protocols/electrum/electrum_server.cpp @@ -26,6 +26,8 @@ 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 }; +// server.add_peer + // server.banner BOOST_AUTO_TEST_CASE(electrum__server_banner__jsonrpc_unspecified_no_aparams__dropped) @@ -88,6 +90,9 @@ BOOST_AUTO_TEST_CASE(electrum__server_donation_address__jsonrpc_2__expected) BOOST_CHECK_EQUAL(response.at("result").as_string(), "donation_address"); } +// server.features +// server.peers.subscribe + // server.ping BOOST_AUTO_TEST_CASE(electrum__server_ping__null) From 0c2d3ce261bd8428454b43722e0766f965f6b357 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Tue, 31 Mar 2026 15:37:35 -0400 Subject: [PATCH 06/12] Expose electrum version range as public statics. --- .../bitcoin/server/protocols/protocol_electrum_version.hpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/include/bitcoin/server/protocols/protocol_electrum_version.hpp b/include/bitcoin/server/protocols/protocol_electrum_version.hpp index 2d21a322..5dcde0cb 100644 --- a/include/bitcoin/server/protocols/protocol_electrum_version.hpp +++ b/include/bitcoin/server/protocols/protocol_electrum_version.hpp @@ -37,6 +37,9 @@ class BCS_API protocol_electrum_version typedef std::shared_ptr ptr; using rpc_interface = interface::electrum; + static constexpr electrum::version minimum = electrum::version::v1_4; + static constexpr electrum::version maximum = electrum::version::v1_4_2; + inline protocol_electrum_version(const auto& session, const network::channel::ptr& channel, const options_t& options) NOEXCEPT @@ -51,8 +54,6 @@ 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 size_t max_client_name_length = 1024; void handle_server_version(const code& ec, From 027835abe5878afae78afc0093d8d0f8a31ccbc2 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Tue, 31 Mar 2026 16:28:23 -0400 Subject: [PATCH 07/12] Add and parse electrum advertise_binds/safes. --- include/bitcoin/server/settings.hpp | 6 +++++- src/parser.cpp | 10 ++++++++++ test/settings.cpp | 2 ++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/include/bitcoin/server/settings.hpp b/include/bitcoin/server/settings.hpp index 6acc9d64..a74a014d 100644 --- a/include/bitcoin/server/settings.hpp +++ b/include/bitcoin/server/settings.hpp @@ -97,7 +97,7 @@ class BCS_API settings /// Maximum cumulative number of address subscriptions per channel. uint32_t maximum_subscriptions{ 1'000'000 }; - /// Arbitrary server name returned by server.version. + /// Arbitrary name returned by server.version and server.features. std::string server_name{ BC_USER_AGENT }; /// Arbitrary string returned by server.donation_address. @@ -105,6 +105,10 @@ class BCS_API settings /// Arbitrary string returned by server.banner. std::string banner_message{}; + + /// Advertised as self via server.features (limit one each per host). + network::config::endpoints advertise_binds{}; + network::config::endpoints advertise_safes{}; }; /// html (http/s) document server settings (has directory/default). diff --git a/src/parser.cpp b/src/parser.cpp index b86a8fb1..25aef634 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -1154,6 +1154,16 @@ options_metadata parser::load_settings() THROWS value(&configured.server.electrum.banner_message), "String returned by server.banner, defaults to empty." ) + ( + "electrum.advertise_bind", + value(&configured.server.electrum.advertise_binds), + "Advertised host:port at which this server can be reached (defaults to empty)." + ) + ( + "electrum.advertise_safe", + value(&configured.server.electrum.advertise_safes), + "Advertised secure host:port at which this server can be reached (defaults to empty)." + ) /* [stratum_v1] */ ( diff --git a/test/settings.cpp b/test/settings.cpp index 45e1e935..c8580e91 100644 --- a/test/settings.cpp +++ b/test/settings.cpp @@ -237,6 +237,8 @@ BOOST_AUTO_TEST_CASE(server__electrum_server__defaults__expected) BOOST_REQUIRE_EQUAL(server.server_name, BC_USER_AGENT); BOOST_REQUIRE(server.donation_address.empty()); BOOST_REQUIRE(server.banner_message.empty()); + BOOST_REQUIRE(server.advertise_binds.empty()); + BOOST_REQUIRE(server.advertise_safes.empty()); } BOOST_AUTO_TEST_CASE(server__stratum_v1_server__defaults__expected) From 1e864a776306ab2e4ac498e69ac83ffa53f893e2 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Tue, 31 Mar 2026 23:45:10 -0400 Subject: [PATCH 08/12] Add wrong_version code. --- include/bitcoin/server/error.hpp | 1 + src/error.cpp | 1 + test/error.cpp | 9 +++++++++ 3 files changed, 11 insertions(+) diff --git a/include/bitcoin/server/error.hpp b/include/bitcoin/server/error.hpp index 7f6d921a..2f1576f4 100644 --- a/include/bitcoin/server/error.hpp +++ b/include/bitcoin/server/error.hpp @@ -64,6 +64,7 @@ enum error_t : uint8_t unconfirmable_transaction, argument_overflow, target_overflow, + wrong_version, server_error }; diff --git a/src/error.cpp b/src/error.cpp index 48345abe..687725fc 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -54,6 +54,7 @@ DEFINE_ERROR_T_MESSAGE_MAP(error) { unconfirmable_transaction, "unconfirmable_transaction" }, { argument_overflow, "argument_overflow" }, { target_overflow, "target_overflow" }, + { wrong_version, "wrong_version" }, { server_error, "server_error" } }; diff --git a/test/error.cpp b/test/error.cpp index bb79189b..7063dfc2 100644 --- a/test/error.cpp +++ b/test/error.cpp @@ -236,6 +236,15 @@ BOOST_AUTO_TEST_CASE(error_t__code__target_overflow__true_expected_message) BOOST_REQUIRE_EQUAL(ec.message(), "target_overflow"); } +BOOST_AUTO_TEST_CASE(error_t__code__wrong_version__true_expected_message) +{ + constexpr auto value = error::wrong_version; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "wrong_version"); +} + BOOST_AUTO_TEST_CASE(error_t__code__server_error__true_expected_message) { constexpr auto value = error::server_error; From 74b7f8222e6ac498e3a2bdfd8fa6f3449fa30560 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Tue, 31 Mar 2026 23:46:10 -0400 Subject: [PATCH 09/12] Add REQUIRE_NO_THROW_TRUE() test macro. --- test/test.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/test.hpp b/test/test.hpp index bbbd1a9a..903d14df 100644 --- a/test/test.hpp +++ b/test/test.hpp @@ -24,6 +24,10 @@ #include #include +#define REQUIRE_NO_THROW_AND_TRUE(expression) \ + BOOST_REQUIRE_NO_THROW(expression); \ + BOOST_REQUIRE(expression) + #define TEST_NAME \ boost::unit_test::framework::current_test_case().p_name.get() #define SUITE_NAME \ From 26ee76e5728002ec9af01de683fef147c0f0fd03 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Wed, 1 Apr 2026 01:41:33 -0400 Subject: [PATCH 10/12] Add electrum method versions, refactor test exception safety. --- .../bitcoin/server/interfaces/electrum.hpp | 28 +- .../server/parsers/electrum_version.hpp | 20 +- .../server/protocols/protocol_electrum.hpp | 15 +- .../protocols/protocol_electrum_version.hpp | 4 +- src/parsers/electrum_version.cpp | 6 +- src/protocols/protocol_electrum.cpp | 540 +++++++++++++----- src/protocols/protocol_electrum_version.cpp | 16 +- test/parsers/electrum_version.cpp | 2 + test/protocols/electrum/electrum.cpp | 29 +- test/protocols/electrum/electrum.hpp | 2 +- test/protocols/electrum/electrum_fees.cpp | 9 +- test/protocols/electrum/electrum_headers.cpp | 300 ++++++---- test/protocols/electrum/electrum_server.cpp | 47 +- .../electrum/electrum_server_version.cpp | 106 ++-- .../electrum/electrum_transactions.cpp | 190 +++--- test/test.hpp | 2 +- 16 files changed, 907 insertions(+), 409 deletions(-) diff --git a/include/bitcoin/server/interfaces/electrum.hpp b/include/bitcoin/server/interfaces/electrum.hpp index 7aba73da..71b88515 100644 --- a/include/bitcoin/server/interfaces/electrum.hpp +++ b/include/bitcoin/server/interfaces/electrum.hpp @@ -44,6 +44,7 @@ struct electrum_methods method<"blockchain.scripthash.subscribe", string_t>{ "scripthash" }, method<"blockchain.scripthash.unsubscribe", string_t>{ "scripthash" }, method<"blockchain.transaction.broadcast", string_t>{ "raw_tx" }, + method<"blockchain.transaction.broadcast_package", string_t, optional>{ "raw_txs", "verbose" }, method<"blockchain.transaction.get", string_t, boolean_t>{ "tx_hash", "verbose" }, method<"blockchain.transaction.get_merkle", string_t, number_t>{ "tx_hash", "height" }, method<"blockchain.transaction.id_from_pos", number_t, number_t, optional>{ "height", "tx_pos", "merkle" }, @@ -58,7 +59,8 @@ struct electrum_methods method<"server.version", string_t, optional>{ "client_name", "protocol_version" }, /// Mempool methods. - method<"mempool.get_fee_histogram">{} + method<"mempool.get_fee_histogram">{}, + method<"mempool.get_info">{} }; template @@ -80,17 +82,19 @@ struct electrum_methods using blockchain_scripthash_subscribe = at<9>; using blockchain_scripthash_unsubscribe = at<10>; using blockchain_transaction_broadcast = at<11>; - using blockchain_transaction_get = at<12>; - using blockchain_transaction_get_merkle = at<13>; - using blockchain_transaction_id_from_pos = at<14>; - using server_add_peer = at<15>; - using server_banner = at<16>; - using server_donation_address = at<17>; - using server_features = at<18>; - using server_peers_subscribe = at<19>; - using server_ping = at<20>; - using server_version = at<21>; - using mempool_get_fee_histogram = at<22>; + using blockchain_transaction_broadcast_package = at<12>; + using blockchain_transaction_get = at<13>; + using blockchain_transaction_get_merkle = at<14>; + using blockchain_transaction_id_from_pos = at<15>; + using server_add_peer = at<16>; + using server_banner = at<17>; + using server_donation_address = at<18>; + using server_features = at<19>; + using server_peers_subscribe = at<20>; + using server_ping = at<21>; + using server_version = at<22>; + using mempool_get_fee_histogram = at<23>; + using mempool_get_info = at<24>; }; } // namespace interface diff --git a/include/bitcoin/server/parsers/electrum_version.hpp b/include/bitcoin/server/parsers/electrum_version.hpp index 5ac26112..decb0923 100644 --- a/include/bitcoin/server/parsers/electrum_version.hpp +++ b/include/bitcoin/server/parsers/electrum_version.hpp @@ -31,19 +31,19 @@ enum class version /// Invalid version. v0_0, - /// 2011, initial protocol negotiation. + /// 2011, initial protocol negotiation (usupported). v0_6, - /// 2012, enhanced protocol negotiation. + /// 2012, enhanced protocol negotiation (usupported). v0_8, - /// 2012, added pruning limits and transport indicators. + /// 2012, added pruning limits and transport indicators (usupported). v0_9, - /// 2013, baseline for core methods in the official specification. + /// 2013, baseline for core methods in official specification (usupported). v0_10, - /// 2014, 1.x series, deprecations of utxo and block number methods. + /// 2014, deprecations of utxo and block number methods (minimum). v1_0, /// 2015, updated version response and introduced scripthash methods. @@ -64,8 +64,14 @@ enum class version /// 2020, added scripthash unsubscribe functionality. v1_4_2, - /// 2022, updated response formats and added fee estimation modes. - v1_6 + /// There is no v1.5 release (skipped). + //v1_5, + + /// 2022, updated response formats, added fee estimation modes (maximum). + v1_6, + + /// Not valid, just defined for out of bounds testing. + v1_7 }; std::string_view version_to_string(version value) NOEXCEPT; diff --git a/include/bitcoin/server/protocols/protocol_electrum.hpp b/include/bitcoin/server/protocols/protocol_electrum.hpp index 65b30789..89e2c43c 100644 --- a/include/bitcoin/server/protocols/protocol_electrum.hpp +++ b/include/bitcoin/server/protocols/protocol_electrum.hpp @@ -25,6 +25,7 @@ #include #include #include +#include #include namespace libbitcoin { @@ -91,6 +92,9 @@ class BCS_API protocol_electrum void handle_blockchain_transaction_broadcast(const code& ec, rpc_interface::blockchain_transaction_broadcast, const std::string& raw_tx) NOEXCEPT; + void handle_blockchain_transaction_broadcast_package(const code& ec, + rpc_interface::blockchain_transaction_broadcast_package, + const std::string& raw_txs, bool verbose) NOEXCEPT; void handle_blockchain_transaction_get(const code& ec, rpc_interface::blockchain_transaction_get, const std::string& tx_hash, bool verbose) NOEXCEPT; @@ -122,6 +126,8 @@ class BCS_API protocol_electrum /// Handlers (mempool). void handle_mempool_get_fee_histogram(const code& ec, rpc_interface::mempool_get_fee_histogram) NOEXCEPT; + void handle_mempool_get_info(const code& ec, + rpc_interface::mempool_get_info) NOEXCEPT; protected: /// Common implementation for block_header/s. @@ -131,7 +137,7 @@ class BCS_API protocol_electrum /// Notify client of new header. void do_header(node::header_t link) NOEXCEPT; - inline bool is_version(server::electrum::version version) const NOEXCEPT + inline bool at_least(server::electrum::version version) const NOEXCEPT { return channel_->version() >= version; } @@ -142,6 +148,13 @@ class BCS_API protocol_electrum } private: + using version_t = protocol_electrum_version; + static constexpr electrum::version minimum = version_t::minimum; + static constexpr electrum::version maximum = version_t::maximum; + + // Compute server.features.hosts value from config. + network::rpc::object_t advertised_hosts() const NOEXCEPT; + // These are thread safe. const options_t& options_; std::atomic_bool subscribed_{}; diff --git a/include/bitcoin/server/protocols/protocol_electrum_version.hpp b/include/bitcoin/server/protocols/protocol_electrum_version.hpp index 5dcde0cb..c9e028a9 100644 --- a/include/bitcoin/server/protocols/protocol_electrum_version.hpp +++ b/include/bitcoin/server/protocols/protocol_electrum_version.hpp @@ -37,8 +37,8 @@ class BCS_API protocol_electrum_version typedef std::shared_ptr ptr; using rpc_interface = interface::electrum; - 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_0; + static constexpr electrum::version maximum = electrum::version::v1_6; inline protocol_electrum_version(const auto& session, const network::channel::ptr& channel, diff --git a/src/parsers/electrum_version.cpp b/src/parsers/electrum_version.cpp index b7994842..6573207c 100644 --- a/src/parsers/electrum_version.cpp +++ b/src/parsers/electrum_version.cpp @@ -40,7 +40,8 @@ std::string_view version_to_string(version value) NOEXCEPT { version::v1_4, "1.4" }, { version::v1_4_1, "1.4.1" }, { version::v1_4_2, "1.4.2" }, - { version::v1_6, "1.6" } + { version::v1_6, "1.6" }, + { version::v1_7, "1.7" } }; const auto it = map.find(value); @@ -63,7 +64,8 @@ version version_from_string( const std::string_view& value) NOEXCEPT { "1.4", version::v1_4 }, { "1.4.1", version::v1_4_1 }, { "1.4.2", version::v1_4_2 }, - { "1.6", version::v1_6 } + { "1.6", version::v1_6 }, + { "1.7", version::v1_7 } }; const auto it = map.find(value); diff --git a/src/protocols/protocol_electrum.cpp b/src/protocols/protocol_electrum.cpp index 41b27785..059d13ed 100644 --- a/src/protocols/protocol_electrum.cpp +++ b/src/protocols/protocol_electrum.cpp @@ -19,19 +19,23 @@ #include #include +#include #include #include #include #include +#include #include +// github.com/spesmilo/electrum-protocol/blob/master/docs/protocol-methods.rst + namespace libbitcoin { namespace server { #define CLASS protocol_electrum using namespace system; -using namespace interface; +using namespace network::rpc; using namespace std::placeholders; BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) @@ -64,6 +68,7 @@ void protocol_electrum::start() NOEXCEPT SUBSCRIBE_RPC(handle_blockchain_scripthash_subscribe, _1, _2, _3); SUBSCRIBE_RPC(handle_blockchain_scripthash_unsubscribe, _1, _2, _3); SUBSCRIBE_RPC(handle_blockchain_transaction_broadcast, _1, _2, _3); + SUBSCRIBE_RPC(handle_blockchain_transaction_broadcast_package, _1, _2, _3, _4); SUBSCRIBE_RPC(handle_blockchain_transaction_get, _1, _2, _3, _4); SUBSCRIBE_RPC(handle_blockchain_transaction_get_merkle, _1, _2, _3, _4); SUBSCRIBE_RPC(handle_blockchain_transaction_id_from_pos, _1, _2, _3, _4, _5); @@ -79,6 +84,7 @@ void protocol_electrum::start() NOEXCEPT // Mempool methods. SUBSCRIBE_RPC(handle_mempool_get_fee_histogram, _1, _2); + SUBSCRIBE_RPC(handle_mempool_get_info, _1, _2); protocol_rpc::start(); } @@ -122,21 +128,6 @@ bool protocol_electrum::handle_event(const code&, node::chase event_, return true; } -// Utility. -// ---------------------------------------------------------------------------- - -// TODO: centralize in server (also used in bitcoind and native interfaces). -template -std::string to_hex(const Object& object, size_t size, Args&&... args) NOEXCEPT -{ - std::string out(two * size, '\0'); - stream::out::fast sink{ out }; - write::base16::fast writer{ sink }; - object.to_data(writer, std::forward(args)...); - BC_ASSERT(writer); - return out; -} - // Handlers (blockchain). // ---------------------------------------------------------------------------- @@ -148,6 +139,12 @@ void protocol_electrum::handle_blockchain_block_header(const code& ec, if (stopped(ec)) return; + if (!at_least(electrum::version::v1_3)) + { + send_code(error::wrong_version); + return; + } + size_t starting{}; size_t waypoint{}; if (!to_integer(starting, height) || @@ -168,6 +165,12 @@ void protocol_electrum::handle_blockchain_block_headers(const code& ec, if (stopped(ec)) return; + if (!at_least(electrum::version::v1_2)) + { + send_code(error::wrong_version); + return; + } + size_t quantity{}; size_t waypoint{}; size_t starting{}; @@ -179,6 +182,12 @@ void protocol_electrum::handle_blockchain_block_headers(const code& ec, return; } + if (!is_zero(cp_height) && !at_least(electrum::version::v1_4)) + { + send_code(error::wrong_version); + return; + } + blockchain_block_headers(starting, quantity, waypoint, true); } @@ -217,41 +226,63 @@ void protocol_electrum::blockchain_block_headers(size_t starting, // Recommended to be at least one difficulty retarget period, e.g. 2016. // The maximum number of headers the server will return in single request. - const auto maximum = server_settings().electrum.maximum_headers; + const auto maximum_headers = server_settings().electrum.maximum_headers; // Returned headers are assured to be contiguous despite intervening reorg. // No headers may be returned, which implies start > confirmed top block. - const auto count = limit(quantity, maximum); + const auto count = limit(quantity, maximum_headers); const auto links = query.get_confirmed_headers(starting, count); - constexpr auto header_size = chain::header::serialized_size(); - auto size = two * header_size * links.size(); + auto size = two * chain::header::serialized_size() * links.size(); - // Fetch and serialize headers. - array_t headers{}; - headers.reserve(links.size()); - for (const auto& link: links) + value_t value{ object_t{} }; + auto& result = std::get(value.value()); + if (multiplicity) + { + result["max"] = maximum_headers; + result["count"] = links.size(); + } + else if (links.empty()) { - if (const auto header = query.get_header(link); header) - { - headers.push_back(to_hex(*header, header_size)); - continue; - } - send_code(error::server_error); return; - }; + } - value_t value{ object_t{} }; - auto& result = std::get(value.value()); - if (multiplicity) + if (at_least(electrum::version::v1_6)) { - result["max"] = maximum; - result["count"] = uint64_t{ headers.size() }; - result["headers"] = std::move(headers); + array_t headers{}; + headers.reserve(links.size()); + for (const auto& link: links) + { + const auto header = query.get_wire_header(link); + if (header.empty()) + { + send_code(error::server_error); + return; + } + + headers.push_back(encode_base16(header)); + }; + + if (multiplicity) + result["headers"] = std::move(headers); + else + result["header"] = std::move(headers.front()); } - else if (!headers.empty()) + else { - result["header"] = headers.front(); + std::string headers(size, '\0'); + stream::out::fast sink{ headers }; + write::base16::fast writer{ sink }; + for (const auto& link: links) + { + if (!query.get_wire_header(writer, link)) + { + send_code(error::server_error); + return; + } + }; + + result["hex"] = std::move(headers); } // There is a very slim chance of inconsistency given an intervening reorg @@ -286,6 +317,12 @@ void protocol_electrum::handle_blockchain_headers_subscribe(const code& ec, if (stopped(ec)) return; + if (!at_least(electrum::version::v1_0)) + { + send_code(error::wrong_version); + return; + } + const auto& query = archive(); const auto top = query.get_top_confirmed(); const auto link = query.to_confirmed(top); @@ -297,8 +334,8 @@ void protocol_electrum::handle_blockchain_headers_subscribe(const code& ec, return; } - const auto header = query.get_header(link); - if (!header) + const auto header = query.get_wire_header(link); + if (header.empty()) { send_code(error::server_error); return; @@ -309,8 +346,8 @@ void protocol_electrum::handle_blockchain_headers_subscribe(const code& ec, { object_t { - { "height", uint64_t{ top } }, - { "hex", to_hex(*header, chain::header::serialized_size()) } + { "height", top }, + { "hex", encode_base16(header) } } }, 256, BIND(complete, _1)); } @@ -322,9 +359,9 @@ void protocol_electrum::do_header(node::header_t link) NOEXCEPT const auto& query = archive(); const auto height = query.get_height(link); - const auto header = query.get_header(link); + const auto header = query.get_wire_header(link); - if (height.is_terminal() || !header) + if (height.is_terminal()) { LOGF("Electrum::do_header, object not found (" << link << ")."); return; @@ -335,20 +372,28 @@ void protocol_electrum::do_header(node::header_t link) NOEXCEPT object_t { { "height", height.value }, - { "hex", to_hex(*header, chain::header::serialized_size()) } + { "hex", encode_base16(header) } } }, 100, BIND(complete, _1)); } void protocol_electrum::handle_blockchain_estimate_fee(const code& ec, - rpc_interface::blockchain_estimate_fee, double number, + rpc_interface::blockchain_estimate_fee, double , const std::string& ) NOEXCEPT { if (stopped(ec)) return; - // TODO: mode argument added in 1.6. - send_result(number, 70, BIND(complete, _1)); + if (!at_least(electrum::version::v1_0)) + { + send_code(error::wrong_version); + return; + } + + ////const auto mode_enabled = at_least(electrum::version::v1_6); + + ////send_result(number, 70, BIND(complete, _1)); + send_code(error::not_implemented); } void protocol_electrum::handle_blockchain_relay_fee(const code& ec, @@ -357,7 +402,13 @@ void protocol_electrum::handle_blockchain_relay_fee(const code& ec, if (stopped(ec)) return; - // TODO: deprecated in 1.4.2, removed in 1.6. + if (!at_least(electrum::version::v1_0) || + at_least(electrum::version::v1_6)) + { + send_code(error::wrong_version); + return; + } + send_result(node_settings().minimum_fee_rate, 42, BIND(complete, _1)); } @@ -365,7 +416,15 @@ void protocol_electrum::handle_blockchain_scripthash_get_balance(const code& ec, rpc_interface::blockchain_scripthash_get_balance, const std::string& ) NOEXCEPT { - if (stopped(ec)) return; + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_1)) + { + send_code(error::wrong_version); + return; + } + send_code(error::not_implemented); } @@ -373,7 +432,15 @@ void protocol_electrum::handle_blockchain_scripthash_get_history(const code& ec, rpc_interface::blockchain_scripthash_get_history, const std::string& ) NOEXCEPT { - if (stopped(ec)) return; + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_1)) + { + send_code(error::wrong_version); + return; + } + send_code(error::not_implemented); } @@ -381,7 +448,17 @@ void protocol_electrum::handle_blockchain_scripthash_get_mempool(const code& ec, rpc_interface::blockchain_scripthash_get_mempool, const std::string& ) NOEXCEPT { - if (stopped(ec)) return; + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_1)) + { + send_code(error::wrong_version); + return; + } + + ////const auto sort = at_least(electrum::version::v1_6); + send_code(error::not_implemented); } @@ -389,7 +466,15 @@ void protocol_electrum::handle_blockchain_scripthash_list_unspent(const code& ec rpc_interface::blockchain_scripthash_list_unspent, const std::string& ) NOEXCEPT { - if (stopped(ec)) return; + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_1)) + { + send_code(error::wrong_version); + return; + } + send_code(error::not_implemented); } @@ -397,7 +482,15 @@ void protocol_electrum::handle_blockchain_scripthash_subscribe(const code& ec, rpc_interface::blockchain_scripthash_subscribe, const std::string& ) NOEXCEPT { - if (stopped(ec)) return; + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_1)) + { + send_code(error::wrong_version); + return; + } + send_code(error::not_implemented); } @@ -405,7 +498,15 @@ void protocol_electrum::handle_blockchain_scripthash_unsubscribe(const code& ec, rpc_interface::blockchain_scripthash_unsubscribe, const std::string& ) NOEXCEPT { - if (stopped(ec)) return; + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_4_2)) + { + send_code(error::wrong_version); + return; + } + send_code(error::not_implemented); } @@ -416,8 +517,16 @@ void protocol_electrum::handle_blockchain_transaction_broadcast(const code& ec, if (stopped(ec)) return; - // ElectrumX changed in version 1.1 to return error vs. bitcoind result. + if (!at_least(electrum::version::v1_0)) + { + send_code(error::wrong_version); + return; + } + + // TODO: implement error_object. + // Changed in version 1.1: return error vs. bitcoind result. // Previously it returned text string (bitcoind message) in the error case. + ////const auto error_object = at_least(electrum::version::v1_1); data_chunk tx_data{}; if (!decode_base16(tx_data, raw_tx)) @@ -446,6 +555,37 @@ void protocol_electrum::handle_blockchain_transaction_broadcast(const code& ec, send_result(encode_base16(tx->hash(false)), size, BIND(complete, _1)); } +void protocol_electrum::handle_blockchain_transaction_broadcast_package( + const code& ec, rpc_interface::blockchain_transaction_broadcast_package, + const std::string& raw_txs, bool ) NOEXCEPT +{ + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_6)) + { + send_code(error::wrong_version); + return; + } + + data_chunk txs_data{}; + if (!decode_base16(txs_data, raw_txs)) + { + send_code(error::invalid_argument); + return; + } + + // TODO: consider whether to support the lousy package p2p protocol. + constexpr auto confirmable = false; + if (!confirmable) + { + send_code(error::unconfirmable_transaction); + return; + } + + send_code(error::not_implemented); +} + void protocol_electrum::handle_blockchain_transaction_get(const code& ec, rpc_interface::blockchain_transaction_get, const std::string& tx_hash, bool verbose) NOEXCEPT @@ -453,8 +593,15 @@ void protocol_electrum::handle_blockchain_transaction_get(const code& ec, if (stopped(ec)) return; - // Changed in version 1.2: verbose argument added. - // Changed in version 1.1: ignored argument height removed. + // TODO: changed in version 1.1: ignored height argument removed. + // Requires additional same-name method implementation for v1.0. + // This implies and override to channel_rpc::dispatch(). + if ((!at_least(electrum::version::v1_0)) || + (!at_least(electrum::version::v1_2) && verbose)) + { + send_code(error::wrong_version); + return; + } hash_digest hash{}; if (!decode_hash(hash, tx_hash)) @@ -465,56 +612,68 @@ void protocol_electrum::handle_blockchain_transaction_get(const code& ec, const auto& query = archive(); const auto link = query.to_tx(hash); - const auto tx = query.get_transaction(link, true); - if (!tx) + if (link.is_terminal()) { send_code(error::not_found); return; } - const auto size = tx->serialized_size(true); - if (verbose) + if (!verbose) { - auto value = value_from(bitcoind(*tx)); - if (!value.is_object()) + const auto tx = query.get_wire_tx(link, true); + if (tx.empty()) { send_code(error::server_error); return; } - if (const auto header = query.to_strong(link); !header.is_terminal()) - { - using namespace system; - const auto top = query.get_top_confirmed(); - const auto height = query.get_height(header); - const auto block_hash = query.get_header_key(header); - - uint32_t timestamp{}; - if (height.is_terminal() || (block_hash == null_hash) || - !query.get_timestamp(timestamp, header)) - { - send_code(error::server_error); - return; - } - - // Floor manages race between getting confirmed top and height. - const auto confirmations = add1(floored_subtract(top, height.value)); + send_result(encode_base16(tx), two * tx.size(), BIND(complete, _1)); + return; + } - auto& transaction = value.as_object(); - transaction["in_active_chain"] = true; - transaction["blockhash"] = encode_hash(block_hash); - transaction["confirmations"] = confirmations; - transaction["blocktime"] = timestamp; - transaction["time"] = timestamp; - } + const auto tx = query.get_transaction(link, true); + if (!tx) + { + send_code(error::server_error); + return; + } - // Verbose means whatever bitcoind returns for getrawtransaction, lolz. - send_result(std::move(value), two * size, BIND(complete, _1)); + auto value = value_from(bitcoind(*tx)); + if (!value.is_object()) + { + send_code(error::server_error); + return; } - else + + if (const auto header = query.to_strong(link); !header.is_terminal()) { - send_result(to_hex(*tx, size, true), two * size, BIND(complete, _1)); + using namespace system; + const auto top = query.get_top_confirmed(); + const auto height = query.get_height(header); + const auto block_hash = query.get_header_key(header); + + uint32_t timestamp{}; + if (height.is_terminal() || (block_hash == null_hash) || + !query.get_timestamp(timestamp, header)) + { + send_code(error::server_error); + return; + } + + // Floor manages race between getting confirmed top and height. + const auto confirms = add1(floored_subtract(top, height.value)); + + auto& transaction = value.as_object(); + transaction["in_active_chain"] = true; + transaction["blockhash"] = encode_hash(block_hash); + transaction["confirmations"] = confirms; + transaction["blocktime"] = timestamp; + transaction["time"] = timestamp; } + + // Verbose means whatever bitcoind returns for getrawtransaction, lolz. + const auto size = tx->serialized_size(true); + send_result(std::move(value), two * size, BIND(complete, _1)); } void protocol_electrum::handle_blockchain_transaction_get_merkle(const code& ec, @@ -525,6 +684,12 @@ void protocol_electrum::handle_blockchain_transaction_get_merkle(const code& ec, if (stopped(ec)) return; + if (!at_least(electrum::version::v1_4)) + { + send_code(error::wrong_version); + return; + } + hash_digest hash{}; size_t block_height{}; if (!to_integer(block_height, height) || !decode_hash(hash, tx_hash)) @@ -581,6 +746,12 @@ void protocol_electrum::handle_blockchain_transaction_id_from_pos(const code& ec if (stopped(ec)) return; + if (!at_least(electrum::version::v1_4)) + { + send_code(error::wrong_version); + return; + } + size_t position{}; size_t block_height{}; if (!to_integer(block_height, height) || @@ -610,38 +781,37 @@ void protocol_electrum::handle_blockchain_transaction_id_from_pos(const code& ec if (!merkle) { send_result(encode_hash(hash), two * hash_size, BIND(complete, _1)); + return; } - else + + auto hashes = query.get_tx_keys(block_link); + if (hashes.empty()) { - auto hashes = query.get_tx_keys(block_link); - if (hashes.empty()) - { - send_code(error::server_error); - return; - } + send_code(error::server_error); + return; + } - if (position >= hashes.size()) - { - send_code(error::not_found); - return; - } + if (position >= hashes.size()) + { + send_code(error::not_found); + return; + } - using namespace chain; - const auto proof = block::merkle_branch(position, std::move(hashes)); + using namespace chain; + const auto proof = block::merkle_branch(position, std::move(hashes)); - array_t branch(proof.size()); - std::ranges::transform(proof, branch.begin(), - [](const auto& hash) NOEXCEPT { return encode_hash(hash); }); + array_t branch(proof.size()); + std::ranges::transform(proof, branch.begin(), + [](const auto& hash) NOEXCEPT { return encode_hash(hash); }); - send_result( + send_result( + { + object_t { - object_t - { - { "tx_hash", encode_hash(hash) }, - { "merkle", std::move(branch) } - } - }, two * hash_size * add1(branch.size()), BIND(complete, _1)); - } + { "tx_hash", encode_hash(hash) }, + { "merkle", std::move(branch) } + } + }, two * hash_size * add1(branch.size()), BIND(complete, _1)); } // Handlers (server). @@ -650,7 +820,15 @@ void protocol_electrum::handle_blockchain_transaction_id_from_pos(const code& ec void protocol_electrum::handle_server_add_peer(const code& ec, rpc_interface::server_add_peer, const interface::object_t& ) NOEXCEPT { - if (stopped(ec)) return; + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_1)) + { + send_code(error::wrong_version); + return; + } + send_code(error::not_implemented); } @@ -660,6 +838,12 @@ void protocol_electrum::handle_server_banner(const code& ec, if (stopped(ec)) return; + if (!at_least(electrum::version::v1_0)) + { + send_code(error::wrong_version); + return; + } + send_result(options().banner_message, 42, BIND(complete, _1)); } @@ -669,21 +853,68 @@ void protocol_electrum::handle_server_donation_address(const code& ec, if (stopped(ec)) return; + if (!at_least(electrum::version::v1_0)) + { + send_code(error::wrong_version); + return; + } + send_result(options().donation_address, 42, BIND(complete, _1)); } void protocol_electrum::handle_server_features(const code& ec, rpc_interface::server_features) NOEXCEPT { - if (stopped(ec)) return; - send_code(error::not_implemented); + using namespace system; + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_0)) + { + send_code(error::wrong_version); + return; + } + + const auto& query = archive(); + const auto genesis = query.to_confirmed(zero); + if (genesis.is_terminal()) + { + send_code(error::not_found); + return; + } + + const auto hash = query.get_header_key(genesis); + if (hash == null_hash) + { + send_code(error::server_error); + return; + } + + send_result(object_t + { + { "genesis_hash", encode_hash(hash) }, + { "hosts", advertised_hosts() }, + { "hash_function", "sha256" }, + { "server_version", options().server_name }, + { "protocol_min", string_t{ version_to_string(minimum) } }, + { "protocol_max", string_t{ version_to_string(maximum) } }, + { "pruning", null_t{} } + }, 1024, BIND(complete, _1)); } // This is not actually a subscription method. void protocol_electrum::handle_server_peers_subscribe(const code& ec, rpc_interface::server_peers_subscribe) NOEXCEPT { - if (stopped(ec)) return; + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_0)) + { + send_code(error::wrong_version); + return; + } + send_code(error::not_implemented); } @@ -693,6 +924,12 @@ void protocol_electrum::handle_server_ping(const code& ec, if (stopped(ec)) return; + if (!at_least(electrum::version::v1_2)) + { + send_code(error::wrong_version); + return; + } + // Any receive, including ping, resets the base channel inactivity timer. send_result(null_t{}, 42, BIND(complete, _1)); } @@ -706,16 +943,59 @@ void protocol_electrum::handle_mempool_get_fee_histogram(const code& ec, if (stopped(ec)) return; + if (!at_least(electrum::version::v1_2)) + { + send_code(error::wrong_version); + return; + } + // TODO: requires tx pool metadata graph. - send_result(value_t + send_code(error::not_implemented); +} + +void protocol_electrum::handle_mempool_get_info(const code& ec, + rpc_interface::mempool_get_info) NOEXCEPT +{ + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_0)) { - array_t - { - array_t{ 1, 1024 }, - array_t{ 2, 2048 }, - array_t{ 4, 4096 } - } - }, 256, BIND(complete, _1)); + send_code(error::wrong_version); + return; + } + + // TODO: requires tx pool metadata graph. + send_code(error::not_implemented); +} + +// utilities +// ---------------------------------------------------------------------------- + +// One of each type allowed for given host, last writer wins if more than one. +object_t protocol_electrum::advertised_hosts() const NOEXCEPT +{ + std::map map{}; + + for (const auto& bind: options().advertise_binds) + if (!bind.host().empty()) + map[bind.host()]["tcp_port"] = bind.port(); + + for (const auto& safe: options().advertise_safes) + if (!safe.host().empty()) + map[safe.host()]["ssl_port"] = safe.port(); + + object_t hosts{}; + for (const auto& [host, object]: map) + hosts[host] = object; + + if (hosts.empty()) return + { + { "tcp_port", null_t{} }, + { "ssl_port", null_t{} } + }; + + return hosts; } BC_POP_WARNING() diff --git a/src/protocols/protocol_electrum_version.cpp b/src/protocols/protocol_electrum_version.cpp index cf1553b5..a63f5fd7 100644 --- a/src/protocols/protocol_electrum_version.cpp +++ b/src/protocols/protocol_electrum_version.cpp @@ -79,7 +79,7 @@ void protocol_electrum_version::finished(const code& ec, // Handler. // ---------------------------------------------------------------------------- -// Changed in version 1.6: server must tolerate and ignore extraneous args. +// TODO: Changed in version 1.6: server must tolerate and ignore extra args. void protocol_electrum_version::handle_server_version(const code& ec, rpc_interface::server_version, const std::string& client_name, const value_t& protocol_version) NOEXCEPT @@ -97,13 +97,13 @@ void protocol_electrum_version::handle_server_version(const code& ec, else { send_result(value_t + { + array_t { - array_t - { - { string_t{ server_name() } }, - { string_t{ negotiated_version() } } - } - }, 70, BIND(finished, _1, error::success)); + { string_t{ server_name() } }, + { string_t{ negotiated_version() } } + } + }, 70, BIND(finished, _1, error::success)); } // Handshake must leave channel paused, before leaving stranded handler. @@ -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 = maximum; return true; } diff --git a/test/parsers/electrum_version.cpp b/test/parsers/electrum_version.cpp index 74091357..4ffb4373 100644 --- a/test/parsers/electrum_version.cpp +++ b/test/parsers/electrum_version.cpp @@ -37,6 +37,7 @@ BOOST_AUTO_TEST_CASE(electrum_version__version_to_string__all__expected) 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_REQUIRE_EQUAL(version_to_string(version::v1_7), "1.7"); } BOOST_AUTO_TEST_CASE(electrum_version__version_from_string__all__expected) @@ -54,6 +55,7 @@ BOOST_AUTO_TEST_CASE(electrum_version__version_from_string__all__expected) 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_REQUIRE(version_from_string("1.7") == version::v1_7); } BOOST_AUTO_TEST_SUITE_END() diff --git a/test/protocols/electrum/electrum.cpp b/test/protocols/electrum/electrum.cpp index 5b284598..b6025780 100644 --- a/test/protocols/electrum/electrum.cpp +++ b/test/protocols/electrum/electrum.cpp @@ -104,23 +104,30 @@ boost::json::value electrum_setup_fixture::get(const std::string& request) return boost::json::parse(response); } -bool electrum_setup_fixture::handshake(const std::string& version, +bool electrum_setup_fixture::handshake(electrum::version 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; + ) % id % name % electrum::version_to_string(version); const auto response = get(request.str()); - if (!response.at("id").is_int64() || response.at("id").as_int64() != id || - !response.at("result").is_array()) - return false; + try + { + if (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_.server.electrum.server_name) && + (result.at(1).as_string() == electrum::version_to_string(version)); - // Assumes server always accepts proposed version. - const auto& result = response.at("result").as_array(); - return (result.size() == two) && - (result.at(0).is_string() && result.at(1).is_string()) && - (result.at(0).as_string() == config_.server.electrum.server_name) && - (result.at(1).as_string() == version); + } + catch (...) + { + return false; + } } diff --git a/test/protocols/electrum/electrum.hpp b/test/protocols/electrum/electrum.hpp index 0a80b644..f984f21d 100644 --- a/test/protocols/electrum/electrum.hpp +++ b/test/protocols/electrum/electrum.hpp @@ -32,7 +32,7 @@ struct electrum_setup_fixture ~electrum_setup_fixture(); boost::json::value get(const std::string& request); - bool handshake(const std::string& version="1.4", + bool handshake(electrum::version version, const std::string& name="test", network::rpc::code_t id=0); protected: diff --git a/test/protocols/electrum/electrum_fees.cpp b/test/protocols/electrum/electrum_fees.cpp index c76a91f7..2ad93d2a 100644 --- a/test/protocols/electrum/electrum_fees.cpp +++ b/test/protocols/electrum/electrum_fees.cpp @@ -27,13 +27,14 @@ BOOST_FIXTURE_TEST_SUITE(electrum_tests, electrum_setup_fixture) BOOST_AUTO_TEST_CASE(electrum__blockchain_relay_fee__default__expected) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_2)); constexpr auto expected = 99.0; const auto response = get(R"({"id":90,"method":"blockchain.relayfee","params":[]})" "\n"); - BOOST_CHECK_EQUAL(response.at("id").as_int64(), 90); - BOOST_CHECK(response.at("result").is_number()); - BOOST_CHECK_EQUAL(response.at("result").as_double(), expected); + REQUIRE_NO_THROW_TRUE(response.at("id").is_int64()); + REQUIRE_NO_THROW_TRUE(response.at("result").is_double()); + BOOST_REQUIRE_EQUAL(response.at("id").as_int64(), 90); + BOOST_REQUIRE_EQUAL(response.at("result").as_double(), expected); } // get_fee_histogram diff --git a/test/protocols/electrum/electrum_headers.cpp b/test/protocols/electrum/electrum_headers.cpp index 3247c379..f47e406b 100644 --- a/test/protocols/electrum/electrum_headers.cpp +++ b/test/protocols/electrum/electrum_headers.cpp @@ -23,39 +23,55 @@ BOOST_FIXTURE_TEST_SUITE(electrum_tests, electrum_setup_fixture) using namespace system; static const code not_found{ server::error::not_found }; +static const code wrong_version{ server::error::wrong_version }; static const code target_overflow{ server::error::target_overflow }; static const code invalid_argument{ server::error::invalid_argument }; // blockchain.block.header +BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__insufficient_version__wrong_version) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_2)); + + const auto response = get(R"({"id":43,"method":"blockchain.block.header","params":[0]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), wrong_version.value()); +} + BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__genesis_no_checkpoint__expected_no_proof) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_4)); 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)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + + // "hex" prior to v1.6 + const auto& result = response.at("result").as_object(); + REQUIRE_NO_THROW_TRUE(result.at("hex").is_string()); + BOOST_REQUIRE_EQUAL(result.at("hex").as_string(), encode_base16(header0_data)); } BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__block1_no_checkpoint__expected_no_proof) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_6)); 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)); + REQUIRE_NO_THROW_TRUE(response.at("result").as_object().at("header").is_string()); + BOOST_REQUIRE_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()); + BOOST_REQUIRE(handshake(electrum::version::v1_6)); 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)); + REQUIRE_NO_THROW_TRUE(response.at("result").as_object().at("header").is_string()); + BOOST_REQUIRE_EQUAL(response.at("result").as_object().at("header").as_string(), encode_base16(header0_data)); } BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__proof_self_block1__expected) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_6)); const auto expected_header = encode_base16(header1_data); const auto expected_root = encode_hash(merkle_root( { @@ -64,18 +80,24 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__proof_self_block1__expec })); const auto response = get(R"({"id":46,"method":"blockchain.block.header","params":[1,1]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + 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); + REQUIRE_NO_THROW_TRUE(result.at("header").is_string()); + REQUIRE_NO_THROW_TRUE(result.at("root").is_string()); + REQUIRE_NO_THROW_TRUE(result.at("branch").is_array()); + BOOST_REQUIRE_EQUAL(result.at("header").as_string(), expected_header); + BOOST_REQUIRE_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_REQUIRE(branch.at(0).is_string()); + BOOST_REQUIRE_EQUAL(branch.size(), 1u); + BOOST_REQUIRE_EQUAL(branch.at(0).as_string(), encode_hash(block0_hash)); } BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__proof_example__expected) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_6)); const auto expected_root = encode_hash(merkle_root( { @@ -99,122 +121,174 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__proof_example__expected) }; const auto response = get(R"({"id":50,"method":"blockchain.block.header","params":[5,8]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + 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); + REQUIRE_NO_THROW_TRUE(result.at("header").is_string()); + REQUIRE_NO_THROW_TRUE(result.at("root").is_string()); + REQUIRE_NO_THROW_TRUE(result.at("branch").is_array()); + BOOST_REQUIRE_EQUAL(result.at("header").as_string(), encode_base16(header5_data)); + BOOST_REQUIRE_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_REQUIRE(branch.at(0).is_string()); + BOOST_REQUIRE_EQUAL(branch.size(), expected_branch.size()); + BOOST_REQUIRE_EQUAL(branch.at(0).as_string(), expected_branch[0]); + BOOST_REQUIRE_EQUAL(branch.at(1).as_string(), expected_branch[1]); + BOOST_REQUIRE_EQUAL(branch.at(2).as_string(), expected_branch[2]); + BOOST_REQUIRE_EQUAL(branch.at(3).as_string(), expected_branch[3]); } BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__checkpoint_below_height__target_overflow) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_6)); 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()); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_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()); + BOOST_REQUIRE(handshake(electrum::version::v1_6)); 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()); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value()); } BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__checkpoint_above_top__not_found) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_6)); 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()); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_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()); + BOOST_REQUIRE(handshake(electrum::version::v1_6)); 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()); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_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()); + BOOST_REQUIRE(handshake(electrum::version::v1_6)); 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()); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_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()); + BOOST_REQUIRE(handshake(electrum::version::v1_6)); 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()); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value()); } // blockchain.block.headers -BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__genesis_count1_no_checkpoint__expected) +BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__insufficient_version__wrong_version) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_1)); + + const auto response = get(R"({"id":60,"method":"blockchain.block.headers","params":[0,1]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), wrong_version.value()); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__genesis_count1_no_checkpoint_v_1_2__expected) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_2)); const auto response = get(R"({"id":60,"method":"blockchain.block.headers","params":[0,1]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + const auto& result = response.at("result").as_object(); - BOOST_CHECK_EQUAL(result.at("max").as_int64(), 5); - BOOST_CHECK_EQUAL(result.at("count").as_int64(), 1); - BOOST_CHECK(result.at("headers").is_array()); - BOOST_CHECK_EQUAL(result.at("headers").as_array().size(), 1u); - BOOST_CHECK_EQUAL(result.at("headers").as_array().at(0).as_string(), encode_base16(header0_data)); + REQUIRE_NO_THROW_TRUE(result.at("max").is_int64()); + REQUIRE_NO_THROW_TRUE(result.at("count").is_int64()); + BOOST_REQUIRE_EQUAL(result.at("max").as_int64(), 5); + BOOST_REQUIRE_EQUAL(result.at("count").as_int64(), 1); + + // "hex" prior to 1.6 + REQUIRE_NO_THROW_TRUE(result.at("hex").is_string()); + BOOST_REQUIRE_EQUAL(result.at("hex").as_string(), encode_base16(header0_data)); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__genesis_count1_no_checkpoint_v1_6__expected) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_6)); + + const auto response = get(R"({"id":60,"method":"blockchain.block.headers","params":[0,1]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + + const auto& result = response.at("result").as_object(); + REQUIRE_NO_THROW_TRUE(result.at("max").is_int64()); + REQUIRE_NO_THROW_TRUE(result.at("count").is_int64()); + BOOST_REQUIRE_EQUAL(result.at("max").as_int64(), 5); + BOOST_REQUIRE_EQUAL(result.at("count").as_int64(), 1); + REQUIRE_NO_THROW_TRUE(result.at("headers").is_array()); + BOOST_REQUIRE(result.at("headers").as_array().at(0).is_string()); + BOOST_REQUIRE_EQUAL(result.at("headers").as_array().size(), 1u); + BOOST_REQUIRE_EQUAL(result.at("headers").as_array().at(0).as_string(), encode_base16(header0_data)); } BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__block1to3_no_checkpoint__expected) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_6)); const auto response = get(R"({"id":61,"method":"blockchain.block.headers","params":[1,3]})" "\n"); const auto& result = response.at("result").as_object(); - BOOST_CHECK_EQUAL(result.at("max").as_int64(), 5); - BOOST_CHECK_EQUAL(result.at("count").as_int64(), 3); - BOOST_CHECK(result.at("headers").is_array()); + REQUIRE_NO_THROW_TRUE(result.at("max").is_int64()); + REQUIRE_NO_THROW_TRUE(result.at("count").is_int64()); + BOOST_REQUIRE_EQUAL(result.at("max").as_int64(), 5); + BOOST_REQUIRE_EQUAL(result.at("count").as_int64(), 3); + REQUIRE_NO_THROW_TRUE(result.at("headers").is_array()); const auto& headers = result.at("headers").as_array(); - BOOST_CHECK_EQUAL(headers.size(), 3u); - BOOST_CHECK_EQUAL(headers.at(0).as_string(), encode_base16(header1_data)); - BOOST_CHECK_EQUAL(headers.at(1).as_string(), encode_base16(header2_data)); - BOOST_CHECK_EQUAL(headers.at(2).as_string(), encode_base16(header3_data)); + BOOST_REQUIRE(result.at("headers").as_array().at(0).is_string()); + BOOST_REQUIRE_EQUAL(headers.size(), 3u); + BOOST_REQUIRE_EQUAL(headers.at(0).as_string(), encode_base16(header1_data)); + BOOST_REQUIRE_EQUAL(headers.at(1).as_string(), encode_base16(header2_data)); + BOOST_REQUIRE_EQUAL(headers.at(2).as_string(), encode_base16(header3_data)); } BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__count_exceeds_max__capped) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_6)); const auto response = get(R"({"id":62,"method":"blockchain.block.headers","params":[0,10]})" "\n"); const auto& result = response.at("result").as_object(); - BOOST_CHECK_EQUAL(result.at("max").as_int64(), 5); - BOOST_CHECK_EQUAL(result.at("count").as_int64(), 5); - BOOST_CHECK_EQUAL(result.at("headers").as_array().size(), 5u); + REQUIRE_NO_THROW_TRUE(result.at("max").is_int64()); + REQUIRE_NO_THROW_TRUE(result.at("count").is_int64()); + REQUIRE_NO_THROW_TRUE(result.at("headers").is_array()); + BOOST_REQUIRE_EQUAL(result.at("max").as_int64(), 5); + BOOST_REQUIRE_EQUAL(result.at("count").as_int64(), 5); + BOOST_REQUIRE_EQUAL(result.at("headers").as_array().size(), 5u); } BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__count_zero__empty_headers) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_6)); const auto response = get(R"({"id":63,"method":"blockchain.block.headers","params":[5,0]})" "\n"); const auto& result = response.at("result").as_object(); - BOOST_CHECK_EQUAL(result.at("count").as_int64(), 0); - BOOST_CHECK(result.at("headers").as_array().empty()); + REQUIRE_NO_THROW_TRUE(result.at("count").is_int64()); + REQUIRE_NO_THROW_TRUE(result.at("headers").is_array()); + BOOST_REQUIRE_EQUAL(result.at("count").as_int64(), 0); + BOOST_REQUIRE(result.at("headers").as_array().empty()); } BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__proof_no_offset__expected) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_6)); const auto expected_root = encode_hash(merkle_root( { @@ -238,23 +312,29 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__proof_no_offset__expect }; const auto response = get(R"({"id":64,"method":"blockchain.block.headers","params":[5,1,8]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + const auto& result = response.at("result").as_object(); - BOOST_CHECK_EQUAL(result.at("max").as_int64(), 5); - BOOST_CHECK_EQUAL(result.at("count").as_int64(), 1); - BOOST_CHECK_EQUAL(result.at("headers").as_array().size(), 1u); - BOOST_CHECK_EQUAL(result.at("root").as_string(), expected_root); + REQUIRE_NO_THROW_TRUE(result.at("max").is_int64()); + REQUIRE_NO_THROW_TRUE(result.at("count").is_int64()); + REQUIRE_NO_THROW_TRUE(result.at("headers").is_array()); + BOOST_REQUIRE_EQUAL(result.at("max").as_int64(), 5); + BOOST_REQUIRE_EQUAL(result.at("count").as_int64(), 1); + BOOST_REQUIRE_EQUAL(result.at("headers").as_array().size(), 1u); + BOOST_REQUIRE_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_REQUIRE(branch.at(0).is_string()); + BOOST_REQUIRE_EQUAL(branch.size(), expected_branch.size()); + BOOST_REQUIRE_EQUAL(branch.at(0).as_string(), expected_branch[0]); + BOOST_REQUIRE_EQUAL(branch.at(1).as_string(), expected_branch[1]); + BOOST_REQUIRE_EQUAL(branch.at(2).as_string(), expected_branch[2]); + BOOST_REQUIRE_EQUAL(branch.at(3).as_string(), expected_branch[3]); } BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__proof_offset__expected) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_6)); const auto expected_root = encode_hash(merkle_root( { @@ -278,108 +358,132 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__proof_offset__expected) }; const auto response = get(R"({"id":64,"method":"blockchain.block.headers","params":[5,3,8]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + const auto& result = response.at("result").as_object(); - BOOST_CHECK_EQUAL(result.at("max").as_int64(), 5); - BOOST_CHECK_EQUAL(result.at("count").as_int64(), 3); - BOOST_CHECK_EQUAL(result.at("headers").as_array().size(), 3u); - BOOST_CHECK_EQUAL(result.at("root").as_string(), expected_root); + REQUIRE_NO_THROW_TRUE(result.at("max").is_int64()); + REQUIRE_NO_THROW_TRUE(result.at("count").is_int64()); + REQUIRE_NO_THROW_TRUE(result.at("headers").is_array()); + REQUIRE_NO_THROW_TRUE(result.at("root").is_string()); + BOOST_REQUIRE_EQUAL(result.at("max").as_int64(), 5); + BOOST_REQUIRE_EQUAL(result.at("count").as_int64(), 3); + BOOST_REQUIRE_EQUAL(result.at("headers").as_array().size(), 3u); + BOOST_REQUIRE_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_REQUIRE(branch.at(0).is_string()); + BOOST_REQUIRE_EQUAL(branch.size(), expected_branch.size()); + BOOST_REQUIRE_EQUAL(branch.at(0).as_string(), expected_branch[0]); + BOOST_REQUIRE_EQUAL(branch.at(1).as_string(), expected_branch[1]); + BOOST_REQUIRE_EQUAL(branch.at(2).as_string(), expected_branch[2]); + BOOST_REQUIRE_EQUAL(branch.at(3).as_string(), expected_branch[3]); } BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__start_above_top__not_found) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_6)); const auto response = get(R"({"id":65,"method":"blockchain.block.headers","params":[10,1]})" "\n"); - BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value()); } BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__target_exceeds_waypoint__target_overflow) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_6)); const auto response = get(R"({"id":66,"method":"blockchain.block.headers","params":[2,3,1]})" "\n"); - BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), target_overflow.value()); + REQUIRE_NO_THROW_TRUE(response.at("error").is_object()); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), target_overflow.value()); } BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__waypoint_above_top__not_found) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_6)); const auto response = get(R"({"id":67,"method":"blockchain.block.headers","params":[0,1,10]})" "\n"); - BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value()); + REQUIRE_NO_THROW_TRUE(response.at("error").is_object()); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value()); } BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__negative_start__invalid_argument) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_6)); const auto response = get(R"({"id":68,"method":"blockchain.block.headers","params":[-1,1]})" "\n"); - BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); + REQUIRE_NO_THROW_TRUE(response.at("error").is_object()); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); } BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__fractional_count__invalid_argument) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_6)); const auto response = get(R"({"id":69,"method":"blockchain.block.headers","params":[0,1.5]})" "\n"); - BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); + REQUIRE_NO_THROW_TRUE(response.at("error").is_object()); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); } BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__start_plus_count_huge__not_found) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_6)); // argument_overflow is not actually reachable via json due to its integer limits. const auto response = get(R"({"id":70,"method":"blockchain.block.headers","params":[9007199254740991,2]})" "\n"); - BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value()); + REQUIRE_NO_THROW_TRUE(response.at("error").is_object()); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value()); } // blockchain.headers.subscribe BOOST_AUTO_TEST_CASE(electrum__blockchain_headers_subscribe__default__expected) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_6)); const auto response = get(R"({"id":80,"method":"blockchain.headers.subscribe","params":[]})" "\n"); const auto& result = response.at("result").as_object(); - BOOST_CHECK_EQUAL(result.at("height").as_int64(), 9); - BOOST_CHECK_EQUAL(result.at("hex").as_string(), system::encode_base16(header9_data)); + REQUIRE_NO_THROW_TRUE(result.at("height").is_int64()); + REQUIRE_NO_THROW_TRUE(result.at("hex").is_string()); + BOOST_REQUIRE_EQUAL(result.at("height").as_int64(), 9); + BOOST_REQUIRE_EQUAL(result.at("hex").as_string(), system::encode_base16(header9_data)); } BOOST_AUTO_TEST_CASE(electrum__blockchain_headers_subscribe__jsonrpc_2__expected) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_6)); const auto response = get(R"({"jsonrpc":"2.0","id":81,"method":"blockchain.headers.subscribe"})" "\n"); const auto& result = response.at("result").as_object(); - BOOST_CHECK_EQUAL(result.at("height").as_int64(), 9); - BOOST_CHECK_EQUAL(result.at("hex").as_string(), system::encode_base16(header9_data)); + REQUIRE_NO_THROW_TRUE(result.at("height").is_int64()); + REQUIRE_NO_THROW_TRUE(result.at("hex").is_string()); + BOOST_REQUIRE_EQUAL(result.at("height").as_int64(), 9); + BOOST_REQUIRE_EQUAL(result.at("hex").as_string(), system::encode_base16(header9_data)); } BOOST_AUTO_TEST_CASE(electrum__blockchain_headers_subscribe__id_preserved__expected) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_6)); const auto response = get(R"({"id":123,"method":"blockchain.headers.subscribe","params":[]})" "\n"); - BOOST_CHECK_EQUAL(response.at("id").as_int64(), 123); - BOOST_CHECK(response.at("result").is_object()); + REQUIRE_NO_THROW_TRUE(response.at("id").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("id").as_int64(), 123); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); } BOOST_AUTO_TEST_CASE(electrum__blockchain_headers_subscribe__empty_params__expected) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_6)); const auto response = get(R"({"id":82,"method":"blockchain.headers.subscribe","params":[]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + const auto& result = response.at("result").as_object(); - BOOST_CHECK_EQUAL(result.at("height").as_int64(), 9); - BOOST_CHECK_EQUAL(result.at("hex").as_string(), system::encode_base16(header9_data)); + BOOST_REQUIRE_EQUAL(result.at("height").as_int64(), 9); + BOOST_REQUIRE_EQUAL(result.at("hex").as_string(), system::encode_base16(header9_data)); } BOOST_AUTO_TEST_SUITE_END() diff --git a/test/protocols/electrum/electrum_server.cpp b/test/protocols/electrum/electrum_server.cpp index 95302397..e1f40fff 100644 --- a/test/protocols/electrum/electrum_server.cpp +++ b/test/protocols/electrum/electrum_server.cpp @@ -32,62 +32,67 @@ static const code invalid_argument{ server::error::invalid_argument }; BOOST_AUTO_TEST_CASE(electrum__server_banner__jsonrpc_unspecified_no_aparams__dropped) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_2)); // 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()); + REQUIRE_NO_THROW_TRUE(response.at("dropped").as_bool()); } BOOST_AUTO_TEST_CASE(electrum__server_banner__jsonrpc_unspecified_named_params__dropped) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_2)); // 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()); + REQUIRE_NO_THROW_TRUE(response.at("dropped").as_bool()); } BOOST_AUTO_TEST_CASE(electrum__server_banner__jsonrpc_unspecified_empty_params__expected) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_2)); const auto response = get(R"({"id":42,"method":"server.banner","params":[]})" "\n"); - BOOST_CHECK_EQUAL(response.at("result").as_string(), "banner_message"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); + BOOST_REQUIRE_EQUAL(response.at("result").as_string(), "banner_message"); } BOOST_AUTO_TEST_CASE(electrum__server_banner__jsonrpc_1__expected) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_2)); 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"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); + BOOST_REQUIRE_EQUAL(response.at("result").as_string(), "banner_message"); } BOOST_AUTO_TEST_CASE(electrum__server_banner__jsonrpc_2__expected) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_2)); const auto response = get(R"({"jsonrpc":"2.0","id":42,"method":"server.banner"})" "\n"); - BOOST_CHECK_EQUAL(response.at("result").as_string(), "banner_message"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); + BOOST_REQUIRE_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()); + BOOST_REQUIRE(handshake(electrum::version::v1_2)); 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"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); + BOOST_REQUIRE_EQUAL(response.at("result").as_string(), "donation_address"); } BOOST_AUTO_TEST_CASE(electrum__server_donation_address__jsonrpc_2__expected) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_2)); 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"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); + BOOST_REQUIRE_EQUAL(response.at("result").as_string(), "donation_address"); } // server.features @@ -97,26 +102,26 @@ BOOST_AUTO_TEST_CASE(electrum__server_donation_address__jsonrpc_2__expected) BOOST_AUTO_TEST_CASE(electrum__server_ping__null) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_2)); const auto response = get(R"({"id":200,"method":"server.ping","params":[]})" "\n"); - BOOST_CHECK(response.at("result").is_null()); + REQUIRE_NO_THROW_TRUE(response.at("result").is_null()); } -BOOST_AUTO_TEST_CASE(electrum__server_ping__jsonrpc_unspecified_no_aparams__dropped) +BOOST_AUTO_TEST_CASE(electrum__server_ping__jsonrpc_unspecified_no_params__dropped) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_2)); const auto response = get(R"({"id":201,"method":"server.ping"})" "\n"); - BOOST_CHECK(response.at("dropped").as_bool()); + REQUIRE_NO_THROW_TRUE(response.at("dropped").as_bool()); } BOOST_AUTO_TEST_CASE(electrum__server_ping__extra_param__dropped) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_2)); const auto response = get(R"({"id":202,"method":"server.ping","params":["extra"]})" "\n"); - BOOST_CHECK(response.at("dropped").as_bool()); + REQUIRE_NO_THROW_TRUE(response.at("dropped").as_bool()); } BOOST_AUTO_TEST_SUITE_END() diff --git a/test/protocols/electrum/electrum_server_version.cpp b/test/protocols/electrum/electrum_server_version.cpp index 3c534580..4ce993c2 100644 --- a/test/protocols/electrum/electrum_server_version.cpp +++ b/test/protocols/electrum/electrum_server_version.cpp @@ -28,114 +28,130 @@ static const code invalid_argument{ error::invalid_argument }; BOOST_AUTO_TEST_CASE(electrum__server_version__default__expected) { const auto response = get(R"({"id":0,"method":"server.version","params":["foobar"]})" "\n"); - BOOST_CHECK_EQUAL(response.at("id").as_int64(), 0); - BOOST_CHECK(response.at("result").is_array()); + REQUIRE_NO_THROW_TRUE(response.at("id").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("id").as_int64(), 0); + REQUIRE_NO_THROW_TRUE(response.at("result").is_array()); const auto& result = response.at("result").as_array(); - BOOST_CHECK_EQUAL(result.size(), 2u); - BOOST_CHECK(result.at(0).is_string()); - BOOST_CHECK(result.at(1).is_string()); - BOOST_CHECK_EQUAL(result.at(0).as_string(), "server_name"); - BOOST_CHECK_EQUAL(result.at(1).as_string(), "1.4"); + BOOST_REQUIRE_EQUAL(result.size(), 2u); + BOOST_REQUIRE(result.at(0).is_string()); + BOOST_REQUIRE_EQUAL(result.at(0).as_string(), "server_name"); + BOOST_REQUIRE_EQUAL(result.at(1).as_string(), "1.6"); } BOOST_AUTO_TEST_CASE(electrum__server_version__minimum__expected) { - const auto response = get(R"({"id":42,"method":"server.version","params":["foobar","1.4"]})" "\n"); - BOOST_CHECK_EQUAL(response.at("id").as_int64(), 42); - BOOST_CHECK(response.at("result").is_array()); + const auto response = get(R"({"id":42,"method":"server.version","params":["foobar","1.0"]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("id").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("id").as_int64(), 42); + REQUIRE_NO_THROW_TRUE(response.at("result").is_array()); const auto& result = response.at("result").as_array(); - BOOST_CHECK_EQUAL(result.size(), 2u); - BOOST_CHECK(result.at(0).is_string()); - BOOST_CHECK(result.at(1).is_string()); - BOOST_CHECK_EQUAL(result.at(0).as_string(), "server_name"); - BOOST_CHECK_EQUAL(result.at(1).as_string(), "1.4"); + BOOST_REQUIRE_EQUAL(result.size(), 2u); + BOOST_REQUIRE(result.at(0).is_string()); + BOOST_REQUIRE_EQUAL(result.at(0).as_string(), "server_name"); + BOOST_REQUIRE_EQUAL(result.at(1).as_string(), "1.0"); } BOOST_AUTO_TEST_CASE(electrum__server_version__maximum__expected) { - const auto response = get(R"({"id":42,"method":"server.version","params":["foobar","1.4.2"]})" "\n"); - BOOST_CHECK_EQUAL(response.at("id").as_int64(), 42); - BOOST_CHECK(response.at("result").is_array()); + const auto response = get(R"({"id":42,"method":"server.version","params":["foobar","1.6"]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("id").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("id").as_int64(), 42); + REQUIRE_NO_THROW_TRUE(response.at("result").is_array()); const auto& result = response.at("result").as_array(); - BOOST_CHECK_EQUAL(result.size(), 2u); - BOOST_CHECK(result.at(0).is_string()); - BOOST_CHECK(result.at(1).is_string()); - BOOST_CHECK_EQUAL(result.at(0).as_string(), "server_name"); - BOOST_CHECK_EQUAL(result.at(1).as_string(), "1.4.2"); + BOOST_REQUIRE_EQUAL(result.size(), 2u); + BOOST_REQUIRE(result.at(0).is_string()); + BOOST_REQUIRE_EQUAL(result.at(0).as_string(), "server_name"); + BOOST_REQUIRE_EQUAL(result.at(1).as_string(), "1.6"); } BOOST_AUTO_TEST_CASE(electrum__server_version__valid_range__expected) { - const auto response = get(R"({"id":42,"method":"server.version","params":["foobar",["1.4","1.4.2"]]})" "\n"); - BOOST_CHECK_EQUAL(response.at("result").as_array().at(1).as_string(), "1.4.2"); + const auto response = get(R"({"id":42,"method":"server.version","params":["foobar",["1.0","1.6"]]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_array()); + BOOST_REQUIRE_EQUAL(response.at("result").as_array().at(1).as_string(), "1.6"); } BOOST_AUTO_TEST_CASE(electrum__server_version__invalid__invalid_argument) { const auto response = get(R"({"id":42,"method":"server.version","params":["foobar","42"]})" "\n"); - BOOST_CHECK_EQUAL(response.at("id").as_int64(), 42); - BOOST_CHECK(response.at("error").is_object()); - BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); + REQUIRE_NO_THROW_TRUE(response.at("id").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("id").as_int64(), 42); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + 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_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); + const auto response = get(R"({"id":42,"method":"server.version","params":["foobar","0.0"]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); } BOOST_AUTO_TEST_CASE(electrum__server_version__array_min_exceeds_max__invalid_argument) { const auto response = get(R"({"id":42,"method":"server.version","params":["foobar",["1.4.2","1.4"]]})" "\n"); - BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + 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_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + 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_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + 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_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + 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_CHECK(handshake(expected)); - - const auto response = get(R"({"id":42,"method":"server.version","params":["newname","1.4.2"]})" "\n"); - BOOST_CHECK_EQUAL(response.at("result").as_array().at(1).as_string(), expected); + const auto version = electrum::version::v1_4_2; + const auto expected = electrum::version_to_string(version); + BOOST_REQUIRE(handshake(version)); + + const auto request = R"({"id":42,"method":"server.version","params":["newname","%1%"]})" "\n"; + const auto response = get((boost::format(request) % expected).str()); + REQUIRE_NO_THROW_TRUE(response.at("result").is_array()); + BOOST_REQUIRE_EQUAL(response.at("result").as_array().size(), 2u); + BOOST_REQUIRE(response.at("result").as_array().at(1).is_string()); + 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_CHECK(handshake(expected)); + const auto version = electrum::version::v1_4; + const auto expected = electrum::version_to_string(version); + BOOST_REQUIRE(handshake(version)); const auto response = get(R"({"id":57,"method":"server.version","params":["foobar","invalid"]})" "\n"); - BOOST_CHECK_EQUAL(response.at("result").as_array().at(1).as_string(), expected); + REQUIRE_NO_THROW_TRUE(response.at("result").is_array()); + BOOST_REQUIRE_EQUAL(response.at("result").as_array().size(), 2u); + BOOST_REQUIRE(response.at("result").as_array().at(1).is_string()); + 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_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); + const auto response = get((boost::format(R"({"id":42,"method":"server.version","params":["%1%","1.4"]})" "\n") % name).str()); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); } BOOST_AUTO_TEST_SUITE_END() diff --git a/test/protocols/electrum/electrum_transactions.cpp b/test/protocols/electrum/electrum_transactions.cpp index 01442f08..1e7cb0fc 100644 --- a/test/protocols/electrum/electrum_transactions.cpp +++ b/test/protocols/electrum/electrum_transactions.cpp @@ -27,116 +27,138 @@ BOOST_FIXTURE_TEST_SUITE(electrum_tests, electrum_setup_fixture) using namespace system; static const code not_found{ server::error::not_found }; +static const code wrong_version{ server::error::wrong_version }; static const code not_implemented{ server::error::not_implemented }; static const code invalid_argument{ server::error::invalid_argument }; static const code unconfirmable_transaction{ server::error::unconfirmable_transaction }; BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_broadcast__empty__invalid_argument) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_0)); const auto response = get(R"({"id":74,"method":"blockchain.transaction.broadcast","params":[""]})" "\n"); - BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); } BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_broadcast__invalid_encoding__invalid_argument) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_0)); const auto response = get(R"({"id":75,"method":"blockchain.transaction.broadcast","params":["deadbeef"]})" "\n"); - BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); } BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_broadcast__invalid_tx__invalid_argument) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_0)); const auto response = get(R"({"id":76,"method":"blockchain.transaction.broadcast","params":["0100000001"]})" "\n"); - BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); } BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_broadcast__genesis_coinbase__unconfirmable_transaction) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_0)); const auto tx0_text = encode_base16(genesis.transactions_ptr()->front()->to_data(true)); constexpr auto request = R"({"id":73,"method":"blockchain.transaction.broadcast","params":["%1%"]})" "\n"; const auto response = get((boost::format(request) % tx0_text).str()); - BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), unconfirmable_transaction.value()); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), unconfirmable_transaction.value()); } // blockchain.transaction.get BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get__empty_hash__invalid_argument) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_0)); const auto response = get(R"({"id":77,"method":"blockchain.transaction.get","params":["",false]})" "\n"); - BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); } BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get__invalid_hash__invalid_argument) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_0)); const auto response = get(R"({"id":78,"method":"blockchain.transaction.get","params":["deadbeef",false]})" "\n"); - BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); } BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get__nonexistent_tx__not_found) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_0)); const auto bogus = "0000000000000000000000000000000000000000000000000000000000000042"; const auto request = R"({"id":79,"method":"blockchain.transaction.get","params":["%1%",false]})" "\n"; const auto response = get((boost::format(request) % bogus).str()); - BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value()); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value()); } BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get__missing_param__dropped) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_0)); const auto& coinbase = *genesis.transactions_ptr()->front(); const auto tx0_hash = encode_hash(coinbase.hash(false)); const auto request = R"({"id":80,"method":"blockchain.transaction.get","params":["%1%"]})" "\n"; const auto response = get((boost::format(request) % tx0_hash).str()); - BOOST_CHECK(response.at("dropped").as_bool()); + REQUIRE_NO_THROW_TRUE(response.at("dropped").as_bool()); } BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get__extra_param__dropped) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_0)); const auto& coinbase = *genesis.transactions_ptr()->front(); const auto tx0_hash = encode_hash(coinbase.hash(false)); const auto request = R"({"id":81,"method":"blockchain.transaction.get","params":["%1%",false,"extra"]})" "\n"; const auto response = get((boost::format(request) % tx0_hash).str()); - BOOST_CHECK(response.at("dropped").as_bool()); + REQUIRE_NO_THROW_TRUE(response.at("dropped").as_bool()); } BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get__genesis_coinbase_verbose_false__expected) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_0)); const auto& coinbase = *genesis.transactions_ptr()->front(); const auto tx0_hash = encode_hash(coinbase.hash(false)); const auto request = R"({"id":82,"method":"blockchain.transaction.get","params":["%1%",false]})" "\n"; const auto response = get((boost::format(request) % tx0_hash).str()); - BOOST_CHECK_EQUAL(response.at("result").as_string(), encode_base16(coinbase.to_data(true))); + REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); + BOOST_REQUIRE_EQUAL(response.at("result").as_string(), encode_base16(coinbase.to_data(true))); } -BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get__genesis_coinbase_verbose_true__expected) +BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get__version_1_1_verbose__wrong_version) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_1)); const auto& coinbase = *genesis.transactions_ptr()->front(); const auto tx0_hash = encode_hash(coinbase.hash(false)); const auto request = R"({"id":83,"method":"blockchain.transaction.get","params":["%1%",true]})" "\n"; const auto response = get((boost::format(request) % tx0_hash).str()); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), wrong_version.value()); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get__version_1_2_verbose__expected) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_2)); + + const auto& coinbase = *genesis.transactions_ptr()->front(); + const auto tx0_hash = encode_hash(coinbase.hash(false)); + const auto request = R"({"id":83,"method":"blockchain.transaction.get","params":["%1%",true]})" "\n"; + const auto response = get((boost::format(request) % tx0_hash).str()); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); auto expected = value_from(bitcoind(coinbase)); - BOOST_CHECK(expected.is_object()); + BOOST_REQUIRE(expected.is_object()); // The test store is ten confirmed blocks, so top height of nine. constexpr auto top = 9u; @@ -146,64 +168,82 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get__genesis_coinbase_verb tx["confirmations"] = add1(top); tx["blocktime"] = genesis.header().timestamp(); tx["time"] = genesis.header().timestamp(); - BOOST_CHECK_EQUAL(response.at("result").as_object(), expected); + BOOST_REQUIRE_EQUAL(response.at("result").as_object(), expected); } // blockchain.transaction.get_merkle +BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get_merkle__insufficient_version__wrong_version) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_3)); + + const auto response = get(R"({"id":100,"method":"blockchain.transaction.get_merkle","params":["",0]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), wrong_version.value()); +} + BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get_merkle__empty_hash__invalid_argument) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_4)); const auto response = get(R"({"id":100,"method":"blockchain.transaction.get_merkle","params":["",0]})" "\n"); - BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); } BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get_merkle__invalid_hash_encoding__invalid_argument) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_4)); const auto response = get(R"({"id":101,"method":"blockchain.transaction.get_merkle","params":["deadbeef",0]})" "\n"); - BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value()); } BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get_merkle__nonexistent_height__not_found) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_4)); const auto bogus = "0000000000000000000000000000000000000000000000000000000000000042"; const auto request = R"({"id":102,"method":"blockchain.transaction.get_merkle","params":["%1%",999]})" "\n"; const auto response = get((boost::format(request) % bogus).str()); - BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value()); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value()); } BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get_merkle__tx_not_in_block__not_found) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_4)); const auto bogus = "0000000000000000000000000000000000000000000000000000000000000042"; const auto request = R"({"id":103,"method":"blockchain.transaction.get_merkle","params":["%1%",0]})" "\n"; const auto response = get((boost::format(request) % bogus).str()); - BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value()); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value()); } BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get_merkle__genesis_coinbase__expected) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_4)); const auto& coinbase = *genesis.transactions_ptr()->front(); const auto tx_hash = encode_hash(coinbase.hash(false)); const auto request = R"({"id":104,"method":"blockchain.transaction.get_merkle","params":["%1%",0]})" "\n"; const auto response = get((boost::format(request) % tx_hash).str()); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + const auto& result = response.at("result").as_object(); - BOOST_CHECK_EQUAL(result.at("block_height").as_int64(), 0); - BOOST_CHECK_EQUAL(result.at("pos").as_int64(), 0); - BOOST_CHECK(result.at("merkle").as_array().empty()); + REQUIRE_NO_THROW_TRUE(result.at("block_height").is_int64()); + REQUIRE_NO_THROW_TRUE(result.at("pos").is_int64()); + REQUIRE_NO_THROW_TRUE(result.at("merkle").is_array()); + BOOST_REQUIRE_EQUAL(result.at("block_height").as_int64(), 0); + BOOST_REQUIRE_EQUAL(result.at("pos").as_int64(), 0); + BOOST_REQUIRE(result.at("merkle").as_array().empty()); } BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get_merkle__mutiple_txs_block__expected) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_4)); const auto& txs = *bogus_block10.transactions_ptr(); const auto& tx0 = *txs.at(0); @@ -219,91 +259,109 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get_merkle__mutiple_txs_bl const auto request = R"({"id":104,"method":"blockchain.transaction.get_merkle","params":["%1%",10]})" "\n"; const auto response = get((boost::format(request) % encode_hash(tx1_hash)).str()); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + const auto& result = response.at("result").as_object(); - BOOST_CHECK_EQUAL(result.at("block_height").as_int64(), 10); - BOOST_CHECK_EQUAL(result.at("pos").as_int64(), 1); + REQUIRE_NO_THROW_TRUE(result.at("block_height").is_int64()); + REQUIRE_NO_THROW_TRUE(result.at("pos").is_int64()); + REQUIRE_NO_THROW_TRUE(result.at("merkle").is_array()); + BOOST_REQUIRE_EQUAL(result.at("block_height").as_int64(), 10); + BOOST_REQUIRE_EQUAL(result.at("pos").as_int64(), 1); const auto& merkle = result.at("merkle").as_array(); - BOOST_CHECK_EQUAL(merkle.size(), 2u); - BOOST_CHECK_EQUAL(merkle.at(0).as_string(), encode_hash(tx0_hash)); - BOOST_CHECK_EQUAL(merkle.at(1).as_string(), encode_hash(bitcoin_hash(tx2_hash, tx2_hash))); + BOOST_REQUIRE_EQUAL(merkle.size(), 2u); + BOOST_REQUIRE(merkle.at(0).is_string()); + BOOST_REQUIRE_EQUAL(merkle.at(0).as_string(), encode_hash(tx0_hash)); + BOOST_REQUIRE_EQUAL(merkle.at(1).as_string(), encode_hash(bitcoin_hash(tx2_hash, tx2_hash))); } BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get_merkle__missing_param__dropped) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_4)); const auto& coinbase = *genesis.transactions_ptr()->front(); const auto tx_hash = encode_hash(coinbase.hash(false)); const auto request = R"({"id":105,"method":"blockchain.transaction.get_merkle","params":["%1%"]})" "\n"; const auto response = get((boost::format(request) % tx_hash).str()); - BOOST_CHECK(response.at("dropped").as_bool()); + REQUIRE_NO_THROW_TRUE(response.at("dropped").as_bool()); } BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get_merkle__extra_param__dropped) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_4)); const auto& coinbase = *genesis.transactions_ptr()->front(); const auto tx_hash = encode_hash(coinbase.hash(false)); const auto request = R"({"id":106,"method":"blockchain.transaction.get_merkle","params":["%1%",0,"extra"]})" "\n"; const auto response = get((boost::format(request) % tx_hash).str()); - BOOST_CHECK(response.at("dropped").as_bool()); + REQUIRE_NO_THROW_TRUE(response.at("dropped").as_bool()); } // blockchain.transaction.id_from_pos +BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_id_from_pos__insufficient_version__wrong_version) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_3)); + + const auto response = get(R"({"id":90,"method":"blockchain.transaction.id_from_pos","params":[0,0]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), wrong_version.value()); +} + BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_id_from_pos__genesis_coinbase_default__expected) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_4)); const auto& coinbase = *genesis.transactions_ptr()->front(); const auto tx0_hash = encode_hash(coinbase.hash(false)); - const auto request = R"({"id":90,"method":"blockchain.transaction.id_from_pos","params":[0,0]})" "\n"; - const auto response = get(request); - BOOST_CHECK_EQUAL(response.at("result").as_string(), tx0_hash); + const auto response = get(R"({"id":90,"method":"blockchain.transaction.id_from_pos","params":[0,0]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); + BOOST_REQUIRE_EQUAL(response.at("result").as_string(), tx0_hash); } BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_id_from_pos__coinbase_false__expected) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_4)); const auto& coinbase = *block2.transactions_ptr()->front(); const auto tx0_hash = encode_hash(coinbase.hash(false)); - const auto request = R"({"id":91,"method":"blockchain.transaction.id_from_pos","params":[2,0,false]})" "\n"; - const auto response = get(request); - BOOST_CHECK_EQUAL(response.at("result").as_string(), tx0_hash); + const auto response = get(R"({"id":91,"method":"blockchain.transaction.id_from_pos","params":[2,0,false]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); + BOOST_REQUIRE_EQUAL(response.at("result").as_string(), tx0_hash); } BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_id_from_pos__merkle_proof_one_tx__empty) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_4)); const auto& coinbase = *block9.transactions_ptr()->front(); const auto tx0_hash = encode_hash(coinbase.hash(false)); - const auto request = R"({"id":92,"method":"blockchain.transaction.id_from_pos","params":[9,0,true]})" "\n"; - const auto response = get(request); - BOOST_CHECK(response.at("result").is_object()); + const auto response = get(R"({"id":92,"method":"blockchain.transaction.id_from_pos","params":[9,0,true]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); const auto& object = response.at("result").as_object(); - BOOST_CHECK_EQUAL(object.at("tx_hash").as_string(), tx0_hash); - BOOST_CHECK(object.at("merkle").as_array().empty()); + REQUIRE_NO_THROW_TRUE(object.at("tx_hash").is_string()); + REQUIRE_NO_THROW_TRUE(object.at("merkle").is_array()); + BOOST_REQUIRE_EQUAL(object.at("tx_hash").as_string(), tx0_hash); + BOOST_REQUIRE(object.at("merkle").as_array().empty()); } BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_id_from_pos__missing_block__not_found) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_4)); - const auto request = R"({"id":93,"method":"blockchain.transaction.id_from_pos","params":[11,0]})" "\n"; - BOOST_CHECK_EQUAL(get(request).at("error").as_object().at("code").as_int64(), not_found.value()); + const auto response = get(R"({"id":93,"method":"blockchain.transaction.id_from_pos","params":[11,0]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value()); } BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_id_from_pos__missing_position__not_found) { - BOOST_CHECK(handshake()); + BOOST_REQUIRE(handshake(electrum::version::v1_4)); - const auto request = R"({"id":94,"method":"blockchain.transaction.id_from_pos","params":[0,1]})" "\n"; - BOOST_CHECK_EQUAL(get(request).at("error").as_object().at("code").as_int64(), not_found.value()); + const auto response = get(R"({"id":94,"method":"blockchain.transaction.id_from_pos","params":[0,1]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("error").as_object().at("code").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value()); } BOOST_AUTO_TEST_SUITE_END() diff --git a/test/test.hpp b/test/test.hpp index 903d14df..d1ff41a6 100644 --- a/test/test.hpp +++ b/test/test.hpp @@ -24,7 +24,7 @@ #include #include -#define REQUIRE_NO_THROW_AND_TRUE(expression) \ +#define REQUIRE_NO_THROW_TRUE(expression) \ BOOST_REQUIRE_NO_THROW(expression); \ BOOST_REQUIRE(expression) From 84a8abf27fe8e08953e2bc01b456bad3db98d4e9 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Wed, 1 Apr 2026 01:48:56 -0400 Subject: [PATCH 11/12] Delint. --- src/parsers/electrum_version.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/parsers/electrum_version.cpp b/src/parsers/electrum_version.cpp index 6573207c..2a0330b6 100644 --- a/src/parsers/electrum_version.cpp +++ b/src/parsers/electrum_version.cpp @@ -24,6 +24,8 @@ namespace libbitcoin { namespace server { namespace electrum { +BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) + std::string_view version_to_string(version value) NOEXCEPT { static const std::unordered_map map @@ -72,6 +74,8 @@ version version_from_string( const std::string_view& value) NOEXCEPT return it != map.end() ? it->second : version::v0_0; } +BC_POP_WARNING() + } // namespace electrum } // namespace server } // namespace libbitcoin From 2ce7425f4eb1e5eb59754a734757c38110e7b798 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Wed, 1 Apr 2026 02:29:00 -0400 Subject: [PATCH 12/12] Refactor electrum sources into individual files. --- Makefile.am | 17 +- .../libbitcoin-server-test.vcxproj | 9 +- .../libbitcoin-server-test.vcxproj.filters | 7 +- .../libbitcoin-server.vcxproj | 10 +- .../libbitcoin-server.vcxproj.filters | 55 +- src/protocols/electrum/protocol_electrum.cpp | 122 ++ .../electrum/protocol_electrum_addresses.cpp | 139 +++ .../electrum/protocol_electrum_fees.cpp | 71 ++ .../electrum/protocol_electrum_headers.cpp | 286 +++++ .../electrum/protocol_electrum_mempool.cpp | 68 ++ .../electrum/protocol_electrum_server.cpp | 183 +++ .../protocol_electrum_transactions.cpp | 343 ++++++ .../protocol_electrum_version.cpp | 0 src/protocols/protocol_electrum.cpp | 1006 ----------------- test/protocols/electrum/electrum_mempool.cpp | 27 + ...erver_version.cpp => electrum_version.cpp} | 0 16 files changed, 1309 insertions(+), 1034 deletions(-) create mode 100644 src/protocols/electrum/protocol_electrum.cpp create mode 100644 src/protocols/electrum/protocol_electrum_addresses.cpp create mode 100644 src/protocols/electrum/protocol_electrum_fees.cpp create mode 100644 src/protocols/electrum/protocol_electrum_headers.cpp create mode 100644 src/protocols/electrum/protocol_electrum_mempool.cpp create mode 100644 src/protocols/electrum/protocol_electrum_server.cpp create mode 100644 src/protocols/electrum/protocol_electrum_transactions.cpp rename src/protocols/{ => electrum}/protocol_electrum_version.cpp (100%) delete mode 100644 src/protocols/protocol_electrum.cpp create mode 100644 test/protocols/electrum/electrum_mempool.cpp rename test/protocols/electrum/{electrum_server_version.cpp => electrum_version.cpp} (100%) diff --git a/Makefile.am b/Makefile.am index 7fddf4a6..ba9d3b9c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -48,12 +48,18 @@ src_libbitcoin_server_la_SOURCES = \ src/parsers/native_target.cpp \ src/protocols/protocol_bitcoind_rest.cpp \ src/protocols/protocol_bitcoind_rpc.cpp \ - src/protocols/protocol_electrum.cpp \ - src/protocols/protocol_electrum_version.cpp \ src/protocols/protocol_html.cpp \ src/protocols/protocol_http.cpp \ src/protocols/protocol_native.cpp \ - src/protocols/protocol_stratum_v1.cpp + src/protocols/protocol_stratum_v1.cpp \ + src/protocols/electrum/protocol_electrum.cpp \ + src/protocols/electrum/protocol_electrum_addresses.cpp \ + src/protocols/electrum/protocol_electrum_fees.cpp \ + src/protocols/electrum/protocol_electrum_headers.cpp \ + src/protocols/electrum/protocol_electrum_mempool.cpp \ + src/protocols/electrum/protocol_electrum_server.cpp \ + src/protocols/electrum/protocol_electrum_transactions.cpp \ + src/protocols/electrum/protocol_electrum_version.cpp # local: test/libbitcoin-server-test #------------------------------------------------------------------------------ @@ -84,9 +90,10 @@ test_libbitcoin_server_test_SOURCES = \ test/protocols/electrum/electrum_addresses.cpp \ test/protocols/electrum/electrum_fees.cpp \ test/protocols/electrum/electrum_headers.cpp \ + test/protocols/electrum/electrum_mempool.cpp \ test/protocols/electrum/electrum_server.cpp \ - test/protocols/electrum/electrum_server_version.cpp \ - test/protocols/electrum/electrum_transactions.cpp + test/protocols/electrum/electrum_transactions.cpp \ + test/protocols/electrum/electrum_version.cpp endif WITH_TESTS 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 a5b4ee32..7f21d1bf 100644 --- a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj @@ -124,7 +124,9 @@ - + + $(IntDir)test_parsers_electrum_version.obj + @@ -136,9 +138,12 @@ + - + + $(IntDir)test_protocols_electrum_electrum_version.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 60060d12..10b85dc9 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 @@ -63,15 +63,18 @@ src\protocols\electrum - + src\protocols\electrum - + src\protocols\electrum src\protocols\electrum + + src\protocols\electrum + src diff --git a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj index 577d1b32..d9d6b314 100644 --- a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj @@ -130,10 +130,16 @@ + + + + + + + + - - diff --git a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters index ae7d511c..35991acd 100644 --- a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters +++ b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters @@ -8,37 +8,37 @@ - {73CE0AC2-ECB2-4E8D-0000-000000000003} + {73CE0AC2-ECB2-4E8D-0000-000000000004} - {73CE0AC2-ECB2-4E8D-0000-000000000004} + {73CE0AC2-ECB2-4E8D-0000-000000000005} - {73CE0AC2-ECB2-4E8D-0000-000000000005} + {73CE0AC2-ECB2-4E8D-0000-000000000006} - {73CE0AC2-ECB2-4E8D-0000-000000000006} + {73CE0AC2-ECB2-4E8D-0000-000000000007} - {73CE0AC2-ECB2-4E8D-0000-000000000007} + {73CE0AC2-ECB2-4E8D-0000-000000000008} - {73CE0AC2-ECB2-4E8D-0000-00000000000C} + {73CE0AC2-ECB2-4E8D-0000-00000000000D} - {73CE0AC2-ECB2-4E8D-0000-000000000008} + {73CE0AC2-ECB2-4E8D-0000-000000000009} - {73CE0AC2-ECB2-4E8D-0000-000000000009} + {73CE0AC2-ECB2-4E8D-0000-00000000000A} - {73CE0AC2-ECB2-4E8D-0000-00000000000A} + {73CE0AC2-ECB2-4E8D-0000-00000000000B} - {73CE0AC2-ECB2-4E8D-0000-00000000000B} + {73CE0AC2-ECB2-4E8D-0000-00000000000C} - {73CE0AC2-ECB2-4E8D-0000-00000000000D} + {73CE0AC2-ECB2-4E8D-0000-00000000000E} {73CE0AC2-ECB2-4E8D-0000-000000000000} @@ -49,6 +49,9 @@ {73CE0AC2-ECB2-4E8D-0000-000000000002} + + {73CE0AC2-ECB2-4E8D-0000-000000000003} + @@ -78,16 +81,34 @@ src\parsers - - src\protocols + + src\protocols\electrum - - src\protocols + + src\protocols\electrum - + + src\protocols\electrum + + + src\protocols\electrum + + + src\protocols\electrum + + + src\protocols\electrum + + + src\protocols\electrum + + + src\protocols\electrum + + src\protocols - + src\protocols diff --git a/src/protocols/electrum/protocol_electrum.cpp b/src/protocols/electrum/protocol_electrum.cpp new file mode 100644 index 00000000..da9332a3 --- /dev/null +++ b/src/protocols/electrum/protocol_electrum.cpp @@ -0,0 +1,122 @@ +/** + * 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 +#include +#include +#include +#include + +namespace libbitcoin { +namespace server { + +#define CLASS protocol_electrum + +using namespace std::placeholders; + +// Start. +// ---------------------------------------------------------------------------- +// github.com/spesmilo/electrum-protocol/blob/master/docs/protocol-methods.rst + +void protocol_electrum::start() NOEXCEPT +{ + BC_ASSERT(stranded()); + + if (started()) + return; + + // Events subscription is asynchronous, events may be missed. + subscribe_events(BIND(handle_event, _1, _2, _3)); + + // Blockchain methods. + SUBSCRIBE_RPC(handle_blockchain_block_header, _1, _2, _3, _4); + SUBSCRIBE_RPC(handle_blockchain_block_headers, _1, _2, _3, _4, _5); + SUBSCRIBE_RPC(handle_blockchain_headers_subscribe, _1, _2); + SUBSCRIBE_RPC(handle_blockchain_estimate_fee, _1, _2, _3, _4); + SUBSCRIBE_RPC(handle_blockchain_relay_fee, _1, _2); + SUBSCRIBE_RPC(handle_blockchain_scripthash_get_balance, _1, _2, _3); + SUBSCRIBE_RPC(handle_blockchain_scripthash_get_history, _1, _2, _3); + SUBSCRIBE_RPC(handle_blockchain_scripthash_get_mempool, _1, _2, _3); + SUBSCRIBE_RPC(handle_blockchain_scripthash_list_unspent, _1, _2, _3); + SUBSCRIBE_RPC(handle_blockchain_scripthash_subscribe, _1, _2, _3); + SUBSCRIBE_RPC(handle_blockchain_scripthash_unsubscribe, _1, _2, _3); + SUBSCRIBE_RPC(handle_blockchain_transaction_broadcast, _1, _2, _3); + SUBSCRIBE_RPC(handle_blockchain_transaction_broadcast_package, _1, _2, _3, _4); + SUBSCRIBE_RPC(handle_blockchain_transaction_get, _1, _2, _3, _4); + SUBSCRIBE_RPC(handle_blockchain_transaction_get_merkle, _1, _2, _3, _4); + SUBSCRIBE_RPC(handle_blockchain_transaction_id_from_pos, _1, _2, _3, _4, _5); + + // Server methods + SUBSCRIBE_RPC(handle_server_add_peer, _1, _2, _3); + SUBSCRIBE_RPC(handle_server_banner, _1, _2); + SUBSCRIBE_RPC(handle_server_donation_address, _1, _2); + SUBSCRIBE_RPC(handle_server_features, _1, _2); + SUBSCRIBE_RPC(handle_server_peers_subscribe, _1, _2); + SUBSCRIBE_RPC(handle_server_ping, _1, _2); + ////SUBSCRIBE_RPC(handle_server_version, _1, _2, _3, _4); + + // Mempool methods. + SUBSCRIBE_RPC(handle_mempool_get_fee_histogram, _1, _2); + SUBSCRIBE_RPC(handle_mempool_get_info, _1, _2); + protocol_rpc::start(); +} + +void protocol_electrum::stopping(const code& ec) NOEXCEPT +{ + BC_ASSERT(stranded()); + + // Unsubscription is asynchronous, race is ok. + unsubscribe_events(); + protocol_rpc::stopping(ec); +} + +// Handlers (event subscription). +// ---------------------------------------------------------------------------- + +bool protocol_electrum::handle_event(const code&, node::chase event_, + node::event_value value) NOEXCEPT +{ + // Do not pass ec to stopped as it is not a call status. + if (stopped()) + return false; + + switch (event_) + { + case node::chase::organized: + { + if (subscribed_.load(std::memory_order_relaxed)) + { + BC_ASSERT(std::holds_alternative(value)); + POST(do_header, std::get(value)); + } + + break; + } + default: + { + break; + } + } + + return true; +} + +} // namespace server +} // namespace libbitcoin diff --git a/src/protocols/electrum/protocol_electrum_addresses.cpp b/src/protocols/electrum/protocol_electrum_addresses.cpp new file mode 100644 index 00000000..994f9b96 --- /dev/null +++ b/src/protocols/electrum/protocol_electrum_addresses.cpp @@ -0,0 +1,139 @@ +/** + * 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 { + +#define CLASS protocol_electrum + +using namespace system; +using namespace network::rpc; +using namespace std::placeholders; + +BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) +BC_PUSH_WARNING(SMART_PTR_NOT_NEEDED) +BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR) + +void protocol_electrum::handle_blockchain_scripthash_get_balance(const code& ec, + rpc_interface::blockchain_scripthash_get_balance, + const std::string& ) NOEXCEPT +{ + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_1)) + { + send_code(error::wrong_version); + return; + } + + send_code(error::not_implemented); +} + +void protocol_electrum::handle_blockchain_scripthash_get_history(const code& ec, + rpc_interface::blockchain_scripthash_get_history, + const std::string& ) NOEXCEPT +{ + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_1)) + { + send_code(error::wrong_version); + return; + } + + send_code(error::not_implemented); +} + +void protocol_electrum::handle_blockchain_scripthash_get_mempool(const code& ec, + rpc_interface::blockchain_scripthash_get_mempool, + const std::string& ) NOEXCEPT +{ + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_1)) + { + send_code(error::wrong_version); + return; + } + + ////const auto sort = at_least(electrum::version::v1_6); + + send_code(error::not_implemented); +} + +void protocol_electrum::handle_blockchain_scripthash_list_unspent(const code& ec, + rpc_interface::blockchain_scripthash_list_unspent, + const std::string& ) NOEXCEPT +{ + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_1)) + { + send_code(error::wrong_version); + return; + } + + send_code(error::not_implemented); +} + +void protocol_electrum::handle_blockchain_scripthash_subscribe(const code& ec, + rpc_interface::blockchain_scripthash_subscribe, + const std::string& ) NOEXCEPT +{ + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_1)) + { + send_code(error::wrong_version); + return; + } + + send_code(error::not_implemented); +} + +void protocol_electrum::handle_blockchain_scripthash_unsubscribe(const code& ec, + rpc_interface::blockchain_scripthash_unsubscribe, + const std::string& ) NOEXCEPT +{ + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_4_2)) + { + send_code(error::wrong_version); + return; + } + + send_code(error::not_implemented); +} + +BC_POP_WARNING() +BC_POP_WARNING() +BC_POP_WARNING() + +} // namespace server +} // namespace libbitcoin diff --git a/src/protocols/electrum/protocol_electrum_fees.cpp b/src/protocols/electrum/protocol_electrum_fees.cpp new file mode 100644 index 00000000..752df507 --- /dev/null +++ b/src/protocols/electrum/protocol_electrum_fees.cpp @@ -0,0 +1,71 @@ +/** + * 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 { + +#define CLASS protocol_electrum + +using namespace network::rpc; +using namespace std::placeholders; + +BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) + +void protocol_electrum::handle_blockchain_estimate_fee(const code& ec, + rpc_interface::blockchain_estimate_fee, double , + const std::string& ) NOEXCEPT +{ + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_0)) + { + send_code(error::wrong_version); + return; + } + + ////const auto mode_enabled = at_least(electrum::version::v1_6); + + ////send_result(number, 70, BIND(complete, _1)); + send_code(error::not_implemented); +} + +void protocol_electrum::handle_blockchain_relay_fee(const code& ec, + rpc_interface::blockchain_relay_fee) NOEXCEPT +{ + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_0) || + at_least(electrum::version::v1_6)) + { + send_code(error::wrong_version); + return; + } + + send_result(node_settings().minimum_fee_rate, 42, BIND(complete, _1)); +} + +BC_POP_WARNING() + +} // namespace server +} // namespace libbitcoin diff --git a/src/protocols/electrum/protocol_electrum_headers.cpp b/src/protocols/electrum/protocol_electrum_headers.cpp new file mode 100644 index 00000000..caffba3a --- /dev/null +++ b/src/protocols/electrum/protocol_electrum_headers.cpp @@ -0,0 +1,286 @@ +/** + * 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 +#include +#include +#include + +namespace libbitcoin { +namespace server { + +#define CLASS protocol_electrum + +using namespace system; +using namespace network::rpc; +using namespace std::placeholders; + +BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) + +void protocol_electrum::handle_blockchain_block_header(const code& ec, + rpc_interface::blockchain_block_header, double height, + double cp_height) NOEXCEPT +{ + using namespace system; + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_3)) + { + send_code(error::wrong_version); + return; + } + + size_t starting{}; + size_t waypoint{}; + if (!to_integer(starting, height) || + !to_integer(waypoint, cp_height)) + { + send_code(error::invalid_argument); + return; + } + + blockchain_block_headers(starting, one, waypoint, false); +} + +void protocol_electrum::handle_blockchain_block_headers(const code& ec, + rpc_interface::blockchain_block_headers, double start_height, double count, + double cp_height) NOEXCEPT +{ + using namespace system; + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_2)) + { + send_code(error::wrong_version); + return; + } + + size_t quantity{}; + size_t waypoint{}; + size_t starting{}; + if (!to_integer(quantity, count) || + !to_integer(waypoint, cp_height) || + !to_integer(starting, start_height)) + { + send_code(error::invalid_argument); + return; + } + + if (!is_zero(cp_height) && !at_least(electrum::version::v1_4)) + { + send_code(error::wrong_version); + return; + } + + blockchain_block_headers(starting, quantity, waypoint, true); +} + +// Common implementation for blockchain_block_header/s. +void protocol_electrum::blockchain_block_headers(size_t starting, + size_t quantity, size_t waypoint, bool multiplicity) NOEXCEPT +{ + 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 + // ambiguous at count = 0 so guard must be applied to both args and prover. + if (is_add_overflow(starting, quantity)) + { + send_code(error::argument_overflow); + return; + } + else if (starting > top) + { + send_code(error::not_found); + return; + } + 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. + const auto maximum_headers = server_settings().electrum.maximum_headers; + + // Returned headers are assured to be contiguous despite intervening reorg. + // No headers may be returned, which implies start > confirmed top block. + const auto count = limit(quantity, maximum_headers); + const auto links = query.get_confirmed_headers(starting, count); + auto size = two * chain::header::serialized_size() * links.size(); + + value_t value{ object_t{} }; + auto& result = std::get(value.value()); + if (multiplicity) + { + result["max"] = maximum_headers; + result["count"] = links.size(); + } + else if (links.empty()) + { + send_code(error::server_error); + return; + } + + if (at_least(electrum::version::v1_6)) + { + array_t headers{}; + headers.reserve(links.size()); + for (const auto& link: links) + { + const auto header = query.get_wire_header(link); + if (header.empty()) + { + send_code(error::server_error); + return; + } + + headers.push_back(encode_base16(header)); + }; + + if (multiplicity) + result["headers"] = std::move(headers); + else + result["header"] = std::move(headers.front()); + } + else + { + std::string headers(size, '\0'); + stream::out::fast sink{ headers }; + write::base16::fast writer{ sink }; + for (const auto& link: links) + { + if (!query.get_wire_header(writer, link)) + { + send_code(error::server_error); + return; + } + }; + + result["hex"] = std::move(headers); + } + + // There is a very slim chance of inconsistency given an intervening reorg + // because of get_merkle_root_and_proof() use of height-based calculations. + // This is acceptable as it must be verified by caller in any case. + if (prove) + { + hashes proof{}; + hash_digest root{}; + if (const auto code = query.get_merkle_root_and_proof(root, proof, + target, waypoint)) + { + send_code(code); + return; + } + + array_t branch(proof.size()); + std::ranges::transform(proof, branch.begin(), + [](const auto& hash) NOEXCEPT { return encode_hash(hash); }); + + result["branch"] = std::move(branch); + result["root"] = encode_hash(root); + size += two * hash_size * add1(proof.size()); + } + + send_result(std::move(value), size + 42u, BIND(complete, _1)); +} + +void protocol_electrum::handle_blockchain_headers_subscribe(const code& ec, + rpc_interface::blockchain_headers_subscribe) NOEXCEPT +{ + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_0)) + { + send_code(error::wrong_version); + return; + } + + const auto& query = archive(); + const auto top = query.get_top_confirmed(); + const auto link = query.to_confirmed(top); + + // This is unlikely but possible due to a race condition during reorg. + if (link.is_terminal()) + { + send_code(error::not_found); + return; + } + + const auto header = query.get_wire_header(link); + if (header.empty()) + { + send_code(error::server_error); + return; + } + + subscribed_.store(true, std::memory_order_relaxed); + send_result( + { + object_t + { + { "height", top }, + { "hex", encode_base16(header) } + } + }, 256, BIND(complete, _1)); +} + +// Notifier for blockchain_headers_subscribe events. +void protocol_electrum::do_header(node::header_t link) NOEXCEPT +{ + BC_ASSERT(stranded()); + + const auto& query = archive(); + const auto height = query.get_height(link); + const auto header = query.get_wire_header(link); + + if (height.is_terminal()) + { + LOGF("Electrum::do_header, object not found (" << link << ")."); + return; + } + + send_notification("blockchain.headers.subscribe", + { + object_t + { + { "height", height.value }, + { "hex", encode_base16(header) } + } + }, 100, BIND(complete, _1)); +} + +BC_POP_WARNING() + +} // namespace server +} // namespace libbitcoin diff --git a/src/protocols/electrum/protocol_electrum_mempool.cpp b/src/protocols/electrum/protocol_electrum_mempool.cpp new file mode 100644 index 00000000..a7d3b51b --- /dev/null +++ b/src/protocols/electrum/protocol_electrum_mempool.cpp @@ -0,0 +1,68 @@ +/** + * 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 { + +#define CLASS protocol_electrum + +using namespace network::rpc; +using namespace std::placeholders; + +BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) + +void protocol_electrum::handle_mempool_get_fee_histogram(const code& ec, + rpc_interface::mempool_get_fee_histogram) NOEXCEPT +{ + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_2)) + { + send_code(error::wrong_version); + return; + } + + // TODO: requires tx pool metadata graph. + send_code(error::not_implemented); +} + +void protocol_electrum::handle_mempool_get_info(const code& ec, + rpc_interface::mempool_get_info) NOEXCEPT +{ + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_0)) + { + send_code(error::wrong_version); + return; + } + + // TODO: requires tx pool metadata graph. + send_code(error::not_implemented); +} + +BC_POP_WARNING() + +} // namespace server +} // namespace libbitcoin diff --git a/src/protocols/electrum/protocol_electrum_server.cpp b/src/protocols/electrum/protocol_electrum_server.cpp new file mode 100644 index 00000000..824cdc3a --- /dev/null +++ b/src/protocols/electrum/protocol_electrum_server.cpp @@ -0,0 +1,183 @@ +/** + * 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 +#include + +namespace libbitcoin { +namespace server { + +#define CLASS protocol_electrum + +using namespace network::rpc; +using namespace std::placeholders; + +BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) + +void protocol_electrum::handle_server_add_peer(const code& ec, + rpc_interface::server_add_peer, const interface::object_t& ) NOEXCEPT +{ + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_1)) + { + send_code(error::wrong_version); + return; + } + + send_code(error::not_implemented); +} + +void protocol_electrum::handle_server_banner(const code& ec, + rpc_interface::server_banner) NOEXCEPT +{ + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_0)) + { + send_code(error::wrong_version); + return; + } + + 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; + + if (!at_least(electrum::version::v1_0)) + { + send_code(error::wrong_version); + return; + } + + send_result(options().donation_address, 42, BIND(complete, _1)); +} + +void protocol_electrum::handle_server_features(const code& ec, + rpc_interface::server_features) NOEXCEPT +{ + using namespace system; + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_0)) + { + send_code(error::wrong_version); + return; + } + + const auto& query = archive(); + const auto genesis = query.to_confirmed(zero); + if (genesis.is_terminal()) + { + send_code(error::not_found); + return; + } + + const auto hash = query.get_header_key(genesis); + if (hash == null_hash) + { + send_code(error::server_error); + return; + } + + send_result(object_t + { + { "genesis_hash", encode_hash(hash) }, + { "hosts", advertised_hosts() }, + { "hash_function", "sha256" }, + { "server_version", options().server_name }, + { "protocol_min", string_t{ version_to_string(minimum) } }, + { "protocol_max", string_t{ version_to_string(maximum) } }, + { "pruning", null_t{} } + }, 1024, BIND(complete, _1)); +} + +// This is not actually a subscription method. +void protocol_electrum::handle_server_peers_subscribe(const code& ec, + rpc_interface::server_peers_subscribe) NOEXCEPT +{ + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_0)) + { + send_code(error::wrong_version); + return; + } + + send_code(error::not_implemented); +} + +void protocol_electrum::handle_server_ping(const code& ec, + rpc_interface::server_ping) NOEXCEPT +{ + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_2)) + { + send_code(error::wrong_version); + return; + } + + // Any receive, including ping, resets the base channel inactivity timer. + send_result(null_t{}, 42, BIND(complete, _1)); +} + +// utilities +// ---------------------------------------------------------------------------- + +// One of each type allowed for given host, last writer wins if more than one. +object_t protocol_electrum::advertised_hosts() const NOEXCEPT +{ + std::map map{}; + + for (const auto& bind: options().advertise_binds) + if (!bind.host().empty()) + map[bind.host()]["tcp_port"] = bind.port(); + + for (const auto& safe: options().advertise_safes) + if (!safe.host().empty()) + map[safe.host()]["ssl_port"] = safe.port(); + + object_t hosts{}; + for (const auto& [host, object] : map) + hosts[host] = object; + + if (hosts.empty()) return + { + { "tcp_port", null_t{} }, + { "ssl_port", null_t{} } + }; + + return hosts; +} + +BC_POP_WARNING() + +} // namespace server +} // namespace libbitcoin diff --git a/src/protocols/electrum/protocol_electrum_transactions.cpp b/src/protocols/electrum/protocol_electrum_transactions.cpp new file mode 100644 index 00000000..1a38c95c --- /dev/null +++ b/src/protocols/electrum/protocol_electrum_transactions.cpp @@ -0,0 +1,343 @@ +/** + * 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 +#include +#include + +namespace libbitcoin { +namespace server { + +#define CLASS protocol_electrum + +using namespace system; +using namespace network::rpc; +using namespace std::placeholders; + +BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) + +void protocol_electrum::handle_blockchain_transaction_broadcast(const code& ec, + rpc_interface::blockchain_transaction_broadcast, + const std::string& raw_tx) NOEXCEPT +{ + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_0)) + { + send_code(error::wrong_version); + return; + } + + // TODO: implement error_object. + // Changed in version 1.1: return error vs. bitcoind result. + // Previously it returned text string (bitcoind message) in the error case. + ////const auto error_object = at_least(electrum::version::v1_1); + + data_chunk tx_data{}; + if (!decode_base16(tx_data, raw_tx)) + { + send_code(error::invalid_argument); + return; + } + + const auto tx = to_shared(tx_data, true); + if (!tx->is_valid()) + { + send_code(error::invalid_argument); + return; + } + + // TODO: handle just as any peer annoucement, validate and relay. + // TODO: requires tx pool in order to validate against unconfirmed txs. + constexpr auto confirmable = false; + if (!confirmable) + { + send_code(error::unconfirmable_transaction); + return; + } + + constexpr auto size = two * hash_size; + send_result(encode_base16(tx->hash(false)), size, BIND(complete, _1)); +} + +void protocol_electrum::handle_blockchain_transaction_broadcast_package( + const code& ec, rpc_interface::blockchain_transaction_broadcast_package, + const std::string& raw_txs, bool ) NOEXCEPT +{ + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_6)) + { + send_code(error::wrong_version); + return; + } + + data_chunk txs_data{}; + if (!decode_base16(txs_data, raw_txs)) + { + send_code(error::invalid_argument); + return; + } + + // TODO: consider whether to support the lousy package p2p protocol. + constexpr auto confirmable = false; + if (!confirmable) + { + send_code(error::unconfirmable_transaction); + return; + } + + send_code(error::not_implemented); +} + +void protocol_electrum::handle_blockchain_transaction_get(const code& ec, + rpc_interface::blockchain_transaction_get, const std::string& tx_hash, + bool verbose) NOEXCEPT +{ + if (stopped(ec)) + return; + + // TODO: changed in version 1.1: ignored height argument removed. + // Requires additional same-name method implementation for v1.0. + // This implies and override to channel_rpc::dispatch(). + if ((!at_least(electrum::version::v1_0)) || + (!at_least(electrum::version::v1_2) && verbose)) + { + send_code(error::wrong_version); + return; + } + + hash_digest hash{}; + if (!decode_hash(hash, tx_hash)) + { + send_code(error::invalid_argument); + return; + } + + const auto& query = archive(); + const auto link = query.to_tx(hash); + if (link.is_terminal()) + { + send_code(error::not_found); + return; + } + + if (!verbose) + { + const auto tx = query.get_wire_tx(link, true); + if (tx.empty()) + { + send_code(error::server_error); + return; + } + + send_result(encode_base16(tx), two * tx.size(), BIND(complete, _1)); + return; + } + + const auto tx = query.get_transaction(link, true); + if (!tx) + { + send_code(error::server_error); + return; + } + + auto value = value_from(bitcoind(*tx)); + if (!value.is_object()) + { + send_code(error::server_error); + return; + } + + if (const auto header = query.to_strong(link); !header.is_terminal()) + { + using namespace system; + const auto top = query.get_top_confirmed(); + const auto height = query.get_height(header); + const auto block_hash = query.get_header_key(header); + + uint32_t timestamp{}; + if (height.is_terminal() || (block_hash == null_hash) || + !query.get_timestamp(timestamp, header)) + { + send_code(error::server_error); + return; + } + + // Floor manages race between getting confirmed top and height. + const auto confirms = add1(floored_subtract(top, height.value)); + + auto& transaction = value.as_object(); + transaction["in_active_chain"] = true; + transaction["blockhash"] = encode_hash(block_hash); + transaction["confirmations"] = confirms; + transaction["blocktime"] = timestamp; + transaction["time"] = timestamp; + } + + // Verbose means whatever bitcoind returns for getrawtransaction, lolz. + const auto size = tx->serialized_size(true); + send_result(std::move(value), two * size, BIND(complete, _1)); +} + +void protocol_electrum::handle_blockchain_transaction_get_merkle(const code& ec, + rpc_interface::blockchain_transaction_get_merkle, const std::string& tx_hash, + double height) NOEXCEPT +{ + using namespace system; + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_4)) + { + send_code(error::wrong_version); + return; + } + + hash_digest hash{}; + size_t block_height{}; + if (!to_integer(block_height, height) || !decode_hash(hash, tx_hash)) + { + send_code(error::invalid_argument); + return; + } + + const auto& query = archive(); + const auto block_link = query.to_confirmed(block_height); + if (block_link.is_terminal()) + { + send_code(error::not_found); + return; + } + + auto hashes = query.get_tx_keys(block_link); + if (hashes.empty()) + { + send_code(error::server_error); + return; + } + + const auto index = find_position(hashes, hash); + if (is_negative(index)) + { + send_code(error::not_found); + return; + } + + using namespace chain; + const auto position = to_unsigned(index); + const auto proof = block::merkle_branch(index, std::move(hashes)); + + array_t branch(proof.size()); + std::ranges::transform(proof, branch.begin(), + [](const auto& hash) NOEXCEPT{ return encode_hash(hash); }); + + send_result( + { + object_t + { + { "merkle", std::move(branch) }, + { "block_height", block_height }, + { "pos", position } + } + }, two * hash_size * add1(branch.size()), BIND(complete, _1)); +} + +void protocol_electrum::handle_blockchain_transaction_id_from_pos(const code& ec, + rpc_interface::blockchain_transaction_id_from_pos, double height, + double tx_pos, bool merkle) NOEXCEPT +{ + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_4)) + { + send_code(error::wrong_version); + return; + } + + size_t position{}; + size_t block_height{}; + if (!to_integer(block_height, height) || + !to_integer(position, tx_pos)) + { + send_code(error::invalid_argument); + return; + } + + const auto& query = archive(); + const auto block_link = query.to_confirmed(block_height); + const auto tx_link = query.get_position_tx(block_link, position); + if (tx_link.is_terminal()) + { + send_code(error::not_found); + return; + } + + using namespace system; + const auto hash = query.get_tx_key(tx_link); + if (hash == null_hash) + { + send_code(error::server_error); + return; + } + + if (!merkle) + { + send_result(encode_hash(hash), two * hash_size, BIND(complete, _1)); + return; + } + + auto hashes = query.get_tx_keys(block_link); + if (hashes.empty()) + { + send_code(error::server_error); + return; + } + + if (position >= hashes.size()) + { + send_code(error::not_found); + return; + } + + using namespace chain; + const auto proof = block::merkle_branch(position, std::move(hashes)); + + array_t branch(proof.size()); + std::ranges::transform(proof, branch.begin(), + [](const auto& hash) NOEXCEPT { return encode_hash(hash); }); + + send_result( + { + object_t + { + { "tx_hash", encode_hash(hash) }, + { "merkle", std::move(branch) } + } + }, two * hash_size * add1(branch.size()), BIND(complete, _1)); +} + +BC_POP_WARNING() + +} // namespace server +} // namespace libbitcoin diff --git a/src/protocols/protocol_electrum_version.cpp b/src/protocols/electrum/protocol_electrum_version.cpp similarity index 100% rename from src/protocols/protocol_electrum_version.cpp rename to src/protocols/electrum/protocol_electrum_version.cpp diff --git a/src/protocols/protocol_electrum.cpp b/src/protocols/protocol_electrum.cpp deleted file mode 100644 index 059d13ed..00000000 --- a/src/protocols/protocol_electrum.cpp +++ /dev/null @@ -1,1006 +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 - -#include -#include -#include -#include -#include -#include -#include -#include - -// github.com/spesmilo/electrum-protocol/blob/master/docs/protocol-methods.rst - -namespace libbitcoin { -namespace server { - -#define CLASS protocol_electrum - -using namespace system; -using namespace network::rpc; -using namespace std::placeholders; - -BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) -BC_PUSH_WARNING(SMART_PTR_NOT_NEEDED) -BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR) - -// Start. -// ---------------------------------------------------------------------------- - -void protocol_electrum::start() NOEXCEPT -{ - BC_ASSERT(stranded()); - - if (started()) - return; - - // Events subscription is asynchronous, events may be missed. - subscribe_events(BIND(handle_event, _1, _2, _3)); - - // Blockchain methods. - SUBSCRIBE_RPC(handle_blockchain_block_header, _1, _2, _3, _4); - SUBSCRIBE_RPC(handle_blockchain_block_headers, _1, _2, _3, _4, _5); - SUBSCRIBE_RPC(handle_blockchain_headers_subscribe, _1, _2); - SUBSCRIBE_RPC(handle_blockchain_estimate_fee, _1, _2, _3, _4); - SUBSCRIBE_RPC(handle_blockchain_relay_fee, _1, _2); - SUBSCRIBE_RPC(handle_blockchain_scripthash_get_balance, _1, _2, _3); - SUBSCRIBE_RPC(handle_blockchain_scripthash_get_history, _1, _2, _3); - SUBSCRIBE_RPC(handle_blockchain_scripthash_get_mempool, _1, _2, _3); - SUBSCRIBE_RPC(handle_blockchain_scripthash_list_unspent, _1, _2, _3); - SUBSCRIBE_RPC(handle_blockchain_scripthash_subscribe, _1, _2, _3); - SUBSCRIBE_RPC(handle_blockchain_scripthash_unsubscribe, _1, _2, _3); - SUBSCRIBE_RPC(handle_blockchain_transaction_broadcast, _1, _2, _3); - SUBSCRIBE_RPC(handle_blockchain_transaction_broadcast_package, _1, _2, _3, _4); - SUBSCRIBE_RPC(handle_blockchain_transaction_get, _1, _2, _3, _4); - SUBSCRIBE_RPC(handle_blockchain_transaction_get_merkle, _1, _2, _3, _4); - SUBSCRIBE_RPC(handle_blockchain_transaction_id_from_pos, _1, _2, _3, _4, _5); - - // Server methods - SUBSCRIBE_RPC(handle_server_add_peer, _1, _2, _3); - SUBSCRIBE_RPC(handle_server_banner, _1, _2); - SUBSCRIBE_RPC(handle_server_donation_address, _1, _2); - SUBSCRIBE_RPC(handle_server_features, _1, _2); - SUBSCRIBE_RPC(handle_server_peers_subscribe, _1, _2); - SUBSCRIBE_RPC(handle_server_ping, _1, _2); - ////SUBSCRIBE_RPC(handle_server_version, _1, _2, _3, _4); - - // Mempool methods. - SUBSCRIBE_RPC(handle_mempool_get_fee_histogram, _1, _2); - SUBSCRIBE_RPC(handle_mempool_get_info, _1, _2); - protocol_rpc::start(); -} - -void protocol_electrum::stopping(const code& ec) NOEXCEPT -{ - BC_ASSERT(stranded()); - - // Unsubscription is asynchronous, race is ok. - unsubscribe_events(); - protocol_rpc::stopping(ec); -} - -// Handlers (event subscription). -// ---------------------------------------------------------------------------- - -bool protocol_electrum::handle_event(const code&, node::chase event_, - node::event_value value) NOEXCEPT -{ - // Do not pass ec to stopped as it is not a call status. - if (stopped()) - return false; - - switch (event_) - { - case node::chase::organized: - { - if (subscribed_.load(std::memory_order_relaxed)) - { - BC_ASSERT(std::holds_alternative(value)); - POST(do_header, std::get(value)); - } - - break; - } - default: - { - break; - } - } - - return true; -} - -// Handlers (blockchain). -// ---------------------------------------------------------------------------- - -void protocol_electrum::handle_blockchain_block_header(const code& ec, - rpc_interface::blockchain_block_header, double height, - double cp_height) NOEXCEPT -{ - using namespace system; - if (stopped(ec)) - return; - - if (!at_least(electrum::version::v1_3)) - { - send_code(error::wrong_version); - return; - } - - size_t starting{}; - size_t waypoint{}; - if (!to_integer(starting, height) || - !to_integer(waypoint, cp_height)) - { - send_code(error::invalid_argument); - return; - } - - blockchain_block_headers(starting, one, waypoint, false); -} - -void protocol_electrum::handle_blockchain_block_headers(const code& ec, - rpc_interface::blockchain_block_headers, double start_height, double count, - double cp_height) NOEXCEPT -{ - using namespace system; - if (stopped(ec)) - return; - - if (!at_least(electrum::version::v1_2)) - { - send_code(error::wrong_version); - return; - } - - size_t quantity{}; - size_t waypoint{}; - size_t starting{}; - if (!to_integer(quantity, count) || - !to_integer(waypoint, cp_height) || - !to_integer(starting, start_height)) - { - send_code(error::invalid_argument); - return; - } - - if (!is_zero(cp_height) && !at_least(electrum::version::v1_4)) - { - send_code(error::wrong_version); - return; - } - - blockchain_block_headers(starting, quantity, waypoint, true); -} - -// Common implementation for blockchain_block_header/s. -void protocol_electrum::blockchain_block_headers(size_t starting, - size_t quantity, size_t waypoint, bool multiplicity) NOEXCEPT -{ - 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 - // ambiguous at count = 0 so guard must be applied to both args and prover. - if (is_add_overflow(starting, quantity)) - { - send_code(error::argument_overflow); - return; - } - else if (starting > top) - { - send_code(error::not_found); - return; - } - 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. - const auto maximum_headers = server_settings().electrum.maximum_headers; - - // Returned headers are assured to be contiguous despite intervening reorg. - // No headers may be returned, which implies start > confirmed top block. - const auto count = limit(quantity, maximum_headers); - const auto links = query.get_confirmed_headers(starting, count); - auto size = two * chain::header::serialized_size() * links.size(); - - value_t value{ object_t{} }; - auto& result = std::get(value.value()); - if (multiplicity) - { - result["max"] = maximum_headers; - result["count"] = links.size(); - } - else if (links.empty()) - { - send_code(error::server_error); - return; - } - - if (at_least(electrum::version::v1_6)) - { - array_t headers{}; - headers.reserve(links.size()); - for (const auto& link: links) - { - const auto header = query.get_wire_header(link); - if (header.empty()) - { - send_code(error::server_error); - return; - } - - headers.push_back(encode_base16(header)); - }; - - if (multiplicity) - result["headers"] = std::move(headers); - else - result["header"] = std::move(headers.front()); - } - else - { - std::string headers(size, '\0'); - stream::out::fast sink{ headers }; - write::base16::fast writer{ sink }; - for (const auto& link: links) - { - if (!query.get_wire_header(writer, link)) - { - send_code(error::server_error); - return; - } - }; - - result["hex"] = std::move(headers); - } - - // There is a very slim chance of inconsistency given an intervening reorg - // because of get_merkle_root_and_proof() use of height-based calculations. - // This is acceptable as it must be verified by caller in any case. - if (prove) - { - hashes proof{}; - hash_digest root{}; - if (const auto code = query.get_merkle_root_and_proof(root, proof, - target, waypoint)) - { - send_code(code); - return; - } - - array_t branch(proof.size()); - std::ranges::transform(proof, branch.begin(), - [](const auto& hash) NOEXCEPT { return encode_hash(hash); }); - - result["branch"] = std::move(branch); - result["root"] = encode_hash(root); - size += two * hash_size * add1(proof.size()); - } - - send_result(std::move(value), size + 42u, BIND(complete, _1)); -} - -void protocol_electrum::handle_blockchain_headers_subscribe(const code& ec, - rpc_interface::blockchain_headers_subscribe) NOEXCEPT -{ - if (stopped(ec)) - return; - - if (!at_least(electrum::version::v1_0)) - { - send_code(error::wrong_version); - return; - } - - const auto& query = archive(); - const auto top = query.get_top_confirmed(); - const auto link = query.to_confirmed(top); - - // This is unlikely but possible due to a race condition during reorg. - if (link.is_terminal()) - { - send_code(error::not_found); - return; - } - - const auto header = query.get_wire_header(link); - if (header.empty()) - { - send_code(error::server_error); - return; - } - - subscribed_.store(true, std::memory_order_relaxed); - send_result( - { - object_t - { - { "height", top }, - { "hex", encode_base16(header) } - } - }, 256, BIND(complete, _1)); -} - -// Notifier for blockchain_headers_subscribe events. -void protocol_electrum::do_header(node::header_t link) NOEXCEPT -{ - BC_ASSERT(stranded()); - - const auto& query = archive(); - const auto height = query.get_height(link); - const auto header = query.get_wire_header(link); - - if (height.is_terminal()) - { - LOGF("Electrum::do_header, object not found (" << link << ")."); - return; - } - - send_notification("blockchain.headers.subscribe", - { - object_t - { - { "height", height.value }, - { "hex", encode_base16(header) } - } - }, 100, BIND(complete, _1)); -} - -void protocol_electrum::handle_blockchain_estimate_fee(const code& ec, - rpc_interface::blockchain_estimate_fee, double , - const std::string& ) NOEXCEPT -{ - if (stopped(ec)) - return; - - if (!at_least(electrum::version::v1_0)) - { - send_code(error::wrong_version); - return; - } - - ////const auto mode_enabled = at_least(electrum::version::v1_6); - - ////send_result(number, 70, BIND(complete, _1)); - send_code(error::not_implemented); -} - -void protocol_electrum::handle_blockchain_relay_fee(const code& ec, - rpc_interface::blockchain_relay_fee) NOEXCEPT -{ - if (stopped(ec)) - return; - - if (!at_least(electrum::version::v1_0) || - at_least(electrum::version::v1_6)) - { - send_code(error::wrong_version); - return; - } - - send_result(node_settings().minimum_fee_rate, 42, BIND(complete, _1)); -} - -void protocol_electrum::handle_blockchain_scripthash_get_balance(const code& ec, - rpc_interface::blockchain_scripthash_get_balance, - const std::string& ) NOEXCEPT -{ - if (stopped(ec)) - return; - - if (!at_least(electrum::version::v1_1)) - { - send_code(error::wrong_version); - return; - } - - send_code(error::not_implemented); -} - -void protocol_electrum::handle_blockchain_scripthash_get_history(const code& ec, - rpc_interface::blockchain_scripthash_get_history, - const std::string& ) NOEXCEPT -{ - if (stopped(ec)) - return; - - if (!at_least(electrum::version::v1_1)) - { - send_code(error::wrong_version); - return; - } - - send_code(error::not_implemented); -} - -void protocol_electrum::handle_blockchain_scripthash_get_mempool(const code& ec, - rpc_interface::blockchain_scripthash_get_mempool, - const std::string& ) NOEXCEPT -{ - if (stopped(ec)) - return; - - if (!at_least(electrum::version::v1_1)) - { - send_code(error::wrong_version); - return; - } - - ////const auto sort = at_least(electrum::version::v1_6); - - send_code(error::not_implemented); -} - -void protocol_electrum::handle_blockchain_scripthash_list_unspent(const code& ec, - rpc_interface::blockchain_scripthash_list_unspent, - const std::string& ) NOEXCEPT -{ - if (stopped(ec)) - return; - - if (!at_least(electrum::version::v1_1)) - { - send_code(error::wrong_version); - return; - } - - send_code(error::not_implemented); -} - -void protocol_electrum::handle_blockchain_scripthash_subscribe(const code& ec, - rpc_interface::blockchain_scripthash_subscribe, - const std::string& ) NOEXCEPT -{ - if (stopped(ec)) - return; - - if (!at_least(electrum::version::v1_1)) - { - send_code(error::wrong_version); - return; - } - - send_code(error::not_implemented); -} - -void protocol_electrum::handle_blockchain_scripthash_unsubscribe(const code& ec, - rpc_interface::blockchain_scripthash_unsubscribe, - const std::string& ) NOEXCEPT -{ - if (stopped(ec)) - return; - - if (!at_least(electrum::version::v1_4_2)) - { - send_code(error::wrong_version); - return; - } - - send_code(error::not_implemented); -} - -void protocol_electrum::handle_blockchain_transaction_broadcast(const code& ec, - rpc_interface::blockchain_transaction_broadcast, - const std::string& raw_tx) NOEXCEPT -{ - if (stopped(ec)) - return; - - if (!at_least(electrum::version::v1_0)) - { - send_code(error::wrong_version); - return; - } - - // TODO: implement error_object. - // Changed in version 1.1: return error vs. bitcoind result. - // Previously it returned text string (bitcoind message) in the error case. - ////const auto error_object = at_least(electrum::version::v1_1); - - data_chunk tx_data{}; - if (!decode_base16(tx_data, raw_tx)) - { - send_code(error::invalid_argument); - return; - } - - const auto tx = to_shared(tx_data, true); - if (!tx->is_valid()) - { - send_code(error::invalid_argument); - return; - } - - // TODO: handle just as any peer annoucement, validate and relay. - // TODO: requires tx pool in order to validate against unconfirmed txs. - constexpr auto confirmable = false; - if (!confirmable) - { - send_code(error::unconfirmable_transaction); - return; - } - - constexpr auto size = two * hash_size; - send_result(encode_base16(tx->hash(false)), size, BIND(complete, _1)); -} - -void protocol_electrum::handle_blockchain_transaction_broadcast_package( - const code& ec, rpc_interface::blockchain_transaction_broadcast_package, - const std::string& raw_txs, bool ) NOEXCEPT -{ - if (stopped(ec)) - return; - - if (!at_least(electrum::version::v1_6)) - { - send_code(error::wrong_version); - return; - } - - data_chunk txs_data{}; - if (!decode_base16(txs_data, raw_txs)) - { - send_code(error::invalid_argument); - return; - } - - // TODO: consider whether to support the lousy package p2p protocol. - constexpr auto confirmable = false; - if (!confirmable) - { - send_code(error::unconfirmable_transaction); - return; - } - - send_code(error::not_implemented); -} - -void protocol_electrum::handle_blockchain_transaction_get(const code& ec, - rpc_interface::blockchain_transaction_get, const std::string& tx_hash, - bool verbose) NOEXCEPT -{ - if (stopped(ec)) - return; - - // TODO: changed in version 1.1: ignored height argument removed. - // Requires additional same-name method implementation for v1.0. - // This implies and override to channel_rpc::dispatch(). - if ((!at_least(electrum::version::v1_0)) || - (!at_least(electrum::version::v1_2) && verbose)) - { - send_code(error::wrong_version); - return; - } - - hash_digest hash{}; - if (!decode_hash(hash, tx_hash)) - { - send_code(error::invalid_argument); - return; - } - - const auto& query = archive(); - const auto link = query.to_tx(hash); - if (link.is_terminal()) - { - send_code(error::not_found); - return; - } - - if (!verbose) - { - const auto tx = query.get_wire_tx(link, true); - if (tx.empty()) - { - send_code(error::server_error); - return; - } - - send_result(encode_base16(tx), two * tx.size(), BIND(complete, _1)); - return; - } - - const auto tx = query.get_transaction(link, true); - if (!tx) - { - send_code(error::server_error); - return; - } - - auto value = value_from(bitcoind(*tx)); - if (!value.is_object()) - { - send_code(error::server_error); - return; - } - - if (const auto header = query.to_strong(link); !header.is_terminal()) - { - using namespace system; - const auto top = query.get_top_confirmed(); - const auto height = query.get_height(header); - const auto block_hash = query.get_header_key(header); - - uint32_t timestamp{}; - if (height.is_terminal() || (block_hash == null_hash) || - !query.get_timestamp(timestamp, header)) - { - send_code(error::server_error); - return; - } - - // Floor manages race between getting confirmed top and height. - const auto confirms = add1(floored_subtract(top, height.value)); - - auto& transaction = value.as_object(); - transaction["in_active_chain"] = true; - transaction["blockhash"] = encode_hash(block_hash); - transaction["confirmations"] = confirms; - transaction["blocktime"] = timestamp; - transaction["time"] = timestamp; - } - - // Verbose means whatever bitcoind returns for getrawtransaction, lolz. - const auto size = tx->serialized_size(true); - send_result(std::move(value), two * size, BIND(complete, _1)); -} - -void protocol_electrum::handle_blockchain_transaction_get_merkle(const code& ec, - rpc_interface::blockchain_transaction_get_merkle, const std::string& tx_hash, - double height) NOEXCEPT -{ - using namespace system; - if (stopped(ec)) - return; - - if (!at_least(electrum::version::v1_4)) - { - send_code(error::wrong_version); - return; - } - - hash_digest hash{}; - size_t block_height{}; - if (!to_integer(block_height, height) || !decode_hash(hash, tx_hash)) - { - send_code(error::invalid_argument); - return; - } - - const auto& query = archive(); - const auto block_link = query.to_confirmed(block_height); - if (block_link.is_terminal()) - { - send_code(error::not_found); - return; - } - - auto hashes = query.get_tx_keys(block_link); - if (hashes.empty()) - { - send_code(error::server_error); - return; - } - - const auto index = find_position(hashes, hash); - if (is_negative(index)) - { - send_code(error::not_found); - return; - } - - using namespace chain; - const auto position = to_unsigned(index); - const auto proof = block::merkle_branch(index, std::move(hashes)); - - array_t branch(proof.size()); - std::ranges::transform(proof, branch.begin(), - [](const auto& hash) NOEXCEPT{ return encode_hash(hash); }); - - send_result( - { - object_t - { - { "merkle", std::move(branch) }, - { "block_height", block_height }, - { "pos", position } - } - }, two * hash_size * add1(branch.size()), BIND(complete, _1)); -} - -void protocol_electrum::handle_blockchain_transaction_id_from_pos(const code& ec, - rpc_interface::blockchain_transaction_id_from_pos, double height, - double tx_pos, bool merkle) NOEXCEPT -{ - if (stopped(ec)) - return; - - if (!at_least(electrum::version::v1_4)) - { - send_code(error::wrong_version); - return; - } - - size_t position{}; - size_t block_height{}; - if (!to_integer(block_height, height) || - !to_integer(position, tx_pos)) - { - send_code(error::invalid_argument); - return; - } - - const auto& query = archive(); - const auto block_link = query.to_confirmed(block_height); - const auto tx_link = query.get_position_tx(block_link, position); - if (tx_link.is_terminal()) - { - send_code(error::not_found); - return; - } - - using namespace system; - const auto hash = query.get_tx_key(tx_link); - if (hash == null_hash) - { - send_code(error::server_error); - return; - } - - if (!merkle) - { - send_result(encode_hash(hash), two * hash_size, BIND(complete, _1)); - return; - } - - auto hashes = query.get_tx_keys(block_link); - if (hashes.empty()) - { - send_code(error::server_error); - return; - } - - if (position >= hashes.size()) - { - send_code(error::not_found); - return; - } - - using namespace chain; - const auto proof = block::merkle_branch(position, std::move(hashes)); - - array_t branch(proof.size()); - std::ranges::transform(proof, branch.begin(), - [](const auto& hash) NOEXCEPT { return encode_hash(hash); }); - - send_result( - { - object_t - { - { "tx_hash", encode_hash(hash) }, - { "merkle", std::move(branch) } - } - }, two * hash_size * add1(branch.size()), BIND(complete, _1)); -} - -// Handlers (server). -// ---------------------------------------------------------------------------- - -void protocol_electrum::handle_server_add_peer(const code& ec, - rpc_interface::server_add_peer, const interface::object_t& ) NOEXCEPT -{ - if (stopped(ec)) - return; - - if (!at_least(electrum::version::v1_1)) - { - send_code(error::wrong_version); - return; - } - - send_code(error::not_implemented); -} - -void protocol_electrum::handle_server_banner(const code& ec, - rpc_interface::server_banner) NOEXCEPT -{ - if (stopped(ec)) - return; - - if (!at_least(electrum::version::v1_0)) - { - send_code(error::wrong_version); - return; - } - - 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; - - if (!at_least(electrum::version::v1_0)) - { - send_code(error::wrong_version); - return; - } - - send_result(options().donation_address, 42, BIND(complete, _1)); -} - -void protocol_electrum::handle_server_features(const code& ec, - rpc_interface::server_features) NOEXCEPT -{ - using namespace system; - if (stopped(ec)) - return; - - if (!at_least(electrum::version::v1_0)) - { - send_code(error::wrong_version); - return; - } - - const auto& query = archive(); - const auto genesis = query.to_confirmed(zero); - if (genesis.is_terminal()) - { - send_code(error::not_found); - return; - } - - const auto hash = query.get_header_key(genesis); - if (hash == null_hash) - { - send_code(error::server_error); - return; - } - - send_result(object_t - { - { "genesis_hash", encode_hash(hash) }, - { "hosts", advertised_hosts() }, - { "hash_function", "sha256" }, - { "server_version", options().server_name }, - { "protocol_min", string_t{ version_to_string(minimum) } }, - { "protocol_max", string_t{ version_to_string(maximum) } }, - { "pruning", null_t{} } - }, 1024, BIND(complete, _1)); -} - -// This is not actually a subscription method. -void protocol_electrum::handle_server_peers_subscribe(const code& ec, - rpc_interface::server_peers_subscribe) NOEXCEPT -{ - if (stopped(ec)) - return; - - if (!at_least(electrum::version::v1_0)) - { - send_code(error::wrong_version); - return; - } - - send_code(error::not_implemented); -} - -void protocol_electrum::handle_server_ping(const code& ec, - rpc_interface::server_ping) NOEXCEPT -{ - if (stopped(ec)) - return; - - if (!at_least(electrum::version::v1_2)) - { - send_code(error::wrong_version); - return; - } - - // Any receive, including ping, resets the base channel inactivity timer. - send_result(null_t{}, 42, BIND(complete, _1)); -} - -// Handlers (mempool). -// ---------------------------------------------------------------------------- - -void protocol_electrum::handle_mempool_get_fee_histogram(const code& ec, - rpc_interface::mempool_get_fee_histogram) NOEXCEPT -{ - if (stopped(ec)) - return; - - if (!at_least(electrum::version::v1_2)) - { - send_code(error::wrong_version); - return; - } - - // TODO: requires tx pool metadata graph. - send_code(error::not_implemented); -} - -void protocol_electrum::handle_mempool_get_info(const code& ec, - rpc_interface::mempool_get_info) NOEXCEPT -{ - if (stopped(ec)) - return; - - if (!at_least(electrum::version::v1_0)) - { - send_code(error::wrong_version); - return; - } - - // TODO: requires tx pool metadata graph. - send_code(error::not_implemented); -} - -// utilities -// ---------------------------------------------------------------------------- - -// One of each type allowed for given host, last writer wins if more than one. -object_t protocol_electrum::advertised_hosts() const NOEXCEPT -{ - std::map map{}; - - for (const auto& bind: options().advertise_binds) - if (!bind.host().empty()) - map[bind.host()]["tcp_port"] = bind.port(); - - for (const auto& safe: options().advertise_safes) - if (!safe.host().empty()) - map[safe.host()]["ssl_port"] = safe.port(); - - object_t hosts{}; - for (const auto& [host, object]: map) - hosts[host] = object; - - if (hosts.empty()) return - { - { "tcp_port", null_t{} }, - { "ssl_port", null_t{} } - }; - - return hosts; -} - -BC_POP_WARNING() -BC_POP_WARNING() -BC_POP_WARNING() - -} // namespace server -} // namespace libbitcoin diff --git a/test/protocols/electrum/electrum_mempool.cpp b/test/protocols/electrum/electrum_mempool.cpp new file mode 100644 index 00000000..7b562362 --- /dev/null +++ b/test/protocols/electrum/electrum_mempool.cpp @@ -0,0 +1,27 @@ +/** + * 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) + +// mempool.get_fee_histogram +// mempool.get_info + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/protocols/electrum/electrum_server_version.cpp b/test/protocols/electrum/electrum_version.cpp similarity index 100% rename from test/protocols/electrum/electrum_server_version.cpp rename to test/protocols/electrum/electrum_version.cpp