From 1c216faa85db73e2c51bd1495fd8a39af8c7dc45 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Sun, 17 May 2026 23:32:17 -0400 Subject: [PATCH 01/10] Add node.fee_estimate_horizon to parser. --- src/parser.cpp | 5 +++++ 1 file changed, 5 insertions(+) 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), From 136d2d312fc0da0693b45934d9998fa073f4ac9a Mon Sep 17 00:00:00 2001 From: evoskuil Date: Mon, 18 May 2026 00:29:58 -0400 Subject: [PATCH 02/10] Integrate node fee estimator. --- .../server/protocols/protocol_electrum.hpp | 4 +- .../electrum/protocol_electrum_fees.cpp | 44 ++++++++++++++++--- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/include/bitcoin/server/protocols/protocol_electrum.hpp b/include/bitcoin/server/protocols/protocol_electrum.hpp index d5239ced..723a556b 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,8 @@ 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 complete_estimate_fee(const code& ec, uint64_t fee) NOEXCEPT; + /// Notification event handlers. /// ----------------------------------------------------------------------- diff --git a/src/protocols/electrum/protocol_electrum_fees.cpp b/src/protocols/electrum/protocol_electrum_fees.cpp index ad3f9936..4b7ca478 100644 --- a/src/protocols/electrum/protocol_electrum_fees.cpp +++ b/src/protocols/electrum/protocol_electrum_fees.cpp @@ -31,6 +31,17 @@ 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 @@ -51,18 +62,41 @@ 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; } - // TODO: integrate fee estimator. - ////send_code(error::not_implemented); + const auto mode_ = mode_from_string(mode); + if (mode_ == mode_t::unknown) + { + send_code(error::invalid_argument); + return; + } + + estimate(target, mode_, BIND(complete_estimate_fee, _1, _2)); +} + +void protocol_electrum::complete_estimate_fee(const code& ec, + uint64_t fee) NOEXCEPT +{ + if (stopped()) + return; + + const auto disabled = + ec == node::error::estimates_disabled || + ec == node::error::estimates_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 : fee, 42); } void protocol_electrum::handle_blockchain_relay_fee(const code& ec, From a5bf2a7083dded638a9119b32fa9f2feecd020c3 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Mon, 18 May 2026 01:41:06 -0400 Subject: [PATCH 03/10] Electrum fee estimate test updates (WIP). --- test/protocols/electrum/electrum_fees.cpp | 41 +++++++++++++++---- .../electrum/electrum_setup_fixture.cpp | 1 + 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/test/protocols/electrum/electrum_fees.cpp b/test/protocols/electrum/electrum_fees.cpp index 7b42d267..dfecf6ea 100644 --- a/test/protocols/electrum/electrum_fees.cpp +++ b/test/protocols/electrum/electrum_fees.cpp @@ -48,17 +48,42 @@ 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__expected) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_6)); + + // TODO: chaser start() requires server start(). + ////// Trigger node chaser event to initialize fee estimator. + ////BOOST_REQUIRE(query_.set(test::bogus_block10, database::context{ 0, 10, 0 }, false, false)); + ////BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::bogus_block10.hash()), true)); + ////notify(node::chase::organized, { 10_u32 }); + + 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(), 42); + 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..9012fb60 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; From 755e4b4619e86ce7c5c36ed9fe6c400c817a8c51 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Mon, 18 May 2026 01:43:28 -0400 Subject: [PATCH 04/10] Disable 2 electrum tests (WIP). --- test/protocols/electrum/electrum_fees.cpp | 48 +++++++++++------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/test/protocols/electrum/electrum_fees.cpp b/test/protocols/electrum/electrum_fees.cpp index dfecf6ea..f7f73c99 100644 --- a/test/protocols/electrum/electrum_fees.cpp +++ b/test/protocols/electrum/electrum_fees.cpp @@ -60,30 +60,30 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_estimate_fee__nvalid_mode__invalid_arg 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__expected) -{ - BOOST_REQUIRE(handshake(electrum::version::v1_6)); - - // TODO: chaser start() requires server start(). - ////// Trigger node chaser event to initialize fee estimator. - ////BOOST_REQUIRE(query_.set(test::bogus_block10, database::context{ 0, 10, 0 }, false, false)); - ////BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::bogus_block10.hash()), true)); - ////notify(node::chase::organized, { 10_u32 }); - - 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(), 42); - BOOST_REQUIRE_EQUAL(response.at("result").as_int64(), -1); -} +////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__expected) +////{ +//// BOOST_REQUIRE(handshake(electrum::version::v1_6)); +//// +//// // TODO: chaser start() requires server start(). +//// ////// Trigger node chaser event to initialize fee estimator. +//// ////BOOST_REQUIRE(query_.set(test::bogus_block10, database::context{ 0, 10, 0 }, false, false)); +//// ////BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::bogus_block10.hash()), true)); +//// ////notify(node::chase::organized, { 10_u32 }); +//// +//// 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(), 42); +//// BOOST_REQUIRE_EQUAL(response.at("result").as_int64(), -1); +////} // blockchain.relayfee From 7ea289f5848f026fe398f06ab04732d8051714ab Mon Sep 17 00:00:00 2001 From: evoskuil Date: Mon, 18 May 2026 14:53:48 -0400 Subject: [PATCH 05/10] Comment. --- test/protocols/blocks.cpp | 1 + 1 file changed, 1 insertion(+) 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{}, From 9d7415c3caee128dd40634a07ebdd2ead9cf0f14 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Mon, 18 May 2026 15:10:08 -0400 Subject: [PATCH 06/10] Start node in test fixture (not just run()). --- test/protocols/electrum/electrum_setup_fixture.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/protocols/electrum/electrum_setup_fixture.cpp b/test/protocols/electrum/electrum_setup_fixture.cpp index 9012fb60..9a10cc5f 100644 --- a/test/protocols/electrum/electrum_setup_fixture.cpp +++ b/test/protocols/electrum/electrum_setup_fixture.cpp @@ -75,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 { From 7fd50371422f89c8f14374ca75e30d2263d49323 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Mon, 18 May 2026 15:10:28 -0400 Subject: [PATCH 07/10] Fix electrum fee estimate return type. --- src/protocols/electrum/protocol_electrum_fees.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/protocols/electrum/protocol_electrum_fees.cpp b/src/protocols/electrum/protocol_electrum_fees.cpp index 4b7ca478..6d53f23a 100644 --- a/src/protocols/electrum/protocol_electrum_fees.cpp +++ b/src/protocols/electrum/protocol_electrum_fees.cpp @@ -96,7 +96,7 @@ void protocol_electrum::complete_estimate_fee(const code& ec, } // If not enough information to make an estimate, -1 is returned. - send_result(disabled ? -1 : fee, 42); + send_result(disabled ? -1 : possible_narrow_sign_cast(fee), 42); } void protocol_electrum::handle_blockchain_relay_fee(const code& ec, From b922cc861d2ea27eac5ed2f15c32cf8c0aaec56b Mon Sep 17 00:00:00 2001 From: evoskuil Date: Mon, 18 May 2026 15:27:34 -0400 Subject: [PATCH 08/10] All not found fee estimate to return -1 (false) and update test. --- .../electrum/protocol_electrum_fees.cpp | 1 + test/protocols/electrum/electrum_fees.cpp | 46 +++++++++---------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/protocols/electrum/protocol_electrum_fees.cpp b/src/protocols/electrum/protocol_electrum_fees.cpp index 6d53f23a..daf1e190 100644 --- a/src/protocols/electrum/protocol_electrum_fees.cpp +++ b/src/protocols/electrum/protocol_electrum_fees.cpp @@ -85,6 +85,7 @@ void protocol_electrum::complete_estimate_fee(const code& ec, return; const auto disabled = + ec == node::error::estimate_failed || ec == node::error::estimates_disabled || ec == node::error::estimates_premature; diff --git a/test/protocols/electrum/electrum_fees.cpp b/test/protocols/electrum/electrum_fees.cpp index f7f73c99..05cbf1be 100644 --- a/test/protocols/electrum/electrum_fees.cpp +++ b/test/protocols/electrum/electrum_fees.cpp @@ -60,30 +60,28 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_estimate_fee__nvalid_mode__invalid_arg 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__expected) -////{ -//// BOOST_REQUIRE(handshake(electrum::version::v1_6)); -//// -//// // TODO: chaser start() requires server start(). -//// ////// Trigger node chaser event to initialize fee estimator. -//// ////BOOST_REQUIRE(query_.set(test::bogus_block10, database::context{ 0, 10, 0 }, false, false)); -//// ////BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::bogus_block10.hash()), true)); -//// ////notify(node::chase::organized, { 10_u32 }); -//// -//// 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(), 42); -//// BOOST_REQUIRE_EQUAL(response.at("result").as_int64(), -1); -////} +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 From e9848f77cf1b559cd4e9627e73e5f0d2709cc8d6 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Mon, 18 May 2026 16:37:11 -0400 Subject: [PATCH 09/10] Adapt to node error code changes. --- src/protocols/electrum/protocol_electrum_fees.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/protocols/electrum/protocol_electrum_fees.cpp b/src/protocols/electrum/protocol_electrum_fees.cpp index daf1e190..88327078 100644 --- a/src/protocols/electrum/protocol_electrum_fees.cpp +++ b/src/protocols/electrum/protocol_electrum_fees.cpp @@ -85,9 +85,9 @@ void protocol_electrum::complete_estimate_fee(const code& ec, return; const auto disabled = - ec == node::error::estimate_failed || - ec == node::error::estimates_disabled || - ec == node::error::estimates_premature; + ec == node::error::estimate_false || + ec == node::error::estimate_disabled || + ec == node::error::estimate_premature; if (!disabled && ec) { From 7db88e9b27d182dfd9ec541089eab34574a8e93d Mon Sep 17 00:00:00 2001 From: evoskuil Date: Mon, 18 May 2026 21:25:31 -0400 Subject: [PATCH 10/10] Fix thread safety on electrum fee estimate completion. --- .../bitcoin/server/protocols/protocol_electrum.hpp | 1 + src/protocols/electrum/protocol_electrum_fees.cpp | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/include/bitcoin/server/protocols/protocol_electrum.hpp b/include/bitcoin/server/protocols/protocol_electrum.hpp index 723a556b..bf0e84fb 100644 --- a/include/bitcoin/server/protocols/protocol_electrum.hpp +++ b/include/bitcoin/server/protocols/protocol_electrum.hpp @@ -257,6 +257,7 @@ 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/protocols/electrum/protocol_electrum_fees.cpp b/src/protocols/electrum/protocol_electrum_fees.cpp index 88327078..1600b7d4 100644 --- a/src/protocols/electrum/protocol_electrum_fees.cpp +++ b/src/protocols/electrum/protocol_electrum_fees.cpp @@ -46,6 +46,8 @@ 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; @@ -75,12 +77,20 @@ void protocol_electrum::handle_blockchain_estimate_fee(const code& ec, return; } - estimate(target, mode_, BIND(complete_estimate_fee, _1, _2)); + 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; @@ -103,6 +113,8 @@ void protocol_electrum::complete_estimate_fee(const code& ec, void protocol_electrum::handle_blockchain_relay_fee(const code& ec, rpc_interface::blockchain_relay_fee) NOEXCEPT { + BC_ASSERT(stranded()); + if (stopped(ec)) return;