From 19e064420b0cad0ffba45f5eec6720006805aa91 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 29 Sep 2026 11:50:12 -0400 Subject: [PATCH 01/21] Require rpc fixture store setup to succeed. --- test/protocols/fixture/rpc_setup_fixture.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/protocols/fixture/rpc_setup_fixture.cpp b/test/protocols/fixture/rpc_setup_fixture.cpp index 7262dd96..8152b6a5 100644 --- a/test/protocols/fixture/rpc_setup_fixture.cpp +++ b/test/protocols/fixture/rpc_setup_fixture.cpp @@ -64,7 +64,7 @@ rpc_setup_fixture::rpc_setup_fixture(const initializer& setup, ec = config_.initialize(); BOOST_REQUIRE_MESSAGE(!ec, ec.message()); - setup(query_); + BOOST_REQUIRE(setup(query_)); // The node (chasers and address pool), bypassed by default. if (start) From 849bf752f10a598b976888554d3d315daea1368b Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 29 Sep 2026 11:50:12 -0400 Subject: [PATCH 02/21] Add faulted store fixtures and bitcoind store fault tests. --- test/mocks/blocks.cpp | 15 ++++++ test/mocks/blocks.hpp | 2 + test/protocols/bitcoind/bitcoind_rpc.cpp | 49 +++++++++++++++++++ .../bitcoind/bitcoind_setup_fixture.hpp | 24 +++++++++ 4 files changed, 90 insertions(+) diff --git a/test/mocks/blocks.cpp b/test/mocks/blocks.cpp index 099b9975..39d67c3f 100644 --- a/test/mocks/blocks.cpp +++ b/test/mocks/blocks.cpp @@ -160,6 +160,21 @@ bool setup_broadcast_store(query_t& query) NOEXCEPT query.push_confirmed(query.to_header(block1c.hash()), true); } +// A faulted store: block 3 is confirmed without its transactions. +bool setup_unassociated_store(query_t& query) NOEXCEPT +{ + return setup_three_block_store(query) && + query.set(block3.header(), database::context{ 0, 3, 0 }, {}, false) && + query.push_confirmed(query.to_header(block3_hash), false); +} + +// A faulted store: the confirmed top is not a header. +bool setup_dangling_store(query_t& query) NOEXCEPT +{ + return setup_three_block_store(query) && + query.push_confirmed(database::header_link{ 42 }, false); +} + bool setup_three_block_unconfirmed_address_store(query_t& query) NOEXCEPT { return query.initialize(genesis) && diff --git a/test/mocks/blocks.hpp b/test/mocks/blocks.hpp index 64b789dd..8e5d3ebb 100644 --- a/test/mocks/blocks.hpp +++ b/test/mocks/blocks.hpp @@ -109,6 +109,8 @@ bool setup_three_block_witness_store(query_t& query) NOEXCEPT; bool setup_three_block_confirmed_address_store(query_t& query) NOEXCEPT; bool setup_three_block_unconfirmed_address_store(query_t& query) NOEXCEPT; bool setup_broadcast_store(query_t& query) NOEXCEPT; +bool setup_unassociated_store(query_t& query) NOEXCEPT; +bool setup_dangling_store(query_t& query) NOEXCEPT; // TODO: move. boost::json::value parse_json(std::string_view value) NOEXCEPT; diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index a5eb03d7..67dbeec4 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -2849,6 +2849,55 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__getrawtransaction__witness_tx__wtxid_differs) BOOST_REQUIRE_EQUAL(result.at("vsize").as_int64(), (weight + 3) / 4); } +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblockstats__missing_prevouts__internal_error) +{ + BOOST_REQUIRE(has_code(rpc("getblockstats", "[2]"), -32603)); +} + +BOOST_AUTO_TEST_SUITE_END() + +// unassociated +// ---------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_SUITE(bitcoind_unassociated_tests, bitcoind_unassociated_setup_fixture) + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblockstats__unassociated__internal_error) +{ + BOOST_REQUIRE(has_code(rpc("getblockstats", "[3]"), -32603)); +} + +BOOST_AUTO_TEST_SUITE_END() + +// dangling +// ---------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_SUITE(bitcoind_dangling_tests, bitcoind_dangling_setup_fixture) + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__gettxoutsetinfo__dangling__internal_error) +{ + BOOST_REQUIRE(has_code(rpc("gettxoutsetinfo"), -32603)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getdifficulty__dangling__internal_error) +{ + BOOST_REQUIRE(has_code(rpc("getdifficulty"), -32603)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblockchaininfo__dangling__internal_error) +{ + BOOST_REQUIRE(has_code(rpc("getblockchaininfo"), -32603)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getchainstates__dangling__internal_error) +{ + BOOST_REQUIRE(has_code(rpc("getchainstates"), -32603)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__scantxoutset__dangling__internal_error) +{ + BOOST_REQUIRE(has_code(rpc("scantxoutset", "[\"start\", [\"addr(1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa)\"]]"), -32603)); +} + BOOST_AUTO_TEST_SUITE_END() // submission diff --git a/test/protocols/bitcoind/bitcoind_setup_fixture.hpp b/test/protocols/bitcoind/bitcoind_setup_fixture.hpp index 6bb71941..540f8334 100644 --- a/test/protocols/bitcoind/bitcoind_setup_fixture.hpp +++ b/test/protocols/bitcoind/bitcoind_setup_fixture.hpp @@ -244,6 +244,30 @@ struct bitcoind_witness_setup_fixture } }; +struct bitcoind_unassociated_setup_fixture + : bitcoind_setup_fixture +{ + inline bitcoind_unassociated_setup_fixture() + : bitcoind_setup_fixture([](test::query_t& query) + { + return test::setup_unassociated_store(query); + }) + { + } +}; + +struct bitcoind_dangling_setup_fixture + : bitcoind_setup_fixture +{ + inline bitcoind_dangling_setup_fixture() + : bitcoind_setup_fixture([](test::query_t& query) + { + return test::setup_dangling_store(query); + }) + { + } +}; + // Configured with a populated address pool -- loaded from a hosts file. // The pool is loaded by the start sequence, which the base fixture bypasses. struct bitcoind_hosts_setup_fixture From 06cf26ff94ee5e4d5646af24cbd0278f05720193 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 29 Sep 2026 11:25:43 -0400 Subject: [PATCH 03/21] Add error, settings, merkle, native, admin and server node tests. --- test/error.cpp | 136 ++++++++++++++++++ test/parsers/native_target.cpp | 19 +++ test/protocols/admin/admin_diagnostics.cpp | 23 +++ test/protocols/admin/admin_setup_fixture.cpp | 9 ++ test/protocols/admin/admin_setup_fixture.hpp | 1 + test/protocols/native/native_block.cpp | 73 ++++++++++ .../protocols/native/native_configuration.cpp | 124 ++++++++++++++++ .../protocols/native/native_setup_fixture.cpp | 84 ++++++++++- .../protocols/native/native_setup_fixture.hpp | 69 +++++++++ test/settings.cpp | 21 +++ test/utilities/merkle.cpp | 24 ++++ 11 files changed, 582 insertions(+), 1 deletion(-) diff --git a/test/error.cpp b/test/error.cpp index 982326f2..0246bea4 100644 --- a/test/error.cpp +++ b/test/error.cpp @@ -281,4 +281,140 @@ BOOST_AUTO_TEST_CASE(error_t__code__server_error__true_expected_message) BOOST_REQUIRE_EQUAL(ec.message(), "server_error"); } +// electrum::translate + +BOOST_AUTO_TEST_CASE(error_t__electrum_translate__success__success) +{ + BOOST_REQUIRE(!error::electrum::translate(error::success, error::electrum::bad_request)); +} + +BOOST_AUTO_TEST_CASE(error_t__electrum_translate__electrum_code__unchanged) +{ + const code ec{ error::electrum::server_busy }; + BOOST_REQUIRE(error::electrum::translate(ec, error::electrum::bad_request) == ec); +} + +BOOST_AUTO_TEST_CASE(error_t__electrum_translate__depth_limited__excessive_history) +{ + const code ec{ database::error::depth_limited }; + BOOST_REQUIRE(error::electrum::translate(ec, error::electrum::bad_request) == error::electrum::excessive_history); +} + +BOOST_AUTO_TEST_CASE(error_t__electrum_translate__database_code__daemon_error) +{ + const code ec{ database::error::integrity }; + BOOST_REQUIRE(error::electrum::translate(ec, error::electrum::bad_request) == error::electrum::daemon_error); +} + +BOOST_AUTO_TEST_CASE(error_t__electrum_translate__other_code__failure) +{ + const code ec{ error::not_found }; + BOOST_REQUIRE(error::electrum::translate(ec, error::electrum::bad_request) == error::electrum::bad_request); +} + +// btcd::translate + +BOOST_AUTO_TEST_CASE(error_t__btcd_translate__success__success) +{ + BOOST_REQUIRE(!error::btcd::translate(error::success, error::btcd::misc_error)); +} + +BOOST_AUTO_TEST_CASE(error_t__btcd_translate__btcd_code__unchanged) +{ + const code ec{ error::btcd::invalid_parameter }; + BOOST_REQUIRE(error::btcd::translate(ec, error::btcd::misc_error) == ec); +} + +BOOST_AUTO_TEST_CASE(error_t__btcd_translate__database_code__internal_error) +{ + const code ec{ database::error::integrity }; + BOOST_REQUIRE(error::btcd::translate(ec, error::btcd::misc_error) == error::btcd::internal_error); +} + +BOOST_AUTO_TEST_CASE(error_t__btcd_translate__other_code__failure) +{ + const code ec{ error::not_found }; + BOOST_REQUIRE(error::btcd::translate(ec, error::btcd::misc_error) == error::btcd::misc_error); +} + +// bitcoind::translate + +BOOST_AUTO_TEST_CASE(error_t__bitcoind_translate__success__success) +{ + BOOST_REQUIRE(!error::bitcoind::translate(error::success, error::bitcoind::misc_error)); +} + +BOOST_AUTO_TEST_CASE(error_t__bitcoind_translate__bitcoind_code__unchanged) +{ + const code ec{ error::bitcoind::invalid_parameter }; + BOOST_REQUIRE(error::bitcoind::translate(ec, error::bitcoind::misc_error) == ec); +} + +BOOST_AUTO_TEST_CASE(error_t__bitcoind_translate__database_code__internal_error) +{ + const code ec{ database::error::integrity }; + BOOST_REQUIRE(error::bitcoind::translate(ec, error::bitcoind::misc_error) == error::bitcoind::internal_error); +} + +BOOST_AUTO_TEST_CASE(error_t__bitcoind_translate__other_code__failure) +{ + const code ec{ error::not_found }; + BOOST_REQUIRE(error::bitcoind::translate(ec, error::bitcoind::misc_error) == error::bitcoind::misc_error); +} + +// bitcoind::reject + +BOOST_AUTO_TEST_CASE(error_t__bitcoind_reject__node_orphan_block__prev_blk_not_found) +{ + BOOST_REQUIRE_EQUAL(error::bitcoind::reject(node::error::orphan_block), "prev-blk-not-found"); +} + +BOOST_AUTO_TEST_CASE(error_t__bitcoind_reject__node_orphan_header__prev_blk_not_found) +{ + BOOST_REQUIRE_EQUAL(error::bitcoind::reject(node::error::orphan_header), "prev-blk-not-found"); +} + +BOOST_AUTO_TEST_CASE(error_t__bitcoind_reject__node_duplicate_block__duplicate) +{ + BOOST_REQUIRE_EQUAL(error::bitcoind::reject(node::error::duplicate_block), "duplicate"); +} + +BOOST_AUTO_TEST_CASE(error_t__bitcoind_reject__node_duplicate_header__duplicate) +{ + BOOST_REQUIRE_EQUAL(error::bitcoind::reject(node::error::duplicate_header), "duplicate"); +} + +BOOST_AUTO_TEST_CASE(error_t__bitcoind_reject__empty_transaction__bad_txns_vin_empty) +{ + BOOST_REQUIRE_EQUAL(error::bitcoind::reject(system::error::empty_transaction), "bad-txns-vin-empty"); +} + +BOOST_AUTO_TEST_CASE(error_t__bitcoind_reject__unmapped_transaction_code__message) +{ + const code ec{ system::error::double_spend }; + BOOST_REQUIRE_EQUAL(error::bitcoind::reject(ec), ec.message()); +} + +BOOST_AUTO_TEST_CASE(error_t__bitcoind_reject__invalid_proof_of_work__high_hash) +{ + BOOST_REQUIRE_EQUAL(error::bitcoind::reject(system::error::invalid_proof_of_work), "high-hash"); +} + +BOOST_AUTO_TEST_CASE(error_t__bitcoind_reject__block_weight_limit__bad_blk_weight) +{ + BOOST_REQUIRE_EQUAL(error::bitcoind::reject(system::error::block_weight_limit), "bad-blk-weight"); +} + +BOOST_AUTO_TEST_CASE(error_t__bitcoind_reject__unmapped_block_code__message) +{ + const code ec{ system::error::checkpoint_conflict }; + BOOST_REQUIRE_EQUAL(error::bitcoind::reject(ec), ec.message()); +} + +BOOST_AUTO_TEST_CASE(error_t__bitcoind_reject__other_code__message) +{ + const code ec{ error::not_found }; + BOOST_REQUIRE_EQUAL(error::bitcoind::reject(ec), "not_found"); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/parsers/native_target.cpp b/test/parsers/native_target.cpp index 5a326264..04eb7556 100644 --- a/test/parsers/native_target.cpp +++ b/test/parsers/native_target.cpp @@ -63,6 +63,25 @@ BOOST_AUTO_TEST_CASE(parsers__native_target__invalid_target__invalid_target) BOOST_REQUIRE_EQUAL(native_target(out, "/v3/invalid"), server::error::invalid_target); } +BOOST_AUTO_TEST_CASE(parsers__native_target__top_invalid_subcomponent__invalid_subcomponent) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(native_target(out, "/v3/top/invalid"), server::error::invalid_subcomponent); +} + +BOOST_AUTO_TEST_CASE(parsers__native_target__input_invalid_subcomponent__invalid_subcomponent) +{ + const std::string path = "/v3/input/0000000000000000000000000000000000000000000000000000000000000000/3/invalid"; + request_t out{}; + BOOST_REQUIRE_EQUAL(native_target(out, path), server::error::invalid_subcomponent); +} + +BOOST_AUTO_TEST_CASE(parsers__native_target__block_missing_id_type__missing_id_type) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(native_target(out, "/v3/block"), server::error::missing_id_type); +} + // configuration BOOST_AUTO_TEST_CASE(parsers__native_target__configuration_valid__expected) diff --git a/test/protocols/admin/admin_diagnostics.cpp b/test/protocols/admin/admin_diagnostics.cpp index ce3bfcaa..e0caa14c 100644 --- a/test/protocols/admin/admin_diagnostics.cpp +++ b/test/protocols/admin/admin_diagnostics.cpp @@ -109,6 +109,19 @@ BOOST_AUTO_TEST_CASE(admin__event_subscribe__json__previous_zero) BOOST_REQUIRE_EQUAL(response.at("previous").as_int64(), 0); } +// dispatch (http) +// ---------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(admin__dispatch__invalid_target__bad_request) +{ + BOOST_REQUIRE(get_status("/v1/bogus") == status::bad_request); +} + +BOOST_AUTO_TEST_CASE(admin__dispatch__html__bad_request) +{ + BOOST_REQUIRE(get_status("/v1/log/subscribe?filter=1&format=html") == status::bad_request); +} + // subscribe (websockets) // ---------------------------------------------------------------------------- @@ -155,6 +168,16 @@ BOOST_AUTO_TEST_CASE(admin__ws_log_subscribe__html__error_eof) BOOST_REQUIRE(ws_dropped("/v1/log/subscribe?filter=1&format=html")); } +BOOST_AUTO_TEST_CASE(admin__ws_dispatch__invalid_target__ignored) +{ + BOOST_REQUIRE(!ws_upgrade()); + ws_send("/v1/bogus"); + + const auto ack = ws_get_json("/v1/log/subscribe?filter=1024"); + REQUIRE_NO_THROW_TRUE(ack.at("previous").is_int64()); + BOOST_REQUIRE_EQUAL(ack.at("previous").as_int64(), 0); +} + // streams (websockets) // ---------------------------------------------------------------------------- diff --git a/test/protocols/admin/admin_setup_fixture.cpp b/test/protocols/admin/admin_setup_fixture.cpp index 732b1dfc..476839ab 100644 --- a/test/protocols/admin/admin_setup_fixture.cpp +++ b/test/protocols/admin/admin_setup_fixture.cpp @@ -204,6 +204,15 @@ network::boost_code admin_setup_fixture::ws_upgrade() return ec; } +void admin_setup_fixture::ws_send(std::string_view message) +{ + network::boost_code ec{}; + BOOST_CHECK(websocket_.has_value()); + + websocket_.value().write(net::buffer(message), ec); + BOOST_CHECK_MESSAGE(!ec, ec.message()); +} + data_chunk admin_setup_fixture::ws_receive() { flat_buffer buffer{}; diff --git a/test/protocols/admin/admin_setup_fixture.hpp b/test/protocols/admin/admin_setup_fixture.hpp index 2af58151..4d54a115 100644 --- a/test/protocols/admin/admin_setup_fixture.hpp +++ b/test/protocols/admin/admin_setup_fixture.hpp @@ -43,6 +43,7 @@ struct admin_setup_fixture boost::json::value get_json(std::string_view target); network::boost_code ws_upgrade(); + void ws_send(std::string_view message); system::data_chunk ws_receive(); std::string ws_receive_text(); boost::json::value ws_receive_json(); diff --git a/test/protocols/native/native_block.cpp b/test/protocols/native/native_block.cpp index 0b80157c..7ed8f2cd 100644 --- a/test/protocols/native/native_block.cpp +++ b/test/protocols/native/native_block.cpp @@ -181,6 +181,30 @@ BOOST_AUTO_TEST_CASE(native__ws_top_subscribe__reorganized__emits_top_height) BOOST_REQUIRE_EQUAL(to_string(ws_receive()), "0b"); } +BOOST_AUTO_TEST_CASE(native__ws_top_subscribe__notify_data__expected) +{ + BOOST_REQUIRE(!ws_upgrade()); + BOOST_REQUIRE(query_.set(test::mock_block10, database::context{ 0, 10, 0 }, {}, false, false)); + BOOST_REQUIRE_EQUAL(ws_get_data("/v1/top/subscribe?format=data"), base16_chunk("09")); + + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::mock_block10.hash()), true)); + notify(node::chases::block{ 10 }); + + BOOST_REQUIRE_EQUAL(ws_receive(), base16_chunk("0a")); +} + +BOOST_AUTO_TEST_CASE(native__ws_top_subscribe__notify_json__expected) +{ + BOOST_REQUIRE(!ws_upgrade()); + BOOST_REQUIRE(query_.set(test::mock_block10, database::context{ 0, 10, 0 }, {}, false, false)); + BOOST_REQUIRE_EQUAL(ws_get_json("/v1/top/subscribe?format=json").as_int64(), 9); + + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::mock_block10.hash()), true)); + notify(node::chases::block{ 10 }); + + BOOST_REQUIRE_EQUAL(test::parse_json(to_string(ws_receive())).as_int64(), 10); +} + // dispatch // ---------------------------------------------------------------------------- @@ -215,6 +239,29 @@ BOOST_AUTO_TEST_CASE(native__ws_dispatch__html__error_eof) BOOST_REQUIRE(ws_dropped("/v1/top?format=html")); } +BOOST_AUTO_TEST_CASE(native__ws_dispatch__invalid_target__ignored) +{ + BOOST_REQUIRE(!ws_upgrade()); + ws_send("/v1/bogus"); + BOOST_REQUIRE_EQUAL(ws_get_text("/v1/top?format=text"), "09"); +} + +BOOST_AUTO_TEST_CASE(native__ws_dispatch__unsupported_parameter__error_eof) +{ + BOOST_REQUIRE(!ws_upgrade()); + BOOST_REQUIRE(ws_dropped("/v1/top?format=json&witness=false")); +} + +BOOST_AUTO_TEST_CASE(native__dispatch__absolute_form_target__bad_request) +{ + BOOST_REQUIRE_EQUAL(get_status("http://localhost/v1/top?format=json"), http::status::bad_request); +} + +BOOST_AUTO_TEST_CASE(native__dispatch__opaque_origin__forbidden) +{ + BOOST_REQUIRE_EQUAL(get_status("/v1/top?format=json", http::field::origin, "null"), http::status::forbidden); +} + // block // ---------------------------------------------------------------------------- @@ -661,4 +708,30 @@ BOOST_AUTO_TEST_CASE(native__file__missing__not_found) BOOST_REQUIRE_EQUAL(get_status("/missing.css"), http::status::not_found); } +BOOST_AUTO_TEST_SUITE_END() + +BOOST_FIXTURE_TEST_SUITE(native_no_default_tests, native_no_default_setup_fixture) + +BOOST_AUTO_TEST_CASE(native__no_default__extensionless__not_implemented) +{ + BOOST_REQUIRE_EQUAL(get_status("/some/route"), http::status::not_implemented); +} + +BOOST_AUTO_TEST_SUITE_END() + +// hosts +// ---------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_SUITE(native_hosts_tests, native_hosts_setup_fixture) + +BOOST_AUTO_TEST_CASE(native__hosts__unlisted_host__bad_request) +{ + BOOST_REQUIRE_EQUAL(get_status("/v1/top?format=json"), http::status::bad_request); +} + +BOOST_AUTO_TEST_CASE(native__hosts__listed_host__expected) +{ + BOOST_REQUIRE_EQUAL(get_status("/v1/top?format=json", http::field::host, "example.com"), http::status::ok); +} + BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file diff --git a/test/protocols/native/native_configuration.cpp b/test/protocols/native/native_configuration.cpp index f8e8c4bf..e4f47e50 100644 --- a/test/protocols/native/native_configuration.cpp +++ b/test/protocols/native/native_configuration.cpp @@ -62,3 +62,127 @@ BOOST_AUTO_TEST_CASE(native__ws_configuration__default__json) } BOOST_AUTO_TEST_SUITE_END() + +BOOST_FIXTURE_TEST_SUITE(native_limited_tests, native_limited_setup_fixture) + +BOOST_AUTO_TEST_CASE(native__configuration__limited_blocks_unreached_milestone__pruned_zero) +{ + const auto response = get_json("/v1/configuration?format=json"); + BOOST_REQUIRE(response.is_object()); + BOOST_REQUIRE_EQUAL(response.as_object().at("pruned").as_int64(), 0); +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_FIXTURE_TEST_SUITE(server_node_tests, server_node_setup_fixture) + +static const std::string unavailable{ "192.0.2.1:65009" }; + +BOOST_AUTO_TEST_CASE(server_node__server_config__always__configuration) +{ + BOOST_REQUIRE(&server_.server_config() == &config_); +} + +BOOST_AUTO_TEST_CASE(server_node__server_settings__always__server_settings) +{ + BOOST_REQUIRE(&server_.server_settings() == &config_.server); +} + +BOOST_AUTO_TEST_CASE(server_node__run__default__success) +{ + BOOST_REQUIRE(!run([](server::configuration&) {})); +} + +BOOST_AUTO_TEST_CASE(server_node__run__inbound_unavailable__failure) +{ + BOOST_REQUIRE(run([](server::configuration& config) + { + config.network.inbound.binds = { { unavailable } }; + config.network.inbound.connections = 1; + })); +} + +BOOST_AUTO_TEST_CASE(server_node__run__admin_unavailable__failure) +{ + BOOST_REQUIRE(run([](server::configuration& config) + { + config.server.admin.path = "unused"; + config.server.admin.binds = { { unavailable } }; + config.server.admin.connections = 1; + })); +} + +BOOST_AUTO_TEST_CASE(server_node__run__native_unavailable__failure) +{ + BOOST_REQUIRE(run([](server::configuration& config) + { + config.server.native.path = "unused"; + config.server.native.binds = { { unavailable } }; + config.server.native.connections = 1; + })); +} + +BOOST_AUTO_TEST_CASE(server_node__run__bitcoind_unavailable__failure) +{ + BOOST_REQUIRE(run([](server::configuration& config) + { + config.server.bitcoind.binds = { { unavailable } }; + config.server.bitcoind.connections = 1; + })); +} + +BOOST_AUTO_TEST_CASE(server_node__run__btcd_unavailable__failure) +{ + BOOST_REQUIRE(run([](server::configuration& config) + { + config.server.btcd.binds = { { unavailable } }; + config.server.btcd.connections = 1; + })); +} + +BOOST_AUTO_TEST_CASE(server_node__run__electrum_unavailable__failure) +{ + BOOST_REQUIRE(run([](server::configuration& config) + { + config.server.electrum.binds = { { unavailable } }; + config.server.electrum.connections = 1; + })); +} + +BOOST_AUTO_TEST_CASE(server_node__run__sparrow_unavailable__failure) +{ + BOOST_REQUIRE(run([](server::configuration& config) + { + config.server.sparrow.binds = { { unavailable } }; + config.server.sparrow.connections = 1; + })); +} + +BOOST_AUTO_TEST_CASE(server_node__run__esplora_unavailable__failure) +{ + BOOST_REQUIRE(run([](server::configuration& config) + { + config.server.esplora.binds = { { unavailable } }; + config.server.esplora.connections = 1; + })); +} + +BOOST_AUTO_TEST_CASE(server_node__run__stratum_v1_unavailable__failure) +{ + BOOST_REQUIRE(run([](server::configuration& config) + { + config.server.stratum_v1.binds = { { unavailable } }; + config.server.stratum_v1.connections = 1; + })); +} + +BOOST_AUTO_TEST_CASE(server_node__run__stratum_v2_unavailable__failure) +{ + BOOST_REQUIRE(run([](server::configuration& config) + { + config.server.stratum_v2.binds = { { unavailable } }; + config.server.stratum_v2.connections = 1; + })); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/protocols/native/native_setup_fixture.cpp b/test/protocols/native/native_setup_fixture.cpp index ea8e22c6..74f09660 100644 --- a/test/protocols/native/native_setup_fixture.cpp +++ b/test/protocols/native/native_setup_fixture.cpp @@ -195,6 +195,22 @@ http::status native_setup_fixture::get_status(std::string_view target) return response.result(); } +http::status native_setup_fixture::get_status(std::string_view target, + http::field name, std::string_view value) +{ + auto request = create_request(target); + request.set(name, value); + http::write(socket_, request); + + flat_buffer buffer{}; + network::boost_code ec{}; + http::response response{}; + http::read(socket_, buffer, response, ec); + BOOST_CHECK_MESSAGE(!ec, ec.message()); + + return response.result(); +} + std::string native_setup_fixture::get_text(std::string_view target) { http::write(socket_, create_request(target)); @@ -251,6 +267,15 @@ network::boost_code native_setup_fixture::ws_upgrade() return ec; } +void native_setup_fixture::ws_send(std::string_view message) +{ + network::boost_code ec{}; + BOOST_CHECK(websocket_.has_value()); + + websocket_.value().write(net::buffer(message), ec); + BOOST_CHECK_MESSAGE(!ec, ec.message()); +} + data_chunk native_setup_fixture::ws_receive() { flat_buffer buffer{}; @@ -308,4 +333,61 @@ data_chunk native_setup_fixture::ws_get_data(std::string_view message) void native_setup_fixture::notify(node::event_value value) { server_.notify(node::error::success, value); -} \ No newline at end of file +} + +BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) + +server_node_setup_fixture::server_node_setup_fixture() + : config_ + { + system::chain::selection::mainnet, + test::web_pages, + test::web_pages + }, + store_ + { + [&]() NOEXCEPT -> const database::settings& + { + config_.database.path = TEST_DIRECTORY; + return config_.database; + }() + }, + query_{ store_ }, log_{}, + server_{ query_, config_, log_ } +{ + test::clear(test::directory); + config_.database.interval_depth = 2; + config_.node.minimum_fee_rate = 99.0; + config_.network.inbound.connections = 0; + config_.network.outbound.connections = 0; + + const auto ec = store_.create([](auto, auto) {}); + BOOST_REQUIRE_MESSAGE(!ec, ec.message()); +} + +server_node_setup_fixture::~server_node_setup_fixture() +{ + server_.close(); + const auto ec = store_.close([](auto, auto){}); + BOOST_WARN_MESSAGE(!ec, ec.message()); + test::clear(test::directory); +} + +code server_node_setup_fixture::run(const configurator& configure) +{ + configure(config_); + + const auto ec = config_.initialize(); + BOOST_REQUIRE_MESSAGE(!ec, ec.message()); + BOOST_REQUIRE(test::setup_ten_block_store(query_)); + + std::promise running{}; + server_.run([&](const code& ec) NOEXCEPT + { + running.set_value(ec); + }); + + return running.get_future().get(); +} + +BC_POP_WARNING() \ No newline at end of file diff --git a/test/protocols/native/native_setup_fixture.hpp b/test/protocols/native/native_setup_fixture.hpp index 7b36255b..eb62ac97 100644 --- a/test/protocols/native/native_setup_fixture.hpp +++ b/test/protocols/native/native_setup_fixture.hpp @@ -40,12 +40,15 @@ struct native_setup_fixture bool expect_dropped(std::string_view target); status get_status(std::string_view target); + status get_status(std::string_view target, boost::beast::http::field name, + std::string_view value); std::string get_text(std::string_view target); system::data_chunk get_data(std::string_view target); boost::json::value get_json(std::string_view target); network::boost_code ws_upgrade(); + void ws_send(std::string_view message); system::data_chunk ws_receive(); bool ws_dropped(std::string_view message); std::string ws_get_text(std::string_view message); @@ -171,4 +174,70 @@ struct native_file_setup_fixture } }; +struct native_no_default_setup_fixture + : native_setup_fixture +{ + inline native_no_default_setup_fixture() + : native_setup_fixture([](test::query_t& query) + { + return test::setup_ten_block_store(query); + }, [](server::configuration& config) + { + const auto path = std::filesystem::absolute(TEST_DIRECTORY); + config.server.native.path = path; + config.server.native.default_ = {}; + }) + { + } +}; + +struct native_hosts_setup_fixture + : native_setup_fixture +{ + inline native_hosts_setup_fixture() + : native_setup_fixture([](test::query_t& query) + { + return test::setup_ten_block_store(query); + }, [](server::configuration& config) + { + config.server.native.hosts = { { "example.com" } }; + }) + { + } +}; + +struct native_limited_setup_fixture + : native_setup_fixture +{ + inline native_limited_setup_fixture() + : native_setup_fixture([](test::query_t& query) + { + return test::setup_ten_block_store(query); + }, [](server::configuration& config) + { + config.node.limited_blocks = true; + }) + { + } +}; + +// Runs a server node on demand, for start sequence outcomes. +struct server_node_setup_fixture +{ + using configurator = std::function; + + DELETE_COPY_MOVE(server_node_setup_fixture); + server_node_setup_fixture(); + ~server_node_setup_fixture(); + + code run(const configurator& configure); + +protected: + server::configuration config_; + test::store_t store_; + test::query_t query_; + network::logger log_; + server::server_node server_; +}; + #endif diff --git a/test/settings.cpp b/test/settings.cpp index 7772030b..c79b9835 100644 --- a/test/settings.cpp +++ b/test/settings.cpp @@ -367,4 +367,25 @@ BOOST_AUTO_TEST_CASE(wallet__to_context__configured_prefixes__override_network) instance.hd_public_prefix = prefix::hd::test::btc.pub; BOOST_REQUIRE(descriptor("pkh(" TESTNET_M "/1/*)", instance.to_context())); } + +BOOST_AUTO_TEST_CASE(wallet__default_construct__mainnet__expected) +{ + const server::settings::wallet_settings instance{}; + 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__copy_construct__testnet__expected) +{ + const server::settings::wallet_settings original{ selection::testnet3 }; + const server::settings::wallet_settings instance{ original }; + BOOST_REQUIRE_EQUAL(instance.p2kh_prefix, prefix::p2kh::test::btc); + BOOST_REQUIRE_EQUAL(instance.witness_prefix, prefix::p2w::test::btc); + BOOST_REQUIRE_EQUAL(instance.hd_public_prefix, prefix::hd::test::btc.pub); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/utilities/merkle.cpp b/test/utilities/merkle.cpp index 831c0d52..fb7caf65 100644 --- a/test/utilities/merkle.cpp +++ b/test/utilities/merkle.cpp @@ -126,4 +126,28 @@ BOOST_AUTO_TEST_CASE(merkle__excess_hashes__false) BOOST_REQUIRE(!server::extract_partial_merkle(root, matched, positions, 1, { 0x00 }, { one_hash, one_hash })); } +BOOST_AUTO_TEST_CASE(merkle__insufficient_flag_bits__false) +{ + hash_digest root{}; + hashes matched{}; + std::vector positions{}; + BOOST_REQUIRE(!server::extract_partial_merkle(root, matched, positions, 16, { 0xff }, { one_hash, null_hash })); +} + +BOOST_AUTO_TEST_CASE(merkle__insufficient_hashes__false) +{ + hash_digest root{}; + hashes matched{}; + std::vector positions{}; + BOOST_REQUIRE(!server::extract_partial_merkle(root, matched, positions, 1, { 0x00 }, {})); +} + +BOOST_AUTO_TEST_CASE(merkle__duplicate_siblings__false) +{ + hash_digest root{}; + hashes matched{}; + std::vector positions{}; + BOOST_REQUIRE(!server::extract_partial_merkle(root, matched, positions, 2, { 0x07 }, { one_hash, one_hash })); +} + BOOST_AUTO_TEST_SUITE_END() From 89c89b4c55faf3ed33d47155ea156ba78f80cdad Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 29 Sep 2026 11:23:41 -0400 Subject: [PATCH 04/21] Add bitcoind rest spenttxouts, origin, and zmq transaction tests. --- test/protocols/bitcoind/bitcoind_rest.cpp | 88 +++++++++++++++++++++++ test/protocols/zmq/zmq.cpp | 47 ++++++++++++ 2 files changed, 135 insertions(+) diff --git a/test/protocols/bitcoind/bitcoind_rest.cpp b/test/protocols/bitcoind/bitcoind_rest.cpp index 6443bf32..61ba9cda 100644 --- a/test/protocols/bitcoind/bitcoind_rest.cpp +++ b/test/protocols/bitcoind/bitcoind_rest.cpp @@ -453,6 +453,48 @@ BOOST_AUTO_TEST_CASE(bitcoind_rest__options__allowed__ok) BOOST_REQUIRE_EQUAL(options_status("/"), status::ok); } +// spenttxouts (spending block) +// ---------------------------------------------------------------------------- + +static const auto& spent3 = *test::block3.transactions_ptr()->front()->outputs_ptr()->front(); +static const auto& spent13 = *test::mock_tx13.outputs_ptr()->front(); +static const auto block13 = encode_hash(test::mock_block13.hash()); + +BOOST_AUTO_TEST_CASE(bitcoind_rest__spenttxouts_bin__block13__undo_framing) +{ + BOOST_REQUIRE(query_.set(test::mock_block13, database::context{ 0, 10, 0 }, {}, false, false)); + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::mock_block13.hash()), true)); + + const auto expected = build_chunk({ base16_chunk("030001"), spent3.to_data(), base16_chunk("01"), spent13.to_data() }); + BOOST_REQUIRE_EQUAL(rest_data("/rest/spenttxouts/" + block13 + ".bin"), expected); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rest__spenttxouts_hex__block13__undo_framing) +{ + BOOST_REQUIRE(query_.set(test::mock_block13, database::context{ 0, 10, 0 }, {}, false, false)); + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::mock_block13.hash()), true)); + + const auto expected = "030001" + encode_base16(spent3.to_data()) + "01" + encode_base16(spent13.to_data()); + BOOST_REQUIRE_EQUAL(rest_text("/rest/spenttxouts/" + block13 + ".hex"), expected); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rest__spenttxouts_json__block13__prevouts_per_tx) +{ + BOOST_REQUIRE(query_.set(test::mock_block13, database::context{ 0, 10, 0 }, {}, false, false)); + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::mock_block13.hash()), true)); + + const auto result = rest_json("/rest/spenttxouts/" + block13 + ".json"); + BOOST_REQUIRE_EQUAL(result.as_array().size(), 3u); + BOOST_REQUIRE(result.at(0).as_array().empty()); + BOOST_REQUIRE_EQUAL(result.at(1).as_array().size(), 1u); + BOOST_REQUIRE_EQUAL(result.at(2).as_array().size(), 1u); + BOOST_REQUIRE_EQUAL(result.at(1).at(0).at("value").to_number(), 50.0); + BOOST_REQUIRE_EQUAL(as_text(result.at(1).at(0).at("scriptPubKey").at("hex")), encode_base16(spent3.script().to_data(false))); + BOOST_REQUIRE_EQUAL(result.at(2).at(0).at("value").to_number(), 9 / 100'000'000.0); + BOOST_REQUIRE_EQUAL(as_text(result.at(2).at(0).at("scriptPubKey").at("hex")), encode_base16(spent13.script().to_data(false))); + BOOST_REQUIRE_EQUAL(as_text(result.at(2).at(0).at("scriptPubKey").at("address")), "1BaMPFdqMUQ46BV8iRcwbVfsam57oBLMM"); +} + BOOST_AUTO_TEST_SUITE_END() BOOST_FIXTURE_TEST_SUITE(bitcoind_rest_host_tests, bitcoind_hosted_setup_fixture) @@ -473,3 +515,49 @@ BOOST_AUTO_TEST_CASE(bitcoind_rest__post_disallowed_host__bad_request) } BOOST_AUTO_TEST_SUITE_END() + +struct bitcoind_origin_setup_fixture + : bitcoind_setup_fixture +{ + inline bitcoind_origin_setup_fixture() + : bitcoind_setup_fixture([](test::query_t& query) + { + return test::setup_ten_block_store(query); + }, [](configuration& config) + { + config.server.bitcoind.origins = { { "example.com" } }; + config.server.bitcoind.connections = 2; + }) + { + } + + status origin_status(std::string_view target, std::string_view origin) + { + using namespace boost::beast; + rpc_client client{ io_ }; + client.connect(config_.server.bitcoind.binds.back().to_endpoint()); + + auto request = rpc_client::create_get(target); + request.set(http::field::origin, origin); + http::write(client.stream(), request); + + flat_buffer buffer{}; + http::response response{}; + http::read(client.stream(), buffer, response); + return response.result(); + } +}; + +BOOST_FIXTURE_TEST_SUITE(bitcoind_rest_origin_tests, bitcoind_origin_setup_fixture) + +BOOST_AUTO_TEST_CASE(bitcoind_rest__disallowed_origin__forbidden) +{ + BOOST_REQUIRE_EQUAL(origin_status("/rest/chaininfo.json", "http://example.org"), status::forbidden); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rest__opaque_origin__forbidden) +{ + BOOST_REQUIRE_EQUAL(origin_status("/rest/chaininfo.json", "null"), status::forbidden); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/protocols/zmq/zmq.cpp b/test/protocols/zmq/zmq.cpp index 576ffd75..1a1e4721 100644 --- a/test/protocols/zmq/zmq.cpp +++ b/test/protocols/zmq/zmq.cpp @@ -489,6 +489,53 @@ BOOST_AUTO_TEST_CASE(zmq__maximum_subscriptions__cancelled__subscription_release BOOST_REQUIRE_EQUAL(message.at(0), to_chunk("hashblock")); } +BOOST_AUTO_TEST_CASE(zmq__hashtx__transaction__reversed_hash_and_sequence) +{ + peer_handshake(socket_); + peer_subscribe(socket_, "hashtx"); + peer_ping_pong(socket_); + + const auto hash = test::block1.transactions_ptr()->front()->hash(false); + notify(node::chases::transaction{ query_.to_tx(hash).value }); + const auto message = peer_read_message(socket_); + BOOST_REQUIRE_EQUAL(message.size(), 3u); + BOOST_REQUIRE_EQUAL(message.at(0), to_chunk("hashtx")); + BOOST_REQUIRE_EQUAL(message.at(1), to_chunk(reverse_copy(hash))); + BOOST_REQUIRE_EQUAL(message.at(2), base16_chunk("00000000")); +} + +BOOST_AUTO_TEST_CASE(zmq__rawtx__transaction__wire_transaction) +{ + peer_handshake(socket_); + peer_subscribe(socket_, "rawtx"); + peer_ping_pong(socket_); + + const auto& tx = *test::block1.transactions_ptr()->front(); + notify(node::chases::transaction{ query_.to_tx(tx.hash(false)).value }); + const auto message = peer_read_message(socket_); + BOOST_REQUIRE_EQUAL(message.size(), 3u); + BOOST_REQUIRE_EQUAL(message.at(0), to_chunk("rawtx")); + BOOST_REQUIRE_EQUAL(message.at(1), tx.to_data(true)); +} + +BOOST_AUTO_TEST_CASE(zmq__sequence__transaction__reversed_hash_and_accepted_label) +{ + peer_handshake(socket_); + peer_subscribe(socket_, "sequence"); + peer_ping_pong(socket_); + + const auto hash = test::block1.transactions_ptr()->front()->hash(false); + auto expected = to_chunk(reverse_copy(hash)); + expected.push_back('A'); + + notify(node::chases::transaction{ query_.to_tx(hash).value }); + const auto message = peer_read_message(socket_); + BOOST_REQUIRE_EQUAL(message.size(), 3u); + BOOST_REQUIRE_EQUAL(message.at(0), to_chunk("sequence")); + BOOST_REQUIRE_EQUAL(message.at(1).size(), 41u); + BOOST_REQUIRE_EQUAL(data_chunk(message.at(1).begin(), std::next(message.at(1).begin(), 33)), expected); +} + BOOST_AUTO_TEST_SUITE_END() // CURVE mechanism. From dc2a736e3f74429f76544eec28a0d4a17e677ada Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 29 Sep 2026 11:31:11 -0400 Subject: [PATCH 05/21] Add bitcoind parser, serializer, and descriptor tests. --- test/parsers/bitcoind_query.cpp | 31 +++++++++ test/parsers/bitcoind_target.cpp | 80 +++++++++++++++++++++++ test/serializers/bitcoind_block_stats.cpp | 15 +++++ test/serializers/bitcoind_json.cpp | 75 +++++++++++++++++++++ test/utilities/bitcoind_descriptor.cpp | 38 +++++++++++ 5 files changed, 239 insertions(+) diff --git a/test/parsers/bitcoind_query.cpp b/test/parsers/bitcoind_query.cpp index 09d31deb..5835c8b5 100644 --- a/test/parsers/bitcoind_query.cpp +++ b/test/parsers/bitcoind_query.cpp @@ -61,4 +61,35 @@ BOOST_AUTO_TEST_CASE(parsers__bitcoind_query__malformed_count__false) BOOST_REQUIRE(!bitcoind_query(out, target)); } +BOOST_AUTO_TEST_CASE(parsers__bitcoind_query__empty_count__false) +{ + request_t out{}; + const auto target = "/rest/headers/" + test_hash + ".json?count="; + BOOST_REQUIRE(!bitcoind_target(out, target)); + BOOST_REQUIRE(!bitcoind_query(out, target)); +} + +BOOST_AUTO_TEST_CASE(parsers__bitcoind_query__empty_target__false) +{ + request_t out{}; + BOOST_REQUIRE(!bitcoind_target(out, "/rest/chaininfo.json")); + BOOST_REQUIRE(!bitcoind_query(out, "")); +} + +BOOST_AUTO_TEST_CASE(parsers__bitcoind_query__no_params__false) +{ + request_t out{}; + BOOST_REQUIRE(!bitcoind_query(out, "/rest/headers/" + test_hash + ".json?count=7")); +} + +BOOST_AUTO_TEST_CASE(parsers__bitcoind_query__offset_and_size__overlaid) +{ + request_t out{}; + const auto target = "/rest/blockpart/" + test_hash + ".bin?offset=80&size=1"; + BOOST_REQUIRE(!bitcoind_target(out, target)); + BOOST_REQUIRE(bitcoind_query(out, target)); + BOOST_REQUIRE_EQUAL(std::get(params_of(out).at("offset").value()), 80u); + BOOST_REQUIRE_EQUAL(std::get(params_of(out).at("size").value()), 1u); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/parsers/bitcoind_target.cpp b/test/parsers/bitcoind_target.cpp index 618cb25e..1f46e5cc 100644 --- a/test/parsers/bitcoind_target.cpp +++ b/test/parsers/bitcoind_target.cpp @@ -320,4 +320,84 @@ BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__blockpart__block_part) BOOST_REQUIRE_EQUAL(media_of(object), to_value(media_type::application_octet_stream)); } +// error paths + +BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__tx_no_hash__missing_hash) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(bitcoind_target(out, "/rest/tx"), server::error::missing_hash); +} + +BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__tx_no_extension__invalid_target) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(bitcoind_target(out, "/rest/tx/" + test_hash), server::error::invalid_target); +} + +BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__tx_not_hex__invalid_hash) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(bitcoind_target(out, "/rest/tx/nothex.json"), server::error::invalid_hash); +} + +BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__blockhashbyheight_no_extension__invalid_target) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(bitcoind_target(out, "/rest/blockhashbyheight/5"), server::error::invalid_target); +} + +BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__blockfilter_no_extension__invalid_target) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(bitcoind_target(out, "/rest/blockfilter/basic/" + test_hash), server::error::invalid_target); +} + +BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__blockfilter_not_hex__invalid_hash) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(bitcoind_target(out, "/rest/blockfilter/basic/nothex.json"), server::error::invalid_hash); +} + +BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__blockfilterheaders_no_type__missing_target) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(bitcoind_target(out, "/rest/blockfilterheaders"), server::error::missing_target); +} + +BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__blockfilterheaders_extended__invalid_target) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(bitcoind_target(out, "/rest/blockfilterheaders/extended/" + test_hash + ".json"), server::error::invalid_target); +} + +BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__blockfilterheaders_invalid_count__invalid_number) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(bitcoind_target(out, "/rest/blockfilterheaders/basic/abc/" + test_hash + ".json"), server::error::invalid_number); +} + +BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__blockfilterheaders_no_extension__invalid_target) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(bitcoind_target(out, "/rest/blockfilterheaders/basic/" + test_hash), server::error::invalid_target); +} + +BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__blockfilterheaders_not_hex__invalid_hash) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(bitcoind_target(out, "/rest/blockfilterheaders/basic/nothex.json"), server::error::invalid_hash); +} + +BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__getutxos_no_extension__invalid_target) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(bitcoind_target(out, "/rest/getutxos/" + test_hash + "-0"), server::error::invalid_target); +} + +BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__spenttxouts_no_extension__invalid_target) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(bitcoind_target(out, "/rest/spenttxouts/" + test_hash), server::error::invalid_target); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/serializers/bitcoind_block_stats.cpp b/test/serializers/bitcoind_block_stats.cpp index d3f996aa..acaab2d4 100644 --- a/test/serializers/bitcoind_block_stats.cpp +++ b/test/serializers/bitcoind_block_stats.cpp @@ -101,4 +101,19 @@ BOOST_AUTO_TEST_CASE(bitcoind_block_stats__unspendable_output__excluded_from_act BOOST_REQUIRE_EQUAL(std::get(stats.at("utxo_increase_actual").value()), 0); } +BOOST_AUTO_TEST_CASE(bitcoind_block_stats__segregated_paying__witness_statistics) +{ + transaction segregated{ 1, inputs{ { point{ one_hash, 0 }, script{}, witness{ "[242424]" }, 0 } }, outputs{ { 90'000, script{} } }, 0 }; + segregated.inputs_ptr()->front()->prevout = to_shared(100'000, script{}); + const auto block = make_block({ make_coinbase(), make_paying(50'000, 48'000), std::move(segregated) }); + const auto stats = server::block_stats(block, 2, 40, test_subsidy); + + const auto& tx = *block.transactions_ptr()->back(); + BOOST_REQUIRE(tx.is_segregated()); + BOOST_REQUIRE_EQUAL(std::get(stats.at("swtxs").value()), 1u); + BOOST_REQUIRE_EQUAL(std::get(stats.at("swtotal_size").value()), tx.serialized_size(true)); + BOOST_REQUIRE_EQUAL(std::get(stats.at("swtotal_weight").value()), tx.weight()); + BOOST_REQUIRE_EQUAL(std::get(stats.at("totalfee").value()), 12'000u); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/serializers/bitcoind_json.cpp b/test/serializers/bitcoind_json.cpp index 1e318c9a..e7a35fe5 100644 --- a/test/serializers/bitcoind_json.cpp +++ b/test/serializers/bitcoind_json.cpp @@ -193,4 +193,79 @@ BOOST_AUTO_TEST_CASE(bitcoind_json__inject_tx_context__unknown__zero_confirmatio BOOST_REQUIRE(!out.contains("in_active_chain")); } +// inject_block_context (unknown) + +BOOST_AUTO_TEST_CASE(bitcoind_json__inject_block_context__unknown__unchanged) +{ + const system::settings settings{ chain::selection::mainnet }; + boost::json::object out{}; + inject_block_context(out, query_, settings, database::header_link{}, test::block1.header()); + BOOST_REQUIRE(out.empty()); +} + +// chain_states_entry + +BOOST_AUTO_TEST_CASE(bitcoind_json__chain_states_entry__top__expected) +{ + const auto link = query_.to_header(test::block9_hash); + const auto out = chain_states_entry(query_, link, 1.0, true); + BOOST_REQUIRE_EQUAL(std::get(out.at("blocks").value()), 9u); + BOOST_REQUIRE_EQUAL(std::get(out.at("bestblockhash").value()), encode_hash(test::block9_hash)); + BOOST_REQUIRE_EQUAL(std::get(out.at("bits").value()), "1d00ffff"); + BOOST_REQUIRE(std::get(out.at("validated").value())); +} + +BOOST_AUTO_TEST_CASE(bitcoind_json__chain_states_entry__unknown__empty) +{ + BOOST_REQUIRE(chain_states_entry(query_, database::header_link{}, 1.0, true).empty()); +} + +// inject_activity + +BOOST_AUTO_TEST_CASE(bitcoind_json__inject_activity__watched_output__receive_and_spend) +{ + using namespace network::rpc; + BOOST_REQUIRE(query_.set(test::mock_block13, database::context{ 0, 10, 0 }, {}, false, false)); + const auto block = query_.get_block(query_.to_header(test::mock_block13.hash()), false); + BOOST_REQUIRE(block); + BOOST_REQUIRE(query_.populate_without_metadata(*block)); + + const auto& txs = *test::mock_block13.transactions_ptr(); + const auto blockhash = encode_hash(test::mock_block13.hash()); + const auto script = test::mock_tx13.outputs_ptr()->front()->script().to_data(false); + const std::unordered_set watch{ encode_base16(script) }; + + array_t out{}; + inject_activity(out, *block, 10, blockhash, watch, 0x00, 0x05, "bc", 0); + BOOST_REQUIRE_EQUAL(out.size(), 2u); + + const auto& receive = std::get(out.at(0).value()); + BOOST_REQUIRE_EQUAL(std::get(receive.at("type").value()), "receive"); + BOOST_REQUIRE_EQUAL(std::get(receive.at("amount").value()), 9 / 100'000'000.0); + BOOST_REQUIRE_EQUAL(std::get(receive.at("blockhash").value()), blockhash); + BOOST_REQUIRE_EQUAL(std::get(receive.at("height").value()), 10u); + BOOST_REQUIRE_EQUAL(std::get(receive.at("txid").value()), encode_hash(test::mock_tx13.hash(false))); + BOOST_REQUIRE_EQUAL(std::get(receive.at("vout").value()), 0u); + + const auto& spend = std::get(out.at(1).value()); + BOOST_REQUIRE_EQUAL(std::get(spend.at("type").value()), "spend"); + BOOST_REQUIRE_EQUAL(std::get(spend.at("amount").value()), 9 / 100'000'000.0); + BOOST_REQUIRE_EQUAL(std::get(spend.at("blockhash").value()), blockhash); + BOOST_REQUIRE_EQUAL(std::get(spend.at("height").value()), 10u); + BOOST_REQUIRE_EQUAL(std::get(spend.at("spend_txid").value()), encode_hash(txs.back()->hash(false))); + BOOST_REQUIRE_EQUAL(std::get(spend.at("spend_vin").value()), 0u); + BOOST_REQUIRE_EQUAL(std::get(spend.at("prevout_txid").value()), encode_hash(test::mock_tx13.hash(false))); + BOOST_REQUIRE_EQUAL(std::get(spend.at("prevout_vout").value()), 0u); + + const auto& prevout_spk = std::get(spend.at("prevout_spk").value()); + BOOST_REQUIRE_EQUAL(as_text(prevout_spk.at("address")), "1BaMPFdqMUQ46BV8iRcwbVfsam57oBLMM"); +} + +BOOST_AUTO_TEST_CASE(bitcoind_json__inject_activity__unwatched__empty) +{ + network::rpc::array_t out{}; + inject_activity(out, test::block1, 1, encode_hash(test::block1_hash), {}, 0x00, 0x05, "bc", 0); + BOOST_REQUIRE(out.empty()); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/utilities/bitcoind_descriptor.cpp b/test/utilities/bitcoind_descriptor.cpp index 4b87b3b8..8f8f5e18 100644 --- a/test/utilities/bitcoind_descriptor.cpp +++ b/test/utilities/bitcoind_descriptor.cpp @@ -43,4 +43,42 @@ BOOST_AUTO_TEST_CASE(bitcoind_descriptor__checksum__invalid_character__empty) BOOST_REQUIRE(server::descriptor_checksum("raw(\x01)").empty()); } +static const std::string compressed_key{ "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd" }; +static const std::string uncompressed_key{ "04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235" }; +static const std::string bip67_key1{ "02fe6f0a5a297eb38c391581c4413e084773ea23954d93f7753db7dc0adc188b2f" }; +static const std::string bip67_key2{ "02ff12471208c14bd580709cb2358d98975247d8765f92bc25eab3b2763ed605f8" }; +static const std::string bip67_script{ "522102fe6f0a5a297eb38c391581c4413e084773ea23954d93f7753db7dc0adc188b2f2102ff12471208c14bd580709cb2358d98975247d8765f92bc25eab3b2763ed605f852ae" }; + +BOOST_AUTO_TEST_CASE(bitcoind_descriptor__infer_descriptor__bare_multisig__multi) +{ + system::data_chunk data{}; + BOOST_REQUIRE(system::decode_base16(data, "5121" + compressed_key + "41" + uncompressed_key + "52ae")); + const system::chain::script script{ data, false }; + const auto body = "multi(1," + compressed_key + "," + uncompressed_key + ")"; + BOOST_REQUIRE_EQUAL(server::infer_descriptor(script, 0x00, 0x05, "bc"), body + "#" + server::descriptor_checksum(body)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_descriptor__create_multisig__p2sh_segwit__nested_descriptor) +{ + const network::rpc::array_t keys{ bip67_key1, bip67_key2 }; + const auto result = server::create_multisig(2, keys, "p2sh-segwit", 0x05, "bc"); + const auto body = "sh(wsh(multi(2," + bip67_key1 + "," + bip67_key2 + ")))"; + BOOST_REQUIRE_EQUAL(std::get(result.at("redeemScript").value()), bip67_script); + BOOST_REQUIRE_EQUAL(std::get(result.at("descriptor").value()), body + "#" + server::descriptor_checksum(body)); + BOOST_REQUIRE(std::get(result.at("address").value()).starts_with('3')); + BOOST_REQUIRE(!result.contains("warnings")); +} + +BOOST_AUTO_TEST_CASE(bitcoind_descriptor__create_multisig__invalid_key__empty) +{ + const network::rpc::array_t keys{ bip67_key1, std::string{ "nothex" } }; + BOOST_REQUIRE(server::create_multisig(1, keys, "legacy", 0x05, "bc").empty()); +} + +BOOST_AUTO_TEST_CASE(bitcoind_descriptor__create_multisig__legacy_oversized__empty) +{ + const network::rpc::array_t keys{ uncompressed_key, uncompressed_key, uncompressed_key, uncompressed_key, uncompressed_key, uncompressed_key, uncompressed_key, uncompressed_key }; + BOOST_REQUIRE(server::create_multisig(1, keys, "legacy", 0x05, "bc").empty()); +} + BOOST_AUTO_TEST_SUITE_END() From 198e17289d5b0d15975e2069b6b484083914a291 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 29 Sep 2026 11:21:07 -0400 Subject: [PATCH 06/21] Add electrum version, history limit, notification and parameter tests. --- test/protocols/electrum/electrum_fees.cpp | 30 +++++++ test/protocols/electrum/electrum_headers.cpp | 36 ++++++++ test/protocols/electrum/electrum_mempool.cpp | 10 +++ .../protocols/electrum/electrum_outpoints.cpp | 90 +++++++++++++++++++ .../electrum/electrum_scripthash.cpp | 28 ++++++ .../electrum/electrum_setup_fixture.hpp | 15 ++++ .../protocols/electrum/electrum_subscribe.cpp | 52 +++++++++++ .../electrum/electrum_transactions.cpp | 26 ++++++ test/protocols/electrum/electrum_version.cpp | 64 +++++++++++++ test/protocols/sparrow/sparrow.cpp | 8 ++ 10 files changed, 359 insertions(+) diff --git a/test/protocols/electrum/electrum_fees.cpp b/test/protocols/electrum/electrum_fees.cpp index e5579cd8..44573830 100644 --- a/test/protocols/electrum/electrum_fees.cpp +++ b/test/protocols/electrum/electrum_fees.cpp @@ -70,6 +70,36 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_estimate_fee__uninitialized__negative_ BOOST_REQUIRE_EQUAL(response.at("result").as_int64(), -1); } +BOOST_AUTO_TEST_CASE(electrum__blockchain_estimate_fee__geometric_uninitialized__negative_one) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_6)); + + const auto response = get(R"({"id":801,"method":"blockchain.estimatefee","params":[0,"geometric"]})" "\n"); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + 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__economical_uninitialized__negative_one) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_6)); + + const auto response = get(R"({"id":801,"method":"blockchain.estimatefee","params":[0,"economical"]})" "\n"); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + 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__conservative_uninitialized__negative_one) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_6)); + + const auto response = get(R"({"id":801,"method":"blockchain.estimatefee","params":[0,"conservative"]})" "\n"); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + 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)); diff --git a/test/protocols/electrum/electrum_headers.cpp b/test/protocols/electrum/electrum_headers.cpp index 468184f2..d933d8fd 100644 --- a/test/protocols/electrum/electrum_headers.cpp +++ b/test/protocols/electrum/electrum_headers.cpp @@ -231,6 +231,33 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__proof_self_block1__expec BOOST_REQUIRE_EQUAL(branch.at(0).as_string(), encode_hash(test::block0_hash)); } +BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__proof_self_block1_v1_4__expected) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_4)); + const auto expected_header = encode_base16(test::header1_data); + const auto expected_root = encode_hash(merkle_root( + { + test::block0_hash, + test::block1_hash + })); + + const auto response = get(R"({"id":47,"method":"blockchain.block.header","params":[1,1]})" "\n"); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + + const auto& result = response.at("result").as_object(); + 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_REQUIRE(branch.at(0).is_string()); + BOOST_REQUIRE_EQUAL(branch.size(), 1u); + BOOST_REQUIRE_EQUAL(branch.at(0).as_string(), encode_hash(test::block0_hash)); +} + BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__proof_example__expected) { BOOST_REQUIRE(handshake(electrum::version::v1_6)); @@ -816,6 +843,15 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__start_plus_count_huge__ BOOST_REQUIRE_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value()); } +BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__start_plus_count_overflow__invalid_argument) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_6)); + + const auto response = get(R"({"id":71,"method":"blockchain.block.headers","params":[1e19,1e19]})" "\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()); +} + // TODO: add optional bool parameter "raw". // blockchain.headers.subscribe diff --git a/test/protocols/electrum/electrum_mempool.cpp b/test/protocols/electrum/electrum_mempool.cpp index 103555ff..4eff004a 100644 --- a/test/protocols/electrum/electrum_mempool.cpp +++ b/test/protocols/electrum/electrum_mempool.cpp @@ -59,6 +59,16 @@ BOOST_AUTO_TEST_CASE(electrum__mempool_get_fee_histogram__extra_param__dropped) //// BOOST_REQUIRE_EQUAL(result, not_implemented.value()); ////} +BOOST_AUTO_TEST_CASE(electrum__mempool_get_fee_histogram__empty_params__empty) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_2)); + + const auto response = get(R"({"id":603,"method":"mempool.get_fee_histogram","params":[]})" "\n"); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_array()); + BOOST_REQUIRE(response.at("result").as_array().empty()); +} + // mempool.get_info BOOST_AUTO_TEST_CASE(electrum__mempool_get_info__insufficient_version__wrong_version) diff --git a/test/protocols/electrum/electrum_outpoints.cpp b/test/protocols/electrum/electrum_outpoints.cpp index 5c9740b6..787deb1a 100644 --- a/test/protocols/electrum/electrum_outpoints.cpp +++ b/test/protocols/electrum/electrum_outpoints.cpp @@ -84,6 +84,17 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_utxo_get_address__p2pk__null) REQUIRE_NO_THROW_TRUE(response.at("result").is_null()); } +BOOST_AUTO_TEST_CASE(electrum__blockchain_utxo_get_address__genesis_p2pk__null) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_0)); + + const auto hash = test::genesis.transactions_ptr()->front()->hash(false); + constexpr auto request = R"({{"id":902,"method":"blockchain.utxo.get_address","params":["{}",0]}})" "\n"; + const auto response = get(std::format(request, encode_hash(hash))); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_null()); +} + BOOST_AUTO_TEST_CASE(electrum__blockchain_utxo_get_address__p2kh__expected) { BOOST_REQUIRE(handshake(electrum::version::v1_0)); @@ -156,6 +167,33 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_outpoint_get_status__invalid_index__in BOOST_REQUIRE_EQUAL(result, invalid_argument.value()); } +BOOST_AUTO_TEST_CASE(electrum__blockchain_outpoint_get_status__invalid_hint_encoding__invalid_argument) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_7)); + + constexpr auto request = R"({{"id":1104,"method":"blockchain.outpoint.get_status","params":["{}",0,"not_hex"]}})" "\n"; + const auto result = get_error(std::format(request, bogus_hash)); + BOOST_REQUIRE_EQUAL(result, invalid_argument.value()); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_outpoint_get_status__script_hint__expected) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_7)); + + const auto& coinbase = *test::block1.transactions_ptr()->at(0); + const auto hint = encode_base16(coinbase.outputs_ptr()->at(0)->script().to_data(false)); + constexpr auto request = R"({{"id":1108,"method":"blockchain.outpoint.get_status","params":["{}",0,"{}"]}})" "\n"; + const auto response = get(std::format(request, encode_hash(coinbase.hash(false)), hint)); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + + const auto& history = response.at("result").as_object(); + REQUIRE_NO_THROW_TRUE(history.at("funder_height").is_int64()); + BOOST_REQUIRE_EQUAL(history.at("funder_height").as_int64(), 1); + BOOST_REQUIRE(!history.contains("spender_txhash")); + BOOST_REQUIRE(!history.contains("spender_height")); +} + BOOST_AUTO_TEST_CASE(electrum__blockchain_outpoint_get_status__tx_not_found__empty_object) { BOOST_REQUIRE(handshake(electrum::version::v1_7)); @@ -576,6 +614,58 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_outpoint_subscribe__not_found_progress BOOST_REQUIRE_EQUAL(history5.at("spender_txhash").as_string(), hash2); } +BOOST_AUTO_TEST_CASE(electrum__blockchain_outpoint_subscribe__transaction_notify__found_spender_notification) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_7)); + + // Pop blocks 9-1 from default fixture. + query_.pop_confirmed(); // 9 + query_.pop_confirmed(); // 8 + query_.pop_confirmed(); // 7 + query_.pop_confirmed(); // 6 + query_.pop_confirmed(); // 5 + query_.pop_confirmed(); // 4 + query_.pop_confirmed(); // 3 + query_.pop_confirmed(); // 2 + query_.pop_confirmed(); // 1 + BOOST_REQUIRE_EQUAL(query_.get_top_confirmed(), 0u); + + BOOST_REQUIRE(query_.set(test::block1a, database::context{ 0, 1, 0 }, {}, false, false)); + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::block1a.hash()), true)); + const auto hash1 = encode_hash(test::block1a.transactions_ptr()->at(0)->hash(false)); + const auto hash4 = encode_hash(test::tx4.hash(false)); + + constexpr auto request = R"({{"id":1109,"method":"blockchain.outpoint.subscribe","params":["{}",0]}})" "\n"; + const auto response1 = get(std::format(request, bogus_hash)); + REQUIRE_NO_THROW_TRUE(response1.at("result").as_object().empty()); + + const auto response2 = get(std::format(request, hash1)); + REQUIRE_NO_THROW_TRUE(response2.at("result").is_object()); + BOOST_REQUIRE_EQUAL(response2.at("result").as_object().at("funder_height").as_int64(), 1); + + BOOST_REQUIRE(query_.set(test::tx4)); + notify(node::chases::transaction{ query_.to_tx(test::tx4.hash(false)).value }); + + const auto notification = receive(); + REQUIRE_NO_THROW_TRUE(notification.at("method").is_string()); + REQUIRE_NO_THROW_TRUE(notification.at("params").is_array()); + BOOST_REQUIRE_EQUAL(notification.at("method").as_string(), "blockchain.outpoint.subscribe"); + + const auto& params = notification.at("params").as_array(); + BOOST_REQUIRE_EQUAL(params.size(), 3u); + BOOST_REQUIRE(params.at(2).is_object()); + BOOST_REQUIRE_EQUAL(params.at(0).as_string(), hash1); + BOOST_REQUIRE_EQUAL(params.at(1).as_int64(), 0); + + const auto& history = params.at(2).as_object(); + REQUIRE_NO_THROW_TRUE(history.at("funder_height").is_int64()); + REQUIRE_NO_THROW_TRUE(history.at("spender_height").is_int64()); + REQUIRE_NO_THROW_TRUE(history.at("spender_txhash").is_string()); + BOOST_REQUIRE_EQUAL(history.at("funder_height").as_int64(), 1); + BOOST_REQUIRE_EQUAL(history.at("spender_height").as_int64(), 0); + BOOST_REQUIRE_EQUAL(history.at("spender_txhash").as_string(), hash4); +} + // blockchain.outpoint.unsubscribe BOOST_AUTO_TEST_CASE(electrum__blockchain_outpoint_unsubscribe__insufficient_version__wrong_version) diff --git a/test/protocols/electrum/electrum_scripthash.cpp b/test/protocols/electrum/electrum_scripthash.cpp index eea70e08..40c3d5ca 100644 --- a/test/protocols/electrum/electrum_scripthash.cpp +++ b/test/protocols/electrum/electrum_scripthash.cpp @@ -23,6 +23,7 @@ using namespace system; static const code not_found{ server::error::electrum::bad_request }; static const code wrong_version{ server::error::electrum::bad_request }; static const code invalid_argument{ server::error::electrum::bad_request }; +static const code excessive_history{ server::error::electrum::excessive_history }; 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 }) }; @@ -225,6 +226,20 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_scripthash_get_history__confirmed_and_ BOOST_REQUIRE_EQUAL(tx3.at("tx_hash").as_string(), encode_hash(hash3)); } +BOOST_AUTO_TEST_CASE(electrum__blockchain_scripthash_get_history__history_exceeds_maximum__excessive_history) +{ + config_.server.electrum.maximum_history = 1; + BOOST_REQUIRE(handshake(electrum::version::v1_1)); + + BOOST_REQUIRE(query_.set(test::mock_block10, database::context{ 0, 10, 0 }, {}, false, false)); + BOOST_REQUIRE(query_.set(test::mock_block11, database::context{ 0, 11, 0 }, {}, false, false)); + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::mock_block10.hash()), true)); + + constexpr auto request = R"({{"id":1007,"method":"blockchain.scripthash.get_history","params":["{}"]}})" "\n"; + const auto result = get_error(std::format(request, found_scripthash)); + BOOST_REQUIRE_EQUAL(result, excessive_history.value()); +} + // blockchain.scripthash.get_mempool BOOST_AUTO_TEST_CASE(electrum__blockchain_scripthash_get_mempool__missing_arguments__dropped) @@ -319,6 +334,19 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_scripthash_get_mempool__confirmed_and_ BOOST_REQUIRE_EQUAL(tx2.at("tx_hash").as_string(), encode_hash(hash2)); } +BOOST_AUTO_TEST_CASE(electrum__blockchain_scripthash_get_mempool__history_exceeds_maximum__excessive_history) +{ + config_.server.electrum.maximum_history = 1; + BOOST_REQUIRE(handshake(electrum::version::v1_1)); + + BOOST_REQUIRE(query_.set(test::mock_block10, database::context{ 0, 10, 0 }, {}, false, false)); + BOOST_REQUIRE(query_.set(test::mock_block11, database::context{ 0, 11, 0 }, {}, false, false)); + + constexpr auto request = R"({{"id":1007,"method":"blockchain.scripthash.get_mempool","params":["{}"]}})" "\n"; + const auto result = get_error(std::format(request, found_scripthash)); + BOOST_REQUIRE_EQUAL(result, excessive_history.value()); +} + // blockchain.scripthash.list_unspent BOOST_AUTO_TEST_CASE(electrum__blockchain_scripthash_list_unspent__missing_arguments__dropped) diff --git a/test/protocols/electrum/electrum_setup_fixture.hpp b/test/protocols/electrum/electrum_setup_fixture.hpp index a25d58d8..2a5a4e96 100644 --- a/test/protocols/electrum/electrum_setup_fixture.hpp +++ b/test/protocols/electrum/electrum_setup_fixture.hpp @@ -173,6 +173,21 @@ struct electrum_restricted_version_setup_fixture } }; +struct electrum_legacy_version_setup_fixture + : electrum_setup_fixture +{ + inline electrum_legacy_version_setup_fixture() + : electrum_setup_fixture([](test::query_t& query) + { + return test::setup_ten_block_store(query); + }, true, [](configuration& config) + { + config.server.electrum.protocol_minimum = { 0, 6 }; + }) + { + } +}; + // Configured with a server ping interval (v1.7 websocket and downgraded tcp). struct electrum_ping_setup_fixture : electrum_setup_fixture diff --git a/test/protocols/electrum/electrum_subscribe.cpp b/test/protocols/electrum/electrum_subscribe.cpp index f97ce04b..7eb42670 100644 --- a/test/protocols/electrum/electrum_subscribe.cpp +++ b/test/protocols/electrum/electrum_subscribe.cpp @@ -23,6 +23,7 @@ using namespace system; static const code not_found{ server::error::electrum::bad_request }; static const code wrong_version{ server::error::electrum::bad_request }; static const code invalid_argument{ server::error::electrum::bad_request }; +static const code excessive_history{ server::error::electrum::excessive_history }; static const std::string bogus_address{ "1JqDybm2nWTENrHvMyafbSXXtTk5Uv5QAn" }; static const std::string found_address{ "1BaMPFdqMUQ46BV8iRcwbVfsam57oBLMM" }; static const std::string bogus_scripthash{ "9c2c84a6cf9809e08af19557e28d38257e6fee6981269760637a5f9dfb000b05" }; @@ -511,6 +512,57 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_scripthash_subscribe__progressive_noti BOOST_REQUIRE_EQUAL(params2.at(1).as_string(), expected_confirm12); } +BOOST_AUTO_TEST_CASE(electrum__blockchain_scripthash_subscribe__transaction_notify__expected) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_1)); + + BOOST_REQUIRE(query_.set(test::mock_block10, database::context{ 0, 10, 0 }, {}, false, false)); + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::mock_block10.hash()), true)); + const auto hash10 = test::mock_block10.transactions_ptr()->at(1)->hash(false); + const auto hash11 = test::mock_block11.transactions_ptr()->at(0)->hash(false); + const auto expected_initial = encode_base16(sha256_hash(encode_hash(hash10) + ":10:")); + + constexpr auto request = R"({{"id":1101,"method":"blockchain.scripthash.subscribe","params":["{}"]}})" "\n"; + const auto response = get(std::format(request, found_scripthash)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); + BOOST_REQUIRE_EQUAL(response.at("result").as_string(), expected_initial); + + BOOST_REQUIRE(query_.set(test::mock_block11, database::context{ 0, 11, 0 }, {}, false, false)); + const auto expected_rooted11 = encode_base16(sha256_hash + ( + encode_hash(hash10) + ":10:" + + encode_hash(hash11) + ":0:" + )); + + notify(node::chases::transaction{ query_.to_tx(hash11).value }); + + const auto notification = receive(); + REQUIRE_NO_THROW_TRUE(notification.at("method").is_string()); + REQUIRE_NO_THROW_TRUE(notification.at("params").is_array()); + BOOST_REQUIRE_EQUAL(notification.at("method").as_string(), "blockchain.scripthash.subscribe"); + + const auto& params = notification.at("params").as_array(); + BOOST_REQUIRE_EQUAL(params.size(), 2u); + BOOST_REQUIRE(params.at(0).is_string()); + BOOST_REQUIRE(params.at(1).is_string()); + BOOST_REQUIRE_EQUAL(params.at(0).as_string(), found_scripthash); + BOOST_REQUIRE_EQUAL(params.at(1).as_string(), expected_rooted11); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_scripthash_subscribe__history_exceeds_maximum__excessive_history) +{ + config_.server.electrum.maximum_history = 1; + BOOST_REQUIRE(handshake(electrum::version::v1_1)); + + BOOST_REQUIRE(query_.set(test::mock_block10, database::context{ 0, 10, 0 }, {}, false, false)); + BOOST_REQUIRE(query_.set(test::mock_block11, database::context{ 0, 11, 0 }, {}, false, false)); + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::mock_block10.hash()), true)); + + constexpr auto request = R"({{"id":1101,"method":"blockchain.scripthash.subscribe","params":["{}"]}})" "\n"; + const auto result = get_error(std::format(request, found_scripthash)); + BOOST_REQUIRE_EQUAL(result, excessive_history.value()); +} + // blockchain.scripthash.unsubscribe BOOST_AUTO_TEST_CASE(electrum__blockchain_scripthash_unsubscribe__insufficient_version__wrong_version) diff --git a/test/protocols/electrum/electrum_transactions.cpp b/test/protocols/electrum/electrum_transactions.cpp index 0d2d079f..ffcae5a8 100644 --- a/test/protocols/electrum/electrum_transactions.cpp +++ b/test/protocols/electrum/electrum_transactions.cpp @@ -196,6 +196,14 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_testmempoolaccept__not_arr BOOST_REQUIRE_EQUAL(result, invalid_argument.value()); } +BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_testmempoolaccept__not_string__invalid_argument) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_7)); + + const auto result = get_error(R"({"id":91,"method":"blockchain.transaction.testmempoolaccept","params":[[true]]})" "\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)); @@ -564,6 +572,24 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_id_from_pos__missing_posit 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__negative_height__invalid_argument) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_4)); + + const auto response = get(R"({"id":95,"method":"blockchain.transaction.id_from_pos","params":[-1,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__blockchain_transaction_id_from_pos__fractional_position__invalid_argument) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_4)); + + const auto response = get(R"({"id":96,"method":"blockchain.transaction.id_from_pos","params":[0,1.5]})" "\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_SUITE_END() // Broadcast tx retention. diff --git a/test/protocols/electrum/electrum_version.cpp b/test/protocols/electrum/electrum_version.cpp index 8fb685e1..e309d8b1 100644 --- a/test/protocols/electrum/electrum_version.cpp +++ b/test/protocols/electrum/electrum_version.cpp @@ -43,6 +43,14 @@ BOOST_AUTO_TEST_CASE(electrum__server_version__no_params__minimum) BOOST_REQUIRE_EQUAL(response.at("result").as_string(), "server_name"); } +BOOST_AUTO_TEST_CASE(electrum__server_version__single_value_params__minimum) +{ + const auto response = get(R"({"jsonrpc":"2.0","id":5,"method":"server.version","params":"foobar"})" "\n"); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); + BOOST_REQUIRE_EQUAL(response.at("result").as_string(), "server_name"); +} + BOOST_AUTO_TEST_CASE(electrum__server_version__trailing_arguments__ignored) { const auto response = get(R"({"id":1,"method":"server.version","params":["foobar","1.7",42,"extra"]})" "\n"); @@ -417,3 +425,59 @@ BOOST_AUTO_TEST_CASE(electrum__server_features__restricted__configured_bounds) } BOOST_AUTO_TEST_SUITE_END() + +// Configured protocol_minimum (0.6), a non-version opener is served at 0.6. + +BOOST_FIXTURE_TEST_SUITE(electrum_legacy_version_tests, electrum_legacy_version_setup_fixture) + +static const std::string legacy_opener{ R"({"id":1,"method":"server.banner","params":[]})" "\n" }; + +BOOST_AUTO_TEST_CASE(electrum__server_banner__below_v1_0__invalid_argument) +{ + const auto response = get(legacy_opener + R"({"id":2,"method":"server.banner","params":[]})" "\n"); + BOOST_REQUIRE_EQUAL(response.at("id").as_int64(), 2); + 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_donation_address__below_v1_0__invalid_argument) +{ + const auto response = get(legacy_opener + R"({"id":2,"method":"server.donation_address","params":[]})" "\n"); + BOOST_REQUIRE_EQUAL(response.at("id").as_int64(), 2); + 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_features__below_v1_0__invalid_argument) +{ + const auto response = get(legacy_opener + R"({"id":2,"method":"server.features","params":[]})" "\n"); + BOOST_REQUIRE_EQUAL(response.at("id").as_int64(), 2); + 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_peers_subscribe__below_v1_0__invalid_argument) +{ + const auto response = get(legacy_opener + R"({"id":2,"method":"server.peers.subscribe","params":[]})" "\n"); + BOOST_REQUIRE_EQUAL(response.at("id").as_int64(), 2); + 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_estimate_fee__below_v1_0__invalid_argument) +{ + const auto response = get(legacy_opener + R"({"id":2,"method":"blockchain.estimatefee","params":[1]})" "\n"); + BOOST_REQUIRE_EQUAL(response.at("id").as_int64(), 2); + 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__below_v1_0__invalid_argument) +{ + const auto response = get(legacy_opener + R"({"id":2,"method":"blockchain.transaction.broadcast","params":["00"]})" "\n"); + BOOST_REQUIRE_EQUAL(response.at("id").as_int64(), 2); + 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/sparrow/sparrow.cpp b/test/protocols/sparrow/sparrow.cpp index 9d2e64b9..b570e17e 100644 --- a/test/protocols/sparrow/sparrow.cpp +++ b/test/protocols/sparrow/sparrow.cpp @@ -78,6 +78,14 @@ BOOST_AUTO_TEST_CASE(sparrow__blockchain_block_stats__stub__method_not_found) BOOST_REQUIRE_EQUAL(result, not_implemented.value()); } +BOOST_AUTO_TEST_CASE(sparrow__blockchain_block_stats__missing_arguments__dropped) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_4)); + + const auto response = get(R"({"id":903,"method":"blockchain.block.stats","params":[]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("dropped").as_bool()); +} + BOOST_AUTO_TEST_CASE(sparrow__blockchain_silentpayments_subscribe__stub__method_not_found) { BOOST_REQUIRE(handshake(electrum::version::v1_4)); From a18e9321ed6262a905c4b4c2656bf82e1ad52824 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 29 Sep 2026 11:31:47 -0400 Subject: [PATCH 07/21] Add electrum request parser and outpoint subscription limit tests. --- .../protocols/electrum/electrum_outpoints.cpp | 20 ++++++++++++++++++ test/utilities/electrum_version.cpp | 21 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/test/protocols/electrum/electrum_outpoints.cpp b/test/protocols/electrum/electrum_outpoints.cpp index 787deb1a..c94571f4 100644 --- a/test/protocols/electrum/electrum_outpoints.cpp +++ b/test/protocols/electrum/electrum_outpoints.cpp @@ -25,6 +25,7 @@ using namespace system; static const code not_found{ server::error::electrum::bad_request }; static const code wrong_version{ server::error::electrum::bad_request }; static const code invalid_argument{ server::error::electrum::bad_request }; +static const code excessive_resource_usage{ server::error::electrum::excessive_resource_usage }; static const std::string found_address{ "1BaMPFdqMUQ46BV8iRcwbVfsam57oBLMM" }; static const std::string bogus_hash{ "4242424242424242424242424242424242424242424242424242424242424242" }; @@ -334,6 +335,25 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_outpoint_subscribe__confirmed_unspent_ BOOST_REQUIRE_EQUAL(history.at("funder_height").as_int64(), 1); } +BOOST_AUTO_TEST_CASE(electrum__blockchain_outpoint_subscribe__exceeds_maximum_subscriptions__excessive_resource_usage) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_7)); + BOOST_REQUIRE_EQUAL(config_.server.electrum.maximum_subscriptions, 2u); + + const auto hash1 = encode_hash(test::block1.transactions_ptr()->at(0)->hash(false)); + const auto hash2 = encode_hash(test::block2.transactions_ptr()->at(0)->hash(false)); + const auto hash3 = encode_hash(test::block3.transactions_ptr()->at(0)->hash(false)); + constexpr auto request = R"({{"id":1110,"method":"blockchain.outpoint.subscribe","params":["{}",0]}})" "\n"; + const auto response1 = get(std::format(request, hash1)); + BOOST_REQUIRE_MESSAGE(response1.is_object() && response1.as_object().contains("result"), serialize(response1)); + + const auto response2 = get(std::format(request, hash2)); + BOOST_REQUIRE_MESSAGE(response2.is_object() && response2.as_object().contains("result"), serialize(response2)); + + const auto result = get_error(std::format(request, hash3)); + BOOST_REQUIRE_EQUAL(result, excessive_resource_usage.value()); +} + BOOST_AUTO_TEST_CASE(electrum__blockchain_outpoint_subscribe__one_spender__expected) { BOOST_REQUIRE(handshake(electrum::version::v1_7)); diff --git a/test/utilities/electrum_version.cpp b/test/utilities/electrum_version.cpp index 5a496c97..5b5b9533 100644 --- a/test/utilities/electrum_version.cpp +++ b/test/utilities/electrum_version.cpp @@ -131,3 +131,24 @@ BOOST_AUTO_TEST_CASE(electrum_version__version_floor__undefined__next_lower) } BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE(electrum_request_tests) + +using namespace network::rpc; + +BOOST_AUTO_TEST_CASE(electrum_request__server_version__single_value__minimum_appended) +{ + request_t message{ .method = "server.version", .params = params_t{ value_t{ string_t{ "foobar" } } } }; + electrum_request(message, electrum::version::v0_0, { 1, 0 }, { 1, 7 }); + BOOST_REQUIRE(message.params.has_value()); + BOOST_REQUIRE(std::holds_alternative(message.params.value())); + + const auto& params = std::get(message.params.value()); + BOOST_REQUIRE_EQUAL(params.size(), 2u); + BOOST_REQUIRE(std::holds_alternative(params.at(0).value())); + BOOST_REQUIRE(std::holds_alternative(params.at(1).value())); + BOOST_REQUIRE_EQUAL(std::get(params.at(0).value()), "foobar"); + BOOST_REQUIRE_EQUAL(std::get(params.at(1).value()), "1.0"); +} + +BOOST_AUTO_TEST_SUITE_END() From 022f99141fba14adc90620aaecf2c087c3c52dd9 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 29 Sep 2026 11:39:05 -0400 Subject: [PATCH 08/21] Add electrum estimatefee invalid mode test. --- test/protocols/electrum/electrum_fees.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/protocols/electrum/electrum_fees.cpp b/test/protocols/electrum/electrum_fees.cpp index 44573830..0c71e5b9 100644 --- a/test/protocols/electrum/electrum_fees.cpp +++ b/test/protocols/electrum/electrum_fees.cpp @@ -60,6 +60,14 @@ 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__invalid_mode_v1_6__invalid_argument) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_6)); + + 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)); From f18e930c9079b38830157f18b73bd6471859ac16 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 29 Sep 2026 11:25:38 -0400 Subject: [PATCH 09/21] Add esplora request validation, broadcast, unconfirmed and header-only tests. --- test/parsers/esplora_target.cpp | 6 ++ test/protocols/esplora/esplora_block.cpp | 32 ++++++++ test/protocols/esplora/esplora_mempool.cpp | 43 +++++++++++ .../esplora/esplora_setup_fixture.cpp | 32 ++++++++ .../esplora/esplora_setup_fixture.hpp | 45 +++++++++++ .../protocols/esplora/esplora_transaction.cpp | 74 +++++++++++++++++++ 6 files changed, 232 insertions(+) diff --git a/test/parsers/esplora_target.cpp b/test/parsers/esplora_target.cpp index 8d7abb7b..4d79d16f 100644 --- a/test/parsers/esplora_target.cpp +++ b/test/parsers/esplora_target.cpp @@ -493,6 +493,12 @@ BOOST_AUTO_TEST_CASE(parsers__esplora_target__block_txid_missing_index__missing_ BOOST_REQUIRE_EQUAL(esplora_target(out, "/block/0000000000000000000000000000000000000000000000000000000000000042/txid"), server::error::missing_position); } +BOOST_AUTO_TEST_CASE(parsers__esplora_target__block_txid_invalid_index__invalid_number) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(esplora_target(out, "/block/0000000000000000000000000000000000000000000000000000000000000042/txid/x"), server::error::invalid_number); +} + BOOST_AUTO_TEST_CASE(parsers__esplora_target__block_missing_hash__missing_hash) { request_t out{}; diff --git a/test/protocols/esplora/esplora_block.cpp b/test/protocols/esplora/esplora_block.cpp index 4a39c150..056f090b 100644 --- a/test/protocols/esplora/esplora_block.cpp +++ b/test/protocols/esplora/esplora_block.cpp @@ -207,3 +207,35 @@ BOOST_AUTO_TEST_CASE(esplora__block_txids__unknown_hash__not_found) } BOOST_AUTO_TEST_SUITE_END() + +// unassociated block +// ---------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_SUITE(esplora_header_tests, esplora_header_setup_fixture) + +static const std::string block10_hash = encode_hash(test::mock_block10.hash()); + +BOOST_AUTO_TEST_CASE(esplora__block__unassociated__not_found) +{ + BOOST_REQUIRE_EQUAL(get_status("/block/" + block10_hash), http::status::not_found); +} + +BOOST_AUTO_TEST_CASE(esplora__block_raw__unassociated__not_found) +{ + BOOST_REQUIRE_EQUAL(get_status("/block/" + block10_hash + "/raw"), http::status::not_found); +} + +BOOST_AUTO_TEST_CASE(esplora__block_header__unassociated__expected) +{ + BOOST_REQUIRE_EQUAL(get_text("/block/" + block10_hash + "/header"), encode_base16(test::mock_block10.header().to_data())); +} + +BOOST_AUTO_TEST_CASE(esplora__block_status__unconfirmed__not_in_best_chain) +{ + const auto response = get_json("/block/" + block10_hash + "/status"); + BOOST_REQUIRE(response.is_object()); + BOOST_REQUIRE(!response.as_object().at("in_best_chain").as_bool()); + BOOST_REQUIRE(!response.as_object().contains("next_best")); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/protocols/esplora/esplora_mempool.cpp b/test/protocols/esplora/esplora_mempool.cpp index ca62f945..847db1b4 100644 --- a/test/protocols/esplora/esplora_mempool.cpp +++ b/test/protocols/esplora/esplora_mempool.cpp @@ -91,4 +91,47 @@ BOOST_AUTO_TEST_CASE(esplora__invalid_target__not_found) BOOST_REQUIRE_EQUAL(status, http::status::not_found); } +// request validation +// ---------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(esplora__get__non_origin_form__bad_request) +{ + BOOST_REQUIRE_EQUAL(get_status("blocks/tip/height"), http::status::bad_request); +} + +BOOST_AUTO_TEST_CASE(esplora__get__opaque_origin__forbidden) +{ + BOOST_REQUIRE_EQUAL(get_status("/blocks/tip/height", http::field::origin, "null"), http::status::forbidden); +} + +BOOST_AUTO_TEST_CASE(esplora__get__empty_host__bad_request) +{ + BOOST_REQUIRE_EQUAL(get_status("/blocks/tip/height", http::field::host, ""), http::status::bad_request); +} + +BOOST_AUTO_TEST_CASE(esplora__post__non_origin_form__bad_request) +{ + BOOST_REQUIRE_EQUAL(post_status("tx", "00"), http::status::bad_request); +} + +BOOST_AUTO_TEST_CASE(esplora__post__opaque_origin__forbidden) +{ + BOOST_REQUIRE_EQUAL(post_status("/tx", "00", http::field::origin, "null"), http::status::forbidden); +} + +BOOST_AUTO_TEST_CASE(esplora__post__empty_host__bad_request) +{ + BOOST_REQUIRE_EQUAL(post_status("/tx", "00", http::field::host, ""), http::status::bad_request); +} + +BOOST_AUTO_TEST_CASE(esplora__post__invalid_target__not_found) +{ + BOOST_REQUIRE_EQUAL(post_status("/bogus", "00"), http::status::not_found); +} + +BOOST_AUTO_TEST_CASE(esplora__post__bodyless_method__method_not_allowed) +{ + BOOST_REQUIRE_EQUAL(post_status("/blocks/tip/height", "00"), http::status::method_not_allowed); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/protocols/esplora/esplora_setup_fixture.cpp b/test/protocols/esplora/esplora_setup_fixture.cpp index 67656eb6..29cb095e 100644 --- a/test/protocols/esplora/esplora_setup_fixture.cpp +++ b/test/protocols/esplora/esplora_setup_fixture.cpp @@ -148,6 +148,22 @@ http::status esplora_setup_fixture::get_status(std::string_view target) return response.result(); } +http::status esplora_setup_fixture::get_status(std::string_view target, + http::field name, std::string_view value) +{ + auto request = create_request(target); + request.set(name, value); + http::write(socket_, request); + + flat_buffer buffer{}; + network::boost_code ec{}; + http::response response{}; + http::read(socket_, buffer, response, ec); + BOOST_CHECK_MESSAGE(!ec, ec.message()); + + return response.result(); +} + std::string esplora_setup_fixture::get_text(std::string_view target) { http::write(socket_, create_request(target)); @@ -221,6 +237,22 @@ http::status esplora_setup_fixture::post_status(std::string_view target, return response.result(); } +http::status esplora_setup_fixture::post_status(std::string_view target, + std::string_view body, http::field name, std::string_view value) +{ + auto request = create_post(target, body); + request.set(name, value); + http::write(socket_, request); + + flat_buffer buffer{}; + network::boost_code ec{}; + http::response response{}; + http::read(socket_, buffer, response, ec); + BOOST_CHECK_MESSAGE(!ec, ec.message()); + + return response.result(); +} + std::string esplora_setup_fixture::post_text(std::string_view target, std::string_view body) { diff --git a/test/protocols/esplora/esplora_setup_fixture.hpp b/test/protocols/esplora/esplora_setup_fixture.hpp index 899348a2..e7c6a178 100644 --- a/test/protocols/esplora/esplora_setup_fixture.hpp +++ b/test/protocols/esplora/esplora_setup_fixture.hpp @@ -36,10 +36,14 @@ struct esplora_setup_fixture ~esplora_setup_fixture(); status get_status(std::string_view target); + status get_status(std::string_view target, + boost::beast::http::field name, std::string_view value); std::string get_text(std::string_view target); system::data_chunk get_data(std::string_view target); boost::json::value get_json(std::string_view target); status post_status(std::string_view target, std::string_view body); + status post_status(std::string_view target, std::string_view body, + boost::beast::http::field name, std::string_view value); std::string post_text(std::string_view target, std::string_view body); network::boost_code ws_upgrade(); @@ -87,6 +91,35 @@ struct esplora_submit_setup_fixture } }; +struct esplora_broadcast_setup_fixture + : esplora_setup_fixture +{ + inline esplora_broadcast_setup_fixture() + : esplora_setup_fixture([](test::query_t& query) + { + return test::setup_broadcast_store(query); + }, [](server::configuration& config) + { + config.node.currency_window_minutes = 0; + }, true) + { + } +}; + +struct esplora_header_setup_fixture + : esplora_setup_fixture +{ + inline esplora_header_setup_fixture() + : esplora_setup_fixture([](test::query_t& query) + { + return test::setup_ten_block_store(query) && + query.set(test::mock_block10.header(), + database::context{ 0, 10, 0 }, {}, false); + }) + { + } +}; + struct esplora_ten_block_setup_fixture : esplora_setup_fixture { @@ -111,6 +144,18 @@ struct esplora_witness_setup_fixture } }; +struct esplora_unconfirmed_setup_fixture + : esplora_setup_fixture +{ + inline esplora_unconfirmed_setup_fixture() + : esplora_setup_fixture([](test::query_t& query) + { + return test::setup_three_block_unconfirmed_address_store(query); + }) + { + } +}; + struct esplora_no_address_setup_fixture : esplora_setup_fixture { diff --git a/test/protocols/esplora/esplora_transaction.cpp b/test/protocols/esplora/esplora_transaction.cpp index 914a6400..23cb9065 100644 --- a/test/protocols/esplora/esplora_transaction.cpp +++ b/test/protocols/esplora/esplora_transaction.cpp @@ -258,6 +258,80 @@ BOOST_AUTO_TEST_CASE(esplora__tx_outspends__spent__expected) BOOST_REQUIRE_EQUAL(spend0.at("vin").as_int64(), 0); } +BOOST_AUTO_TEST_CASE(esplora__tx__witness_spend__witness_and_prevout) +{ + const auto& prevouts = *test::block1a.transactions_ptr()->front()->outputs_ptr(); + const auto response = get_json("/tx/" + tx2); + BOOST_REQUIRE(response.is_object()); + + const auto& inputs = response.as_object().at("vin").as_array(); + BOOST_REQUIRE_EQUAL(inputs.size(), 2u); + + const auto& input0 = inputs.at(0).as_object(); + BOOST_REQUIRE(!input0.at("is_coinbase").as_bool()); + BOOST_REQUIRE_EQUAL(input0.at("txid").as_string(), tx1); + BOOST_REQUIRE_EQUAL(input0.at("vout").as_int64(), 0); + BOOST_REQUIRE_EQUAL(input0.at("witness").as_array().size(), 1u); + BOOST_REQUIRE_EQUAL(input0.at("witness").as_array().front().as_string(), "242424"); + BOOST_REQUIRE_EQUAL(input0.at("prevout").as_object().at("value").as_int64(), to_signed(prevouts.at(0)->value())); + BOOST_REQUIRE_EQUAL(input0.at("prevout").as_object().at("scriptpubkey").as_string(), encode_base16(prevouts.at(0)->script().to_data(false))); + + const auto& input1 = inputs.at(1).as_object(); + BOOST_REQUIRE_EQUAL(input1.at("vout").as_int64(), 1); + BOOST_REQUIRE_EQUAL(input1.at("witness").as_array().front().as_string(), "313131"); + BOOST_REQUIRE_EQUAL(input1.at("prevout").as_object().at("value").as_int64(), to_signed(prevouts.at(1)->value())); +} + +BOOST_AUTO_TEST_SUITE_END() + +// unconfirmed tx +// ---------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_SUITE(esplora_unconfirmed_tests, esplora_unconfirmed_setup_fixture) + +static const std::string tx4 = encode_hash(test::tx4.hash(false)); + +BOOST_AUTO_TEST_CASE(esplora__tx_status__unconfirmed__not_confirmed) +{ + const auto response = get_json("/tx/" + tx4 + "/status"); + BOOST_REQUIRE(response.is_object()); + BOOST_REQUIRE_EQUAL(response.as_object().size(), 1u); + BOOST_REQUIRE(!response.as_object().at("confirmed").as_bool()); +} + +BOOST_AUTO_TEST_CASE(esplora__tx__unconfirmed__status_not_confirmed) +{ + const auto response = get_json("/tx/" + tx4); + BOOST_REQUIRE(response.is_object()); + BOOST_REQUIRE_EQUAL(response.as_object().at("txid").as_string(), tx4); + BOOST_REQUIRE(!response.as_object().at("status").as_object().at("confirmed").as_bool()); +} + +BOOST_AUTO_TEST_CASE(esplora__tx_merkle_proof__unconfirmed__not_found) +{ + BOOST_REQUIRE_EQUAL(get_status("/tx/" + tx4 + "/merkle-proof"), http::status::not_found); +} + +BOOST_AUTO_TEST_SUITE_END() + +// broadcast +// ---------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_SUITE(esplora_broadcast_tests, esplora_broadcast_setup_fixture) + +BOOST_AUTO_TEST_CASE(esplora__broadcast__valid__txid) +{ + const auto tx1c = encode_base16(test::tx1c.to_data(true)); + BOOST_REQUIRE_EQUAL(post_text("/tx", tx1c), encode_hash(test::tx1c.hash(false))); +} + +BOOST_AUTO_TEST_CASE(esplora__broadcast__duplicate__txid) +{ + const auto tx1c = encode_base16(test::tx1c.to_data(true)); + BOOST_REQUIRE_EQUAL(post_text("/tx", tx1c), encode_hash(test::tx1c.hash(false))); + BOOST_REQUIRE_EQUAL(post_text("/tx", tx1c), encode_hash(test::tx1c.hash(false))); +} + BOOST_AUTO_TEST_SUITE_END() // open tx pool From 3a216d1b1f1f872972eb90f34826af3d7efc4ca3 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 29 Sep 2026 11:25:38 -0400 Subject: [PATCH 10/21] Add btcd cfilter, node, outpoint filter and watch limit tests. --- test/protocols/btcd/btcd_rpc.cpp | 218 +++++++++++++++++++++ test/protocols/btcd/btcd_setup_fixture.cpp | 4 +- test/protocols/btcd/btcd_setup_fixture.hpp | 2 +- 3 files changed, 221 insertions(+), 3 deletions(-) diff --git a/test/protocols/btcd/btcd_rpc.cpp b/test/protocols/btcd/btcd_rpc.cpp index 1976afcd..d5a08325 100644 --- a/test/protocols/btcd/btcd_rpc.cpp +++ b/test/protocols/btcd/btcd_rpc.cpp @@ -887,6 +887,190 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__searchrawtransactions__reverse__found) BOOST_REQUIRE_EQUAL(response.at("result").as_array().size(), 1u); } +BOOST_AUTO_TEST_CASE(btcd_rpc__searchrawtransactions__count_below_history__truncated) +{ + BOOST_REQUIRE(query_.set(test::mock_block10, database::context{ 0, 10, 0 }, {}, false, false)); + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::mock_block10.hash()), true)); + + const auto& coinbase = *test::block1.transactions_ptr()->front(); + const auto& point = coinbase.outputs_ptr()->front()->script().ops().front().data(); + const auto all = rpc("searchrawtransactions", std::format(R"(["{}"])", encode_base16(point))); + BOOST_REQUIRE_MESSAGE(all.is_object() && all.as_object().contains("result"), serialize(all)); + BOOST_REQUIRE_EQUAL(all.at("result").as_array().size(), 2u); + + const auto response = rpc("searchrawtransactions", std::format(R"(["{}",1,0,1])", encode_base16(point))); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + + const auto& result = response.at("result").as_array(); + BOOST_REQUIRE_EQUAL(result.size(), 1u); + BOOST_REQUIRE_EQUAL(as_text(result.front().at("txid")), encode_hash(coinbase.hash(false))); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__searchrawtransactions__vinextra_addressable_prevout__prevout_address) +{ + BOOST_REQUIRE(query_.set(test::mock_block13, database::context{ 0, 10, 0 }, {}, false, false)); + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::mock_block13.hash()), true)); + + const auto& spending = *test::mock_block13.transactions_ptr()->at(2); + const auto response = rpc("searchrawtransactions", std::format(R"(["{}",1,0,100,1])", found_address)); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + + const auto& result = response.at("result").as_array(); + BOOST_REQUIRE_EQUAL(result.size(), 2u); + BOOST_REQUIRE_EQUAL(as_text(result.back().at("txid")), encode_hash(spending.hash(false))); + + const auto& prevout = result.back().at("vin").as_array().front().at("prevOut"); + BOOST_REQUIRE_EQUAL(prevout.at("addresses").as_array().size(), 1u); + BOOST_REQUIRE_EQUAL(as_text(prevout.at("addresses").as_array().front()), found_address); + BOOST_REQUIRE_EQUAL(prevout.at("value").as_double(), 0.00000009); +} + +// cfilters +// ---------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(btcd_rpc__getcfilter__genesis__single_element_filter) +{ + const auto response = rpc("getcfilter", std::format(R"(["{}",0])", encode_hash(test::block0_hash))); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + + data_chunk filter{}; + BOOST_REQUIRE(decode_base16(filter, as_text(response.at("result")))); + BOOST_REQUIRE_EQUAL(filter.size(), 4u); + BOOST_REQUIRE_EQUAL(filter.front(), 0x01u); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__getcfilterheader__genesis__chained_filter_hash) +{ + const auto body = rpc("getcfilter", std::format(R"(["{}",0])", encode_hash(test::block0_hash))); + const auto response = rpc("getcfilterheader", std::format(R"(["{}",0])", encode_hash(test::block0_hash))); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + + data_chunk filter{}; + BOOST_REQUIRE(decode_base16(filter, as_text(body.at("result")))); + + const auto expected = bitcoin_hash(splice(bitcoin_hash(filter), null_hash)); + BOOST_REQUIRE_EQUAL(as_text(response.at("result")), encode_hash(expected)); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__getcfilterheader__malformed_hash__deserialization) +{ + BOOST_REQUIRE_EQUAL(rpc_error("getcfilterheader", R"(["not-a-hash",0])"), deserialization.value()); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__getheaders__malformed_hashstop__deserialization) +{ + BOOST_REQUIRE_EQUAL(rpc_error("getheaders", std::format(R"([["{}"],"not-a-hash"])", encode_hash(test::block5_hash))), deserialization.value()); +} + +// node +// ---------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(btcd_rpc__node__connect_temp__null_result) +{ + const auto response = rpc("node", R"(["connect","127.0.0.1:1","temp"])"); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_null()); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__node__connect_perm__null_result) +{ + const auto response = rpc("node", R"(["connect","127.0.0.1:1","perm"])"); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_null()); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__node__disconnect_empty_address__invalid_parameter) +{ + BOOST_REQUIRE_EQUAL(rpc_error("node", R"(["disconnect",""])"), invalid_parameter.value()); +} + +// post transport +// ---------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(btcd_rpc__getblockcount__http_post__nine) +{ + const auto response = http_rpc("getblockcount"); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + BOOST_REQUIRE_EQUAL(response.at("result").as_int64(), 9); +} + +// outpoint filters +// ---------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(btcd_rpc__loadtxfilter__undecodable_outpoint_hash__invalid_parameter) +{ + BOOST_REQUIRE_EQUAL(rpc_error("loadtxfilter", R"([true,[],[{"hash":"zz","index":0}]])"), invalid_parameter.value()); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__rescanblocks__outpoint_spent__spending_transaction) +{ + const auto block10 = encode_hash(test::mock_block10.hash()); + const auto& paying = *test::mock_block10.transactions_ptr()->at(1); + BOOST_REQUIRE(query_.set(test::mock_block10, database::context{ 0, 10, 0 }, {}, false, false)); + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::mock_block10.hash()), true)); + + constexpr auto request = R"([true,[],[{{"hash":"{}","index":0}}]])"; + REQUIRE_NO_THROW_TRUE(rpc("loadtxfilter", std::format(request, coinbase_txid(test::block1))).at("result").is_null()); + + const auto response = rpc("rescanblocks", std::format(R"([["{}"]])", block10)); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + + const auto& result = response.at("result").as_array(); + BOOST_REQUIRE_EQUAL(result.size(), 1u); + BOOST_REQUIRE_EQUAL(as_text(result.front().at("hash")), block10); + BOOST_REQUIRE_EQUAL(result.front().at("transactions").as_array().size(), 1u); + BOOST_REQUIRE_EQUAL(as_text(result.front().at("transactions").as_array().front()), encode_base16(paying.to_data(true))); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__filteredblockconnected__outpoint_spent__delivered) +{ + const auto& paying = *test::mock_block10.transactions_ptr()->at(1); + rpc("notifyblocks"); + + constexpr auto request = R"([true,[],[{{"hash":"{}","index":0}}]])"; + REQUIRE_NO_THROW_TRUE(rpc("loadtxfilter", std::format(request, coinbase_txid(test::block1))).at("result").is_null()); + + BOOST_REQUIRE(query_.set(test::mock_block10, database::context{ 0, 10, 0 }, {}, false, false)); + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::mock_block10.hash()), true)); + + notify(node::chases::organized{ 10 }); + + const auto blockconnected = receive_notification(); + BOOST_REQUIRE_EQUAL(as_text(blockconnected.at("method")), "blockconnected"); + + const auto filtered = receive_notification(); + BOOST_REQUIRE_EQUAL(as_text(filtered.at("method")), "filteredblockconnected"); + + const auto& txs = filtered.at("params").as_array()[2].as_array(); + BOOST_REQUIRE_EQUAL(txs.size(), 1u); + BOOST_REQUIRE_EQUAL(as_text(txs.front()), encode_base16(paying.to_data(true))); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__blockdisconnected__address_watches__delivered) +{ + rpc("notifyblocks"); + REQUIRE_NO_THROW_TRUE(rpc("loadtxfilter", std::format(R"([true,["{}"],[]])", found_address)).at("result").is_null()); + REQUIRE_NO_THROW_TRUE(rpc("notifyreceived", std::format(R"([["{}"]])", found_address)).at("result").is_null()); + + notify(node::chases::reorganized{ 9 }); + + const auto disconnected = receive_notification(); + BOOST_REQUIRE_EQUAL(as_text(disconnected.at("method")), "blockdisconnected"); + BOOST_REQUIRE_EQUAL(as_text(disconnected.at("params").as_array()[0]), block9); + + const auto filtered = receive_notification(); + BOOST_REQUIRE_EQUAL(as_text(filtered.at("method")), "filteredblockdisconnected"); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__blockdisconnected__not_subscribed__not_delivered) +{ + notify(node::chases::reorganized{ 9 }); + + const auto response = rpc("session"); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); +} + BOOST_AUTO_TEST_SUITE_END() // Filter limit (btcd.maximum_filters): loadtxfilter watches are bounded per @@ -903,6 +1087,40 @@ BOOST_AUTO_TEST_CASE(btcd_limited_filter__loadtxfilter__over_limit__subscription BOOST_REQUIRE_EQUAL(result, misc_error.value()); } +BOOST_AUTO_TEST_CASE(btcd_limited_filter__loadtxfilter__outpoints_over_limit__misc_error) +{ + constexpr auto request = R"([true,[],[{{"hash":"{0}","index":0}},{{"hash":"{0}","index":1}}]])"; + BOOST_REQUIRE_EQUAL(rpc_error("loadtxfilter", std::format(request, coinbase_txid(test::block1))), misc_error.value()); +} + +BOOST_AUTO_TEST_CASE(btcd_limited_filter__loadtxfilter__addresses_over_limit_with_outpoint__misc_error) +{ + constexpr auto request = R"([true,["{}","{}"],[{{"hash":"{}","index":0}}]])"; + BOOST_REQUIRE_EQUAL(rpc_error("loadtxfilter", std::format(request, found_address, other_address, coinbase_txid(test::block1))), misc_error.value()); +} + +BOOST_AUTO_TEST_CASE(btcd_limited_filter__notifyreceived__over_limit__misc_error) +{ + BOOST_REQUIRE_EQUAL(rpc_error("notifyreceived", std::format(R"([["{}","{}"]])", found_address, other_address)), misc_error.value()); +} + +BOOST_AUTO_TEST_CASE(btcd_limited_filter__notifyspent__over_limit__misc_error) +{ + constexpr auto request = R"([[{{"hash":"{0}","index":0}},{{"hash":"{0}","index":1}}]])"; + BOOST_REQUIRE_EQUAL(rpc_error("notifyspent", std::format(request, coinbase_txid(test::block1))), misc_error.value()); +} + +BOOST_AUTO_TEST_CASE(btcd_limited_filter__recvtx__spent_watch_over_limit__dropped) +{ + REQUIRE_NO_THROW_TRUE(rpc("notifyreceived", std::format(R"([["{}"]])", found_address)).at("result").is_null()); + + BOOST_REQUIRE(query_.set(test::mock_block10, database::context{ 0, 10, 0 }, {}, false, false)); + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::mock_block10.hash()), true)); + + notify(node::chases::organized{ 10 }); + REQUIRE_NO_THROW_TRUE(receive_notification(false).at("dropped").as_bool()); +} + BOOST_AUTO_TEST_SUITE_END() // Address index requirement: address watching and rescan are index queries, diff --git a/test/protocols/btcd/btcd_setup_fixture.cpp b/test/protocols/btcd/btcd_setup_fixture.cpp index 19e4a8fc..b3b2595e 100644 --- a/test/protocols/btcd/btcd_setup_fixture.cpp +++ b/test/protocols/btcd/btcd_setup_fixture.cpp @@ -111,9 +111,9 @@ bool btcd_setup_fixture::authenticate(const std::string& username, } } -boost::json::value btcd_setup_fixture::receive_notification() +boost::json::value btcd_setup_fixture::receive_notification(bool checked) { - return client_.read_frame(true); + return client_.read_frame(checked); } // Raw json on the plain socket, which the server detects and downgrades to a diff --git a/test/protocols/btcd/btcd_setup_fixture.hpp b/test/protocols/btcd/btcd_setup_fixture.hpp index b2110c26..219534d1 100644 --- a/test/protocols/btcd/btcd_setup_fixture.hpp +++ b/test/protocols/btcd/btcd_setup_fixture.hpp @@ -68,7 +68,7 @@ struct btcd_setup_fixture // Read one further (unprompted) server push, e.g. a blockconnected // notification. Returns the parsed json-rpc notification object. - boost::json::value receive_notification(); + boost::json::value receive_notification(bool checked=true); private: // The ws connection, plus the plain one used by http_rpc or tcp_rpc. From 329a785e39c16a94d32b1ee6f5ef6b33de93c44d Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 29 Sep 2026 11:31:11 -0400 Subject: [PATCH 11/21] Add bitcoind scanblocks, wait, chain tips and filter tests. --- test/protocols/bitcoind/bitcoind_rpc.cpp | 191 ++++++++++++++++++ .../bitcoind/bitcoind_setup_fixture.cpp | 60 ++++++ .../bitcoind/bitcoind_setup_fixture.hpp | 87 ++++++++ 3 files changed, 338 insertions(+) diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 67dbeec4..57738ca7 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -2751,6 +2751,197 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__utxoupdatepsbt__matching_output_descriptor__w BOOST_REQUIRE(out.derivations.front().point == to_chunk(point)); } +// scanblocks (the store holds only the genesis filter) + +static const std::string genesis_pk{ "pk(04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f)" }; + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__scanblocks__genesis_key__genesis_block) +{ + const auto response = rpc("scanblocks", "[\"start\", [\"" + genesis_pk + "\"], 0, 0]"); + const auto& result = response.at("result"); + BOOST_REQUIRE_EQUAL(result.at("from_height").as_int64(), 0); + BOOST_REQUIRE_EQUAL(result.at("to_height").as_int64(), 0); + BOOST_REQUIRE_EQUAL(result.at("relevant_blocks").as_array().size(), 1u); + BOOST_REQUIRE_EQUAL(as_text(result.at("relevant_blocks").at(0)), block0); + BOOST_REQUIRE(result.at("completed").as_bool()); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__scanblocks__unmatched_script__no_blocks) +{ + const auto response = rpc("scanblocks", "[\"start\", [\"raw(51)\"], 0, 0]"); + const auto& result = response.at("result"); + BOOST_REQUIRE(result.at("relevant_blocks").as_array().empty()); + BOOST_REQUIRE(result.at("completed").as_bool()); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__scanblocks__unknown_filtertype__invalid_address) +{ + BOOST_REQUIRE(has_code(rpc("scanblocks", "[\"start\", [\"" + genesis_pk + "\"], 0, 0, \"extended\"]"), -5)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__scanblocks__no_scanobjects__misc_error) +{ + BOOST_REQUIRE(has_code(rpc("scanblocks", "[\"start\"]"), -1)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__scanblocks__negative_start__misc_error) +{ + BOOST_REQUIRE(has_code(rpc("scanblocks", "[\"start\", [\"" + genesis_pk + "\"], -1]"), -1)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__scanblocks__stop_below_start__misc_error) +{ + BOOST_REQUIRE(has_code(rpc("scanblocks", "[\"start\", [\"" + genesis_pk + "\"], 5, 2]"), -1)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__scanblocks__invalid_descriptor__invalid_address) +{ + BOOST_REQUIRE(has_code(rpc("scanblocks", "[\"start\", [\"bogus\"], 0, 0]"), -5)); +} + +// scantxoutset + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__scantxoutset__repeated_script__one_unspent) +{ + const auto& coinbase = *test::block1.transactions_ptr()->front(); + const auto raw = "\"raw(" + encode_base16(coinbase.outputs_ptr()->front()->script().to_data(false)) + ")\""; + const auto response = rpc("scantxoutset", "[\"start\", [" + raw + ", " + raw + "]]"); + const auto& result = response.at("result"); + BOOST_REQUIRE(result.at("success").as_bool()); + BOOST_REQUIRE_EQUAL(result.at("unspents").as_array().size(), 1u); + BOOST_REQUIRE_EQUAL(as_text(result.at("unspents").at(0).at("txid")), encode_hash(coinbase.hash(false))); + BOOST_REQUIRE_EQUAL(result.at("total_amount").as_double(), 50.0); +} + +// verifytxoutproof + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__verifytxoutproof__unknown_block__invalid_address) +{ + const auto& header = test::mock_block10.header(); + const auto proof = encode_base16(header.to_data()) + "01000000" + "01" + encode_base16(header.merkle_root()) + "01" + "01"; + BOOST_REQUIRE(has_code(rpc("verifytxoutproof", "[\"" + proof + "\"]"), -5)); +} + +// getdeploymentinfo + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getdeploymentinfo__not_hash__invalid_parameter) +{ + BOOST_REQUIRE(has_code(rpc("getdeploymentinfo", "[\"nothex\"]"), -8)); +} + +// getdescriptoractivity + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getdescriptoractivity__not_hash__invalid_parameter) +{ + BOOST_REQUIRE(has_code(rpc("getdescriptoractivity", "[[\"nothex\"], []]"), -8)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getdescriptoractivity__invalid_descriptor__invalid_address) +{ + BOOST_REQUIRE(has_code(rpc("getdescriptoractivity", "[[\"" + block1 + "\"], [\"bogus\"]]"), -5)); +} + +// waitfor + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__waitforblock__not_hash__invalid_parameter) +{ + BOOST_REQUIRE(has_code(rpc("waitforblock", "[\"nothex\"]"), -8)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__waitforblockheight__negative_timeout__misc_error) +{ + BOOST_REQUIRE(has_code(rpc("waitforblockheight", "[100, -1]"), -1)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__waitfornewblock__stale_tip__immediate_top) +{ + const auto response = rpc("waitfornewblock", "[60000, \"" + block5 + "\"]"); + const auto& result = response.at("result"); + BOOST_REQUIRE_EQUAL(result.at("height").as_int64(), 9); + BOOST_REQUIRE_EQUAL(as_text(result.at("hash")), block9); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__waitforblockheight__organized__new_top) +{ + BOOST_REQUIRE(!ws_upgrade()); + ws_notify(R"({"jsonrpc":"2.0","id":0,"method":"waitforblockheight","params":[10, 60000]})"); + + BOOST_REQUIRE(query_.set(test::mock_block10, database::context{ 0, 10, 0 }, {}, false, false)); + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::mock_block10.hash()), true)); + notify(node::chases::organized{ 10 }); + + const auto response = ws_read(); + const auto& result = response.at("result"); + BOOST_REQUIRE_EQUAL(result.at("height").as_int64(), 10); + BOOST_REQUIRE_EQUAL(as_text(result.at("hash")), encode_hash(test::mock_block10.hash())); +} + +BOOST_AUTO_TEST_SUITE_END() + +// block filters disabled +// ---------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_SUITE(bitcoind_unfiltered_tests, bitcoind_unfiltered_setup_fixture) + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblockfilter__unfiltered__misc_error) +{ + BOOST_REQUIRE(has_code(rpc("getblockfilter", hash_param(test::block0_hash)), -1)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__scanblocks__unfiltered__misc_error) +{ + BOOST_REQUIRE(has_code(rpc("scanblocks", "[\"start\", [\"raw(51)\"]]"), -1)); +} + +BOOST_AUTO_TEST_SUITE_END() + +// candidate above confirmed +// ---------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_SUITE(bitcoind_candidate_tests, bitcoind_candidate_setup_fixture) + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getchaintips__candidate_block__valid_headers) +{ + const auto response = rpc("getchaintips"); + const auto& tips = response.at("result").as_array(); + BOOST_REQUIRE_EQUAL(tips.size(), 2u); + BOOST_REQUIRE_EQUAL(tips.at(0).at("height").as_int64(), 9); + BOOST_REQUIRE_EQUAL(as_text(tips.at(0).at("status")), "active"); + BOOST_REQUIRE_EQUAL(tips.at(1).at("height").as_int64(), 10); + BOOST_REQUIRE_EQUAL(as_text(tips.at(1).at("hash")), encode_hash(test::mock_block10.hash())); + BOOST_REQUIRE_EQUAL(tips.at(1).at("branchlen").as_int64(), 1); + BOOST_REQUIRE_EQUAL(as_text(tips.at(1).at("status")), "valid-headers"); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getchainstates__candidate_block__unvalidated_second_state) +{ + const auto response = rpc("getchainstates"); + const auto& result = response.at("result"); + BOOST_REQUIRE_EQUAL(result.at("headers").as_int64(), 10); + + const auto& states = result.at("chainstates").as_array(); + BOOST_REQUIRE_EQUAL(states.size(), 2u); + BOOST_REQUIRE_EQUAL(states.at(0).at("blocks").as_int64(), 9); + BOOST_REQUIRE(states.at(0).at("validated").as_bool()); + BOOST_REQUIRE_EQUAL(states.at(1).at("blocks").as_int64(), 10); + BOOST_REQUIRE_EQUAL(as_text(states.at(1).at("bestblockhash")), encode_hash(test::mock_block10.hash())); + BOOST_REQUIRE(!states.at(1).at("validated").as_bool()); +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_FIXTURE_TEST_SUITE(bitcoind_candidate_header_tests, bitcoind_candidate_header_setup_fixture) + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getchaintips__candidate_header__headers_only) +{ + const auto response = rpc("getchaintips"); + const auto& tips = response.at("result").as_array(); + BOOST_REQUIRE_EQUAL(tips.size(), 2u); + BOOST_REQUIRE_EQUAL(tips.at(1).at("height").as_int64(), 10); + BOOST_REQUIRE_EQUAL(tips.at(1).at("branchlen").as_int64(), 1); + BOOST_REQUIRE_EQUAL(as_text(tips.at(1).at("status")), "headers-only"); +} + BOOST_AUTO_TEST_SUITE_END() // websocket authorization diff --git a/test/protocols/bitcoind/bitcoind_setup_fixture.cpp b/test/protocols/bitcoind/bitcoind_setup_fixture.cpp index 37e7a564..40ac6d80 100644 --- a/test/protocols/bitcoind/bitcoind_setup_fixture.cpp +++ b/test/protocols/bitcoind/bitcoind_setup_fixture.cpp @@ -24,6 +24,26 @@ using namespace boost::beast; +bool setup_candidate_store(test::query_t& query, bool associated) NOEXCEPT +{ + const auto& header = test::mock_block10.header(); + const database::context context{ 0, 10, 0 }; + return test::setup_ten_block_store(query) && + query.push_candidate(query.to_header(test::block1_hash)) && + query.push_candidate(query.to_header(test::block2_hash)) && + query.push_candidate(query.to_header(test::block3_hash)) && + query.push_candidate(query.to_header(test::block4_hash)) && + query.push_candidate(query.to_header(test::block5_hash)) && + query.push_candidate(query.to_header(test::block6_hash)) && + query.push_candidate(query.to_header(test::block7_hash)) && + query.push_candidate(query.to_header(test::block8_hash)) && + query.push_candidate(query.to_header(test::block9_hash)) && + (associated ? + query.set(test::mock_block10, context, {}, false, false) : + query.set(header, context, {}, false)) && + query.push_candidate(query.to_header(header.hash())); +} + bitcoind_setup_fixture::bitcoind_setup_fixture(const initializer& setup, const configurator& configure, bool start) : rpc_setup_fixture(setup, @@ -112,6 +132,46 @@ void bitcoind_setup_fixture::ws_notify(std::string_view body) client_.write_frame(body); } +boost::json::value bitcoind_setup_fixture::ws_read() +{ + return client_.read_frame(true); +} + +static bitcoind_setup_fixture::status status_of(tcp_stream& socket, + const rpc_client::request& request) +{ + network::boost_code ec{}; + http::write(socket, request, ec); + BOOST_CHECK_MESSAGE(!ec, ec.message()); + + flat_buffer buffer{}; + http::response response{}; + http::read(socket, buffer, response, ec); + BOOST_CHECK_MESSAGE(!ec, ec.message()); + return response.result(); +} + +bitcoind_setup_fixture::status +bitcoind_setup_fixture::rpc_origin_status(std::string_view method, + std::string_view origin) +{ + auto request = rpc_client::create_post("/", body_of(method, "[]")); + request.set(http::field::origin, origin); + return status_of(client_.stream(), request); +} + +bitcoind_setup_fixture::status +bitcoind_setup_fixture::options_origin_status(std::string_view target, + std::string_view origin) +{ + rpc_client::request request{ http::verb::options, target, + network::http::version_1_1 }; + request.set(http::field::host, "localhost"); + request.set(http::field::origin, origin); + request.keep_alive(true); + return status_of(client_.stream(), request); +} + bitcoind_setup_fixture::status bitcoind_setup_fixture::rest_status(std::string_view target) { diff --git a/test/protocols/bitcoind/bitcoind_setup_fixture.hpp b/test/protocols/bitcoind/bitcoind_setup_fixture.hpp index 540f8334..1a3f547a 100644 --- a/test/protocols/bitcoind/bitcoind_setup_fixture.hpp +++ b/test/protocols/bitcoind/bitcoind_setup_fixture.hpp @@ -79,6 +79,16 @@ struct bitcoind_setup_fixture // Write a raw frame over the upgraded websocket without reading. void ws_notify(std::string_view body); + // Read the next frame over the upgraded websocket without writing. + boost::json::value ws_read(); + + // As rpc(), with an origin header, returning only the http status. + status rpc_origin_status(std::string_view method, std::string_view origin); + + // As options_status(), with an origin header. + status options_origin_status(std::string_view target, + std::string_view origin); + // bitcoind REST over HTTP GET (target under "/rest/..."). status rest_status(std::string_view target); @@ -348,4 +358,81 @@ struct bitcoind_submit_setup_fixture } }; +// Configured with no block filter tables. +struct bitcoind_unfiltered_setup_fixture + : bitcoind_setup_fixture +{ + inline bitcoind_unfiltered_setup_fixture() + : bitcoind_setup_fixture([](test::query_t& query) + { + return test::setup_ten_block_store(query); + }, [](configuration& config) + { + config.database.filter_bk.buckets = 0; + config.database.filter_tx.buckets = 0; + }) + { + } +}; + +// The ten block store with its blocks also candidate, and mock block 10 a +// candidate above the confirmed top (block or header only). +bool setup_candidate_store(test::query_t& query, bool associated) NOEXCEPT; + +struct bitcoind_candidate_setup_fixture + : bitcoind_setup_fixture +{ + inline bitcoind_candidate_setup_fixture() + : bitcoind_setup_fixture([](test::query_t& query) + { + return setup_candidate_store(query, true); + }) + { + } +}; + +struct bitcoind_candidate_header_setup_fixture + : bitcoind_setup_fixture +{ + inline bitcoind_candidate_header_setup_fixture() + : bitcoind_setup_fixture([](test::query_t& query) + { + return setup_candidate_store(query, false); + }) + { + } +}; + +// Configured with advertised inbound addresses. +struct bitcoind_selfs_setup_fixture + : bitcoind_setup_fixture +{ + inline bitcoind_selfs_setup_fixture() + : bitcoind_setup_fixture([](test::query_t& query) + { + return test::setup_ten_block_store(query); + }, [](configuration& config) + { + config.network.inbound.selfs = { { "1.2.3.4:8333" } }; + }) + { + } +}; + +// Configured with a zmq binding (no connections, so not served). +struct bitcoind_zmq_bound_setup_fixture + : bitcoind_setup_fixture +{ + inline bitcoind_zmq_bound_setup_fixture() + : bitcoind_setup_fixture([](test::query_t& query) + { + return test::setup_ten_block_store(query); + }, [](configuration& config) + { + config.server.bitcoind_zmq.binds = { { "127.0.0.1:28332" } }; + }) + { + } +}; + #endif From 460422750b683f89631b2e0f91c50b102bf2aabd Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 29 Sep 2026 11:42:40 -0400 Subject: [PATCH 12/21] Add bitcoind psbt, utility, network, origin and submission tests. --- test/protocols/bitcoind/bitcoind_rpc.cpp | 182 +++++++++++++++++++++++ 1 file changed, 182 insertions(+) diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 57738ca7..4b5054d2 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -2876,6 +2876,153 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__waitforblockheight__organized__new_top) BOOST_REQUIRE_EQUAL(as_text(result.at("hash")), encode_hash(test::mock_block10.hash())); } +// psbt combine and join + +static std::string create_psbt_v0(const chain::block& block, const std::string& data) NOEXCEPT +{ + return "{\"jsonrpc\":\"2.0\",\"id\":0,\"method\":\"createpsbt\",\"params\":[[{\"txid\":\"" + encode_hash(block.transactions_ptr()->front()->hash(false)) + "\",\"vout\":0}], {\"data\": \"" + data + "\"}, 0, true, 2, 0]}"; +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__combinepsbt__different_transactions__invalid_parameter) +{ + const auto created = rpc_body(create_psbt_v0(test::block1, "deadbeef")); + BOOST_REQUIRE(has_code(rpc("combinepsbt", "[[\"" PSBT_UPDATER "\", \"" + as_text(created.at("result")) + "\"]]"), -8)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__joinpsbts__distinct_inputs__all_inputs_and_outputs) +{ + const auto first = as_text(rpc_body(create_psbt_v0(test::block1, "deadbeef")).at("result")); + const auto second = as_text(rpc_body(create_psbt_v0(test::block2, "cafe")).at("result")); + const auto joined = rpc("joinpsbts", "[[\"" + first + "\", \"" + second + "\"]]"); + const auto response = rpc("decodepsbt", "[\"" + as_text(joined.at("result")) + "\"]"); + const auto& result = response.at("result"); + BOOST_REQUIRE_EQUAL(result.at("psbt_version").as_int64(), 0); + BOOST_REQUIRE_EQUAL(result.at("inputs").as_array().size(), 2u); + BOOST_REQUIRE_EQUAL(result.at("outputs").as_array().size(), 2u); + BOOST_REQUIRE_EQUAL(result.at("tx").at("vin").as_array().size(), 2u); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__joinpsbts__repeated_input__invalid_parameter) +{ + const auto created = as_text(rpc_body(create_psbt_v0(test::block1, "deadbeef")).at("result")); + BOOST_REQUIRE(has_code(rpc("joinpsbts", "[[\"" + created + "\", \"" + created + "\"]]"), -8)); +} + +// decodepsbt global xpubs (bitcoind test/functional/data/rpc_psbt.json) + +#define PSBT_GLOBAL_XPUBS "cHNidP8BAFICAAAAAZ38ZijCbFiZ/hvT3DOGZb/VXXraEPYiCXPfLTht7BJ2AQAAAAD/////AfA9zR0AAAAAFgAUezoAv9wU0neVwrdJAdCdpu8TNXkAAAAATwEENYfPAto/0AiAAAAAlwSLGtBEWx7IJ1UXcnyHtOTrwYogP/oPlMAVZr046QADUbdDiH7h1A3DKmBDck8tZFmztaTXPa7I+64EcvO8Q+IM2QxqT64AAIAAAACATwEENYfPAto/0AiAAAABuQRSQnE5zXjCz/JES+NTzVhgXj5RMoXlKLQH+uP2FzUD0wpel8itvFV9rCrZp+OcFyLrrGnmaLbyZnzB1nHIPKsM2QxqT64AAIABAACAAAEBKwBlzR0AAAAAIgAgLFSGEmxJeAeagU4TcV1l82RZ5NbMre0mbQUIZFuvpjIBBUdSIQKdoSzbWyNWkrkVNq/v5ckcOrlHPY5DtTODarRWKZyIcSEDNys0I07Xz5wf6l0F1EFVeSe+lUKxYusC4ass6AIkwAtSriIGAp2hLNtbI1aSuRU2r+/lyRw6uUc9jkO1M4NqtFYpnIhxENkMak+uAACAAAAAgAAAAAAiBgM3KzQjTtfPnB/qXQXUQVV5J76VQrFi6wLhqyzoAiTACxDZDGpPrgAAgAEAAIAAAAAAACICA57/H1R6HV+S36K6evaslxpL0DukpzSwMVaiVritOh75EO3kXMUAAACAAAAAgAEAAIAA" + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__decodepsbt__global_xpubs__expected) +{ + const auto response = rpc("decodepsbt", "[\"" PSBT_GLOBAL_XPUBS "\"]"); + const auto& xpubs = response.at("result").at("global_xpubs"); + BOOST_REQUIRE_EQUAL(xpubs.as_array().size(), 2u); + BOOST_REQUIRE_EQUAL(as_text(xpubs.at(0).at("xpub")), "tpubDBkJeJo2X94Yq3RVz65DoUgyLUkaDrkfyrn2VcgyCRSKCRonvKvCF2FpYDGJWDkdRHBajXJGpc63GnumUt63ySvqCu2XaTRGVTKMYGuFk9H"); + BOOST_REQUIRE_EQUAL(as_text(xpubs.at(0).at("master_fingerprint")), "d90c6a4f"); + BOOST_REQUIRE_EQUAL(as_text(xpubs.at(1).at("xpub")), "tpubDBkJeJo2X94YsvtBEU1eKoibEWiNv51nW5iHhs6VZp59jsE6nen8KZMFyGHuGbCvqjRqirgeMcfpVBkttpUUT6brm4duzSGoZeTbhqCNUu6"); + BOOST_REQUIRE_EQUAL(as_text(xpubs.at(1).at("master_fingerprint")), "d90c6a4f"); +} + +// utility + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__decodescript__not_hex__invalid_parameter) +{ + BOOST_REQUIRE(has_code(rpc("decodescript", "[\"zz\"]"), -8)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__createmultisig__unknown_address_type__invalid_address) +{ + BOOST_REQUIRE(has_code(rpc("createmultisig", "[1, [\"03789ed0bb717d88f7d321a368d905e7430207ebbd82bd342cf11ae157a7ace5fd\"], \"bogus\"]"), -5)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__createmultisig__invalid_key__invalid_address) +{ + BOOST_REQUIRE(has_code(rpc("createmultisig", "[1, [\"00\"]]"), -5)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getdescriptorinfo__invalid__invalid_address) +{ + BOOST_REQUIRE(has_code(rpc("getdescriptorinfo", "[\"bogus\"]"), -5)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__deriveaddresses__invalid__invalid_address) +{ + BOOST_REQUIRE(has_code(rpc("deriveaddresses", "[\"bogus\"]"), -5)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__deriveaddresses__no_address__invalid_address) +{ + BOOST_REQUIRE(has_code(rpc("deriveaddresses", "[\"raw(6a)#4mhr9ur5\"]"), -5)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__deriveaddresses__begin_after_end__invalid_parameter) +{ + BOOST_REQUIRE(has_code(rpc("deriveaddresses", "[\"pkh(xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1/*)#8nhtvxel\", [5, 1]]"), -8)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__deriveaddresses__range_too_large__invalid_parameter) +{ + BOOST_REQUIRE(has_code(rpc("deriveaddresses", "[\"pkh(xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1/*)#8nhtvxel\", [0, 1000000]]"), -8)); +} + +// network + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__addnode__onetry__null) +{ + const auto response = rpc("addnode", R"(["127.0.0.1:1","onetry"])"); + BOOST_REQUIRE(response.at("result").is_null()); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__addnode__add__null) +{ + const auto response = rpc("addnode", R"(["127.0.0.1:1","add"])"); + BOOST_REQUIRE(response.at("result").is_null()); +} + +// origin policy (opaque origins are disallowed by default) + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__options__opaque_origin__forbidden) +{ + BOOST_REQUIRE_EQUAL(options_origin_status("/", "null"), status::forbidden); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__post__opaque_origin__forbidden) +{ + BOOST_REQUIRE_EQUAL(rpc_origin_status("getblockcount", "null"), status::forbidden); +} + +BOOST_AUTO_TEST_SUITE_END() + +// advertised inbound addresses +// ---------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_SUITE(bitcoind_selfs_tests, bitcoind_selfs_setup_fixture) + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getnetworkinfo__selfs__localaddresses) +{ + const auto response = rpc("getnetworkinfo"); + const auto& locals = response.at("result").at("localaddresses"); + BOOST_REQUIRE_EQUAL(locals.as_array().size(), 1u); + BOOST_REQUIRE_EQUAL(as_text(locals.at(0).at("address")), "1.2.3.4"); + BOOST_REQUIRE_EQUAL(locals.at(0).at("port").as_int64(), 8333); +} + +BOOST_AUTO_TEST_SUITE_END() + +// zmq bindings +// ---------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_SUITE(bitcoind_zmq_bound_tests, bitcoind_zmq_bound_setup_fixture) + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getzmqnotifications__one_binding__five_publishers) +{ + const auto response = rpc("getzmqnotifications"); + const auto& result = response.at("result"); + BOOST_REQUIRE_EQUAL(result.as_array().size(), 5u); + BOOST_REQUIRE_EQUAL(as_text(result.at(0).at("type")), "pubhashblock"); + BOOST_REQUIRE_EQUAL(as_text(result.at(0).at("address")), "tcp://127.0.0.1:28332"); +} + BOOST_AUTO_TEST_SUITE_END() // block filters disabled @@ -3018,6 +3165,14 @@ BOOST_AUTO_TEST_CASE(bitcoind_scoped_credential__websocket_unlisted_method__drop REQUIRE_NO_THROW_TRUE(response.at("dropped").as_bool()); } +BOOST_AUTO_TEST_CASE(bitcoind_scoped_credential__websocket_unknown_method__dropped) +{ + BOOST_REQUIRE(!ws_upgrade(BITCOIND_TEST_USERNAME, BITCOIND_TEST_PASSWORD)); + + const auto response = ws_rpc_dropped("nosuchmethod"); + REQUIRE_NO_THROW_TRUE(response.at("dropped").as_bool()); +} + BOOST_AUTO_TEST_SUITE_END() // witness @@ -3102,6 +3257,33 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__submitheader__unacceptable_header__error) BOOST_REQUIRE(has_error(rpc("submitheader", "[\"" + header + "\"]"))); } +// Mainnet block 10 header (000000002c05cc2e78923c34df87fd108b22221ac6076c18f3ade378a4d915e9). +BOOST_AUTO_TEST_CASE(bitcoind_rpc__submitheader__mainnet_block10__null) +{ + const auto response = rpc("submitheader", "[\"010000000508085c47cc849eb80ea905cc7800a3be674ffc57263cf210c59d8d00000000112ba175a1e04b14ba9e7ea5f76ab640affeef5ec98173ac9799a852fa39add320cd6649ffff001d1e2de565\"]"); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + BOOST_REQUIRE(response.at("result").is_null()); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__sendrawtransaction__archived_unconfirmed__txid) +{ + const auto& tx = *test::mock_block10.transactions_ptr()->at(1); + BOOST_REQUIRE(query_.set(tx)); + + const auto response = rpc("sendrawtransaction", "[\"" + encode_base16(tx.to_data(true)) + "\"]"); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + BOOST_REQUIRE_EQUAL(as_text(response.at("result")), encode_hash(tx.hash(false))); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__sendrawtransaction__confirmed_spent__verify_error) +{ + BOOST_REQUIRE(query_.set(test::mock_block13, database::context{ 0, 10, 0 }, {}, false, false)); + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::mock_block13.hash()), true)); + + const auto response = rpc("sendrawtransaction", "[\"" + encode_base16(test::mock_tx13.to_data(true)) + "\"]"); + BOOST_REQUIRE_MESSAGE(has_code(response, -25), response); +} + BOOST_AUTO_TEST_CASE(bitcoind_rpc__testmempoolaccept__unsigned__not_allowed_with_reason) { const auto txid = encode_hash(test::block1.transactions_ptr()->front()->hash(false)); From 0a15cbe0f06395196504f5bfe84ff47e24d16852 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 29 Sep 2026 11:51:08 -0400 Subject: [PATCH 13/21] Add bitcoind decodepsbt unknown global test. --- test/protocols/bitcoind/bitcoind_rpc.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 4b5054d2..362499e0 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -2923,6 +2923,17 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__decodepsbt__global_xpubs__expected) BOOST_REQUIRE_EQUAL(as_text(xpubs.at(1).at("master_fingerprint")), "d90c6a4f"); } +// The bip174 updater vector with an unknown global key (0x0a) of value 0x01. +#define PSBT_UNKNOWN_GLOBAL "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAQoBAQABALsCAAAAAarXOTEBi9JfhK5AC2iEi+CdtwbqwqwYKYur7nGrZW+LAAAAAEhHMEQCIFj2/HxqM+GzFUjUgcgmwBW9MBNarULNZ3kNq2bSrSQ7AiBKHO0mBMZzW2OT5bQWkd14sA8MWUL7n3UYVvqpOBV9ugH+////AoDw+gIAAAAAF6kUD7lGNCFpa4LIM68kHHjBfdveSTSH0PIKJwEAAAAXqRQpynT4oI+BmZQoGFyXtdhS5AY/YYdlAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEEIgAgjCNTFzdDtZXftKB7crqOQuN5fadOh/59nXSX47ICiQMBBUdSIQMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3CECOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnNSriIGAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zENkMak8AAACAAAAAgAMAAIAiBgMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3BDZDGpPAAAAgAAAAIACAACAACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=" + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__decodepsbt__unknown_global__unknown) +{ + const auto response = rpc("decodepsbt", "[\"" PSBT_UNKNOWN_GLOBAL "\"]"); + const auto& unknown = response.at("result").at("unknown"); + BOOST_REQUIRE_EQUAL(unknown.as_object().size(), 1u); + BOOST_REQUIRE_EQUAL(as_text(unknown.at("0a")), "01"); +} + // utility BOOST_AUTO_TEST_CASE(bitcoind_rpc__decodescript__not_hex__invalid_parameter) From 14077862ffe57946332e72546218e88b35524a54 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 29 Sep 2026 12:10:28 -0400 Subject: [PATCH 14/21] Add bitcoind store fault tests. --- test/protocols/bitcoind/bitcoind_rest.cpp | 38 ++++++++ test/protocols/bitcoind/bitcoind_rpc.cpp | 94 +++++++++++++++++++ .../bitcoind/bitcoind_setup_fixture.cpp | 40 ++++++++ .../bitcoind/bitcoind_setup_fixture.hpp | 70 ++++++++++++++ 4 files changed, 242 insertions(+) diff --git a/test/protocols/bitcoind/bitcoind_rest.cpp b/test/protocols/bitcoind/bitcoind_rest.cpp index 61ba9cda..01d1e5ad 100644 --- a/test/protocols/bitcoind/bitcoind_rest.cpp +++ b/test/protocols/bitcoind/bitcoind_rest.cpp @@ -561,3 +561,41 @@ BOOST_AUTO_TEST_CASE(bitcoind_rest__opaque_origin__forbidden) } BOOST_AUTO_TEST_SUITE_END() + +BOOST_FIXTURE_TEST_SUITE(bitcoind_rest_dangling_tests, bitcoind_dangling_setup_fixture) + +BOOST_AUTO_TEST_CASE(bitcoind_rest__chaininfo__dangling__not_found) +{ + BOOST_REQUIRE_EQUAL(rest_status("/rest/chaininfo.json"), status::not_found); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rest__deploymentinfo__dangling__internal_server_error) +{ + BOOST_REQUIRE_EQUAL(rest_status("/rest/deploymentinfo.json"), status::internal_server_error); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rest__headers_bin__dangling__internal_server_error) +{ + BOOST_REQUIRE_EQUAL(rest_status("/rest/headers/2/" + encode_hash(test::block2_hash) + ".bin"), status::internal_server_error); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rest__headers_hex__dangling__internal_server_error) +{ + BOOST_REQUIRE_EQUAL(rest_status("/rest/headers/2/" + encode_hash(test::block2_hash) + ".hex"), status::internal_server_error); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rest__headers_json__dangling__internal_server_error) +{ + BOOST_REQUIRE_EQUAL(rest_status("/rest/headers/2/" + encode_hash(test::block2_hash) + ".json"), status::internal_server_error); +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_FIXTURE_TEST_SUITE(bitcoind_rest_witness_tests, bitcoind_witness_setup_fixture) + +BOOST_AUTO_TEST_CASE(bitcoind_rest__spenttxouts__missing_prevouts__internal_server_error) +{ + BOOST_REQUIRE_EQUAL(rest_status("/rest/spenttxouts/" + encode_hash(test::block2a.hash()) + ".bin"), status::internal_server_error); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 362499e0..107acd9e 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -3211,6 +3211,16 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblockstats__missing_prevouts__internal_err BOOST_REQUIRE(has_code(rpc("getblockstats", "[2]"), -32603)); } +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getdescriptoractivity__missing_prevouts__internal_error) +{ + BOOST_REQUIRE(has_code(rpc("getdescriptoractivity", "[[\"" + encode_hash(test::block2a.hash()) + "\"], []]"), -32603)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__gettxoutsetinfo__missing_prevouts__internal_error) +{ + BOOST_REQUIRE(has_code(rpc("gettxoutsetinfo"), -32603)); +} + BOOST_AUTO_TEST_SUITE_END() // unassociated @@ -3255,6 +3265,90 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__scantxoutset__dangling__internal_error) BOOST_REQUIRE(has_code(rpc("scantxoutset", "[\"start\", [\"addr(1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa)\"]]"), -32603)); } +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getdeploymentinfo__dangling__internal_error) +{ + BOOST_REQUIRE(has_code(rpc("getdeploymentinfo"), -32603)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__scanblocks__dangling__internal_error) +{ + BOOST_REQUIRE(has_code(rpc("scanblocks", "[\"start\", [\"raw(51)\"]]"), -32603)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getnetworkhashps__dangling__internal_error) +{ + BOOST_REQUIRE(has_code(rpc("getnetworkhashps"), -32603)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getmininginfo__dangling__internal_error) +{ + BOOST_REQUIRE(has_code(rpc("getmininginfo"), -32603)); +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_FIXTURE_TEST_SUITE(bitcoind_dangling_no_address_tests, bitcoind_dangling_no_address_setup_fixture) + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__scantxoutset__dangling_no_address__internal_error) +{ + BOOST_REQUIRE(has_code(rpc("scantxoutset", "[\"start\", [\"addr(1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa)\"]]"), -32603)); +} + +BOOST_AUTO_TEST_SUITE_END() + +// dangling candidate +// ---------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_SUITE(bitcoind_dangling_candidate_tests, bitcoind_dangling_candidate_setup_fixture) + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getchainstates__dangling_candidate__internal_error) +{ + BOOST_REQUIRE(has_code(rpc("getchainstates"), -32603)); +} + +BOOST_AUTO_TEST_SUITE_END() + +// unrooted +// ---------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_SUITE(bitcoind_unrooted_tests, bitcoind_unrooted_setup_fixture) + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getnetworkhashps__unrooted__internal_error) +{ + BOOST_REQUIRE(has_code(rpc("getnetworkhashps"), -32603)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getmininginfo__unrooted__internal_error) +{ + BOOST_REQUIRE(has_code(rpc("getmininginfo"), -32603)); +} + +BOOST_AUTO_TEST_SUITE_END() + +// misplaced +// ---------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_SUITE(bitcoind_misplaced_tests, bitcoind_misplaced_setup_fixture) + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__gettxoutsetinfo__misplaced__internal_error) +{ + BOOST_REQUIRE(has_code(rpc("gettxoutsetinfo"), -32603)); +} + +BOOST_AUTO_TEST_SUITE_END() + +// simultaneous +// ---------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_SUITE(bitcoind_simultaneous_tests, bitcoind_simultaneous_setup_fixture) + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getnetworkhashps__equal_timestamps__zero) +{ + const auto response = rpc("getnetworkhashps", "[1]"); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + BOOST_REQUIRE_EQUAL(response.at("result").to_number(), 0.0); +} + BOOST_AUTO_TEST_SUITE_END() // submission diff --git a/test/protocols/bitcoind/bitcoind_setup_fixture.cpp b/test/protocols/bitcoind/bitcoind_setup_fixture.cpp index 40ac6d80..8351ec3e 100644 --- a/test/protocols/bitcoind/bitcoind_setup_fixture.cpp +++ b/test/protocols/bitcoind/bitcoind_setup_fixture.cpp @@ -44,6 +44,46 @@ bool setup_candidate_store(test::query_t& query, bool associated) NOEXCEPT query.push_candidate(query.to_header(header.hash())); } +bool setup_simultaneous_store(test::query_t& query) NOEXCEPT +{ + const auto& header = test::block3.header(); + const auto timestamp = test::block2.header().timestamp(); + const system::chain::header top{ header.version(), test::block2_hash, header.merkle_root(), timestamp, header.bits(), header.nonce() }; + return test::setup_three_block_store(query) && + query.set(top, database::context{ 0, 3, 0 }, {}, false) && + query.push_confirmed(query.to_header(top.hash()), false); +} + +// A faulted store: the confirmed top has no parent. +bool setup_unrooted_store(test::query_t& query) NOEXCEPT +{ + const auto& header = test::block3.header(); + const system::chain::header top{ header.version(), system::null_hash, header.merkle_root(), header.timestamp(), header.bits(), header.nonce() }; + return test::setup_three_block_store(query) && + query.set(top, database::context{ 0, 3, 0 }, {}, false) && + query.push_confirmed(query.to_header(top.hash()), false); +} + +// A faulted store: block 3 is confirmed at height 2. +bool setup_misplaced_store(test::query_t& query) NOEXCEPT +{ + return query.initialize(test::genesis) && + query.set(test::block1, database::context{ 0, 1, 0 }, {}, false, false) && + query.set(test::block2, database::context{ 0, 2, 0 }, {}, false, false) && + query.set(test::block3, database::context{ 0, 3, 0 }, {}, false, false) && + query.push_confirmed(query.to_header(test::block1_hash), true) && + query.push_confirmed(query.to_header(test::block3_hash), false); +} + +// A faulted store: the candidate top is not a header. +bool setup_dangling_candidate_store(test::query_t& query) NOEXCEPT +{ + return test::setup_three_block_store(query) && + query.push_candidate(query.to_header(test::block1_hash)) && + query.push_candidate(query.to_header(test::block2_hash)) && + query.push_candidate(database::header_link{ 42 }); +} + bitcoind_setup_fixture::bitcoind_setup_fixture(const initializer& setup, const configurator& configure, bool start) : rpc_setup_fixture(setup, diff --git a/test/protocols/bitcoind/bitcoind_setup_fixture.hpp b/test/protocols/bitcoind/bitcoind_setup_fixture.hpp index 1a3f547a..089b977d 100644 --- a/test/protocols/bitcoind/bitcoind_setup_fixture.hpp +++ b/test/protocols/bitcoind/bitcoind_setup_fixture.hpp @@ -403,6 +403,76 @@ struct bitcoind_candidate_header_setup_fixture } }; +// The three block store with a confirmed block 3 header timestamped as block 2. +bool setup_simultaneous_store(test::query_t& query) NOEXCEPT; +bool setup_unrooted_store(test::query_t& query) NOEXCEPT; +bool setup_misplaced_store(test::query_t& query) NOEXCEPT; +bool setup_dangling_candidate_store(test::query_t& query) NOEXCEPT; + +struct bitcoind_simultaneous_setup_fixture + : bitcoind_setup_fixture +{ + inline bitcoind_simultaneous_setup_fixture() + : bitcoind_setup_fixture([](test::query_t& query) + { + return setup_simultaneous_store(query); + }) + { + } +}; + +struct bitcoind_unrooted_setup_fixture + : bitcoind_setup_fixture +{ + inline bitcoind_unrooted_setup_fixture() + : bitcoind_setup_fixture([](test::query_t& query) + { + return setup_unrooted_store(query); + }) + { + } +}; + +struct bitcoind_misplaced_setup_fixture + : bitcoind_setup_fixture +{ + inline bitcoind_misplaced_setup_fixture() + : bitcoind_setup_fixture([](test::query_t& query) + { + return setup_misplaced_store(query); + }) + { + } +}; + +struct bitcoind_dangling_candidate_setup_fixture + : bitcoind_setup_fixture +{ + inline bitcoind_dangling_candidate_setup_fixture() + : bitcoind_setup_fixture([](test::query_t& query) + { + return setup_dangling_candidate_store(query); + }) + { + } +}; + +// Configured with no address index -- for the utxo set scan path. +struct bitcoind_dangling_no_address_setup_fixture + : bitcoind_setup_fixture +{ + inline bitcoind_dangling_no_address_setup_fixture() + : bitcoind_setup_fixture([](test::query_t& query) + { + return test::setup_dangling_store(query); + }, [](configuration& config) + { + config.database.outs.buckets = 0; + }) + { + } +}; + // Configured with advertised inbound addresses. struct bitcoind_selfs_setup_fixture : bitcoind_setup_fixture From 27e3634f2568c2fc024438e4d5e97ed7e0431c96 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 29 Sep 2026 12:17:19 -0400 Subject: [PATCH 15/21] Add bitcoind psbt field, unspendable scan and broadcast tests. --- test/protocols/bitcoind/bitcoind_rpc.cpp | 60 +++++++++++++++++++ .../bitcoind/bitcoind_setup_fixture.hpp | 16 +++++ 2 files changed, 76 insertions(+) diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 107acd9e..b6262efc 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -2813,6 +2813,15 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__scantxoutset__repeated_script__one_unspent) BOOST_REQUIRE_EQUAL(result.at("total_amount").as_double(), 50.0); } +BOOST_AUTO_TEST_CASE(bitcoind_rpc__scantxoutset__unspendable_script__empty) +{ + const auto response = rpc("scantxoutset", "[\"start\", [\"raw(6a)\"]]"); + const auto& result = response.at("result"); + BOOST_REQUIRE(result.at("success").as_bool()); + BOOST_REQUIRE(result.at("unspents").as_array().empty()); + BOOST_REQUIRE_EQUAL(result.at("total_amount").as_double(), 0.0); +} + // verifytxoutproof BOOST_AUTO_TEST_CASE(bitcoind_rpc__verifytxoutproof__unknown_block__invalid_address) @@ -2934,6 +2943,43 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__decodepsbt__unknown_global__unknown) BOOST_REQUIRE_EQUAL(as_text(unknown.at("0a")), "01"); } +// The bip174 updater vector with sighash types (0x82, 0x03), output 0 redeem and +// witness scripts (OP_1), and an unknown key (0x3f) of value 0x01 in input 0 and output 0. +#define PSBT_ANNOTATED "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEDBIIAAAABPwEBAQC7AgAAAAGq1zkxAYvSX4SuQAtohIvgnbcG6sKsGCmLq+5xq2VviwAAAABIRzBEAiBY9vx8ajPhsxVI1IHIJsAVvTATWq1CzWd5Datm0q0kOwIgShztJgTGc1tjk+W0FpHdeLAPDFlC+591GFb6qTgVfboB/v///wKA8PoCAAAAABepFA+5RjQhaWuCyDOvJBx4wX3b3kk0h9DyCicBAAAAF6kUKcp0+KCPgZmUKBhcl7XYUuQGP2GHZQAAAAEER1IhApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/IQLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU211KuIgYClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8Q2QxqTwAAAIAAAACAAAAAgCIGAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXENkMak8AAACAAAAAgAEAAIAAAQMEAwAAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHAQQiACCMI1MXN0O1ld+0oHtyuo5C43l9p06H/n2ddJfjsgKJAwEFR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuIgYCOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnMQ2QxqTwAAAIAAAACAAwAAgCIGAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcENkMak8AAACAAAAAgAIAAIAAAQABUQEBAVEBPwEBIgIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1Ptnuylh3EQ2QxqTwAAAIAAAACABAAAgAAiAgJ/Y5l1fS7/VaE2rQLGhLGDi2VW5fG2s0KCqUtrUAUQlhDZDGpPAAAAgAAAAIAFAACAAA==" + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__decodepsbt__annotated__expected) +{ + const auto response = rpc("decodepsbt", "[\"" PSBT_ANNOTATED "\"]"); + const auto& result = response.at("result"); + BOOST_REQUIRE_EQUAL(as_text(result.at("inputs").at(0).at("sighash")), "NONE|ANYONECANPAY"); + BOOST_REQUIRE_EQUAL(as_text(result.at("inputs").at(1).at("sighash")), "SINGLE"); + BOOST_REQUIRE_EQUAL(as_text(result.at("inputs").at(0).at("unknown").at("3f")), "01"); + BOOST_REQUIRE_EQUAL(as_text(result.at("outputs").at(0).at("redeem_script").at("hex")), "51"); + BOOST_REQUIRE_EQUAL(as_text(result.at("outputs").at(0).at("witness_script").at("hex")), "51"); + BOOST_REQUIRE_EQUAL(as_text(result.at("outputs").at(0).at("unknown").at("3f")), "01"); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__decodepsbt__finalized__final_scripts) +{ + const chain::transaction tx{ base16_chunk(PSBT_EXTRACTED_TX), true }; + const auto response = rpc("decodepsbt", "[\"" PSBT_FINALIZED "\"]"); + const auto& inputs = response.at("result").at("inputs"); + BOOST_REQUIRE_EQUAL(as_text(inputs.at(0).at("final_scriptSig").at("hex")), encode_base16(tx.inputs_ptr()->at(0)->script().to_data(false))); + BOOST_REQUIRE_EQUAL(inputs.at(1).at("final_scriptwitness").as_array().size(), 4u); + BOOST_REQUIRE_EQUAL(as_text(inputs.at(1).at("final_scriptwitness").at(0)), ""); + BOOST_REQUIRE_EQUAL(as_text(inputs.at(1).at("final_scriptwitness").at(3)), encode_base16(*tx.inputs_ptr()->at(1)->witness().stack().at(3))); +} + +// bitcoind test/functional/data/rpc_psbt.json valid[37] (psbt v2 required locktimes). +#define PSBT_REQUIRED_LOCKTIMES "cHNidP8BAgQCAAAAAQMEAAAAAAEEAQEBBQECAfsEAgAAAAABAFICAAAAAcGqJW4hS5ahgi+T3kK/87Xz/40FGTBuNRXXUVpegFsSAAAAAAD/////ARjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4AAAAAAQEfGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgEOIAsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAQ8EAAAAAAEQBP7///8BEQSMjcRiARIEECcAAAAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAABAwgACK8vAAAAAAEEFgAUxDD2TEdW2jENvRoIVXLvKZkmJywAIgIC42+/9T3VNAcM+P05ZhRoDzV6m4Xbc0C/HPp0XSrXs0AY9p2HPlQAAIABAACAAAAAgAEAAABkAAAAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA==" + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__decodepsbt__required_locktimes__expected) +{ + const auto response = rpc("decodepsbt", "[\"" PSBT_REQUIRED_LOCKTIMES "\"]"); + const auto& input = response.at("result").at("inputs").at(0); + BOOST_REQUIRE_EQUAL(input.at("time_locktime").as_int64(), 1657048460); + BOOST_REQUIRE_EQUAL(input.at("height_locktime").as_int64(), 10000); +} // utility BOOST_AUTO_TEST_CASE(bitcoind_rpc__decodescript__not_hex__invalid_parameter) @@ -3435,6 +3481,20 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__sendrawtransaction__unknown_inputs__verify_er BOOST_AUTO_TEST_SUITE_END() +// broadcast +// ---------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_SUITE(bitcoind_broadcast_tests, bitcoind_broadcast_setup_fixture) + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__sendrawtransaction__spendable__txid) +{ + const auto response = rpc("sendrawtransaction", "[\"" + encode_base16(test::tx1c.to_data(true)) + "\"]"); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + BOOST_REQUIRE_EQUAL(as_text(response.at("result")), encode_hash(test::tx1c.hash(false))); +} + +BOOST_AUTO_TEST_SUITE_END() + // closed tx pool // ---------------------------------------------------------------------------- // The tx chaser refuses submission and testing while the pool is closed. diff --git a/test/protocols/bitcoind/bitcoind_setup_fixture.hpp b/test/protocols/bitcoind/bitcoind_setup_fixture.hpp index 089b977d..b346579e 100644 --- a/test/protocols/bitcoind/bitcoind_setup_fixture.hpp +++ b/test/protocols/bitcoind/bitcoind_setup_fixture.hpp @@ -358,6 +358,22 @@ struct bitcoind_submit_setup_fixture } }; +// Configured as submission, over a store with an anyone-can-spend output. +struct bitcoind_broadcast_setup_fixture + : bitcoind_setup_fixture +{ + inline bitcoind_broadcast_setup_fixture() + : bitcoind_setup_fixture([](test::query_t& query) + { + return test::setup_broadcast_store(query); + }, [](configuration& config) + { + config.node.currency_window_minutes = 0; + }, true) + { + } +}; + // Configured with no block filter tables. struct bitcoind_unfiltered_setup_fixture : bitcoind_setup_fixture From 072db036145d061ccc8f034c976793451b7f7c12 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 29 Sep 2026 12:22:33 -0400 Subject: [PATCH 16/21] Remove bitcoind misplaced store test. --- test/protocols/bitcoind/bitcoind_rpc.cpp | 12 ------------ test/protocols/bitcoind/bitcoind_setup_fixture.cpp | 11 ----------- test/protocols/bitcoind/bitcoind_setup_fixture.hpp | 13 ------------- 3 files changed, 36 deletions(-) diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index b6262efc..51200e9d 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -3371,18 +3371,6 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__getmininginfo__unrooted__internal_error) BOOST_AUTO_TEST_SUITE_END() -// misplaced -// ---------------------------------------------------------------------------- - -BOOST_FIXTURE_TEST_SUITE(bitcoind_misplaced_tests, bitcoind_misplaced_setup_fixture) - -BOOST_AUTO_TEST_CASE(bitcoind_rpc__gettxoutsetinfo__misplaced__internal_error) -{ - BOOST_REQUIRE(has_code(rpc("gettxoutsetinfo"), -32603)); -} - -BOOST_AUTO_TEST_SUITE_END() - // simultaneous // ---------------------------------------------------------------------------- diff --git a/test/protocols/bitcoind/bitcoind_setup_fixture.cpp b/test/protocols/bitcoind/bitcoind_setup_fixture.cpp index 8351ec3e..3f20bebe 100644 --- a/test/protocols/bitcoind/bitcoind_setup_fixture.cpp +++ b/test/protocols/bitcoind/bitcoind_setup_fixture.cpp @@ -64,17 +64,6 @@ bool setup_unrooted_store(test::query_t& query) NOEXCEPT query.push_confirmed(query.to_header(top.hash()), false); } -// A faulted store: block 3 is confirmed at height 2. -bool setup_misplaced_store(test::query_t& query) NOEXCEPT -{ - return query.initialize(test::genesis) && - query.set(test::block1, database::context{ 0, 1, 0 }, {}, false, false) && - query.set(test::block2, database::context{ 0, 2, 0 }, {}, false, false) && - query.set(test::block3, database::context{ 0, 3, 0 }, {}, false, false) && - query.push_confirmed(query.to_header(test::block1_hash), true) && - query.push_confirmed(query.to_header(test::block3_hash), false); -} - // A faulted store: the candidate top is not a header. bool setup_dangling_candidate_store(test::query_t& query) NOEXCEPT { diff --git a/test/protocols/bitcoind/bitcoind_setup_fixture.hpp b/test/protocols/bitcoind/bitcoind_setup_fixture.hpp index b346579e..dd6594cc 100644 --- a/test/protocols/bitcoind/bitcoind_setup_fixture.hpp +++ b/test/protocols/bitcoind/bitcoind_setup_fixture.hpp @@ -422,7 +422,6 @@ struct bitcoind_candidate_header_setup_fixture // The three block store with a confirmed block 3 header timestamped as block 2. bool setup_simultaneous_store(test::query_t& query) NOEXCEPT; bool setup_unrooted_store(test::query_t& query) NOEXCEPT; -bool setup_misplaced_store(test::query_t& query) NOEXCEPT; bool setup_dangling_candidate_store(test::query_t& query) NOEXCEPT; struct bitcoind_simultaneous_setup_fixture @@ -449,18 +448,6 @@ struct bitcoind_unrooted_setup_fixture } }; -struct bitcoind_misplaced_setup_fixture - : bitcoind_setup_fixture -{ - inline bitcoind_misplaced_setup_fixture() - : bitcoind_setup_fixture([](test::query_t& query) - { - return setup_misplaced_store(query); - }) - { - } -}; - struct bitcoind_dangling_candidate_setup_fixture : bitcoind_setup_fixture { From 8c3395ece030e5bd91b041755ee8ab11218e1402 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 29 Sep 2026 12:21:34 -0400 Subject: [PATCH 17/21] Add electrum store fault, credential and transport tests. --- test/protocols/electrum/electrum_headers.cpp | 110 ++++++++++++++++++ .../electrum/electrum_scripthash.cpp | 29 +++++ .../electrum/electrum_setup_fixture.cpp | 20 ++++ .../electrum/electrum_setup_fixture.hpp | 74 ++++++++++++ .../protocols/electrum/electrum_subscribe.cpp | 28 +++++ .../electrum/electrum_transactions.cpp | 20 ++++ .../electrum/electrum_transports.cpp | 42 +++++++ 7 files changed, 323 insertions(+) diff --git a/test/protocols/electrum/electrum_headers.cpp b/test/protocols/electrum/electrum_headers.cpp index d933d8fd..b4f4cbc2 100644 --- a/test/protocols/electrum/electrum_headers.cpp +++ b/test/protocols/electrum/electrum_headers.cpp @@ -1003,4 +1003,114 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__checkpoint_v1_2__wrong_ BOOST_REQUIRE_EQUAL(result, wrong_version.value()); } +BOOST_AUTO_TEST_CASE(electrum__blockchain_number_of_blocks_subscribe__not_header_notification__skipped) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_0)); + + const auto response = get(R"({"id":95,"method":"blockchain.numblocks.subscribe","params":[]})" "\n"); + BOOST_REQUIRE_EQUAL(response.at("result").as_int64(), 9); + + BOOST_REQUIRE(query_.set(test::mock_block10, database::context{ 0, 10, 0 }, {}, false, false)); + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::mock_block10.hash()), true)); + notify(node::chases::organized{ 42 }); + notify(node::chases::organized{ 10 }); + + const auto notification = receive(); + REQUIRE_NO_THROW_TRUE(notification.at("params").is_int64()); + BOOST_REQUIRE_EQUAL(notification.at("method").as_string(), "blockchain.numblocks.subscribe"); + BOOST_REQUIRE_EQUAL(notification.at("params").as_int64(), 10); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_headers_subscribe__not_header_notification__skipped) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_4)); + + const auto response = get(R"({"id":96,"method":"blockchain.headers.subscribe","params":[]})" "\n"); + BOOST_REQUIRE_EQUAL(response.at("result").at("height").as_int64(), 9); + + BOOST_REQUIRE(query_.set(test::mock_block10, database::context{ 0, 10, 0 }, {}, false, false)); + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::mock_block10.hash()), true)); + notify(node::chases::organized{ 42 }); + notify(node::chases::organized{ 10 }); + + const auto notification = receive(); + REQUIRE_NO_THROW_TRUE(notification.at("params").is_array()); + BOOST_REQUIRE_EQUAL(notification.at("method").as_string(), "blockchain.headers.subscribe"); + BOOST_REQUIRE_EQUAL(notification.at("params").at(0).at("height").as_int64(), 10); + BOOST_REQUIRE_EQUAL(notification.at("params").at(0).at("hex").as_string(), encode_base16(test::mock_block10.header().to_data())); +} + +BOOST_AUTO_TEST_SUITE_END() + +// dangling +// ---------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_SUITE(electrum_dangling_tests, electrum_dangling_setup_fixture) + +static const code daemon_error{ server::error::electrum::daemon_error }; + +BOOST_AUTO_TEST_CASE(electrum__blockchain_block_get_chunk__dangling__daemon_error) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_0)); + + const auto result = get_error(R"({"id":1,"method":"blockchain.block.get_chunk","params":[0]})" "\n"); + BOOST_REQUIRE_EQUAL(result, daemon_error.value()); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_block_get_header__dangling__daemon_error) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_0)); + + const auto result = get_error(R"({"id":2,"method":"blockchain.block.get_header","params":[3]})" "\n"); + BOOST_REQUIRE_EQUAL(result, daemon_error.value()); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__dangling__daemon_error) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_4)); + + const auto result = get_error(R"({"id":3,"method":"blockchain.block.header","params":[3]})" "\n"); + BOOST_REQUIRE_EQUAL(result, daemon_error.value()); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_block_header__dangling_checkpoint__daemon_error) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_4)); + + const auto result = get_error(R"({"id":4,"method":"blockchain.block.header","params":[0,3]})" "\n"); + BOOST_REQUIRE_EQUAL(result, daemon_error.value()); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__dangling_v1_4__daemon_error) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_4)); + + const auto result = get_error(R"({"id":5,"method":"blockchain.block.headers","params":[0,4]})" "\n"); + BOOST_REQUIRE_EQUAL(result, daemon_error.value()); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__dangling_v1_6__daemon_error) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_6)); + + const auto result = get_error(R"({"id":6,"method":"blockchain.block.headers","params":[0,4]})" "\n"); + BOOST_REQUIRE_EQUAL(result, daemon_error.value()); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_headers_subscribe__dangling_v1_0__daemon_error) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_0)); + + const auto result = get_error(R"({"id":7,"method":"blockchain.headers.subscribe","params":[]})" "\n"); + BOOST_REQUIRE_EQUAL(result, daemon_error.value()); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_headers_subscribe__dangling_v1_4__daemon_error) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_4)); + + const auto result = get_error(R"({"id":8,"method":"blockchain.headers.subscribe","params":[]})" "\n"); + BOOST_REQUIRE_EQUAL(result, daemon_error.value()); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/protocols/electrum/electrum_scripthash.cpp b/test/protocols/electrum/electrum_scripthash.cpp index 40c3d5ca..b2965c43 100644 --- a/test/protocols/electrum/electrum_scripthash.cpp +++ b/test/protocols/electrum/electrum_scripthash.cpp @@ -460,3 +460,32 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_scripthash_list_unspent__confirmed_and } BOOST_AUTO_TEST_SUITE_END() + +// duplicate +// ---------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_SUITE(electrum_duplicate_tests, electrum_duplicate_setup_fixture) + +static const code daemon_error{ server::error::electrum::daemon_error }; + +BOOST_AUTO_TEST_CASE(electrum__blockchain_scripthash_get_history__duplicate__daemon_error) +{ + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::mock_block10.hash()), true)); + BOOST_REQUIRE(handshake(electrum::version::v1_1)); + + constexpr auto request = R"({{"id":1102,"method":"blockchain.scripthash.get_history","params":["{}"]}})" "\n"; + const auto result = get_error(std::format(request, found_scripthash)); + BOOST_REQUIRE_EQUAL(result, daemon_error.value()); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_scripthash_list_unspent__duplicate__daemon_error) +{ + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::mock_block10.hash()), true)); + BOOST_REQUIRE(handshake(electrum::version::v1_1)); + + constexpr auto request = R"({{"id":1103,"method":"blockchain.scripthash.listunspent","params":["{}"]}})" "\n"; + const auto result = get_error(std::format(request, found_scripthash)); + BOOST_REQUIRE_EQUAL(result, daemon_error.value()); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/protocols/electrum/electrum_setup_fixture.cpp b/test/protocols/electrum/electrum_setup_fixture.cpp index 4b67dc14..842bba7e 100644 --- a/test/protocols/electrum/electrum_setup_fixture.cpp +++ b/test/protocols/electrum/electrum_setup_fixture.cpp @@ -152,6 +152,20 @@ bool electrum_setup_fixture::post_handshake(electrum::version version, return verify(post(version_request(version, name, id)), version, id); } +boost::json::value electrum_setup_fixture::post_authorized( + const std::string& request, const std::string& username, + const std::string& password) +{ + return client_.post_authorized(request, username, password); +} + +rpc_client::status electrum_setup_fixture::post_status_authorized( + const std::string& request, const std::string& username, + const std::string& password) +{ + return client_.post_status_authorized(request, username, password); +} + // websocket. // ---------------------------------------------------------------------------- @@ -160,6 +174,12 @@ network::boost_code electrum_setup_fixture::ws_upgrade() return client_.upgrade(); } +network::boost_code electrum_setup_fixture::ws_upgrade( + const std::string& username, const std::string& password) +{ + return client_.upgrade(username, password); +} + boost::json::value electrum_setup_fixture::ws_receive() { return client_.read_frame(); diff --git a/test/protocols/electrum/electrum_setup_fixture.hpp b/test/protocols/electrum/electrum_setup_fixture.hpp index 2a5a4e96..2456ef2b 100644 --- a/test/protocols/electrum/electrum_setup_fixture.hpp +++ b/test/protocols/electrum/electrum_setup_fixture.hpp @@ -26,6 +26,9 @@ #define ELECTRUM_ENDPOINT "127.0.0.1:65002" #define SPARROW_ENDPOINT "127.0.0.1:65003" +#define ELECTRUM_TEST_USERNAME "user" +#define ELECTRUM_TEST_PASSWORD "pass" +#define ELECTRUM_TEST_SCOPED_METHOD "server.version" struct electrum_setup_fixture : rpc_setup_fixture @@ -54,9 +57,21 @@ struct electrum_setup_fixture bool post_handshake(electrum::version version, const std::string& name="test", network::rpc::code_t id={}); + // As post(), with basic authorization. + boost::json::value post_authorized(const std::string& request, + const std::string& username, const std::string& password); + + // As post_authorized(), returning only the http status. + rpc_client::status post_status_authorized(const std::string& request, + const std::string& username, const std::string& password); + // Upgrade the connection to websocket (no further http requests). network::boost_code ws_upgrade(); + // As ws_upgrade(), with basic authorization on the upgrade request. + network::boost_code ws_upgrade(const std::string& username, + const std::string& password); + // json-rpc over the upgraded websocket connection. boost::json::value ws_get(const std::string& request); bool ws_handshake(electrum::version version, @@ -205,4 +220,63 @@ struct electrum_ping_setup_fixture } }; +struct electrum_scoped_credential_setup_fixture + : electrum_setup_fixture +{ + inline electrum_scoped_credential_setup_fixture() + : electrum_setup_fixture([](test::query_t& query) + { + return test::setup_ten_block_store(query); + }, true, [](configuration& config) + { + config.server.electrum.credentials = + { + { ELECTRUM_TEST_USERNAME ":" ELECTRUM_TEST_PASSWORD ":" + ELECTRUM_TEST_SCOPED_METHOD } + }; + }) + { + } +}; + +struct electrum_unassociated_setup_fixture + : electrum_setup_fixture +{ + inline electrum_unassociated_setup_fixture() + : electrum_setup_fixture([](test::query_t& query) + { + return test::setup_unassociated_store(query); + }) + { + } +}; + +struct electrum_dangling_setup_fixture + : electrum_setup_fixture +{ + inline electrum_dangling_setup_fixture() + : electrum_setup_fixture([](test::query_t& query) + { + return test::setup_dangling_store(query); + }) + { + } +}; + +// A faulted store (once block 10 is confirmed): a block 10 tx is archived +// again, outside of the block. +struct electrum_duplicate_setup_fixture + : electrum_setup_fixture +{ + inline electrum_duplicate_setup_fixture() + : electrum_setup_fixture([](test::query_t& query) + { + return test::setup_ten_block_store(query) && + query.set(test::mock_block10, database::context{ 0, 10, 0 }, {}, false, false) && + query.set(*test::mock_block10.transactions_ptr()->at(1)); + }) + { + } +}; + #endif \ No newline at end of file diff --git a/test/protocols/electrum/electrum_subscribe.cpp b/test/protocols/electrum/electrum_subscribe.cpp index 7eb42670..08d328e0 100644 --- a/test/protocols/electrum/electrum_subscribe.cpp +++ b/test/protocols/electrum/electrum_subscribe.cpp @@ -967,3 +967,31 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_scripthash_subscribe__reorganized_noti } BOOST_AUTO_TEST_SUITE_END() + +// duplicate +// ---------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_SUITE(electrum_duplicate_tests, electrum_duplicate_setup_fixture) + +BOOST_AUTO_TEST_CASE(electrum__blockchain_scripthash_subscribe__duplicate_notify__subsequent_notified) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_1)); + + constexpr auto request = R"({{"id":1101,"method":"blockchain.scripthash.subscribe","params":["{}"]}})" "\n"; + const auto response = get(std::format(request, found_scripthash)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); + + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::mock_block10.hash()), true)); + notify(node::chases::organized{ 0 }); + + BOOST_REQUIRE(query_.set(test::mock_block11, database::context{ 0, 11, 0 }, {}, false, false)); + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::mock_block11.hash()), true)); + notify(node::chases::organized{ 0 }); + + const auto notification = receive(); + REQUIRE_NO_THROW_TRUE(notification.at("params").is_array()); + BOOST_REQUIRE_EQUAL(notification.at("method").as_string(), "blockchain.scripthash.subscribe"); + BOOST_REQUIRE_EQUAL(notification.at("params").at(0).as_string(), found_scripthash); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/protocols/electrum/electrum_transactions.cpp b/test/protocols/electrum/electrum_transactions.cpp index ffcae5a8..31084d91 100644 --- a/test/protocols/electrum/electrum_transactions.cpp +++ b/test/protocols/electrum/electrum_transactions.cpp @@ -836,3 +836,23 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_testmempoolaccept__closed_ } BOOST_AUTO_TEST_SUITE_END() + +// unassociated +// ---------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_SUITE(electrum_unassociated_tests, electrum_unassociated_setup_fixture) + +using namespace system; +static const code daemon_error{ server::error::electrum::daemon_error }; + +BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get_merkle__unassociated__daemon_error) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_4)); + + const auto hash = encode_hash(test::block3.transactions_ptr()->front()->hash(false)); + constexpr auto request = R"({{"id":4000,"method":"blockchain.transaction.get_merkle","params":["{}",3]}})" "\n"; + const auto result = get_error(std::format(request, hash)); + BOOST_REQUIRE_EQUAL(result, daemon_error.value()); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/protocols/electrum/electrum_transports.cpp b/test/protocols/electrum/electrum_transports.cpp index 958fc50e..82b95108 100644 --- a/test/protocols/electrum/electrum_transports.cpp +++ b/test/protocols/electrum/electrum_transports.cpp @@ -24,6 +24,7 @@ using namespace system; // differ only in framing and in the push of notifications. static const code not_implemented{ server::error::electrum::method_not_found }; +static const code bad_request{ server::error::electrum::bad_request }; // http POST @@ -72,6 +73,47 @@ BOOST_AUTO_TEST_CASE(electrum__post__subscribed_event__no_notification) REQUIRE_NO_THROW_TRUE(next.at("result").is_double()); } +BOOST_AUTO_TEST_CASE(electrum__post__repeat_version_from_1_4__bad_request) +{ + BOOST_REQUIRE(post_handshake(electrum::version::v1_4)); + + const auto response = post(R"({"id":704,"method":"server.version","params":["foobar","1.4"]})"); + 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(), bad_request.value()); +} + +BOOST_AUTO_TEST_CASE(electrum__post__missing_arguments__dropped) +{ + BOOST_REQUIRE(post_handshake(electrum::version::v1_1)); + + const auto response = post(R"({"id":705,"method":"blockchain.scripthash.get_balance","params":[]})"); + REQUIRE_NO_THROW_TRUE(response.at("dropped").as_bool()); +} + +BOOST_AUTO_TEST_SUITE_END() + +// scoped credential + +BOOST_FIXTURE_TEST_SUITE(electrum_scoped_credential_tests, electrum_scoped_credential_setup_fixture) + +BOOST_AUTO_TEST_CASE(electrum__post__scoped_credential_unlisted_method__forbidden) +{ + const auto response = post_authorized(R"({"id":900,"method":"server.version","params":["test","1.4"]})", ELECTRUM_TEST_USERNAME, ELECTRUM_TEST_PASSWORD); + REQUIRE_NO_THROW_TRUE(response.at("result").is_array()); + + const auto status = post_status_authorized(R"({"id":901,"method":"server.banner","params":[]})", ELECTRUM_TEST_USERNAME, ELECTRUM_TEST_PASSWORD); + BOOST_REQUIRE_EQUAL(status, rpc_client::status::forbidden); +} + +BOOST_AUTO_TEST_CASE(electrum__ws__scoped_credential_unlisted_method__dropped) +{ + BOOST_REQUIRE(!ws_upgrade(ELECTRUM_TEST_USERNAME, ELECTRUM_TEST_PASSWORD)); + BOOST_REQUIRE(ws_handshake(electrum::version::v1_4)); + + const auto response = ws_get(R"({"id":902,"method":"server.banner","params":[]})"); + REQUIRE_NO_THROW_TRUE(response.at("dropped").as_bool()); +} + BOOST_AUTO_TEST_SUITE_END() // websocket From dd4343f5910fe4f9969db8a597234457f410d9e5 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 29 Sep 2026 12:16:33 -0400 Subject: [PATCH 18/21] Add esplora, native and btcd faulted store tests. --- test/protocols/btcd/btcd_rpc.cpp | 50 +++++++++++++++++++ test/protocols/btcd/btcd_setup_fixture.hpp | 12 +++++ test/protocols/esplora/esplora_block.cpp | 26 ++++++++++ .../esplora/esplora_setup_fixture.cpp | 2 +- .../esplora/esplora_setup_fixture.hpp | 41 +++++++++++++++ .../protocols/esplora/esplora_transaction.cpp | 19 +++++++ test/protocols/native/native_block.cpp | 21 ++++++++ .../protocols/native/native_setup_fixture.cpp | 2 +- .../protocols/native/native_setup_fixture.hpp | 27 ++++++++++ test/protocols/native/native_tx.cpp | 9 ++++ 10 files changed, 207 insertions(+), 2 deletions(-) diff --git a/test/protocols/btcd/btcd_rpc.cpp b/test/protocols/btcd/btcd_rpc.cpp index d5a08325..cfcc73e5 100644 --- a/test/protocols/btcd/btcd_rpc.cpp +++ b/test/protocols/btcd/btcd_rpc.cpp @@ -36,6 +36,7 @@ static const code unimplemented{ server::error::btcd::unimplemented }; static const code misc_error{ server::error::btcd::misc_error }; static const code invalid_parameter{ server::error::btcd::invalid_parameter }; static const code invalid_params{ server::error::btcd::invalid_params }; +static const code internal{ server::error::btcd::internal_error }; static const code block_not_found{ server::error::btcd::invalid_address_or_key }; static const code node_not_added{ server::error::btcd::client_node_not_added }; static const code node_not_connected{ server::error::btcd::client_node_not_connected }; @@ -1071,6 +1072,28 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__blockdisconnected__not_subscribed__not_delivered) REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); } +BOOST_AUTO_TEST_CASE(btcd_rpc__blockconnected__dangling_link__not_delivered) +{ + rpc("notifyblocks"); + notify(node::chases::organized{ 42 }); + notify(node::chases::reorganized{ 9 }); + + const auto disconnected = receive_notification(); + BOOST_REQUIRE_EQUAL(as_text(disconnected.at("method")), "blockdisconnected"); + BOOST_REQUIRE_EQUAL(as_text(disconnected.at("params").as_array()[0]), block9); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__blockdisconnected__dangling_link__not_delivered) +{ + rpc("notifyblocks"); + notify(node::chases::reorganized{ 42 }); + notify(node::chases::reorganized{ 9 }); + + const auto disconnected = receive_notification(); + BOOST_REQUIRE_EQUAL(as_text(disconnected.at("method")), "blockdisconnected"); + BOOST_REQUIRE_EQUAL(as_text(disconnected.at("params").as_array()[0]), block9); +} + BOOST_AUTO_TEST_SUITE_END() // Filter limit (btcd.maximum_filters): loadtxfilter watches are bounded per @@ -1259,3 +1282,30 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__getblockchaininfo__bip9_softforks_taproot__active } BOOST_AUTO_TEST_SUITE_END() + +// dangling +// ---------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_SUITE(btcd_dangling_tests, btcd_dangling_setup_fixture) + +BOOST_AUTO_TEST_CASE(btcd_rpc__getblockchaininfo__dangling__internal_error) +{ + BOOST_REQUIRE_EQUAL(rpc_error("getblockchaininfo"), internal.value()); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__getdifficulty__dangling__internal_error) +{ + BOOST_REQUIRE_EQUAL(rpc_error("getdifficulty"), internal.value()); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__getinfo__dangling__internal_error) +{ + BOOST_REQUIRE_EQUAL(rpc_error("getinfo"), internal.value()); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__rescan__dangling__internal_error) +{ + BOOST_REQUIRE_EQUAL(rpc_error("rescan", std::format(R"(["{}",[],[],""])", encode_hash(test::block1_hash))), internal.value()); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/protocols/btcd/btcd_setup_fixture.hpp b/test/protocols/btcd/btcd_setup_fixture.hpp index 219534d1..1992a00c 100644 --- a/test/protocols/btcd/btcd_setup_fixture.hpp +++ b/test/protocols/btcd/btcd_setup_fixture.hpp @@ -179,4 +179,16 @@ struct btcd_taproot_active_setup_fixture } }; +struct btcd_dangling_setup_fixture + : btcd_setup_fixture +{ + inline btcd_dangling_setup_fixture() + : btcd_setup_fixture([](test::query_t& query) + { + return test::setup_dangling_store(query); + }) + { + } +}; + #endif diff --git a/test/protocols/esplora/esplora_block.cpp b/test/protocols/esplora/esplora_block.cpp index 056f090b..04f6bda6 100644 --- a/test/protocols/esplora/esplora_block.cpp +++ b/test/protocols/esplora/esplora_block.cpp @@ -239,3 +239,29 @@ BOOST_AUTO_TEST_CASE(esplora__block_status__unconfirmed__not_in_best_chain) } BOOST_AUTO_TEST_SUITE_END() + +// faulted stores +// ---------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_SUITE(esplora_unassociated_tests, esplora_unassociated_setup_fixture) + +BOOST_AUTO_TEST_CASE(esplora__blocks__unassociated__empty) +{ + const auto response = get_json("/blocks"); + BOOST_REQUIRE(response.is_array()); + BOOST_REQUIRE(response.as_array().empty()); +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_FIXTURE_TEST_SUITE(esplora_misheighted_tests, esplora_misheighted_setup_fixture) + +BOOST_AUTO_TEST_CASE(esplora__block__misheighted__zero_mediantime) +{ + const auto response = get_json("/block/" + encode_hash(test::block3_hash)); + BOOST_REQUIRE(response.is_object()); + BOOST_REQUIRE_EQUAL(response.as_object().at("height").as_int64(), 9); + BOOST_REQUIRE_EQUAL(response.as_object().at("mediantime").as_int64(), 0); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/protocols/esplora/esplora_setup_fixture.cpp b/test/protocols/esplora/esplora_setup_fixture.cpp index 29cb095e..0def5d2d 100644 --- a/test/protocols/esplora/esplora_setup_fixture.cpp +++ b/test/protocols/esplora/esplora_setup_fixture.cpp @@ -69,7 +69,7 @@ esplora_setup_fixture::esplora_setup_fixture(const initializer& setup, ec = config_.initialize(); BOOST_REQUIRE_MESSAGE(!ec, ec.message()); - setup(query_); + BOOST_REQUIRE(setup(query_)); // The node (chasers and address pool), bypassed by default. if (start) diff --git a/test/protocols/esplora/esplora_setup_fixture.hpp b/test/protocols/esplora/esplora_setup_fixture.hpp index e7c6a178..86ec05c8 100644 --- a/test/protocols/esplora/esplora_setup_fixture.hpp +++ b/test/protocols/esplora/esplora_setup_fixture.hpp @@ -171,4 +171,45 @@ struct esplora_no_address_setup_fixture } }; +struct esplora_unassociated_setup_fixture + : esplora_setup_fixture +{ + inline esplora_unassociated_setup_fixture() + : esplora_setup_fixture([](test::query_t& query) + { + return test::setup_unassociated_store(query); + }) + { + } +}; + +// A faulted store: block 1 is reassociated with the coinbase of block 2. +struct esplora_misassociated_setup_fixture + : esplora_setup_fixture +{ + inline esplora_misassociated_setup_fixture() + : esplora_setup_fixture([](test::query_t& query) + { + const auto coinbase = test::block2.transactions_ptr()->front()->hash(false); + return test::setup_ten_block_store(query) && + !query.set_code(query.to_header(test::block1_hash), database::tx_links{ query.to_tx(coinbase) }, false); + }) + { + } +}; + +// A faulted store: block 3 is archived at height 9 above its height 2 parent. +struct esplora_misheighted_setup_fixture + : esplora_setup_fixture +{ + inline esplora_misheighted_setup_fixture() + : esplora_setup_fixture([](test::query_t& query) + { + return test::setup_three_block_store(query) && + query.set(test::block3, database::context{ 0, 9, 0 }, {}, false, false); + }) + { + } +}; + #endif diff --git a/test/protocols/esplora/esplora_transaction.cpp b/test/protocols/esplora/esplora_transaction.cpp index 23cb9065..c1155780 100644 --- a/test/protocols/esplora/esplora_transaction.cpp +++ b/test/protocols/esplora/esplora_transaction.cpp @@ -348,3 +348,22 @@ BOOST_AUTO_TEST_CASE(esplora__broadcast__genesis_coinbase__coinbase_transaction) } BOOST_AUTO_TEST_SUITE_END() + +// misassociated block +// ---------------------------------------------------------------------------- + +static const std::string coinbase1 = encode_hash(test::block1.transactions_ptr()->front()->hash(false)); + +BOOST_FIXTURE_TEST_SUITE(esplora_misassociated_tests, esplora_misassociated_setup_fixture) + +BOOST_AUTO_TEST_CASE(esplora__tx_merkleblock_proof__misassociated__not_found) +{ + BOOST_REQUIRE_EQUAL(get_status("/tx/" + coinbase1 + "/merkleblock-proof"), http::status::not_found); +} + +BOOST_AUTO_TEST_CASE(esplora__tx_merkle_proof__misassociated__not_found) +{ + BOOST_REQUIRE_EQUAL(get_status("/tx/" + coinbase1 + "/merkle-proof"), http::status::not_found); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/protocols/native/native_block.cpp b/test/protocols/native/native_block.cpp index 7ed8f2cd..0c8aa078 100644 --- a/test/protocols/native/native_block.cpp +++ b/test/protocols/native/native_block.cpp @@ -734,4 +734,25 @@ BOOST_AUTO_TEST_CASE(native__hosts__listed_host__expected) BOOST_REQUIRE_EQUAL(get_status("/v1/top?format=json", http::field::host, "example.com"), http::status::ok); } +BOOST_AUTO_TEST_SUITE_END() + +// faulted stores +// ---------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_SUITE(native_unassociated_tests, native_unassociated_setup_fixture) + +BOOST_AUTO_TEST_CASE(native__block__unassociated__internal_server_error) +{ + BOOST_REQUIRE_EQUAL(get_status("/v1/block/height/3?format=data"), http::status::internal_server_error); +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_FIXTURE_TEST_SUITE(native_witness_tests, native_witness_setup_fixture) + +BOOST_AUTO_TEST_CASE(native__block_details__missing_prevouts__internal_server_error) +{ + BOOST_REQUIRE_EQUAL(get_status("/v1/block/height/2/details?format=json"), http::status::internal_server_error); +} + BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file diff --git a/test/protocols/native/native_setup_fixture.cpp b/test/protocols/native/native_setup_fixture.cpp index 74f09660..4a096799 100644 --- a/test/protocols/native/native_setup_fixture.cpp +++ b/test/protocols/native/native_setup_fixture.cpp @@ -115,7 +115,7 @@ native_setup_fixture::native_setup_fixture(const initializer& setup, ec = config_.initialize(); BOOST_REQUIRE_MESSAGE(!ec, ec.message()); - setup(query_); + BOOST_REQUIRE(setup(query_)); // Run the server. std::promise running{}; diff --git a/test/protocols/native/native_setup_fixture.hpp b/test/protocols/native/native_setup_fixture.hpp index eb62ac97..688a96c4 100644 --- a/test/protocols/native/native_setup_fixture.hpp +++ b/test/protocols/native/native_setup_fixture.hpp @@ -114,6 +114,33 @@ struct native_address_setup_fixture } }; +struct native_unassociated_setup_fixture + : native_setup_fixture +{ + inline native_unassociated_setup_fixture() + : native_setup_fixture([](test::query_t& query) + { + return test::setup_unassociated_store(query); + }) + { + } +}; + +// A faulted store: block 1 is reassociated with the coinbase of block 2. +struct native_misassociated_setup_fixture + : native_setup_fixture +{ + inline native_misassociated_setup_fixture() + : native_setup_fixture([](test::query_t& query) + { + const auto coinbase = test::block2.transactions_ptr()->front()->hash(false); + return test::setup_ten_block_store(query) && + !query.set_code(query.to_header(test::block1_hash), database::tx_links{ query.to_tx(coinbase) }, false); + }) + { + } +}; + struct native_no_address_setup_fixture : native_setup_fixture { diff --git a/test/protocols/native/native_tx.cpp b/test/protocols/native/native_tx.cpp index d1041115..2b233632 100644 --- a/test/protocols/native/native_tx.cpp +++ b/test/protocols/native/native_tx.cpp @@ -220,3 +220,12 @@ BOOST_AUTO_TEST_CASE(native__tx_details__spend_exceeds_value__internal_server_er } BOOST_AUTO_TEST_SUITE_END() + +BOOST_FIXTURE_TEST_SUITE(native_misassociated_tests, native_misassociated_setup_fixture) + +BOOST_AUTO_TEST_CASE(native__tx_details__misassociated__internal_server_error) +{ + BOOST_REQUIRE_EQUAL(get_status("/v1/tx/" + coinbase1_hash + "/details?format=json"), http::status::internal_server_error); +} + +BOOST_AUTO_TEST_SUITE_END() From e78945bcd38fb1726a88bc4fff398a898dbbcd7c Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 29 Sep 2026 12:22:02 -0400 Subject: [PATCH 19/21] Add btcd dispatch failure, rescan and admin event unsubscribe tests. --- test/protocols/admin/admin_diagnostics.cpp | 15 +++++++++++++++ test/protocols/btcd/btcd_rpc.cpp | 22 ++++++++++++++++++++++ test/protocols/btcd/btcd_setup_fixture.cpp | 13 +++++++++++-- test/protocols/btcd/btcd_setup_fixture.hpp | 7 ++++++- 4 files changed, 54 insertions(+), 3 deletions(-) diff --git a/test/protocols/admin/admin_diagnostics.cpp b/test/protocols/admin/admin_diagnostics.cpp index e0caa14c..f699ee58 100644 --- a/test/protocols/admin/admin_diagnostics.cpp +++ b/test/protocols/admin/admin_diagnostics.cpp @@ -271,4 +271,19 @@ BOOST_AUTO_TEST_CASE(admin__ws_event_subscribe__unfiltered_event__not_notified) BOOST_REQUIRE_EQUAL(frame.at("value").as_int64(), 2); } +BOOST_AUTO_TEST_CASE(admin__ws_event_subscribe__unsubscribed__not_notified) +{ + BOOST_REQUIRE(!ws_upgrade()); + BOOST_REQUIRE_EQUAL(ws_get_json("/v1/event/subscribe?filter=8").at("previous").as_int64(), 0); + BOOST_REQUIRE_EQUAL(ws_get_json("/v1/event/subscribe?filter=0").at("previous").as_int64(), 8); + BOOST_REQUIRE_EQUAL(ws_get_json("/v1/log/subscribe?filter=1024").at("previous").as_int64(), 0); + + fire(node::events::block_archived, 1); + write(10, "marker"); + + const auto frame = ws_receive_json(); + REQUIRE_NO_THROW_TRUE(frame.at("message").is_string()); + BOOST_REQUIRE_EQUAL(frame.at("message").as_string(), "marker"); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/protocols/btcd/btcd_rpc.cpp b/test/protocols/btcd/btcd_rpc.cpp index cfcc73e5..849de23d 100644 --- a/test/protocols/btcd/btcd_rpc.cpp +++ b/test/protocols/btcd/btcd_rpc.cpp @@ -722,6 +722,11 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__rescan__unknown_beginblock__not_found) BOOST_REQUIRE_EQUAL(result, block_not_found.value()); } +BOOST_AUTO_TEST_CASE(btcd_rpc__rescan__invalid_beginblock__not_found) +{ + BOOST_REQUIRE_EQUAL(rpc_error("rescan", R"(["not-a-hash",[],[],""])"), block_not_found.value()); +} + BOOST_AUTO_TEST_CASE(btcd_rpc__rescan__no_addresses_or_outpoints__rescan_finished) { const auto response = rpc("rescan", std::format(R"(["{}",[],[],""])", block9)); @@ -772,6 +777,16 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__getblockcount__downgraded_second__nine) BOOST_REQUIRE_EQUAL(tcp_rpc("getblockcount").at("result").as_int64(), 9); } +BOOST_AUTO_TEST_CASE(btcd_rpc__getcfilter__downgraded_numeric_hash__dropped) +{ + REQUIRE_NO_THROW_TRUE(tcp_rpc("getcfilter", "[42,0]").at("dropped").as_bool()); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__getcfilter__http_post_numeric_hash__dropped) +{ + REQUIRE_NO_THROW_TRUE(http_rpc("getcfilter", "[42,0]", false).at("dropped").as_bool()); +} + // service settings // ---------------------------------------------------------------------------- @@ -1213,6 +1228,13 @@ BOOST_AUTO_TEST_CASE(btcd_scoped_credential__notifyblocks__unlisted_method__inva BOOST_REQUIRE_EQUAL(rpc_error("notifyblocks"), invalid_params.value()); } +BOOST_AUTO_TEST_CASE(btcd_scoped_credential__notifyblocks__http_post_unlisted_method__invalid_params) +{ + const auto response = http_rpc("notifyblocks", "[]", BTCD_TEST_USERNAME, BTCD_TEST_PASSWORD); + REQUIRE_NO_THROW_TRUE(response.at("error").is_object()); + BOOST_REQUIRE_EQUAL(response.at("error").at("code").as_int64(), invalid_params.value()); +} + BOOST_AUTO_TEST_SUITE_END() // authenticate (protocol_btcd::handle_authenticate): exercises the diff --git a/test/protocols/btcd/btcd_setup_fixture.cpp b/test/protocols/btcd/btcd_setup_fixture.cpp index b3b2595e..13ec98eb 100644 --- a/test/protocols/btcd/btcd_setup_fixture.cpp +++ b/test/protocols/btcd/btcd_setup_fixture.cpp @@ -126,8 +126,17 @@ boost::json::value btcd_setup_fixture::tcp_rpc(std::string_view method, } boost::json::value btcd_setup_fixture::http_rpc(std::string_view method, - std::string_view params) + std::string_view params, bool checked) +{ + return other_.post(body_of(http_request_id_++, method, params), "/", + checked); +} + +boost::json::value btcd_setup_fixture::http_rpc(std::string_view method, + std::string_view params, const std::string& username, + const std::string& password) { - return other_.post(body_of(http_request_id_++, method, params), "/", true); + return other_.post_authorized(body_of(http_request_id_++, method, params), + username, password); } diff --git a/test/protocols/btcd/btcd_setup_fixture.hpp b/test/protocols/btcd/btcd_setup_fixture.hpp index 1992a00c..b68acddc 100644 --- a/test/protocols/btcd/btcd_setup_fixture.hpp +++ b/test/protocols/btcd/btcd_setup_fixture.hpp @@ -64,7 +64,12 @@ struct btcd_setup_fixture std::string_view params="[]"); boost::json::value http_rpc(std::string_view method, - std::string_view params = "[]"); + std::string_view params = "[]", bool checked=true); + + // As http_rpc(), with basic authorization. + boost::json::value http_rpc(std::string_view method, + std::string_view params, const std::string& username, + const std::string& password); // Read one further (unprompted) server push, e.g. a blockconnected // notification. Returns the parsed json-rpc notification object. From 7581474d08142a6530ecb0c341d295310c9b2bff Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 29 Sep 2026 12:53:57 -0400 Subject: [PATCH 20/21] Add bitcoind transaction, scan, combine and utxo update helper tests. --- test/protocols/bitcoind/bitcoind_rpc.cpp | 425 +++++++++++++++++++++++ 1 file changed, 425 insertions(+) diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 51200e9d..1e56d790 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -906,6 +906,270 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__combinerawtransaction__witness_script_hash_mu BOOST_REQUIRE(*stack.at(3) == witness_script); } +BOOST_AUTO_TEST_CASE(bitcoind_rpc__combinerawtransaction__variant_with_fewer_inputs__unsigned_base) +{ + using namespace chain; + const point prevout1{ test::block1.transactions_ptr()->front()->hash(false), 0 }; + const point prevout2{ test::block2.transactions_ptr()->front()->hash(false), 0 }; + const output out{ 1, script{ script::to_pay_key_hash_pattern(short_hash{}) } }; + const transaction base{ 1, inputs{ input{ prevout1, script{}, witness{}, 0xffffffff }, input{ prevout2, script{}, witness{}, 0xffffffff } }, outputs{ out }, 0 }; + const transaction variant{ 1, inputs{ input{ prevout1, script{}, witness{}, 0xffffffff } }, outputs{ out }, 0 }; + + const auto params = R"([[")" + encode_base16(base.to_data(true)) + R"(",")" + encode_base16(variant.to_data(true)) + R"("]])"; + const auto response = rpc("combinerawtransaction", params); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); + BOOST_REQUIRE_EQUAL(as_text(response.at("result")), encode_base16(base.to_data(true))); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__combinerawtransaction__unsigned_multisig__combined) +{ + using namespace chain; + const ec_secret secret1{ { 0x01 } }; + const ec_secret secret2{ { 0x02 } }; + ec_compressed point1{}; + ec_compressed point2{}; + BOOST_REQUIRE(secret_to_public(point1, secret1)); + BOOST_REQUIRE(secret_to_public(point2, secret2)); + + const script multisig{ script::to_pay_multisig_pattern(2, ec_compresseds{ point1, point2 }) }; + const block block10 + { + header{ 0x31323334, test::block9_hash, hash_digest{ 0x10, 0xca }, 0x41424344, 0x51525354, 0x61626364 }, + transactions{ transaction{ 1, inputs{ input{ point{}, script{}, witness{}, 0x01 } }, outputs{ output{ 100'000'000, multisig } }, 0 } } + }; + + BOOST_REQUIRE(query_.set(block10, database::context{ 0, 10, 0 }, {}, false, false)); + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(block10.hash()), true)); + + const point prevout{ block10.transactions_ptr()->front()->hash(false), 0 }; + const output out{ 1, script{ script::to_pay_key_hash_pattern(short_hash{}) } }; + const transaction spend{ 1, inputs{ input{ prevout, script{}, witness{}, 0xffffffff } }, outputs{ out }, 0 }; + + const auto response = rpc("combinerawtransaction", R"([[")" + encode_base16(spend.to_data(true)) + R"("]])"); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); + + data_chunk data{}; + BOOST_REQUIRE(decode_base16(data, as_text(response.at("result")))); + + const transaction merged{ data, true }; + BOOST_REQUIRE(merged.is_valid()); + BOOST_REQUIRE(merged.inputs_ptr()->front()->point() == prevout); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__combinerawtransaction__multisig_undecodable_partial__merged_in_key_order) +{ + using namespace chain; + const ec_secret secret1{ { 0x01 } }; + const ec_secret secret2{ { 0x02 } }; + const ec_secret secret3{ { 0x03 } }; + ec_compressed point1{}; + ec_compressed point2{}; + ec_compressed point3{}; + BOOST_REQUIRE(secret_to_public(point1, secret1)); + BOOST_REQUIRE(secret_to_public(point2, secret2)); + BOOST_REQUIRE(secret_to_public(point3, secret3)); + + constexpr uint64_t value = 100'000'000; + const script multisig{ script::to_pay_multisig_pattern(2, ec_compresseds{ point1, point2, point3 }) }; + const block block10 + { + header{ 0x31323334, test::block9_hash, hash_digest{ 0x10, 0xcb }, 0x41424344, 0x51525354, 0x61626364 }, + transactions{ transaction{ 1, inputs{ input{ point{}, script{}, witness{}, 0x01 } }, outputs{ output{ value, multisig } }, 0 } } + }; + + BOOST_REQUIRE(query_.set(block10, database::context{ 0, 10, 0 }, {}, false, false)); + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(block10.hash()), true)); + + const point prevout{ block10.transactions_ptr()->front()->hash(false), 0 }; + const output out{ 1, script{ script::to_pay_key_hash_pattern(short_hash{}) } }; + const transaction spend{ 1, inputs{ input{ prevout, script{}, witness{}, 0xffffffff } }, outputs{ out }, 0 }; + + endorsement endorse1{}; + endorsement endorse2{}; + BOOST_REQUIRE(spend.create_endorsement(endorse1, secret1, multisig, 0, value, coverage::hash_all, script_version::unversioned, flags::no_rules)); + BOOST_REQUIRE(spend.create_endorsement(endorse2, secret2, multisig, 0, value, coverage::hash_all, script_version::unversioned, flags::no_rules)); + + const script undecodable{ operations{ { opcode::push_size_0 }, { data_chunk(71, 0x30), false } } }; + const script partial1{ operations{ { opcode::push_size_0 }, { data_chunk{ endorse1 }, false } } }; + const script partial2{ operations{ { opcode::push_size_0 }, { data_chunk{ endorse2 }, false } } }; + const transaction variant0{ 1, inputs{ input{ prevout, undecodable, witness{}, 0xffffffff } }, outputs{ out }, 0 }; + const transaction variant1{ 1, inputs{ input{ prevout, partial1, witness{}, 0xffffffff } }, outputs{ out }, 0 }; + const transaction variant2{ 1, inputs{ input{ prevout, partial2, witness{}, 0xffffffff } }, outputs{ out }, 0 }; + + const auto params = R"([[")" + encode_base16(variant0.to_data(true)) + R"(",")" + encode_base16(variant1.to_data(true)) + R"(",")" + encode_base16(variant2.to_data(true)) + R"("]])"; + const auto response = rpc("combinerawtransaction", params); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); + + data_chunk data{}; + BOOST_REQUIRE(decode_base16(data, as_text(response.at("result")))); + + const transaction merged{ data, true }; + BOOST_REQUIRE(merged.is_valid()); + + const auto& ops = merged.inputs_ptr()->front()->script().ops(); + BOOST_REQUIRE_EQUAL(ops.size(), 3u); + BOOST_REQUIRE(ops.at(1).data() == endorse1); + BOOST_REQUIRE(ops.at(2).data() == endorse2); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__combinerawtransaction__script_hash_multisig_unsigned_base__merged_with_redeem_script) +{ + using namespace chain; + const ec_secret secret1{ { 0x01 } }; + const ec_secret secret2{ { 0x02 } }; + const ec_secret secret3{ { 0x03 } }; + ec_compressed point1{}; + ec_compressed point2{}; + ec_compressed point3{}; + BOOST_REQUIRE(secret_to_public(point1, secret1)); + BOOST_REQUIRE(secret_to_public(point2, secret2)); + BOOST_REQUIRE(secret_to_public(point3, secret3)); + + constexpr uint64_t value = 100'000'000; + const script multisig{ script::to_pay_multisig_pattern(2, ec_compresseds{ point1, point2, point3 }) }; + const auto redeem = multisig.to_data(false); + const script pay{ script::to_pay_script_hash_pattern(bitcoin_short_hash(redeem)) }; + const block block10 + { + header{ 0x31323334, test::block9_hash, hash_digest{ 0x10, 0xdc }, 0x41424344, 0x51525354, 0x61626364 }, + transactions{ transaction{ 1, inputs{ input{ point{}, script{}, witness{}, 0x01 } }, outputs{ output{ value, pay } }, 0 } } + }; + + BOOST_REQUIRE(query_.set(block10, database::context{ 0, 10, 0 }, {}, false, false)); + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(block10.hash()), true)); + + const point prevout{ block10.transactions_ptr()->front()->hash(false), 0 }; + const output out{ 1, script{ script::to_pay_key_hash_pattern(short_hash{}) } }; + const transaction spend{ 1, inputs{ input{ prevout, script{}, witness{}, 0xffffffff } }, outputs{ out }, 0 }; + + endorsement endorse1{}; + endorsement endorse2{}; + BOOST_REQUIRE(spend.create_endorsement(endorse1, secret1, multisig, 0, value, coverage::hash_all, script_version::unversioned, flags::no_rules)); + BOOST_REQUIRE(spend.create_endorsement(endorse2, secret2, multisig, 0, value, coverage::hash_all, script_version::unversioned, flags::no_rules)); + + const script partial1{ operations{ { opcode::push_size_0 }, { data_chunk{ endorse1 }, false }, { redeem, false } } }; + const script partial2{ operations{ { opcode::push_size_0 }, { data_chunk{ endorse2 }, false }, { redeem, false } } }; + const transaction variant1{ 1, inputs{ input{ prevout, partial1, witness{}, 0xffffffff } }, outputs{ out }, 0 }; + const transaction variant2{ 1, inputs{ input{ prevout, partial2, witness{}, 0xffffffff } }, outputs{ out }, 0 }; + + const auto params = R"([[")" + encode_base16(spend.to_data(true)) + R"(",")" + encode_base16(variant1.to_data(true)) + R"(",")" + encode_base16(variant2.to_data(true)) + R"("]])"; + const auto response = rpc("combinerawtransaction", params); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); + + data_chunk data{}; + BOOST_REQUIRE(decode_base16(data, as_text(response.at("result")))); + + const transaction merged{ data, true }; + BOOST_REQUIRE(merged.is_valid()); + + const auto& ops = merged.inputs_ptr()->front()->script().ops(); + BOOST_REQUIRE_EQUAL(ops.size(), 4u); + BOOST_REQUIRE(ops.at(1).data() == endorse1); + BOOST_REQUIRE(ops.at(2).data() == endorse2); + BOOST_REQUIRE(ops.at(3).data() == redeem); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__combinerawtransaction__witness_script_hash_multisig_unsigned_base__merged_stack) +{ + using namespace chain; + const ec_secret secret1{ { 0x01 } }; + const ec_secret secret2{ { 0x02 } }; + const ec_secret secret3{ { 0x03 } }; + ec_compressed point1{}; + ec_compressed point2{}; + ec_compressed point3{}; + BOOST_REQUIRE(secret_to_public(point1, secret1)); + BOOST_REQUIRE(secret_to_public(point2, secret2)); + BOOST_REQUIRE(secret_to_public(point3, secret3)); + + constexpr uint64_t value = 100'000'000; + const script multisig{ script::to_pay_multisig_pattern(2, ec_compresseds{ point1, point2, point3 }) }; + const auto witness_script = multisig.to_data(false); + const script pay{ script::to_pay_witness_script_hash_pattern(sha256_hash(witness_script)) }; + const block block10 + { + header{ 0x31323334, test::block9_hash, hash_digest{ 0x10, 0xed }, 0x41424344, 0x51525354, 0x61626364 }, + transactions{ transaction{ 1, inputs{ input{ point{}, script{}, witness{}, 0x01 } }, outputs{ output{ value, pay } }, 0 } } + }; + + BOOST_REQUIRE(query_.set(block10, database::context{ 0, 10, 0 }, {}, false, false)); + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(block10.hash()), true)); + + const point prevout{ block10.transactions_ptr()->front()->hash(false), 0 }; + const output out{ 1, script{ script::to_pay_key_hash_pattern(short_hash{}) } }; + const transaction spend{ 1, inputs{ input{ prevout, script{}, witness{}, 0xffffffff } }, outputs{ out }, 0 }; + + endorsement endorse1{}; + endorsement endorse2{}; + BOOST_REQUIRE(spend.create_endorsement(endorse1, secret1, multisig, 0, value, coverage::hash_all, script_version::segwit, flags::bip143_rule)); + BOOST_REQUIRE(spend.create_endorsement(endorse2, secret2, multisig, 0, value, coverage::hash_all, script_version::segwit, flags::bip143_rule)); + + const witness partial1{ chunk_cptrs{ to_shared(), to_shared(endorse1), to_shared(witness_script) } }; + const witness partial2{ chunk_cptrs{ to_shared(), to_shared(endorse2), to_shared(witness_script) } }; + const transaction variant1{ 1, inputs{ input{ prevout, script{}, partial1, 0xffffffff } }, outputs{ out }, 0 }; + const transaction variant2{ 1, inputs{ input{ prevout, script{}, partial2, 0xffffffff } }, outputs{ out }, 0 }; + + const auto params = R"([[")" + encode_base16(spend.to_data(true)) + R"(",")" + encode_base16(variant1.to_data(true)) + R"(",")" + encode_base16(variant2.to_data(true)) + R"("]])"; + const auto response = rpc("combinerawtransaction", params); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); + + data_chunk data{}; + BOOST_REQUIRE(decode_base16(data, as_text(response.at("result")))); + + const transaction merged{ data, true }; + BOOST_REQUIRE(merged.is_valid()); + + const auto& stack = merged.inputs_ptr()->front()->witness().stack(); + BOOST_REQUIRE_EQUAL(stack.size(), 4u); + BOOST_REQUIRE(stack.at(0)->empty()); + BOOST_REQUIRE(*stack.at(1) == endorse1); + BOOST_REQUIRE(*stack.at(2) == endorse2); + BOOST_REQUIRE(*stack.at(3) == witness_script); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__combinerawtransaction__witness_script_hash_key__endorsing_witness) +{ + using namespace chain; + const ec_secret secret{ { 0x01 } }; + ec_compressed key{}; + BOOST_REQUIRE(secret_to_public(key, secret)); + + // A block paying p2wsh of a public key script, confirmed at 10. + constexpr uint64_t value = 100'000'000; + const script key_script{ script::to_pay_public_key_pattern(key) }; + const auto witness_script = key_script.to_data(false); + const script pay{ script::to_pay_witness_script_hash_pattern(sha256_hash(witness_script)) }; + const block block10 + { + header{ 0x31323334, test::block9_hash, hash_digest{ 0x10, 0xef }, 0x41424344, 0x51525354, 0x61626364 }, + transactions{ transaction{ 1, inputs{ input{ point{}, script{}, witness{}, 0x01 } }, outputs{ output{ value, pay } }, 0 } } + }; + + BOOST_REQUIRE(query_.set(block10, database::context{ 0, 10, 0 }, {}, false, false)); + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(block10.hash()), true)); + + const point prevout{ block10.transactions_ptr()->front()->hash(false), 0 }; + const output out{ 1, script{ script::to_pay_key_hash_pattern(short_hash{}) } }; + const transaction spend{ 1, inputs{ input{ prevout, script{}, witness{}, 0xffffffff } }, outputs{ out }, 0 }; + + endorsement endorse{}; + BOOST_REQUIRE(spend.create_endorsement(endorse, secret, key_script, 0, value, coverage::hash_all, script_version::segwit, flags::bip143_rule)); + + const witness endorsing{ chunk_cptrs{ to_shared(endorse), to_shared(witness_script) } }; + const transaction signed_tx{ 1, inputs{ input{ prevout, script{}, endorsing, 0xffffffff } }, outputs{ out }, 0 }; + + const auto params = R"([[")" + encode_base16(spend.to_data(true)) + R"(",")" + encode_base16(signed_tx.to_data(true)) + R"("]])"; + const auto response = rpc("combinerawtransaction", params); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); + BOOST_REQUIRE_EQUAL(as_text(response.at("result")), encode_base16(signed_tx.to_data(true))); +} + BOOST_AUTO_TEST_CASE(bitcoind_rpc__decoderawtransaction__iswitness_false__round_trips) { const auto txid = encode_hash(test::block1.transactions_ptr()->front()->hash(false)); @@ -986,6 +1250,54 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__createrawtransaction__replaceable__bip125_seq BOOST_REQUIRE_EQUAL(response.at("result").at("vin").at(0).at("sequence").as_int64(), 4294967293); } +BOOST_AUTO_TEST_CASE(bitcoind_rpc__createrawtransaction__negative_locktime__invalid_parameter) +{ + BOOST_REQUIRE(has_code(rpc("createrawtransaction", "[[], {}, -1]"), -8)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__createrawtransaction__version_zero__invalid_parameter) +{ + BOOST_REQUIRE(has_code(rpc("createrawtransaction", "[[], {}, 0, true, 0]"), -8)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__createrawtransaction__version_four__invalid_parameter) +{ + BOOST_REQUIRE(has_code(rpc("createrawtransaction", "[[], {}, 0, true, 4]"), -8)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__createrawtransaction__input_not_object__type_error) +{ + BOOST_REQUIRE(has_code(rpc("createrawtransaction", "[[1], {}]"), -3)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__createrawtransaction__input_without_vout__invalid_parameter) +{ + const auto txid = encode_hash(test::block1.transactions_ptr()->front()->hash(false)); + BOOST_REQUIRE(has_code(rpc("createrawtransaction", "[[{\"txid\":\"" + txid + "\"}], {}]"), -8)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__createrawtransaction__negative_sequence__invalid_parameter) +{ + const auto txid = encode_hash(test::block1.transactions_ptr()->front()->hash(false)); + BOOST_REQUIRE(has_code(rpc("createrawtransaction", "[[{\"txid\":\"" + txid + "\",\"vout\":0,\"sequence\":-1}], {}]"), -8)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__createrawtransaction__numeric_outputs__type_error) +{ + BOOST_REQUIRE(has_code(rpc("createrawtransaction", "[[], 1]"), -3)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__createrawtransaction__array_output_not_object__error) +{ + BOOST_REQUIRE(has_error(rpc("createrawtransaction", "[[], [1]]"))); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__createrawtransaction__array_output_foreign_network__invalid_address) +{ + const wallet::payment_address testnet{ short_hash{}, 111 }; + BOOST_REQUIRE(has_code(rpc("createrawtransaction", "[[], [{\"" + testnet.encoded() + "\": 0.001}]]"), -5)); +} + BOOST_AUTO_TEST_CASE(bitcoind_rpc__decodescript__p2kh__pubkeyhash) { const auto response = rpc("decodescript", "[\"76a914000000000000000000000000000000000000000088ac\"]"); @@ -2751,6 +3063,85 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__utxoupdatepsbt__matching_output_descriptor__w BOOST_REQUIRE(out.derivations.front().point == to_chunk(point)); } +BOOST_AUTO_TEST_CASE(bitcoind_rpc__utxoupdatepsbt__matching_output_script_hash_descriptor__embedded_script) +{ + using namespace chain; + using namespace wallet; + + const ec_secret secret{ { 0x07 } }; + ec_compressed point{}; + BOOST_REQUIRE(secret_to_public(point, secret)); + + // An sh output paying wpkh, matched by its descriptor. + const script embedded{ script::to_pay_witness_key_hash_pattern(bitcoin_short_hash(point)) }; + const auto address = payment_address{ bitcoin_short_hash(embedded.to_data(false)), 0x05 }.encoded(); + const auto txid = encode_hash(test::block1.transactions_ptr()->front()->hash(false)); + + const auto created = rpc("createpsbt", "[[{\"txid\":\"" + txid + "\",\"vout\":0}], {\"" + address + "\": 0.001}]"); + const auto request = "[\"" + as_text(created.at("result")) + "\", [\"sh(wpkh(" + encode_base16(point) + "))\"]]"; + const auto response = rpc("utxoupdatepsbt", request); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); + + const psbt::transaction updated{ as_text(response.at("result")) }; + BOOST_REQUIRE(updated); + BOOST_REQUIRE_EQUAL(updated.outputs().size(), 1u); + + const auto& out = updated.outputs().front(); + BOOST_REQUIRE(out.embedded_script); + BOOST_REQUIRE(*out.embedded_script == embedded); + BOOST_REQUIRE(!out.witness_script); + BOOST_REQUIRE_EQUAL(out.derivations.size(), 1u); + BOOST_REQUIRE(out.derivations.front().point == to_chunk(point)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__utxoupdatepsbt__unknown_prevout__round_trips) +{ + const auto created = rpc("createpsbt", "[[{\"txid\":\"" + encode_hash(one_hash) + "\",\"vout\":0}], {\"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa\": 0.001}]"); + const auto response = rpc("utxoupdatepsbt", "[\"" + as_text(created.at("result")) + "\"]"); + BOOST_REQUIRE_EQUAL(as_text(response.at("result")), as_text(created.at("result"))); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__utxoupdatepsbt__witness_prevout__witness_utxo) +{ + using namespace chain; + using namespace wallet; + + const ec_secret secret{ { 0x07 } }; + ec_compressed key{}; + BOOST_REQUIRE(secret_to_public(key, secret)); + + // A block paying wpkh of the derived key, confirmed at 10. + constexpr uint64_t value = 100'000'000; + const script pay{ script::to_pay_witness_key_hash_pattern(bitcoin_short_hash(key)) }; + const block block10 + { + header{ 0x31323334, test::block9_hash, hash_digest{ 0x10, 0xab }, 0x41424344, 0x51525354, 0x61626364 }, + transactions{ transaction{ 1, inputs{ input{ point{}, script{}, witness{}, 0x01 } }, outputs{ output{ value, pay } }, 0 } } + }; + + BOOST_REQUIRE(query_.set(block10, database::context{ 0, 10, 0 }, {}, false, false)); + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(block10.hash()), true)); + + const auto txid = encode_hash(block10.transactions_ptr()->front()->hash(false)); + const auto created = rpc("createpsbt", "[[{\"txid\":\"" + txid + "\",\"vout\":0}], {\"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa\": 0.001}]"); + const auto request = "[\"" + as_text(created.at("result")) + "\", [\"wpkh(" + encode_base16(key) + ")\"]]"; + const auto response = rpc("utxoupdatepsbt", request); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); + + const psbt::transaction updated{ as_text(response.at("result")) }; + BOOST_REQUIRE(updated); + BOOST_REQUIRE_EQUAL(updated.inputs().size(), 1u); + + const auto& in = updated.inputs().front(); + BOOST_REQUIRE(in.witness_utxo); + BOOST_REQUIRE_EQUAL(in.witness_utxo->value(), value); + BOOST_REQUIRE(in.witness_utxo->script() == pay); + BOOST_REQUIRE_EQUAL(in.derivations.size(), 1u); + BOOST_REQUIRE(in.derivations.front().point == to_chunk(key)); +} + // scanblocks (the store holds only the genesis filter) static const std::string genesis_pk{ "pk(04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f)" }; @@ -2799,6 +3190,40 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__scanblocks__invalid_descriptor__invalid_addre BOOST_REQUIRE(has_code(rpc("scanblocks", "[\"start\", [\"bogus\"], 0, 0]"), -5)); } +BOOST_AUTO_TEST_CASE(bitcoind_rpc__scanblocks__invalid_address_descriptor__invalid_address) +{ + BOOST_REQUIRE(has_code(rpc("scanblocks", "[\"start\", [\"addr(bogus)\"], 0, 0]"), -5)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__scanblocks__numeric_range__genesis_block) +{ + const auto response = rpc("scanblocks", "[\"start\", [{\"desc\": \"" + genesis_pk + "\", \"range\": 5}], 0, 0]"); + const auto& result = response.at("result"); + BOOST_REQUIRE_EQUAL(result.at("relevant_blocks").as_array().size(), 1u); + BOOST_REQUIRE_EQUAL(as_text(result.at("relevant_blocks").at(0)), block0); + BOOST_REQUIRE(result.at("completed").as_bool()); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__scanblocks__scan_object_without_desc__error) +{ + BOOST_REQUIRE(has_error(rpc("scanblocks", "[\"start\", [{\"range\": 5}], 0, 0]"))); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__scanblocks__numeric_scan_object__error) +{ + BOOST_REQUIRE(has_error(rpc("scanblocks", "[\"start\", [1], 0, 0]"))); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__scanblocks__descending_range__error) +{ + BOOST_REQUIRE(has_error(rpc("scanblocks", "[\"start\", [{\"desc\": \"" + genesis_pk + "\", \"range\": [5, 1]}], 0, 0]"))); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__scanblocks__string_range__error) +{ + BOOST_REQUIRE(has_error(rpc("scanblocks", "[\"start\", [{\"desc\": \"" + genesis_pk + "\", \"range\": \"5\"}], 0, 0]"))); +} + // scantxoutset BOOST_AUTO_TEST_CASE(bitcoind_rpc__scantxoutset__repeated_script__one_unspent) From 65f3c210e3786653004cdf0d056c7bfc6dd018b6 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 29 Sep 2026 17:38:20 -0400 Subject: [PATCH 21/21] Remove electrum subscription test over a faulted store that is platform dependent. --- .../protocols/electrum/electrum_subscribe.cpp | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/test/protocols/electrum/electrum_subscribe.cpp b/test/protocols/electrum/electrum_subscribe.cpp index 08d328e0..7eb42670 100644 --- a/test/protocols/electrum/electrum_subscribe.cpp +++ b/test/protocols/electrum/electrum_subscribe.cpp @@ -967,31 +967,3 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_scripthash_subscribe__reorganized_noti } BOOST_AUTO_TEST_SUITE_END() - -// duplicate -// ---------------------------------------------------------------------------- - -BOOST_FIXTURE_TEST_SUITE(electrum_duplicate_tests, electrum_duplicate_setup_fixture) - -BOOST_AUTO_TEST_CASE(electrum__blockchain_scripthash_subscribe__duplicate_notify__subsequent_notified) -{ - BOOST_REQUIRE(handshake(electrum::version::v1_1)); - - constexpr auto request = R"({{"id":1101,"method":"blockchain.scripthash.subscribe","params":["{}"]}})" "\n"; - const auto response = get(std::format(request, found_scripthash)); - REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); - - BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::mock_block10.hash()), true)); - notify(node::chases::organized{ 0 }); - - BOOST_REQUIRE(query_.set(test::mock_block11, database::context{ 0, 11, 0 }, {}, false, false)); - BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::mock_block11.hash()), true)); - notify(node::chases::organized{ 0 }); - - const auto notification = receive(); - REQUIRE_NO_THROW_TRUE(notification.at("params").is_array()); - BOOST_REQUIRE_EQUAL(notification.at("method").as_string(), "blockchain.scripthash.subscribe"); - BOOST_REQUIRE_EQUAL(notification.at("params").at(0).as_string(), found_scripthash); -} - -BOOST_AUTO_TEST_SUITE_END()