From 37ef6618185018a9c5f96cc248a28c9498aed532 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Thu, 10 Sep 2026 22:46:41 -0400 Subject: [PATCH 1/5] Serve the 1.7 testmempoolaccept, mempool.recent and ping notification. --- .../bitcoin/server/interfaces/electrum.hpp | 26 ++++---- .../server/protocols/protocol_electrum.hpp | 11 ++++ include/bitcoin/server/settings.hpp | 6 ++ src/parser.cpp | 20 ++++++ src/protocols/electrum/protocol_electrum.cpp | 5 ++ .../electrum/protocol_electrum_mempool.cpp | 16 +++++ .../electrum/protocol_electrum_server.cpp | 34 +++++++++- .../protocol_electrum_transactions.cpp | 66 +++++++++++++++++++ test/protocols/electrum/electrum_mempool.cpp | 27 ++++++++ .../electrum/electrum_transactions.cpp | 54 +++++++++++++++ 10 files changed, 253 insertions(+), 12 deletions(-) diff --git a/include/bitcoin/server/interfaces/electrum.hpp b/include/bitcoin/server/interfaces/electrum.hpp index 75b43df7..609eff22 100644 --- a/include/bitcoin/server/interfaces/electrum.hpp +++ b/include/bitcoin/server/interfaces/electrum.hpp @@ -73,6 +73,7 @@ struct electrum_methods method<"blockchain.transaction.get", string_t, optional>{ "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" }, + method<"blockchain.transaction.testmempoolaccept", value_t>{ "raw_txs" }, /// Server methods. method<"server.add_peer", object_t>{ "features" }, @@ -84,7 +85,8 @@ struct electrum_methods /// Mempool methods. method<"mempool.get_fee_histogram">{}, - method<"mempool.get_info">{} + method<"mempool.get_info">{}, + method<"mempool.recent">{} }; template @@ -133,16 +135,18 @@ struct electrum_methods using blockchain_transaction_get = at<31>; using blockchain_transaction_get_merkle = at<32>; using blockchain_transaction_id_from_position = at<33>; - - using server_add_peer = at<34>; - using server_banner = at<35>; - using server_donation_address = at<36>; - using server_features = at<37>; - using server_peers_subscribe = at<38>; - using server_ping = at<39>; - - using mempool_get_fee_histogram = at<40>; - using mempool_get_info = at<41>; + using blockchain_transaction_testmempoolaccept = at<34>; + + using server_add_peer = at<35>; + using server_banner = at<36>; + using server_donation_address = at<37>; + using server_features = at<38>; + using server_peers_subscribe = at<39>; + using server_ping = at<40>; + + using mempool_get_fee_histogram = at<41>; + using mempool_get_info = at<42>; + using mempool_recent = at<43>; }; /// The electrum handshake, published separately as it is served by its own diff --git a/include/bitcoin/server/protocols/protocol_electrum.hpp b/include/bitcoin/server/protocols/protocol_electrum.hpp index a3b15f53..b2489299 100644 --- a/include/bitcoin/server/protocols/protocol_electrum.hpp +++ b/include/bitcoin/server/protocols/protocol_electrum.hpp @@ -53,6 +53,8 @@ class BCS_API protocol_electrum witness_(session->server_settings().wallet.witness_prefix), channel_(std::dynamic_pointer_cast(channel)), notification_strand_(channel_->service().get_executor()), + ping_timer_(std::make_shared(session->log, + channel->strand())), network::tracker(session->log) { } @@ -61,6 +63,9 @@ class BCS_API protocol_electrum void stopping(const code& ec) NOEXCEPT override; protected: + void start_ping() NOEXCEPT; + void handle_ping(const code& ec) NOEXCEPT; + /// Terminal responder (attached last) for unclaimed methods. void handle_unclaimed( const network::rpc::request_t& request) NOEXCEPT override; @@ -182,6 +187,9 @@ class BCS_API protocol_electrum void handle_blockchain_transaction_get_merkle(const code& ec, rpc_interface::blockchain_transaction_get_merkle, const std::string& tx_hash, double height) NOEXCEPT; + void handle_blockchain_transaction_testmempoolaccept(const code& ec, + rpc_interface::blockchain_transaction_testmempoolaccept, + const interface::value_t& raw_txs) NOEXCEPT; void handle_blockchain_transaction_id_from_position(const code& ec, rpc_interface::blockchain_transaction_id_from_position, double height, double tx_pos, bool merkle) NOEXCEPT; @@ -205,6 +213,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_recent(const code& ec, + rpc_interface::mempool_recent) NOEXCEPT; void handle_mempool_get_info(const code& ec, rpc_interface::mempool_get_info) NOEXCEPT; @@ -385,6 +395,7 @@ class BCS_API protocol_electrum const uint8_t p2sh_; const uint32_t flags_; const std::string witness_; + network::deadline::ptr ping_timer_; std::atomic_bool stopping_{}; std::atomic_bool subscribed_height_{}; std::atomic_bool subscribed_header_{}; diff --git a/include/bitcoin/server/settings.hpp b/include/bitcoin/server/settings.hpp index f0cfdfda..69058b9d 100644 --- a/include/bitcoin/server/settings.hpp +++ b/include/bitcoin/server/settings.hpp @@ -105,6 +105,12 @@ class BCS_API settings /// Maximum cumulative number of address subscriptions per channel. uint32_t maximum_subscriptions{ 1'000'000 }; + /// Interval between unrequested pings, zero disables (1.7, not http). + uint32_t ping_interval_seconds{ 0 }; + + /// Number of hex characters of data carried by an unrequested ping. + uint32_t ping_size{ 0 }; + /// Minimum protocol version. system::config::version protocol_minimum{ 1, 0, 0, 0 }; diff --git a/src/parser.cpp b/src/parser.cpp index 760f9c34..9e0dcb6a 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -1358,6 +1358,16 @@ options_metadata parser::load_settings() THROWS value(&configured.server.electrum.maximum_subscriptions), "The maximum allowed address subscriptions per channel, defaults to '1000000'." ) + ( + "electrum.ping_interval_seconds", + value(&configured.server.electrum.ping_interval_seconds), + "The seconds between unrequested pings, defaults to '0' (disabled)." + ) + ( + "electrum.ping_size", + value(&configured.server.electrum.ping_size), + "The hex characters of unrequested ping data, defaults to '0'." + ) ( "electrum.protocol_minimum", value(&configured.server.electrum.protocol_minimum), @@ -1470,6 +1480,16 @@ options_metadata parser::load_settings() THROWS value(&configured.server.sparrow.maximum_subscriptions), "The maximum allowed address subscriptions per channel, defaults to '1000000'." ) + ( + "sparrow.ping_interval_seconds", + value(&configured.server.sparrow.ping_interval_seconds), + "The seconds between unrequested pings, defaults to '0' (disabled)." + ) + ( + "sparrow.ping_size", + value(&configured.server.sparrow.ping_size), + "The hex characters of unrequested ping data, defaults to '0'." + ) ( "sparrow.protocol_minimum", value(&configured.server.sparrow.protocol_minimum), diff --git a/src/protocols/electrum/protocol_electrum.cpp b/src/protocols/electrum/protocol_electrum.cpp index fc1398f5..da2fca2b 100644 --- a/src/protocols/electrum/protocol_electrum.cpp +++ b/src/protocols/electrum/protocol_electrum.cpp @@ -100,6 +100,7 @@ void protocol_electrum::start() NOEXCEPT 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_position, _1, _2, _3, _4, _5); + SUBSCRIBE_RPC(handle_blockchain_transaction_testmempoolaccept, _1, _2, _3); // Server methods. SUBSCRIBE_RPC(handle_server_add_peer, _1, _2, _3); @@ -112,6 +113,9 @@ void protocol_electrum::start() NOEXCEPT // Mempool methods. SUBSCRIBE_RPC(handle_mempool_get_fee_histogram, _1, _2); SUBSCRIBE_RPC(handle_mempool_get_info, _1, _2); + SUBSCRIBE_RPC(handle_mempool_recent, _1, _2); + + start_ping(); protocol_rpc::start(); } @@ -121,6 +125,7 @@ void protocol_electrum::stopping(const code& ec) NOEXCEPT BC_ASSERT(stranded()); stopping_.store(true); unsubscribe_chase(); + ping_timer_->stop(); protocol_rpc::stopping(ec); } diff --git a/src/protocols/electrum/protocol_electrum_mempool.cpp b/src/protocols/electrum/protocol_electrum_mempool.cpp index 760a0e20..7fb87f3c 100644 --- a/src/protocols/electrum/protocol_electrum_mempool.cpp +++ b/src/protocols/electrum/protocol_electrum_mempool.cpp @@ -47,6 +47,22 @@ void protocol_electrum::handle_mempool_get_fee_histogram(const code& ec, ////send_code(error::not_implemented); } +void protocol_electrum::handle_mempool_recent(const code& ec, + rpc_interface::mempool_recent) NOEXCEPT +{ + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_7)) + { + send_code(error::electrum::bad_request); + return; + } + + // There is no tx pool. + send_result(array_t{}, 42); +} + void protocol_electrum::handle_mempool_get_info(const code& ec, rpc_interface::mempool_get_info) NOEXCEPT { diff --git a/src/protocols/electrum/protocol_electrum_server.cpp b/src/protocols/electrum/protocol_electrum_server.cpp index 8fd0a4b8..8305be69 100644 --- a/src/protocols/electrum/protocol_electrum_server.cpp +++ b/src/protocols/electrum/protocol_electrum_server.cpp @@ -171,7 +171,39 @@ void protocol_electrum::handle_server_peers_subscribe(const code& ec, send_result(more_hosts(), 1024); } -// Server does not send ping notifications (or perform other traffic shaping). +// An unrequested ping is a notification, which http cannot carry. +void protocol_electrum::start_ping() NOEXCEPT +{ + BC_ASSERT(stranded()); + + const auto span = options().ping_interval_seconds; + if (stopped() || is_zero(span) || !at_least(electrum::version::v1_7) || + !(websocket() || downgraded())) + return; + + ping_timer_->start(BIND(handle_ping, _1), network::seconds(span)); +} + +void protocol_electrum::handle_ping(const code& ec) NOEXCEPT +{ + BC_ASSERT(stranded()); + + if (stopped() || ec == network::error::operation_canceled) + return; + + if (ec) + { + stop(ec); + return; + } + + const auto size = options().ping_size; + send_notification("server.ping", array_t{ string_t(size, '0') }, + add1(size)); + + start_ping(); +} + void protocol_electrum::handle_server_ping(const code& ec, rpc_interface::server_ping, double pong_len, const std::string& data) NOEXCEPT diff --git a/src/protocols/electrum/protocol_electrum_transactions.cpp b/src/protocols/electrum/protocol_electrum_transactions.cpp index c0d68944..45e4d23d 100644 --- a/src/protocols/electrum/protocol_electrum_transactions.cpp +++ b/src/protocols/electrum/protocol_electrum_transactions.cpp @@ -33,6 +33,9 @@ using namespace std::placeholders; BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) +// The specification requires that at least 25 transactions are accepted. +constexpr size_t maximum_test_txs = 25; + // Electrum sends a single value param (invalid json-rpc). This is enabled via // the lax json-rpc body value !strict option, mapping the singleton to array. void protocol_electrum::handle_blockchain_transaction_broadcast(const code& ec, @@ -147,6 +150,69 @@ void protocol_electrum::handle_blockchain_transaction_broadcast_package( send_result(result, 42 + size); } +void protocol_electrum::handle_blockchain_transaction_testmempoolaccept( + const code& ec, rpc_interface::blockchain_transaction_testmempoolaccept, + const interface::value_t& raw_txs) NOEXCEPT +{ + if (stopped(ec)) + return; + + if (!at_least(electrum::version::v1_7)) + { + send_code(error::electrum::bad_request); + return; + } + + if (!std::holds_alternative(raw_txs.value())) + { + send_code(error::electrum::bad_request); + return; + } + + const auto& txs_hex = std::get(raw_txs.value()); + if (txs_hex.empty() || txs_hex.size() > maximum_test_txs) + { + send_code(error::electrum::bad_request); + return; + } + + array_t out{}; + out.reserve(txs_hex.size()); + + for (const auto& tx_hex: txs_hex) + { + if (!std::holds_alternative(tx_hex.value())) + { + send_code(error::electrum::bad_request); + return; + } + + read::base16::copy hexer{ std::get(tx_hex.value()) }; + const chain::transaction tx{ hexer, true }; + if (!tx.is_valid() || !hexer.is_exhausted()) + { + send_code(error::electrum::bad_request); + return; + } + + // There is no tx pool, so acceptance is validation against the chain. + const auto fault = validate_tx(tx); + object_t value + { + { "txid", encode_hash(tx.hash(false)) }, + { "wtxid", encode_hash(tx.hash(true)) }, + { "allowed", !fault } + }; + + if (fault) + value.emplace("reason", fault.message()); + + out.emplace_back(std::move(value)); + } + + send_result(std::move(out), add1(out.size()) * 128u); +} + void protocol_electrum::handle_blockchain_transaction_get(const code& ec, rpc_interface::blockchain_transaction_get, const std::string& tx_hash, bool verbose) NOEXCEPT diff --git a/test/protocols/electrum/electrum_mempool.cpp b/test/protocols/electrum/electrum_mempool.cpp index 5836d9e3..103555ff 100644 --- a/test/protocols/electrum/electrum_mempool.cpp +++ b/test/protocols/electrum/electrum_mempool.cpp @@ -104,4 +104,31 @@ BOOST_AUTO_TEST_CASE(electrum__mempool_get_info__empty_params__expected) BOOST_REQUIRE_EQUAL(result.at("incrementalrelayfee").as_double(), config_.node.minimum_bump_rate); } +// mempool.recent + +BOOST_AUTO_TEST_CASE(electrum__mempool_recent__insufficient_version__wrong_version) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_6)); + + const auto result = get_error(R"({"id":710,"method":"mempool.recent","params":[]})" "\n"); + BOOST_REQUIRE_EQUAL(result, wrong_version.value()); +} + +BOOST_AUTO_TEST_CASE(electrum__mempool_recent__extra_param__dropped) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_7)); + + const auto response = get(R"({"id":711,"method":"mempool.recent","params":[42]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("dropped").as_bool()); +} + +BOOST_AUTO_TEST_CASE(electrum__mempool_recent__empty_params__empty) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_7)); + + const auto response = get(R"({"id":712,"method":"mempool.recent","params":[]})" "\n"); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").as_array().empty()); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/protocols/electrum/electrum_transactions.cpp b/test/protocols/electrum/electrum_transactions.cpp index 977dba34..4e18defe 100644 --- a/test/protocols/electrum/electrum_transactions.cpp +++ b/test/protocols/electrum/electrum_transactions.cpp @@ -186,6 +186,60 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_broadcast_package__two_tra // blockchain.transaction.get +BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_testmempoolaccept__insufficient_version__wrong_version) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_6)); + + const auto result = get_error(R"({"id":90,"method":"blockchain.transaction.testmempoolaccept","params":[[]]})" "\n"); + BOOST_REQUIRE_EQUAL(result, wrong_version.value()); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_testmempoolaccept__not_array__invalid_argument) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_7)); + + const auto result = get_error(R"({"id":91,"method":"blockchain.transaction.testmempoolaccept","params":["not_an_array"]})" "\n"); + BOOST_REQUIRE_EQUAL(result, invalid_argument.value()); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_testmempoolaccept__empty_array__invalid_argument) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_7)); + + const auto result = get_error(R"({"id":92,"method":"blockchain.transaction.testmempoolaccept","params":[[]]})" "\n"); + BOOST_REQUIRE_EQUAL(result, invalid_argument.value()); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_testmempoolaccept__invalid_encoding__invalid_argument) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_7)); + + const auto result = get_error(R"({"id":93,"method":"blockchain.transaction.testmempoolaccept","params":[["xxxx"]]})" "\n"); + BOOST_REQUIRE_EQUAL(result, invalid_argument.value()); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_testmempoolaccept__genesis_coinbase__not_allowed) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_7)); + + const auto& coinbase = *test::genesis.transactions_ptr()->front(); + const auto tx0_text = encode_base16(coinbase.to_data(true)); + constexpr auto request = R"({"id":94,"method":"blockchain.transaction.testmempoolaccept","params":[["%1%"]]})" "\n"; + const auto response = get((boost_format(request) % tx0_text).str()); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_array()); + + const auto& results = response.at("result").as_array(); + BOOST_REQUIRE_EQUAL(results.size(), 1u); + + const auto& value = results.at(0).as_object(); + BOOST_REQUIRE_EQUAL(value.at("txid").as_string(), encode_hash(coinbase.hash(false))); + BOOST_REQUIRE_EQUAL(value.at("wtxid").as_string(), encode_hash(coinbase.hash(true))); + REQUIRE_NO_THROW_TRUE(value.at("allowed").is_bool()); + BOOST_REQUIRE(!value.at("allowed").as_bool()); + REQUIRE_NO_THROW_TRUE(value.at("reason").is_string()); +} + BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get__empty_hash__invalid_argument) { BOOST_REQUIRE(handshake(electrum::version::v1_0)); From 03068fbe2047b5d89dad833c02e0c9e11a4c71cb Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Thu, 10 Sep 2026 22:54:56 -0400 Subject: [PATCH 2/5] Carry the network wallet context in server settings. --- include/bitcoin/server/settings.hpp | 6 ++++ src/parser.cpp | 15 +++++++++ src/settings.cpp | 51 ++++++++++++++++------------- 3 files changed, 49 insertions(+), 23 deletions(-) diff --git a/include/bitcoin/server/settings.hpp b/include/bitcoin/server/settings.hpp index 69058b9d..76ff8dfb 100644 --- a/include/bitcoin/server/settings.hpp +++ b/include/bitcoin/server/settings.hpp @@ -182,7 +182,13 @@ class BCS_API settings system::config::byte p2kh_prefix; system::config::byte p2sh_prefix; + system::config::byte wif_prefix; std::string witness_prefix; + uint32_t hd_private_prefix; + uint32_t hd_public_prefix; + + /// The configured prefixes as a wallet context. + system::wallet::context to_context() const NOEXCEPT; }; struct bitcoind_server diff --git a/src/parser.cpp b/src/parser.cpp index 9e0dcb6a..99f1d18c 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -908,11 +908,26 @@ options_metadata parser::load_settings() THROWS value(&configured.server.wallet.p2sh_prefix), "The pay-to-script-hash address prefix, defaults to '5' (use '196' for testnet)." ) + ( + "wallet.wif_prefix", + value(&configured.server.wallet.wif_prefix), + "The wallet import format prefix, defaults to '128' (use '239' for testnet)." + ) ( "wallet.witness_prefix", value(&configured.server.wallet.witness_prefix), "The witness address prefix, defaults to 'bc' (use 'tb' for testnet)." ) + ( + "wallet.hd_private_prefix", + value(&configured.server.wallet.hd_private_prefix), + "The extended private key prefix, defaults to '76066276' (use '70615956' for testnet)." + ) + ( + "wallet.hd_public_prefix", + value(&configured.server.wallet.hd_public_prefix), + "The extended public key prefix, defaults to '76067358' (use '71979618' for testnet)." + ) /* [admin] */ ( diff --git a/src/settings.cpp b/src/settings.cpp index 24e85a01..94376e10 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -82,9 +82,6 @@ namespace server { using namespace system::wallet; -// witness_address defines mainnet/testnet prefixes only (bip173). -constexpr auto regtest_witness_prefix = "bcrt"; - // settings::settings settings::settings(system::chain::selection context, const embedded_pages& native, const embedded_pages& admin) NOEXCEPT @@ -100,39 +97,47 @@ settings::wallet_settings::wallet_settings() NOEXCEPT { } -settings::wallet_settings::wallet_settings( +static const context& to_wallet_context( system::chain::selection context) NOEXCEPT - : p2kh_prefix(payment_address::mainnet_p2kh), - p2sh_prefix(payment_address::mainnet_p2sh), - witness_prefix(witness_address::mainnet) { - // Testnet and regtest share base58 versions, regtest differs in bech32. switch (context) { case system::chain::selection::testnet3: case system::chain::selection::testnet4: - { - p2kh_prefix = payment_address::testnet_p2kh; - p2sh_prefix = payment_address::testnet_p2sh; - witness_prefix = witness_address::testnet; - break; - } + return ctx::btc::test; case system::chain::selection::regtest: - { - p2kh_prefix = payment_address::testnet_p2kh; - p2sh_prefix = payment_address::testnet_p2sh; - witness_prefix = regtest_witness_prefix; - break; - } + return ctx::btc::regtest; case system::chain::selection::mainnet: case system::chain::selection::none: default: - { - break; - } + return ctx::btc::main; } } +settings::wallet_settings::wallet_settings( + system::chain::selection context) NOEXCEPT +{ + const auto& wallet = to_wallet_context(context); + p2kh_prefix = wallet.p2kh; + p2sh_prefix = wallet.p2sh; + wif_prefix = wallet.wif; + witness_prefix = wallet.p2w; + hd_private_prefix = wallet.hd.prv; + hd_public_prefix = wallet.hd.pub; +} + +context settings::wallet_settings::to_context() const NOEXCEPT +{ + return + { + { hd_private_prefix, hd_public_prefix }, + witness_prefix, + p2kh_prefix, + p2sh_prefix, + wif_prefix + }; +} + // settings::embedded_pages span_value settings::embedded_pages::css() const NOEXCEPT { return {}; } span_value settings::embedded_pages::html() const NOEXCEPT { return {}; } From 6929df589d778b002e2b46625e776aa7a9e16f08 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Thu, 10 Sep 2026 22:54:57 -0400 Subject: [PATCH 3/5] Parameterize descriptor network in bitcoind protocols. --- include/bitcoin/server/parsers/bitcoind_scan.hpp | 6 ++++-- include/bitcoin/server/protocols/protocol_bitcoind.hpp | 4 +++- src/parsers/bitcoind_scan.cpp | 9 +++++---- src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp | 6 +++--- src/protocols/bitcoind/protocol_bitcoind_transaction.cpp | 2 +- src/protocols/bitcoind/protocol_bitcoind_utility.cpp | 4 ++-- 6 files changed, 18 insertions(+), 13 deletions(-) diff --git a/include/bitcoin/server/parsers/bitcoind_scan.hpp b/include/bitcoin/server/parsers/bitcoind_scan.hpp index 60974c26..3fe6400e 100644 --- a/include/bitcoin/server/parsers/bitcoind_scan.hpp +++ b/include/bitcoin/server/parsers/bitcoind_scan.hpp @@ -27,12 +27,14 @@ namespace server { /// Expand a scan object, a descriptor string or { "desc", "range" } object, /// to its derived output scripts (false if malformed or underivable). BCS_API bool expand_scan_object(system::chain::scripts& out, - const network::rpc::value_t& item) NOEXCEPT; + const network::rpc::value_t& item, + const system::wallet::context& context) NOEXCEPT; /// As expand_scan_object, retaining embedded scripts and key origins. BCS_API bool expand_scan_signings( system::wallet::descriptor::signing::list& out, - const network::rpc::value_t& item) NOEXCEPT; + const network::rpc::value_t& item, + const system::wallet::context& context) NOEXCEPT; /// Parse a derivation range, an end index or a [begin, end] pair. BCS_API bool parse_scan_range(uint32_t& begin, uint32_t& end, diff --git a/include/bitcoin/server/protocols/protocol_bitcoind.hpp b/include/bitcoin/server/protocols/protocol_bitcoind.hpp index b854c04c..b229c508 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind.hpp @@ -55,7 +55,8 @@ class BCS_API protocol_bitcoind p2kh_(session->server_settings().wallet.p2kh_prefix), p2sh_(session->server_settings().wallet.p2sh_prefix), flags_(session->system_settings().flags()), - witness_(session->server_settings().wallet.witness_prefix) + witness_(session->server_settings().wallet.witness_prefix), + context_(session->server_settings().wallet.to_context()) { } @@ -130,6 +131,7 @@ class BCS_API protocol_bitcoind const uint8_t p2sh_; const uint32_t flags_; const std::string witness_; + const system::wallet::context context_; }; } // namespace server diff --git a/src/parsers/bitcoind_scan.cpp b/src/parsers/bitcoind_scan.cpp index 2a187abd..d3edfd33 100644 --- a/src/parsers/bitcoind_scan.cpp +++ b/src/parsers/bitcoind_scan.cpp @@ -29,10 +29,11 @@ using namespace network::rpc; BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) // A scan object is a descriptor string or { "desc", "range" } object. -bool expand_scan_object(chain::scripts& out, const value_t& item) NOEXCEPT +bool expand_scan_object(chain::scripts& out, const value_t& item, + const wallet::context& context) NOEXCEPT { wallet::descriptor::signing::list signings{}; - if (!expand_scan_signings(signings, item)) + if (!expand_scan_signings(signings, item, context)) return false; out.reserve(out.size() + signings.size()); @@ -43,7 +44,7 @@ bool expand_scan_object(chain::scripts& out, const value_t& item) NOEXCEPT } bool expand_scan_signings(wallet::descriptor::signing::list& out, - const value_t& item) NOEXCEPT + const value_t& item, const wallet::context& context) NOEXCEPT { std::string expression{}; uint32_t begin{}; @@ -78,7 +79,7 @@ bool expand_scan_signings(wallet::descriptor::signing::list& out, return false; } - const wallet::descriptor parsed{ expression }; + const wallet::descriptor parsed{ expression, context }; if (!parsed || to_bool(shift_right(end, 31u)) || floored_subtract(end, begin) >= maximum_range) return false; diff --git a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp index 5d848633..5795011e 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp @@ -831,7 +831,7 @@ void protocol_bitcoind_blockchain::do_scan_tx_out_set( chain::scripts scripts{}; for (const auto& item: *objects) { - if (!expand_scan_object(scripts, item)) + if (!expand_scan_object(scripts, item, context_)) { POST(complete_scan, error::bitcoind::invalid_address_or_key, std::move(result), zero); @@ -1277,7 +1277,7 @@ bool protocol_bitcoind_blockchain::handle_get_descriptor_activity( chain::scripts derived{}; for (const auto& item: scanobjects) { - if (!expand_scan_object(derived, item)) + if (!expand_scan_object(derived, item, context_)) { send_error(error::bitcoind::invalid_address_or_key); return true; @@ -1446,7 +1446,7 @@ bool protocol_bitcoind_blockchain::handle_scan_blocks(const code& ec, chain::scripts scripts{}; for (const auto& item: scanobjects) { - if (!expand_scan_object(scripts, item)) + if (!expand_scan_object(scripts, item, context_)) { send_error(error::bitcoind::invalid_address_or_key); return true; diff --git a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp index 80aa8021..93bf2469 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp @@ -711,7 +711,7 @@ bool protocol_bitcoind_transaction::handle_utxo_update_psbt(const code& ec, descriptor::signing::list signings{}; for (const auto& item: descriptors) { - if (!expand_scan_signings(signings, item)) + if (!expand_scan_signings(signings, item, context_)) { send_error(error::bitcoind::invalid_address_or_key); return true; diff --git a/src/protocols/bitcoind/protocol_bitcoind_utility.cpp b/src/protocols/bitcoind/protocol_bitcoind_utility.cpp index 913192b8..293ba90a 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_utility.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_utility.cpp @@ -219,7 +219,7 @@ bool protocol_bitcoind_utility::handle_derive_addresses(const code& ec, if (stopped(ec)) return false; - const wallet::descriptor parsed{ expression }; + const wallet::descriptor parsed{ expression, context_ }; if (!parsed) { send_error(error::bitcoind::invalid_address_or_key); @@ -277,7 +277,7 @@ bool protocol_bitcoind_utility::handle_get_descriptor_info(const code& ec, if (stopped(ec)) return false; - const wallet::descriptor parsed{ expression }; + const wallet::descriptor parsed{ expression, context_ }; if (!parsed) { send_error(error::bitcoind::invalid_address_or_key); From 50ce842332027641de8a4a8c24e6504bc7b6b6c3 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Thu, 10 Sep 2026 23:51:13 -0400 Subject: [PATCH 4/5] Add server wallet settings tests. --- test/settings.cpp | 76 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/test/settings.cpp b/test/settings.cpp index 2ac46f90..7772030b 100644 --- a/test/settings.cpp +++ b/test/settings.cpp @@ -23,6 +23,7 @@ BOOST_AUTO_TEST_SUITE(settings_tests) using namespace bc::network; using namespace bc::system::chain; using version = system::config::version; +using namespace bc::system::wallet; // [log] @@ -291,4 +292,79 @@ BOOST_AUTO_TEST_CASE(server__stratum_v2_server__defaults__expected) BOOST_REQUIRE(server.expiration() == minutes(60)); } + +// [wallet] + +#define MAINNET_M "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi" +#define TESTNET_M "tprv8ZgxMBicQKsPeDgjzdC36fs6bMjGApWDNLR9erAXMs5skhMv36j9MV5ecvfavji5khqjWaWSFhN3YcCUUdiKH6isR4Pwy3U5y5egddBr16m" + +BOOST_AUTO_TEST_CASE(wallet__defaults__mainnet__expected) +{ + const server::settings::wallet_settings instance{ selection::mainnet }; + BOOST_REQUIRE_EQUAL(instance.p2kh_prefix, prefix::p2kh::main::btc); + BOOST_REQUIRE_EQUAL(instance.p2sh_prefix, prefix::p2sh::main::btc); + BOOST_REQUIRE_EQUAL(instance.wif_prefix, prefix::wif::main::btc); + BOOST_REQUIRE_EQUAL(instance.witness_prefix, prefix::p2w::main::btc); + BOOST_REQUIRE_EQUAL(instance.hd_private_prefix, prefix::hd::main::btc.prv); + BOOST_REQUIRE_EQUAL(instance.hd_public_prefix, prefix::hd::main::btc.pub); +} + +BOOST_AUTO_TEST_CASE(wallet__defaults__testnet__expected) +{ + const server::settings::wallet_settings instance{ selection::testnet3 }; + BOOST_REQUIRE_EQUAL(instance.p2kh_prefix, prefix::p2kh::test::btc); + BOOST_REQUIRE_EQUAL(instance.p2sh_prefix, prefix::p2sh::test::btc); + BOOST_REQUIRE_EQUAL(instance.wif_prefix, prefix::wif::test::btc); + BOOST_REQUIRE_EQUAL(instance.witness_prefix, prefix::p2w::test::btc); + BOOST_REQUIRE_EQUAL(instance.hd_private_prefix, prefix::hd::test::btc.prv); + BOOST_REQUIRE_EQUAL(instance.hd_public_prefix, prefix::hd::test::btc.pub); +} + +BOOST_AUTO_TEST_CASE(wallet__defaults__regtest__testnet_versions_with_regtest_witness) +{ + const server::settings::wallet_settings instance{ selection::regtest }; + BOOST_REQUIRE_EQUAL(instance.p2kh_prefix, prefix::p2kh::test::btc); + BOOST_REQUIRE_EQUAL(instance.p2sh_prefix, prefix::p2sh::test::btc); + BOOST_REQUIRE_EQUAL(instance.wif_prefix, prefix::wif::test::btc); + BOOST_REQUIRE_EQUAL(instance.witness_prefix, prefix::p2w::regtest::btc); + BOOST_REQUIRE_EQUAL(instance.hd_private_prefix, prefix::hd::test::btc.prv); +} + +BOOST_AUTO_TEST_CASE(wallet__to_context__mainnet__matches_predefined) +{ + const server::settings::wallet_settings instance{ selection::mainnet }; + BOOST_REQUIRE_EQUAL(instance.to_context().hd_prefixes(), ctx::btc::main.hd_prefixes()); + BOOST_REQUIRE_EQUAL(instance.to_context().versions(), ctx::btc::main.versions()); + BOOST_REQUIRE_EQUAL(instance.to_context().p2w, ctx::btc::main.p2w); +} + +BOOST_AUTO_TEST_CASE(wallet__to_context__regtest__matches_predefined) +{ + const server::settings::wallet_settings instance{ selection::regtest }; + BOOST_REQUIRE_EQUAL(instance.to_context().hd_prefixes(), ctx::btc::regtest.hd_prefixes()); + BOOST_REQUIRE_EQUAL(instance.to_context().versions(), ctx::btc::regtest.versions()); + BOOST_REQUIRE_EQUAL(instance.to_context().p2w, ctx::btc::regtest.p2w); +} + +BOOST_AUTO_TEST_CASE(wallet__to_context__testnet__parses_testnet_descriptor_only) +{ + const server::settings::wallet_settings instance{ selection::testnet3 }; + BOOST_REQUIRE(descriptor("pkh(" TESTNET_M "/1/*)", instance.to_context())); + BOOST_REQUIRE(!descriptor("pkh(" MAINNET_M "/1/*)", instance.to_context())); +} + +BOOST_AUTO_TEST_CASE(wallet__to_context__mainnet__parses_mainnet_descriptor_only) +{ + const server::settings::wallet_settings instance{ selection::mainnet }; + BOOST_REQUIRE(descriptor("pkh(" MAINNET_M "/1/*)", instance.to_context())); + BOOST_REQUIRE(!descriptor("pkh(" TESTNET_M "/1/*)", instance.to_context())); +} + +BOOST_AUTO_TEST_CASE(wallet__to_context__configured_prefixes__override_network) +{ + server::settings::wallet_settings instance{ selection::mainnet }; + instance.hd_private_prefix = prefix::hd::test::btc.prv; + instance.hd_public_prefix = prefix::hd::test::btc.pub; + BOOST_REQUIRE(descriptor("pkh(" TESTNET_M "/1/*)", instance.to_context())); +} BOOST_AUTO_TEST_SUITE_END() From d6a6f3ec689bdd9bb92d3268a2f4cf18c203531b Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Fri, 11 Sep 2026 00:48:31 -0400 Subject: [PATCH 5/5] Delint (hiding warning). --- src/settings.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/settings.cpp b/src/settings.cpp index 94376e10..7904fa84 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -117,13 +117,13 @@ static const context& to_wallet_context( settings::wallet_settings::wallet_settings( system::chain::selection context) NOEXCEPT { - const auto& wallet = to_wallet_context(context); - p2kh_prefix = wallet.p2kh; - p2sh_prefix = wallet.p2sh; - wif_prefix = wallet.wif; - witness_prefix = wallet.p2w; - hd_private_prefix = wallet.hd.prv; - hd_public_prefix = wallet.hd.pub; + const auto& value = to_wallet_context(context); + p2kh_prefix = value.p2kh; + p2sh_prefix = value.p2sh; + wif_prefix = value.wif; + witness_prefix = value.p2w; + hd_private_prefix = value.hd.prv; + hd_public_prefix = value.hd.pub; } context settings::wallet_settings::to_context() const NOEXCEPT