diff --git a/include/bitcoin/server/protocols/protocol_electrum.hpp b/include/bitcoin/server/protocols/protocol_electrum.hpp index d5239ced..bf0e84fb 100644 --- a/include/bitcoin/server/protocols/protocol_electrum.hpp +++ b/include/bitcoin/server/protocols/protocol_electrum.hpp @@ -239,7 +239,7 @@ class BCS_API protocol_electrum void blockchain_block_headers(size_t starting, size_t quantity, size_t waypoint, bool single) NOEXCEPT; - /// Completion handlers (for long-running address queries). + /// Completion handlers (for long-running or other async queries). /// ----------------------------------------------------------------------- void get_balance(const hash_digest& hash) NOEXCEPT; @@ -257,6 +257,9 @@ class BCS_API protocol_electrum void complete_get_mempool(const code& ec, const histories& histories) NOEXCEPT; void complete_list_unspent(const code& ec, const unspents& unspents) NOEXCEPT; + void handle_estimate_fee(const code& ec, uint64_t fee) NOEXCEPT; + void complete_estimate_fee(const code& ec, uint64_t fee) NOEXCEPT; + /// Notification event handlers. /// ----------------------------------------------------------------------- diff --git a/src/parser.cpp b/src/parser.cpp index 6cde4acd..c454af2b 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -1328,6 +1328,11 @@ options_metadata parser::load_settings() THROWS value(&configured.node.defer_confirmation), "Defer confirmation, defaults to 'false'." ) + ( + "node.fee_estimate_horizon", + value(&configured.node.fee_estimate_horizon), + "Fee estimation horizon, limited to 1008, defaults to '0' (0 disables)." + ) ////( //// "node.headers_first", //// value(&configured.node.headers_first), diff --git a/src/protocols/electrum/protocol_electrum_fees.cpp b/src/protocols/electrum/protocol_electrum_fees.cpp index ad3f9936..1600b7d4 100644 --- a/src/protocols/electrum/protocol_electrum_fees.cpp +++ b/src/protocols/electrum/protocol_electrum_fees.cpp @@ -31,10 +31,23 @@ using namespace std::placeholders; BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) +using mode_t = node::estimator::mode; +mode_t mode_from_string(const std::string& mode) NOEXCEPT +{ + if (mode.empty()) return mode_t::basic; + if (mode == "basic") return mode_t::basic; + if (mode == "geometric") return mode_t::geometric; + if (mode == "economical") return mode_t::economical; + if (mode == "conservative") return mode_t::conservative; + return mode_t::unknown; +} + void protocol_electrum::handle_blockchain_estimate_fee(const code& ec, rpc_interface::blockchain_estimate_fee, double number, const std::string& mode) NOEXCEPT { + BC_ASSERT(stranded()); + if (stopped(ec)) return; @@ -51,23 +64,57 @@ void protocol_electrum::handle_blockchain_estimate_fee(const code& ec, return; } - if (!mode.empty() && - !at_least(electrum::version::v1_6)) + if (!mode.empty() && !at_least(electrum::version::v1_6)) + { + send_code(error::invalid_argument); + return; + } + + const auto mode_ = mode_from_string(mode); + if (mode_ == mode_t::unknown) { send_code(error::invalid_argument); return; } - // TODO: integrate fee estimator. - ////send_code(error::not_implemented); + estimate(target, mode_, BIND(handle_estimate_fee, _1, _2)); +} + +void protocol_electrum::handle_estimate_fee(const code& ec, + uint64_t fee) NOEXCEPT +{ + POST(complete_estimate_fee, ec, fee); +} + +void protocol_electrum::complete_estimate_fee(const code& ec, + uint64_t fee) NOEXCEPT +{ + BC_ASSERT(stranded()); + + if (stopped()) + return; + + const auto disabled = + ec == node::error::estimate_false || + ec == node::error::estimate_disabled || + ec == node::error::estimate_premature; + + if (!disabled && ec) + { + // node::error::estimates_failed, implies store fault. + send_code(error::server_error); + return; + } // If not enough information to make an estimate, -1 is returned. - send_result(-1, 42); + send_result(disabled ? -1 : possible_narrow_sign_cast(fee), 42); } void protocol_electrum::handle_blockchain_relay_fee(const code& ec, rpc_interface::blockchain_relay_fee) NOEXCEPT { + BC_ASSERT(stranded()); + if (stopped(ec)) return; diff --git a/test/protocols/blocks.cpp b/test/protocols/blocks.cpp index 74071676..04444bf3 100644 --- a/test/protocols/blocks.cpp +++ b/test/protocols/blocks.cpp @@ -233,6 +233,7 @@ const block bogus_block10 0x0b, inputs { + // Null points in non-first tx (coinbase confusion). input { point{}, diff --git a/test/protocols/electrum/electrum_fees.cpp b/test/protocols/electrum/electrum_fees.cpp index 7b42d267..05cbf1be 100644 --- a/test/protocols/electrum/electrum_fees.cpp +++ b/test/protocols/electrum/electrum_fees.cpp @@ -48,17 +48,40 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_estimate_fee__mode_invalid_version__in { BOOST_REQUIRE(handshake(electrum::version::v1_4)); - const auto result = get_error(R"({"id":801,"method":"blockchain.estimatefee","params":[42,"mode"]})" "\n"); + const auto result = get_error(R"({"id":801,"method":"blockchain.estimatefee","params":[42,"basic"]})" "\n"); BOOST_REQUIRE_EQUAL(result, invalid_argument.value()); } -////BOOST_AUTO_TEST_CASE(electrum__blockchain_estimate_fee__valid__not_implemented) -////{ -//// BOOST_REQUIRE(handshake(electrum::version::v1_6)); -//// -//// const auto result = get_error(R"({"id":801,"method":"blockchain.estimatefee","params":[42,"mode"]})" "\n"); -//// BOOST_REQUIRE_EQUAL(result, not_implemented.value()); -////} +BOOST_AUTO_TEST_CASE(electrum__blockchain_estimate_fee__nvalid_mode__invalid_argument) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_4)); + + const auto result = get_error(R"({"id":801,"method":"blockchain.estimatefee","params":[42,"bogus"]})" "\n"); + BOOST_REQUIRE_EQUAL(result, invalid_argument.value()); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_estimate_fee__uninitialized__negative_one) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_6)); + + const auto response = get(R"({"id":801,"method":"blockchain.estimatefee","params":[0,"basic"]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("result").as_int64(), -1); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_estimate_fee__zero_basic__negative_one) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_6)); + + // Trigger node chaser event to initialize fee estimator. + notify(node::chase::block, { 9_u32 }); + + const auto response = get(R"({"id":801,"method":"blockchain.estimatefee","params":[0,"basic"]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_int64()); + + // None of the first 10 blocks have fees, so no estimate is obtained. + BOOST_REQUIRE_EQUAL(response.at("result").as_int64(), -1); +} // blockchain.relayfee diff --git a/test/protocols/electrum/electrum_setup_fixture.cpp b/test/protocols/electrum/electrum_setup_fixture.cpp index 61cca945..9a10cc5f 100644 --- a/test/protocols/electrum/electrum_setup_fixture.cpp +++ b/test/protocols/electrum/electrum_setup_fixture.cpp @@ -66,6 +66,7 @@ electrum_setup_fixture::electrum_setup_fixture(const initializer& setup, database_settings.interval_depth = 2; node_settings.delay_inbound = false; node_settings.minimum_fee_rate = 99.0; + node_settings.fee_estimate_horizon = 8; network_settings.inbound.connections = 0; network_settings.outbound.connections = 0; @@ -74,7 +75,16 @@ electrum_setup_fixture::electrum_setup_fixture(const initializer& setup, BOOST_REQUIRE_MESSAGE(!ec, ec.message()); BOOST_REQUIRE_MESSAGE(setup(query_), "electrum initialize"); - // Run the server. + std::promise started{}; + server_.start([&](const code& ec) NOEXCEPT + { + started.set_value(ec); + }); + + // Block until server is started. + ec = started.get_future().get(); + BOOST_REQUIRE_MESSAGE(!ec, ec.message()); + std::promise running{}; server_.run([&](const code& ec) NOEXCEPT {