diff --git a/include/bitcoin/server/interfaces/electrum.hpp b/include/bitcoin/server/interfaces/electrum.hpp index 486cded0..506d697f 100644 --- a/include/bitcoin/server/interfaces/electrum.hpp +++ b/include/bitcoin/server/interfaces/electrum.hpp @@ -79,7 +79,7 @@ struct electrum_methods method<"server.donation_address">{}, method<"server.features">{}, method<"server.peers.subscribe">{}, - method<"server.ping">{}, + method<"server.ping", optional<0.0>, optional<""_t>>{ "pong_len", "data" }, method<"server.version", string_t, optional>{ "client_name", "protocol_version" }, /// Mempool methods. diff --git a/include/bitcoin/server/protocols/protocol_electrum.hpp b/include/bitcoin/server/protocols/protocol_electrum.hpp index 305b0d53..6cdc89c0 100644 --- a/include/bitcoin/server/protocols/protocol_electrum.hpp +++ b/include/bitcoin/server/protocols/protocol_electrum.hpp @@ -191,7 +191,8 @@ class BCS_API protocol_electrum void handle_server_peers_subscribe(const code& ec, rpc_interface::server_peers_subscribe) NOEXCEPT; void handle_server_ping(const code& ec, - rpc_interface::server_ping) NOEXCEPT; + rpc_interface::server_ping, double pong_len, + const std::string& data) NOEXCEPT; /// See protocol_electrum_version. ////void handle_server_version(const code& ec, diff --git a/src/protocols/electrum/protocol_electrum.cpp b/src/protocols/electrum/protocol_electrum.cpp index f56b6e4b..d894e0df 100644 --- a/src/protocols/electrum/protocol_electrum.cpp +++ b/src/protocols/electrum/protocol_electrum.cpp @@ -108,7 +108,7 @@ void protocol_electrum::start() NOEXCEPT 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_ping, _1, _2, _3, _4); ////SUBSCRIBE_RPC(handle_server_version, _1, _2, _3, _4); // Mempool methods. diff --git a/src/protocols/electrum/protocol_electrum_headers.cpp b/src/protocols/electrum/protocol_electrum_headers.cpp index 1fbfb5e2..778927e5 100644 --- a/src/protocols/electrum/protocol_electrum_headers.cpp +++ b/src/protocols/electrum/protocol_electrum_headers.cpp @@ -349,26 +349,48 @@ void protocol_electrum::handle_blockchain_headers_subscribe(const code& ec, return; } - const auto header = query.get_wire_header(link); - if (header.empty()) + size_t size{}; + boost::json::value value{}; + if (raw) { - send_code(error::server_error); - return; - } + const auto header = query.get_wire_header(link); + if (header.empty()) + { + send_code(error::server_error); + return; + } - // TODO: determine intended encoding. - if (!raw) + size = two * chain::header::serialized_size(); + value = + { + { "height", top }, + { "hex", encode_base16(header) } + }; + } + else { - send_code(error::not_implemented); - return; + const auto header = query.get_header(link); + if (!header) + { + send_code(error::server_error); + return; + } + + // !raw is a custom electrumx serialization. + value = value_from(electrumx(*header)); + if (!value.is_object()) + { + send_code(error::server_error); + return; + } + + size = 256; + auto& object = value.as_object(); + object["block_height"] = top; } subscribed_header_.store(true, relaxed); - send_result(object_t - { - { "height", top }, - { "hex", encode_base16(header) } - }, 256, BIND(complete, _1)); + send_result(std::move(value), size, BIND(complete, _1)); } // height/header notifications. diff --git a/src/protocols/electrum/protocol_electrum_server.cpp b/src/protocols/electrum/protocol_electrum_server.cpp index 50d1b47a..0f19b5d2 100644 --- a/src/protocols/electrum/protocol_electrum_server.cpp +++ b/src/protocols/electrum/protocol_electrum_server.cpp @@ -27,6 +27,7 @@ namespace server { #define CLASS protocol_electrum +using namespace system; using namespace network::rpc; using namespace std::placeholders; @@ -107,16 +108,38 @@ void protocol_electrum::handle_server_features(const code& ec, return; } - send_result(object_t + object_t value { { "genesis_hash", encode_hash(hash) }, { "hosts", self_hosts() }, - { "hash_function", string_t{ "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)); + }; + + if (!at_least(electrum::version::v1_6)) + { + value["hash_function"] = string_t{ "sha256" }; + } + + if (at_least(electrum::version::v1_7)) + { + value["method_flavours"] = object_t + { + { + // Unreliable verbose tx serialiation option is not supported. + // "Exact structure depends on bitcoind impl and version, and + // should not be relied upon." + "blockchain.transaction.broadcast_package", object_t + { + { "supports_verbose_true", false } + } + } + }; + } + + send_result(std::move(value), 1024, BIND(complete, _1)); } // This is not actually a subscription method. @@ -136,8 +159,10 @@ void protocol_electrum::handle_server_peers_subscribe(const code& ec, send_result(more_hosts(), 1024, BIND(complete, _1)); } +// Server does not send ping notifications (or perform other traffic shaping). void protocol_electrum::handle_server_ping(const code& ec, - rpc_interface::server_ping) NOEXCEPT + rpc_interface::server_ping, double pong_len, + const std::string& data) NOEXCEPT { if (stopped(ec)) return; @@ -148,8 +173,38 @@ void protocol_electrum::handle_server_ping(const code& ec, return; } - // Any receive, including ping, resets the base channel inactivity timer. - send_result(null_t{}, 42, BIND(complete, _1)); + // Default response of null_t. + value_t value{}; + size_t size{ 42 }; + + if (!at_least(electrum::version::v1_7)) + { + if (!data.empty() || is_nonzero(pong_len)) + { + send_code(error::wrong_version); + return; + } + } + else + { + size_t length{}; + data_chunk unused{}; + + // Base16 encoding validation expects whole octets (even char count). + if (!to_integer(length, pong_len) || (length != data.length()) || + !decode_base16(unused, data)) + { + send_code(error::invalid_argument); + return; + } + + // Treat empty as default (args look the same, may not be correct). + if (is_nonzero(length)) + value = string_t(length, '0'); + } + + // Length is limited by maximum_request (DoS protection). + send_result(std::move(value), size, BIND(complete, _1)); } // utilities diff --git a/src/protocols/electrum/protocol_electrum_transactions.cpp b/src/protocols/electrum/protocol_electrum_transactions.cpp index 3013fdaf..a9c8fec7 100644 --- a/src/protocols/electrum/protocol_electrum_transactions.cpp +++ b/src/protocols/electrum/protocol_electrum_transactions.cpp @@ -189,6 +189,8 @@ void protocol_electrum::handle_blockchain_transaction_get(const code& ec, return; } + size_t size{}; + boost::json::value value{}; if (!verbose) { const auto tx = query.get_wire_tx(link, true); @@ -198,53 +200,55 @@ void protocol_electrum::handle_blockchain_transaction_get(const code& ec, 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; + size = two * tx.size(); + value = encode_base16(tx); } - - if (const auto block = query.find_strong(link); !block.is_terminal()) + else { - using namespace system; - const auto top = query.get_top_confirmed(); - const auto height = query.get_height(block); - const auto block_hash = query.get_header_key(block); - - uint32_t timestamp{}; - if (height.is_terminal() || (block_hash == null_hash) || - !query.get_timestamp(timestamp, block)) + const auto tx = query.get_transaction(link, true); + if (!tx) { send_code(error::server_error); return; } - // Floor manages race between getting confirmed top and height. - const auto confirms = add1(floored_subtract(top, height.value)); + // Verbose is whatever bitcoind returns for getrawtransaction, lolz. + value = value_from(bitcoind(*tx)); + if (!value.is_object()) + { + send_code(error::server_error); + return; + } - 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; + size = two * tx->serialized_size(true); + if (const auto block = query.find_strong(link); !block.is_terminal()) + { + using namespace system; + const auto top = query.get_top_confirmed(); + const auto height = query.get_height(block); + const auto block_hash = query.get_header_key(block); + + uint32_t timestamp{}; + if (height.is_terminal() || (block_hash == null_hash) || + !query.get_timestamp(timestamp, block)) + { + 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& object = value.as_object(); + object["in_active_chain"] = true; + object["blockhash"] = encode_hash(block_hash); + object["confirmations"] = confirms; + object["blocktime"] = timestamp; + object["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)); + send_result(std::move(value), size, BIND(complete, _1)); } void protocol_electrum::handle_blockchain_transaction_get_merkle( diff --git a/test/protocols/electrum/electrum_scripthash.cpp b/test/protocols/electrum/electrum_scripthash.cpp index fc4acde6..6fe060e7 100644 --- a/test/protocols/electrum/electrum_scripthash.cpp +++ b/test/protocols/electrum/electrum_scripthash.cpp @@ -24,8 +24,11 @@ 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 std::string bogus_address{ "1JqDybm2nWTENrHvMyafbSXXtTk5Uv5QAn" }; static const std::string bogus_scripthash{ "9c2c84a6cf9809e08af19557e28d38257e6fee6981269760637a5f9dfb000b05" }; static const std::string found_scripthash{ "bad83872c90886be19b98734fd16741611efcd9f5de699c14b712675eec682f5" }; +static const chain::script bogus{ chain::script::to_pay_key_hash_pattern({ 0x42 }) }; +static const auto bogus_script = encode_base16(bogus.to_data(false)); BOOST_FIXTURE_TEST_SUITE(electrum_disabled_address_tests, electrum_disabled_address_index_setup_fixture) @@ -69,6 +72,16 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_scripthash_list_unspent__no_address_in BOOST_REQUIRE_EQUAL(result, not_implemented.value()); } +BOOST_AUTO_TEST_CASE(electrum__blockchain_address_subscribe__no_address_index__not_implemented) +{ + BOOST_REQUIRE(!query_.address_enabled()); + BOOST_REQUIRE(handshake(electrum::version::v1_1)); + + const auto request = R"({"id":1001,"method":"blockchain.address.subscribe","params":["%1%"]})" "\n"; + const auto result = get_error((boost::format(request) % bogus_address).str()); + BOOST_REQUIRE_EQUAL(result, not_implemented.value()); +} + BOOST_AUTO_TEST_CASE(electrum__blockchain_scripthash_subscribe__no_address_index__not_implemented) { BOOST_REQUIRE(!query_.address_enabled()); @@ -89,6 +102,16 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_scripthash_unsubscribe__no_address_ind BOOST_REQUIRE_EQUAL(result, not_implemented.value()); } +BOOST_AUTO_TEST_CASE(electrum__blockchain_scriptpubkey_subscribe__no_address_index__not_implemented) +{ + BOOST_REQUIRE(!query_.address_enabled()); + BOOST_REQUIRE(handshake(electrum::version::v1_7)); + + const auto request = R"({"id":1001,"method":"blockchain.scriptpubkey.subscribe","params":["%1%"]})" "\n"; + const auto result = get_error((boost::format(request) % bogus_script).str()); + BOOST_REQUIRE_EQUAL(result, not_implemented.value()); +} + BOOST_AUTO_TEST_SUITE_END() BOOST_FIXTURE_TEST_SUITE(electrum_tests, electrum_ten_block_setup_fixture) diff --git a/test/protocols/electrum/electrum_server.cpp b/test/protocols/electrum/electrum_server.cpp index 08d58ac6..b69577ab 100644 --- a/test/protocols/electrum/electrum_server.cpp +++ b/test/protocols/electrum/electrum_server.cpp @@ -24,6 +24,7 @@ BOOST_FIXTURE_TEST_SUITE(electrum_tests, electrum_ten_block_setup_fixture) using namespace system; 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 }; // server.add_peer @@ -132,7 +133,7 @@ BOOST_AUTO_TEST_CASE(electrum__server_features__extra_param__dropped) BOOST_AUTO_TEST_CASE(electrum__server_features__default_hosts__expected) { - BOOST_REQUIRE(handshake(electrum::version::v1_2)); + BOOST_REQUIRE(handshake(electrum::version::v1_4)); const auto response = get(R"({"id":300,"method":"server.features","params":[]})" "\n"); REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); @@ -166,6 +167,41 @@ BOOST_AUTO_TEST_CASE(electrum__server_features__default_hosts__expected) REQUIRE_NO_THROW_TRUE(host.at("ssl_port").is_null()); } +BOOST_AUTO_TEST_CASE(electrum__server_features__v1_6__hash_function_removed) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_6)); + + const auto response = get(R"({"id":300,"method":"server.features","params":[]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + + const auto& result = response.at("result").as_object(); + BOOST_REQUIRE(!result.contains("hash_function")); + REQUIRE_NO_THROW_TRUE(result.at("genesis_hash").is_string()); + REQUIRE_NO_THROW_TRUE(result.at("server_version").is_string()); + REQUIRE_NO_THROW_TRUE(result.at("protocol_min").is_string()); + REQUIRE_NO_THROW_TRUE(result.at("protocol_max").is_string()); + REQUIRE_NO_THROW_TRUE(result.at("pruning").is_null()); + REQUIRE_NO_THROW_TRUE(result.at("hosts").is_object()); +} + +BOOST_AUTO_TEST_CASE(electrum__server_features__method_flavours_v1_7__supports_verbose_true_false) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_7)); + + const auto response = get(R"({"id":300,"method":"server.features","params":[]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + + const auto& result = response.at("result").as_object(); + REQUIRE_NO_THROW_TRUE(result.at("method_flavours").is_object()); + + const auto& flavours = result.at("method_flavours").as_object(); + REQUIRE_NO_THROW_TRUE(flavours.at("blockchain.transaction.broadcast_package").is_object()); + + const auto& package = flavours.at("blockchain.transaction.broadcast_package").as_object(); + REQUIRE_NO_THROW_TRUE(package.at("supports_verbose_true").is_bool()); + BOOST_REQUIRE(!package.at("supports_verbose_true").as_bool()); +} + BOOST_AUTO_TEST_CASE(electrum__server_features__hosts__expected) { config_.server.electrum.self_binds.emplace_back("tcp.com:80"); @@ -253,28 +289,70 @@ BOOST_AUTO_TEST_CASE(electrum__server_peers_subscribe__configured_peers__expecte // server.ping -BOOST_AUTO_TEST_CASE(electrum__server_ping__always__null) +BOOST_AUTO_TEST_CASE(electrum__server_ping__jsonrpc_unspecified_no_params__dropped) { BOOST_REQUIRE(handshake(electrum::version::v1_2)); - const auto response = get(R"({"id":200,"method":"server.ping","params":[]})" "\n"); - REQUIRE_NO_THROW_TRUE(response.at("result").is_null()); + const auto response = get(R"({"id":201,"method":"server.ping"})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("dropped").as_bool()); } -BOOST_AUTO_TEST_CASE(electrum__server_ping__jsonrpc_unspecified_no_params__dropped) +BOOST_AUTO_TEST_CASE(electrum__server_ping__extra_param__dropped) { - BOOST_REQUIRE(handshake(electrum::version::v1_2)); + BOOST_REQUIRE(handshake(electrum::version::v1_7)); - const auto response = get(R"({"id":201,"method":"server.ping"})" "\n"); + const auto response = get(R"({"id":215,"method":"server.ping","params":[8,"12345678", 42]})" "\n"); REQUIRE_NO_THROW_TRUE(response.at("dropped").as_bool()); } -BOOST_AUTO_TEST_CASE(electrum__server_ping__extra_param__dropped) +BOOST_AUTO_TEST_CASE(electrum__server_ping__v1_2_defaults__null) { BOOST_REQUIRE(handshake(electrum::version::v1_2)); - const auto response = get(R"({"id":202,"method":"server.ping","params":["extra"]})" "\n"); - REQUIRE_NO_THROW_TRUE(response.at("dropped").as_bool()); + const auto response = get(R"({"id":200,"method":"server.ping","params":[]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_null()); +} + +BOOST_AUTO_TEST_CASE(electrum__server_ping__v1_7_defaults__null) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_7)); + + const auto response = get(R"({"id":210,"method":"server.ping","params":[]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_null()); +} + +// This may not be strictly compliant behavior (possibly empty string is correct). +BOOST_AUTO_TEST_CASE(electrum__server_ping__v1_7_default_values__null) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_7)); + + const auto response = get(R"({"id":210,"method":"server.ping","params":[0,""]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_null()); +} + +BOOST_AUTO_TEST_CASE(electrum__server_ping__v1_7__invalid_data_encoding__invalid_argument) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_7)); + + const auto result = get_error(R"({"id":211,"method":"server.ping","params":[8,"not_hex!"]})" "\n"); + BOOST_REQUIRE_EQUAL(result, invalid_argument.value()); +} + +BOOST_AUTO_TEST_CASE(electrum__server_ping__v1_7__mismatched_data_length__invalid_argument) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_7)); + + const auto result = get_error(R"({"id":213,"method":"server.ping","params":[5,"12345678"]})" "\n"); + BOOST_REQUIRE_EQUAL(result, invalid_argument.value()); +} + +BOOST_AUTO_TEST_CASE(electrum__server_ping__v1_7_data__expected_echo) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_7)); + + const auto response = get(R"({"id":214,"method":"server.ping","params":[8,"12345678"]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); + BOOST_REQUIRE_EQUAL(response.at("result").as_string(), "00000000"); } BOOST_AUTO_TEST_SUITE_END()