From 167aec470cda759227fd95d99f7fde66de9cc4ce Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 25 Aug 2026 23:42:26 -0400 Subject: [PATCH 01/23] Round amounts within tolerance and enforce max money. --- .../bitcoind/protocol_bitcoind_transaction.cpp | 3 ++- test/protocols/bitcoind/bitcoind_rpc.cpp | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp index 465cc44d..d9792266 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp @@ -310,7 +310,8 @@ code protocol_bitcoind_transaction::build_transaction(chain::transaction& out, return error::bitcoind::invalid_address_or_key; const auto btc = std::get(pair.second.value()); - if (!to_integer(satoshi, btc * satoshi_per_bitcoin, false)) + if (!to_integer(satoshi, btc * satoshi_per_bitcoin, true) || + satoshi > system_settings().max_money()) return error::bitcoind::type_error; outs->push_back(to_shared(satoshi, std::move(script))); diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 018cfb08..2b002359 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -445,6 +445,21 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__createrawtransaction__one_in_one_out__hex) REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); } +BOOST_AUTO_TEST_CASE(bitcoind_rpc__createrawtransaction__amount__exact_satoshis) +{ + const auto txid = encode_hash(test::block1.transactions_ptr()->front()->hash(false)); + const auto created = rpc("createrawtransaction", "[[{\"txid\":\"" + txid + "\",\"vout\":0}], {\"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa\": 83052.07783498}]"); + const auto decoded = rpc("decoderawtransaction", "[\"" + std::string{ as_text(created.at("result")) } + "\"]"); + BOOST_REQUIRE_EQUAL(decoded.at("result").at("vout").at(0).at("value").as_double(), 83052.07783498); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__createrawtransaction__excess_amount__error) +{ + const auto txid = encode_hash(test::block1.transactions_ptr()->front()->hash(false)); + const auto response = rpc("createrawtransaction", "[[{\"txid\":\"" + txid + "\",\"vout\":0}], {\"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa\": 21000001}]"); + BOOST_REQUIRE_MESSAGE(has_code(response, -3), response); +} + BOOST_AUTO_TEST_CASE(bitcoind_rpc__decoderawtransaction__iswitness_false__round_trips) { const auto txid = encode_hash(test::block1.transactions_ptr()->front()->hash(false)); From 45a7a05ff963bf7b6ad621dbde738de6bbff26aa Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 25 Aug 2026 23:42:41 -0400 Subject: [PATCH 02/23] Validate base58 address network prefixes. --- src/parsers/bitcoind_script.cpp | 5 +++-- .../bitcoind/protocol_bitcoind_utility.cpp | 2 +- test/protocols/bitcoind/bitcoind_rpc.cpp | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/parsers/bitcoind_script.cpp b/src/parsers/bitcoind_script.cpp index 16a24453..aa332255 100644 --- a/src/parsers/bitcoind_script.cpp +++ b/src/parsers/bitcoind_script.cpp @@ -33,13 +33,14 @@ code output_script(script& out, const std::string& text, uint8_t p2kh, { using namespace wallet; - if (const payment_address payment{ text }; payment) + // The parses accept any prefix, so the configured ones are checks. + if (const payment_address payment{ text }; payment && + ((payment.prefix() == p2kh) || (payment.prefix() == p2sh))) { out = payment.output_script(p2kh, p2sh); return error::success; } - // The parse accepts any prefix, so the configured one is a check. if (const witness_address payment{ text }; payment && payment.prefix() == witness) { diff --git a/src/protocols/bitcoind/protocol_bitcoind_utility.cpp b/src/protocols/bitcoind/protocol_bitcoind_utility.cpp index 82b878c4..fa6fa47e 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_utility.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_utility.cpp @@ -170,7 +170,7 @@ bool protocol_bitcoind_utility::handle_validate_address(const code& ec, using namespace wallet; const payment_address base58(address); - if (base58) + if (base58 && ((base58.prefix() == p2kh_) || (base58.prefix() == p2sh_))) { send_result(object_t { diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 2b002359..8e50bbaa 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -554,6 +554,21 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__validateaddress__genesis__expected_script) BOOST_REQUIRE_EQUAL(as_text(response.at("result").at("scriptPubKey")), "76a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1888ac"); } +BOOST_AUTO_TEST_CASE(bitcoind_rpc__validateaddress__foreign_network__invalid) +{ + const wallet::payment_address testnet{ short_hash{}, 111 }; + const auto response = rpc("validateaddress", "[\"" + testnet.encoded() + "\"]"); + BOOST_REQUIRE(!response.at("result").at("isvalid").as_bool()); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__createrawtransaction__foreign_network__invalid_address) +{ + const wallet::payment_address testnet{ short_hash{}, 111 }; + const auto txid = encode_hash(test::block1.transactions_ptr()->front()->hash(false)); + const auto response = rpc("createrawtransaction", "[[{\"txid\":\"" + txid + "\",\"vout\":0}], {\"" + testnet.encoded() + "\": 0.001}]"); + BOOST_REQUIRE_MESSAGE(has_code(response, -5), response); +} + BOOST_AUTO_TEST_CASE(bitcoind_rpc__validateaddress__witness__expected_script) { const auto response = rpc("validateaddress", "[\"bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\"]"); From 605bd033a18af4f4f7332c963e73b536306ac1ab Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 25 Aug 2026 23:42:41 -0400 Subject: [PATCH 03/23] Answer rpc ping without pinging peers. --- src/protocols/bitcoind/protocol_bitcoind_network.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_network.cpp b/src/protocols/bitcoind/protocol_bitcoind_network.cpp index 38c5a4cf..7c7be602 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_network.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_network.cpp @@ -276,17 +276,14 @@ void protocol_bitcoind_network::do_send_nodes(const code& ec, send_result(std::move(out), size); } -// The nonce is discarded (pong correlation is a channel concern). +// An injected ping would violate channel pong correlation, and there is no +// peer timing instrumentation to serve, so this is a no-op (null result). bool protocol_bitcoind_network::handle_ping(const code& ec, rpc_interface::ping) NOEXCEPT { if (stopped(ec)) return false; - data_array entropy{}; - pseudo_random::fill(entropy); - const auto nonce = from_little_endian(entropy); - BROADCAST(peer::ping, to_shared(nonce)); send_result(null_t{}, 8); return true; } From 0ff563928f06b1cf8fd0488ac9cafbc91dbdfe6d Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 25 Aug 2026 23:42:41 -0400 Subject: [PATCH 04/23] Organize block submission carrying a known header. --- src/protocols/bitcoind/protocol_bitcoind_mining.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_mining.cpp b/src/protocols/bitcoind/protocol_bitcoind_mining.cpp index c8e1d71f..0ce00e55 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_mining.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_mining.cpp @@ -235,8 +235,10 @@ bool protocol_bitcoind_mining::handle_submit_block(const code& ec, return true; } - // bitcoind reports an already-stored block as a duplicate result. - if (!archive().to_header(block->hash()).is_terminal()) + // bitcoind reports an already-stored block as a duplicate result. A known + // header without its block still organizes (the submitheader flow). + const auto link = archive().to_header(block->hash()); + if (!link.is_terminal() && archive().is_associated(link)) { send_result(std::string{ "duplicate" }, 32); return true; From 41a93a8702418b62675a390c458b2cc845447a64 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 25 Aug 2026 23:54:21 -0400 Subject: [PATCH 05/23] Exclude unconfirmed outputs from gettxout. --- .../bitcoind/protocol_bitcoind_blockchain.cpp | 10 ++++++++-- test/protocols/bitcoind/bitcoind_rpc.cpp | 9 +++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp index 8179518c..4d2129a7 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp @@ -576,9 +576,15 @@ bool protocol_bitcoind_blockchain::handle_get_tx_out(const code& ec, const auto top = query.get_top_confirmed(); const auto header_link = query.to_confirmed(top); + // An archived but unconfirmed output is not an unspent coin. size_t height{}; - const auto strong = query.get_tx_height(height, tx_link); - const auto depth = strong ? add1(floored_subtract(top, height)) : zero; + if (!query.get_tx_height(height, tx_link)) + { + send_result(null_t{}, 42); + return true; + } + + const auto depth = add1(floored_subtract(top, height)); const auto coins = to_floating(output->value()) / chain::satoshi_per_bitcoin; diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 8e50bbaa..1b84f345 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -312,6 +312,15 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__gettxout__unspent_coinbase__output) BOOST_REQUIRE(result.as_object().contains("scriptPubKey")); } +BOOST_AUTO_TEST_CASE(bitcoind_rpc__gettxout__archived_unconfirmed__null) +{ + BOOST_REQUIRE(query_.set(test::mock_block10, database::context{ 0, 10, 0 }, false, false)); + + const auto txid = test::mock_block10.transactions_ptr()->at(1)->hash(false); + const auto response = rpc("gettxout", hash_param(txid, "0")); + REQUIRE_NO_THROW_TRUE(response.at("result").is_null()); +} + // rawtransactions // ---------------------------------------------------------------------------- From 4ce85adda3481444a985c6e2fc895c06874a1a5a Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 25 Aug 2026 23:54:21 -0400 Subject: [PATCH 06/23] Report descriptor spends unconditionally as spend_vin. --- .../server/interfaces/bitcoind_blockchain.hpp | 2 +- .../protocols/protocol_bitcoind_blockchain.hpp | 2 +- .../bitcoind/protocol_bitcoind_blockchain.cpp | 16 ++++++++++------ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/include/bitcoin/server/interfaces/bitcoind_blockchain.hpp b/include/bitcoin/server/interfaces/bitcoind_blockchain.hpp index 1b8f1075..eca985d3 100644 --- a/include/bitcoin/server/interfaces/bitcoind_blockchain.hpp +++ b/include/bitcoin/server/interfaces/bitcoind_blockchain.hpp @@ -53,7 +53,7 @@ struct bitcoind_blockchain_methods method<"getchainstates">{}, method<"getchaintips">{}, method<"getdeploymentinfo", optional<""_t>>{ "blockhash" }, - method<"getdescriptoractivity", optional, optional, optional>{ "blockhashes", "scanobjects", "include_spent" }, + method<"getdescriptoractivity", optional, optional, optional>{ "blockhashes", "scanobjects", "include_mempool" }, method<"getdifficulty">{}, method<"preciousblock">{ unimplemented }, method<"scanblocks", string_t, optional, optional<0.0>, optional<-1.0>, optional<"basic"_t>>{ "action", "scanobjects", "start_height", "stop_height", "filtertype" }, diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp index 3cda2ec0..d6f3d389 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp @@ -116,7 +116,7 @@ class BCS_API protocol_bitcoind_blockchain rpc_interface::get_descriptor_activity, const network::rpc::array_t& blockhashes, const network::rpc::array_t& scanobjects, - bool include_spent) NOEXCEPT; + bool include_mempool) NOEXCEPT; bool handle_get_difficulty(const code& ec, rpc_interface::get_difficulty) NOEXCEPT; bool handle_precious_block(const code& ec, diff --git a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp index 4d2129a7..7b1562ca 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp @@ -1388,10 +1388,10 @@ bool protocol_bitcoind_blockchain::handle_get_deployment_info(const code& ec, return true; } +// The mempool option is meaningless here (no mempool), always applied false. bool protocol_bitcoind_blockchain::handle_get_descriptor_activity( const code& ec, rpc_interface::get_descriptor_activity, - const array_t& blockhashes, const array_t& scanobjects, - bool include_spent) NOEXCEPT + const array_t& blockhashes, const array_t& scanobjects, bool) NOEXCEPT { if (stopped(ec)) return false; @@ -1432,9 +1432,13 @@ bool protocol_bitcoind_blockchain::handle_get_descriptor_activity( return true; } + // Confirmed spends are unconditional (as bitcoind, from undo data). const auto encoded = encode_hash(hash); - const auto populated = include_spent && - query.populate_without_metadata(*block); + if (!query.populate_without_metadata(*block)) + { + send_error(error::bitcoind::internal_error); + return true; + } for (const auto& tx: *block->transactions_ptr()) { @@ -1462,7 +1466,7 @@ bool protocol_bitcoind_blockchain::handle_get_descriptor_activity( ++index; } - if (!populated || tx->is_coinbase()) + if (tx->is_coinbase()) continue; uint32_t spend{}; @@ -1481,7 +1485,7 @@ bool protocol_bitcoind_blockchain::handle_get_descriptor_activity( { "blockhash", encoded }, { "height", height }, { "spend_txid", txid }, - { "spend_vout", spend }, + { "spend_vin", spend }, { "prevout_txid", encode_hash(in->point().hash()) }, { "prevout_vout", in->point().index() }, { "prevout_spk", value_from(bitcoind( From d7db845f97f5c6bdc5f442d1b56462c39ae0e620 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 25 Aug 2026 23:54:34 -0400 Subject: [PATCH 07/23] Report live networkactive state. --- src/protocols/bitcoind/protocol_bitcoind_network.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_network.cpp b/src/protocols/bitcoind/protocol_bitcoind_network.cpp index 7c7be602..1ddb81d5 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_network.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_network.cpp @@ -145,7 +145,7 @@ bool protocol_bitcoind_network::handle_get_network_info(const code& ec, { "connections", connections }, { "connections_in", inbound }, { "connections_out", floored_subtract(connections, inbound) }, - { "networkactive", true }, + { "networkactive", !node::protocol::suspended() }, { "networks", array_t{ network("ipv4"), network("ipv6") } }, { "relayfee", node_settings().minimum_fee_rate }, { "incrementalfee", node_settings().minimum_bump_rate }, From 32b62093124e334049ef063796ecef9483f627ed Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 25 Aug 2026 23:55:01 -0400 Subject: [PATCH 08/23] Report warnings as arrays. --- src/protocols/bitcoind/protocol_bitcoind_json.cpp | 2 +- src/protocols/bitcoind/protocol_bitcoind_network.cpp | 2 +- test/protocols/bitcoind/bitcoind_rpc.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_json.cpp b/src/protocols/bitcoind/protocol_bitcoind_json.cpp index de2abfa1..708bb9ec 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_json.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_json.cpp @@ -352,7 +352,7 @@ bool protocol_bitcoind::chain_info(network::rpc::object_t& out, { "chainwork", encode_hash(from_uintx(work)) }, { "size_on_disk", query.store_size() }, { "pruned", pruned }, - { "warnings", std::string{} } + { "warnings", network::rpc::array_t{} } }; return true; diff --git a/src/protocols/bitcoind/protocol_bitcoind_network.cpp b/src/protocols/bitcoind/protocol_bitcoind_network.cpp index 1ddb81d5..84f5ff7a 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_network.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_network.cpp @@ -150,7 +150,7 @@ bool protocol_bitcoind_network::handle_get_network_info(const code& ec, { "relayfee", node_settings().minimum_fee_rate }, { "incrementalfee", node_settings().minimum_bump_rate }, { "localaddresses", std::move(locals) }, - { "warnings", std::string{} } + { "warnings", array_t{} } }, 512); return true; } diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 1b84f345..6b1522b4 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -271,7 +271,7 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblockchaininfo__ten_block_store__expected) BOOST_REQUIRE(result.at("headers").is_int64()); BOOST_REQUIRE_EQUAL(as_text(result.at("bestblockhash")), block9); BOOST_REQUIRE(result.as_object().contains("target")); - BOOST_REQUIRE(result.at("warnings").is_string()); + BOOST_REQUIRE(result.at("warnings").is_array()); BOOST_REQUIRE(result.at("initialblockdownload").is_bool()); BOOST_REQUIRE(result.at("chainwork").is_string()); BOOST_REQUIRE(result.at("size_on_disk").as_int64() > 0); From 9c9fbf3d7ace3771295c0f1bcbd20d593834ebed Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 25 Aug 2026 23:55:01 -0400 Subject: [PATCH 09/23] Limit createmultisig to sixteen keys. --- src/protocols/bitcoind/protocol_bitcoind_utility.cpp | 8 ++++++++ test/protocols/bitcoind/bitcoind_rpc.cpp | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/src/protocols/bitcoind/protocol_bitcoind_utility.cpp b/src/protocols/bitcoind/protocol_bitcoind_utility.cpp index fa6fa47e..56daf633 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_utility.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_utility.cpp @@ -223,6 +223,14 @@ bool protocol_bitcoind_utility::handle_create_multisig(const code& ec, return true; } + // The multisig pattern is limited to op_16 (bitcoind allows 20 for wsh). + constexpr auto maximum_keys = 16_size; + if (keys.size() > maximum_keys) + { + send_error(error::bitcoind::invalid_parameter); + return true; + } + if (address_type != "legacy" && address_type != "p2sh-segwit" && address_type != "bech32") diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 6b1522b4..a0ef4f8c 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -643,6 +643,12 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__createmultisig__excess_required__error) BOOST_REQUIRE(has_error(response)); } +BOOST_AUTO_TEST_CASE(bitcoind_rpc__createmultisig__excess_keys__invalid_parameter) +{ + const auto response = rpc("createmultisig", "[1, [\"00\",\"00\",\"00\",\"00\",\"00\",\"00\",\"00\",\"00\",\"00\",\"00\",\"00\",\"00\",\"00\",\"00\",\"00\",\"00\",\"00\"]]"); + BOOST_REQUIRE_MESSAGE(has_code(response, -8), 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 9b4053b279c4f921999e5746777aa935b1e403a5 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 00:00:24 -0400 Subject: [PATCH 10/23] Report self inclusive median time past as bitcoind. --- .../bitcoind/protocol_bitcoind_json.cpp | 21 +++++++++++++++++-- test/protocols/bitcoind/bitcoind_json.cpp | 11 ++++------ test/protocols/bitcoind/bitcoind_rpc.cpp | 6 ++++++ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_json.cpp b/src/protocols/bitcoind/protocol_bitcoind_json.cpp index 708bb9ec..4fe39ecc 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_json.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_json.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -27,11 +28,27 @@ namespace server { using namespace system; +// bitcoind reports the median time past of the CHILD of the given block (its +// window includes the block's own timestamp). Reproduced for compatibility. uint32_t protocol_bitcoind::median_time_past(const node::query& query, const database::header_link& link) NOEXCEPT { - chain::context ctx{}; - return query.get_context(ctx, link) ? ctx.median_time_past : 0_u32; + std::vector times{}; + times.reserve(chain::median_time_past_interval); + + for (auto walk = link; !walk.is_terminal() && + times.size() < chain::median_time_past_interval; + walk = query.to_parent(walk)) + { + const auto header = query.get_header(walk); + if (!header) + return 0_u32; + + times.push_back(header->timestamp()); + } + + std::sort(times.begin(), times.end()); + return times.empty() ? 0_u32 : times.at(to_half(times.size())); } // Clamped ratio of validated blocks to chain height. diff --git a/test/protocols/bitcoind/bitcoind_json.cpp b/test/protocols/bitcoind/bitcoind_json.cpp index 15033359..6cf852c8 100644 --- a/test/protocols/bitcoind/bitcoind_json.cpp +++ b/test/protocols/bitcoind/bitcoind_json.cpp @@ -117,12 +117,11 @@ BOOST_AUTO_TEST_CASE(bitcoind_json__chain_name__mainnet_genesis__main) // median_time_past -BOOST_AUTO_TEST_CASE(bitcoind_json__median_time_past__matches_stored_context) +// The median of mainnet block 0..5 timestamps (self-inclusive, as bitcoind). +BOOST_AUTO_TEST_CASE(bitcoind_json__median_time_past__self_inclusive_window) { const auto link = query_.to_header(test::block5_hash); - chain::context ctx{}; - BOOST_REQUIRE(query_.get_context(ctx, link)); - BOOST_REQUIRE_EQUAL(json::median_time_past(query_, link), ctx.median_time_past); + BOOST_REQUIRE_EQUAL(json::median_time_past(query_, link), 1231470173u); } // inject_block_context @@ -136,11 +135,9 @@ BOOST_AUTO_TEST_CASE(bitcoind_json__inject_block_context__middle__height_confirm boost::json::object out{}; json::inject_block_context(out, query_, link, *header); - chain::context ctx{}; - BOOST_REQUIRE(query_.get_context(ctx, link)); BOOST_REQUIRE_EQUAL(out.at("height").to_number(), 5u); BOOST_REQUIRE_EQUAL(out.at("confirmations").to_number(), 5); - BOOST_REQUIRE_EQUAL(out.at("mediantime").to_number(), ctx.median_time_past); + BOOST_REQUIRE_EQUAL(out.at("mediantime").to_number(), 1231470173u); BOOST_REQUIRE_EQUAL(as_text(out.at("previousblockhash")), encode_hash(test::block4_hash)); BOOST_REQUIRE_EQUAL(as_text(out.at("nextblockhash")), encode_hash(test::block6_hash)); } diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index a0ef4f8c..df723da0 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -262,6 +262,12 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblock__negative_verbosity__clamped_hex) REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); } +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblockheader__genesis__self_inclusive_mediantime) +{ + const auto response = rpc("getblockheader", hash_param(test::genesis.hash(), "true")); + BOOST_REQUIRE_EQUAL(response.at("result").at("mediantime").as_int64(), 1231006505); +} + BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblockchaininfo__ten_block_store__expected) { const auto response = rpc("getblockchaininfo"); From 12dc670c4adb783229f9bd24b6f6b795ccf0a595 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 00:11:08 -0400 Subject: [PATCH 11/23] Match bitcoind argument names and defaults. --- .../server/interfaces/bitcoind_blockchain.hpp | 6 ++-- .../server/interfaces/bitcoind_control.hpp | 2 +- .../interfaces/bitcoind_transaction.hpp | 8 +++--- .../server/interfaces/bitcoind_utility.hpp | 2 +- .../protocol_bitcoind_blockchain.hpp | 6 ++-- .../protocols/protocol_bitcoind_control.hpp | 2 +- .../protocol_bitcoind_transaction.hpp | 7 +++-- .../protocols/protocol_bitcoind_utility.hpp | 2 +- .../bitcoind/protocol_bitcoind_blockchain.cpp | 19 ++++++++++--- .../bitcoind/protocol_bitcoind_control.cpp | 5 ++-- .../bitcoind/protocol_bitcoind_network.cpp | 9 +++++- .../protocol_bitcoind_transaction.cpp | 28 +++++++++++++++---- .../bitcoind/protocol_bitcoind_utility.cpp | 4 +-- 13 files changed, 69 insertions(+), 31 deletions(-) diff --git a/include/bitcoin/server/interfaces/bitcoind_blockchain.hpp b/include/bitcoin/server/interfaces/bitcoind_blockchain.hpp index eca985d3..3443c953 100644 --- a/include/bitcoin/server/interfaces/bitcoind_blockchain.hpp +++ b/include/bitcoin/server/interfaces/bitcoind_blockchain.hpp @@ -44,7 +44,7 @@ struct bitcoind_blockchain_methods method<"pruneblockchain", number_t>{ unimplemented, "height" }, method<"savemempool">{ unimplemented }, method<"scantxoutset", string_t, optional>{ "action", "scanobjects" }, - method<"verifychain", optional<4.0>, optional<288.0>>{ "checklevel", "nblocks" }, + method<"verifychain", optional<3.0>, optional<6.0>>{ "checklevel", "nblocks" }, method<"dumptxoutset">{ unimplemented }, method<"loadtxoutset">{ unimplemented }, method<"gettxoutproof", array_t, optional<""_t>>{ "txids", "blockhash" }, @@ -56,10 +56,10 @@ struct bitcoind_blockchain_methods method<"getdescriptoractivity", optional, optional, optional>{ "blockhashes", "scanobjects", "include_mempool" }, method<"getdifficulty">{}, method<"preciousblock">{ unimplemented }, - method<"scanblocks", string_t, optional, optional<0.0>, optional<-1.0>, optional<"basic"_t>>{ "action", "scanobjects", "start_height", "stop_height", "filtertype" }, + method<"scanblocks", string_t, optional, optional<0.0>, optional<-1.0>, optional<"basic"_t>, optional>{ "action", "scanobjects", "start_height", "stop_height", "filtertype", "options" }, method<"waitforblock", string_t, optional<0.0>>{ "blockhash", "timeout" }, method<"waitforblockheight", number_t, optional<0.0>>{ "height", "timeout" }, - method<"waitfornewblock", optional<0.0>>{ "timeout" }, + method<"waitfornewblock", optional<0.0>, optional<""_t>>{ "timeout", "current_tip" }, method<"getmempoolancestors">{ unimplemented }, method<"getmempoolcluster">{ unimplemented }, method<"getmempooldescendants">{ unimplemented }, diff --git a/include/bitcoin/server/interfaces/bitcoind_control.hpp b/include/bitcoin/server/interfaces/bitcoind_control.hpp index 4ae9bb90..3c65459c 100644 --- a/include/bitcoin/server/interfaces/bitcoind_control.hpp +++ b/include/bitcoin/server/interfaces/bitcoind_control.hpp @@ -33,7 +33,7 @@ struct bitcoind_control_methods method<"help", optional<""_t>>{ "command" }, method<"stop">{ unimplemented }, method<"getmemoryinfo", optional<"stats"_t>>{ "mode" }, - method<"getopenrpcinfo">{}, + method<"getopenrpcinfo", optional>{ "show_hidden" }, method<"getrpcinfo">{}, method<"logging", optional, optional>{ "include", "exclude" }, method<"uptime">{} diff --git a/include/bitcoin/server/interfaces/bitcoind_transaction.hpp b/include/bitcoin/server/interfaces/bitcoind_transaction.hpp index 497c3e37..d6e6252d 100644 --- a/include/bitcoin/server/interfaces/bitcoind_transaction.hpp +++ b/include/bitcoin/server/interfaces/bitcoind_transaction.hpp @@ -33,12 +33,12 @@ struct bitcoind_transaction_methods method<"createrawtransaction", array_t, object_t, optional<0.0>, optional>{ "inputs", "outputs", "locktime", "replaceable" }, method<"decoderawtransaction", string_t, nullable>{ "hexstring", "iswitness" }, method<"getrawtransaction", string_t, optional<0.0>, optional<""_t>>{ "txid", "verbosity", "blockhash" }, - method<"sendrawtransaction", string_t, optional<0.0>>{ "hexstring", "maxfeerate" }, - method<"testmempoolaccept", array_t, optional<0.0>>{ "rawtxs", "maxfeerate" }, + method<"sendrawtransaction", string_t, optional<0.1>, optional<0.0>>{ "hexstring", "maxfeerate", "maxburnamount" }, + method<"testmempoolaccept", array_t, optional<0.1>>{ "rawtxs", "maxfeerate" }, method<"analyzepsbt", string_t>{ "psbt" }, method<"combinepsbt", array_t>{ "txs" }, - method<"converttopsbt", string_t, optional, nullable>{ "hexstring", "permitsigdata", "iswitness" }, - method<"createpsbt", array_t, object_t, optional<0.0>, optional>{ "inputs", "outputs", "locktime", "replaceable" }, + method<"converttopsbt", string_t, optional, nullable, optional<2.0>>{ "hexstring", "permitsigdata", "iswitness", "psbt_version" }, + method<"createpsbt", array_t, object_t, optional<0.0>, optional, optional<2.0>>{ "inputs", "outputs", "locktime", "replaceable", "psbt_version" }, method<"decodepsbt", string_t>{ "psbt" }, method<"finalizepsbt", string_t, optional>{ "psbt", "extract" }, method<"joinpsbts", array_t>{ "txs" }, diff --git a/include/bitcoin/server/interfaces/bitcoind_utility.hpp b/include/bitcoin/server/interfaces/bitcoind_utility.hpp index 4472f2d5..cdcdf39e 100644 --- a/include/bitcoin/server/interfaces/bitcoind_utility.hpp +++ b/include/bitcoin/server/interfaces/bitcoind_utility.hpp @@ -30,7 +30,7 @@ struct bitcoind_utility_methods { static constexpr std::tuple methods { - method<"decodescript", string_t>{ "hex" }, + method<"decodescript", string_t>{ "hexstring" }, method<"validateaddress", string_t>{ "address" }, method<"createmultisig", number_t, array_t, optional<"legacy"_t>>{ "nrequired", "keys", "address_type" }, method<"deriveaddresses", string_t, nullable>{ "descriptor", "range" }, diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp index d6f3d389..4e93c48d 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp @@ -124,7 +124,8 @@ class BCS_API protocol_bitcoind_blockchain bool handle_scan_blocks(const code& ec, rpc_interface::scan_blocks, const std::string& action, const network::rpc::array_t& scanobjects, double start_height, - double stop_height, const std::string& filtertype) NOEXCEPT; + double stop_height, const std::string& filtertype, + const network::rpc::object_t& options) NOEXCEPT; bool handle_wait_for_block(const code& ec, rpc_interface::wait_for_block, const std::string& blockhash, double timeout) NOEXCEPT; @@ -132,7 +133,8 @@ class BCS_API protocol_bitcoind_blockchain rpc_interface::wait_for_block_height, double height, double timeout) NOEXCEPT; bool handle_wait_for_new_block(const code& ec, - rpc_interface::wait_for_new_block, double timeout) NOEXCEPT; + rpc_interface::wait_for_new_block, double timeout, + const std::string& current_tip) NOEXCEPT; bool handle_get_mempool_ancestors(const code& ec, rpc_interface::get_mempool_ancestors) NOEXCEPT; bool handle_get_mempool_cluster(const code& ec, diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_control.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_control.hpp index 4711be08..c73f4994 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_control.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_control.hpp @@ -62,7 +62,7 @@ class BCS_API protocol_bitcoind_control bool handle_get_memory_info(const code& ec, rpc_interface::get_memory_info, const std::string& mode) NOEXCEPT; bool handle_get_openrpc_info(const code& ec, - rpc_interface::get_openrpc_info) NOEXCEPT; + rpc_interface::get_openrpc_info, bool show_hidden) NOEXCEPT; bool handle_get_rpc_info(const code& ec, rpc_interface::get_rpc_info) NOEXCEPT; bool handle_logging(const code& ec, rpc_interface::logging, diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_transaction.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_transaction.hpp index 399eed9f..b2bfc4e6 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_transaction.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_transaction.hpp @@ -68,7 +68,7 @@ class BCS_API protocol_bitcoind_transaction double verbose, const std::string& blockhash) NOEXCEPT; bool handle_send_raw_transaction(const code& ec, rpc_interface::send_raw_transaction, const std::string& hexstring, - double maxfeerate) NOEXCEPT; + double maxfeerate, double maxburnamount) NOEXCEPT; bool handle_test_mempool_accept(const code& ec, rpc_interface::test_mempool_accept, const network::rpc::array_t& rawtxs, double maxfeerate) NOEXCEPT; @@ -79,11 +79,12 @@ class BCS_API protocol_bitcoind_transaction const network::rpc::array_t& txs) NOEXCEPT; bool handle_convert_to_psbt(const code& ec, rpc_interface::convert_to_psbt, const std::string& hexstring, - bool permitsigdata, const std::optional& iswitness) NOEXCEPT; + bool permitsigdata, const std::optional& iswitness, + double psbt_version) NOEXCEPT; bool handle_create_psbt(const code& ec, rpc_interface::create_psbt, const network::rpc::array_t& inputs, const network::rpc::object_t& outputs, double locktime, - bool replaceable) NOEXCEPT; + bool replaceable, double psbt_version) NOEXCEPT; bool handle_decode_psbt(const code& ec, rpc_interface::decode_psbt, const std::string& psbt) NOEXCEPT; bool handle_finalize_psbt(const code& ec, diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_utility.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_utility.hpp index 62f76443..f10260f3 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_utility.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_utility.hpp @@ -56,7 +56,7 @@ class BCS_API protocol_bitcoind_utility protected: /// Handlers. bool handle_decode_script(const code& ec, - rpc_interface::decode_script, const std::string& hex) NOEXCEPT; + rpc_interface::decode_script, const std::string& hexstring) NOEXCEPT; bool handle_validate_address(const code& ec, rpc_interface::validate_address, const std::string& address) NOEXCEPT; diff --git a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp index 7b1562ca..a2c79df6 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp @@ -105,10 +105,10 @@ void protocol_bitcoind_blockchain::start() NOEXCEPT SUBSCRIBE_BITCOIND(handle_get_descriptor_activity, _1, _2, _3, _4, _5); SUBSCRIBE_BITCOIND(handle_get_difficulty, _1, _2); SUBSCRIBE_BITCOIND(handle_precious_block, _1, _2); - SUBSCRIBE_BITCOIND(handle_scan_blocks, _1, _2, _3, _4, _5, _6, _7); + SUBSCRIBE_BITCOIND(handle_scan_blocks, _1, _2, _3, _4, _5, _6, _7, _8); SUBSCRIBE_BITCOIND(handle_wait_for_block, _1, _2, _3, _4); SUBSCRIBE_BITCOIND(handle_wait_for_block_height, _1, _2, _3, _4); - SUBSCRIBE_BITCOIND(handle_wait_for_new_block, _1, _2, _3); + SUBSCRIBE_BITCOIND(handle_wait_for_new_block, _1, _2, _3, _4); SUBSCRIBE_BITCOIND(handle_get_mempool_ancestors, _1, _2); SUBSCRIBE_BITCOIND(handle_get_mempool_cluster, _1, _2); SUBSCRIBE_BITCOIND(handle_get_mempool_descendants, _1, _2); @@ -1610,10 +1610,11 @@ static bool expand_scan_object(chain::scripts& out, return true; } +// Exact index matching produces no false positives to optionally filter. bool protocol_bitcoind_blockchain::handle_scan_blocks(const code& ec, rpc_interface::scan_blocks, const std::string& action, const array_t& scanobjects, double start_height, double stop_height, - const std::string& filtertype) NOEXCEPT + const std::string& filtertype, const object_t&) NOEXCEPT { if (stopped(ec)) return false; @@ -1735,11 +1736,21 @@ bool protocol_bitcoind_blockchain::handle_wait_for_block_height(const code& ec, } bool protocol_bitcoind_blockchain::handle_wait_for_new_block(const code& ec, - rpc_interface::wait_for_new_block, double timeout) NOEXCEPT + rpc_interface::wait_for_new_block, double timeout, + const std::string& current_tip) NOEXCEPT { if (stopped(ec)) return false; + // A stated top that is no longer current completes the wait immediately. + hash_digest given{}; + if (!current_tip.empty() && decode_hash(given, current_tip) && + (given != archive().get_top_confirmed_hash())) + { + send_tip(); + return true; + } + wait_ = wait::new_block; wait_height_ = add1(archive().get_top_confirmed()); arm_wait(timeout); diff --git a/src/protocols/bitcoind/protocol_bitcoind_control.cpp b/src/protocols/bitcoind/protocol_bitcoind_control.cpp index 89b7e3fa..c6406988 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_control.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_control.cpp @@ -56,7 +56,7 @@ void protocol_bitcoind_control::start() NOEXCEPT SUBSCRIBE_BITCOIND(handle_help, _1, _2, _3); SUBSCRIBE_BITCOIND(handle_stop, _1, _2); SUBSCRIBE_BITCOIND(handle_get_memory_info, _1, _2, _3); - SUBSCRIBE_BITCOIND(handle_get_openrpc_info, _1, _2); + SUBSCRIBE_BITCOIND(handle_get_openrpc_info, _1, _2, _3); SUBSCRIBE_BITCOIND(handle_get_rpc_info, _1, _2); SUBSCRIBE_BITCOIND(handle_logging, _1, _2, _3, _4); SUBSCRIBE_BITCOIND(handle_uptime, _1, _2); @@ -203,8 +203,9 @@ static void append_methods(array_t& out) NOEXCEPT }, Methods::methods); } +// There are no hidden methods (unimplemented rows are refusals, not hidden). bool protocol_bitcoind_control::handle_get_openrpc_info(const code& ec, - rpc_interface::get_openrpc_info) NOEXCEPT + rpc_interface::get_openrpc_info, bool) NOEXCEPT { if (stopped(ec)) return false; diff --git a/src/protocols/bitcoind/protocol_bitcoind_network.cpp b/src/protocols/bitcoind/protocol_bitcoind_network.cpp index 84f5ff7a..d4a07bb3 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_network.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_network.cpp @@ -331,11 +331,18 @@ bool protocol_bitcoind_network::handle_set_ban(const code& ec, // transport is determined by the outbound privacy configuration. bool protocol_bitcoind_network::handle_add_node(const code& ec, rpc_interface::add_node, const std::string& node, - const std::string& command, bool) NOEXCEPT + const std::string& command, bool v2transport) NOEXCEPT { if (stopped(ec)) return false; + // bitcoind reports v2transport as invalid when not enabled. + if (v2transport) + { + send_error(error::bitcoind::invalid_parameter); + return true; + } + if (command != "add" && command != "onetry") { send_error(command == "remove" ? error::bitcoind::client_node_not_added : diff --git a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp index d9792266..b4c795f8 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp @@ -57,12 +57,12 @@ void protocol_bitcoind_transaction::start() NOEXCEPT SUBSCRIBE_BITCOIND(handle_create_raw_transaction, _1, _2, _3, _4, _5, _6); SUBSCRIBE_BITCOIND(handle_decode_raw_transaction, _1, _2, _3, _4); SUBSCRIBE_BITCOIND(handle_get_raw_transaction, _1, _2, _3, _4, _5); - SUBSCRIBE_BITCOIND(handle_send_raw_transaction, _1, _2, _3, _4); + SUBSCRIBE_BITCOIND(handle_send_raw_transaction, _1, _2, _3, _4, _5); SUBSCRIBE_BITCOIND(handle_test_mempool_accept, _1, _2, _3, _4); SUBSCRIBE_BITCOIND(handle_analyze_psbt, _1, _2, _3); SUBSCRIBE_BITCOIND(handle_combine_psbt, _1, _2, _3); - SUBSCRIBE_BITCOIND(handle_convert_to_psbt, _1, _2, _3, _4, _5); - SUBSCRIBE_BITCOIND(handle_create_psbt, _1, _2, _3, _4, _5, _6); + SUBSCRIBE_BITCOIND(handle_convert_to_psbt, _1, _2, _3, _4, _5, _6); + SUBSCRIBE_BITCOIND(handle_create_psbt, _1, _2, _3, _4, _5, _6, _7); SUBSCRIBE_BITCOIND(handle_decode_psbt, _1, _2, _3); SUBSCRIBE_BITCOIND(handle_finalize_psbt, _1, _2, _3, _4); SUBSCRIBE_BITCOIND(handle_join_psbts, _1, _2, _3); @@ -141,7 +141,7 @@ bool protocol_bitcoind_transaction::handle_get_raw_transaction(const code& ec, bool protocol_bitcoind_transaction::handle_send_raw_transaction(const code& ec, rpc_interface::send_raw_transaction, const std::string& hexstring, - double) NOEXCEPT + double, double) NOEXCEPT { if (stopped(ec)) return false; @@ -739,11 +739,19 @@ bool protocol_bitcoind_transaction::handle_combine_psbt(const code& ec, bool protocol_bitcoind_transaction::handle_convert_to_psbt(const code& ec, rpc_interface::convert_to_psbt, const std::string& hexstring, - bool permitsigdata, const std::optional& iswitness) NOEXCEPT + bool permitsigdata, const std::optional& iswitness, + double psbt_version) NOEXCEPT { if (stopped(ec)) return false; + // Construction is bip370 only. + if (psbt_version != 2.0) + { + send_error(error::bitcoind::invalid_parameter); + return true; + } + data_chunk data{}; if (!decode_base16(data, hexstring)) { @@ -799,11 +807,19 @@ bool protocol_bitcoind_transaction::handle_convert_to_psbt(const code& ec, bool protocol_bitcoind_transaction::handle_create_psbt(const code& ec, rpc_interface::create_psbt, const array_t& inputs, - const object_t& outputs, double locktime, bool replaceable) NOEXCEPT + const object_t& outputs, double locktime, bool replaceable, + double psbt_version) NOEXCEPT { if (stopped(ec)) return false; + // Construction is bip370 only. + if (psbt_version != 2.0) + { + send_error(error::bitcoind::invalid_parameter); + return true; + } + chain::transaction tx{}; if (const auto fault = build_transaction(tx, inputs, outputs, locktime, replaceable)) diff --git a/src/protocols/bitcoind/protocol_bitcoind_utility.cpp b/src/protocols/bitcoind/protocol_bitcoind_utility.cpp index 56daf633..75766915 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_utility.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_utility.cpp @@ -94,13 +94,13 @@ static std::string to_script_type(chain::script_pattern pattern) NOEXCEPT } bool protocol_bitcoind_utility::handle_decode_script(const code& ec, - rpc_interface::decode_script, const std::string& hex) NOEXCEPT + rpc_interface::decode_script, const std::string& hexstring) NOEXCEPT { if (stopped(ec)) return false; data_chunk data{}; - if (!decode_base16(data, hex)) + if (!decode_base16(data, hexstring)) { send_error(error::bitcoind::invalid_parameter); return true; From 3bcc73159254ae293b49d5e75e878b214f6dabfc Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 00:19:03 -0400 Subject: [PATCH 12/23] Match bitcoind transaction construction surface. --- .../interfaces/bitcoind_transaction.hpp | 4 +- .../protocol_bitcoind_transaction.hpp | 12 +-- .../protocol_bitcoind_transaction.cpp | 84 ++++++++++++++----- test/protocols/bitcoind/bitcoind_rpc.cpp | 39 ++++++++- 4 files changed, 108 insertions(+), 31 deletions(-) diff --git a/include/bitcoin/server/interfaces/bitcoind_transaction.hpp b/include/bitcoin/server/interfaces/bitcoind_transaction.hpp index d6e6252d..26a73f39 100644 --- a/include/bitcoin/server/interfaces/bitcoind_transaction.hpp +++ b/include/bitcoin/server/interfaces/bitcoind_transaction.hpp @@ -30,7 +30,7 @@ struct bitcoind_transaction_methods { static constexpr std::tuple methods { - method<"createrawtransaction", array_t, object_t, optional<0.0>, optional>{ "inputs", "outputs", "locktime", "replaceable" }, + method<"createrawtransaction", array_t, value_t, optional<0.0>, optional, optional<2.0>>{ "inputs", "outputs", "locktime", "replaceable", "version" }, method<"decoderawtransaction", string_t, nullable>{ "hexstring", "iswitness" }, method<"getrawtransaction", string_t, optional<0.0>, optional<""_t>>{ "txid", "verbosity", "blockhash" }, method<"sendrawtransaction", string_t, optional<0.1>, optional<0.0>>{ "hexstring", "maxfeerate", "maxburnamount" }, @@ -38,7 +38,7 @@ struct bitcoind_transaction_methods method<"analyzepsbt", string_t>{ "psbt" }, method<"combinepsbt", array_t>{ "txs" }, method<"converttopsbt", string_t, optional, nullable, optional<2.0>>{ "hexstring", "permitsigdata", "iswitness", "psbt_version" }, - method<"createpsbt", array_t, object_t, optional<0.0>, optional, optional<2.0>>{ "inputs", "outputs", "locktime", "replaceable", "psbt_version" }, + method<"createpsbt", array_t, value_t, optional<0.0>, optional, optional<2.0>, optional<2.0>>{ "inputs", "outputs", "locktime", "replaceable", "version", "psbt_version" }, method<"decodepsbt", string_t>{ "psbt" }, method<"finalizepsbt", string_t, optional>{ "psbt", "extract" }, method<"joinpsbts", array_t>{ "txs" }, diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_transaction.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_transaction.hpp index b2bfc4e6..1d85535a 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_transaction.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_transaction.hpp @@ -58,8 +58,8 @@ class BCS_API protocol_bitcoind_transaction bool handle_create_raw_transaction(const code& ec, rpc_interface::create_raw_transaction, const network::rpc::array_t& inputs, - const network::rpc::object_t& outputs, double locktime, - bool replaceable) NOEXCEPT; + const network::rpc::value_t& outputs, double locktime, + bool replaceable, double version) NOEXCEPT; bool handle_decode_raw_transaction(const code& ec, rpc_interface::decode_raw_transaction, const std::string& hexstring, const std::optional& iswitness) NOEXCEPT; @@ -83,8 +83,8 @@ class BCS_API protocol_bitcoind_transaction double psbt_version) NOEXCEPT; bool handle_create_psbt(const code& ec, rpc_interface::create_psbt, const network::rpc::array_t& inputs, - const network::rpc::object_t& outputs, double locktime, - bool replaceable, double psbt_version) NOEXCEPT; + const network::rpc::value_t& outputs, double locktime, + bool replaceable, double version, double psbt_version) NOEXCEPT; bool handle_decode_psbt(const code& ec, rpc_interface::decode_psbt, const std::string& psbt) NOEXCEPT; bool handle_finalize_psbt(const code& ec, @@ -102,8 +102,8 @@ class BCS_API protocol_bitcoind_transaction /// Shared transaction construction (createrawtransaction, createpsbt). code build_transaction(system::chain::transaction& out, const network::rpc::array_t& inputs, - const network::rpc::object_t& outputs, double locktime, - bool replaceable) const NOEXCEPT; + const network::rpc::value_t& outputs, double locktime, + bool replaceable, double version) const NOEXCEPT; bool handle_abort_private_broadcast(const code& ec, rpc_interface::abort_private_broadcast) NOEXCEPT; bool handle_get_private_broadcast_info(const code& ec, diff --git a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp index b4c795f8..86411aa1 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp @@ -54,7 +54,7 @@ void protocol_bitcoind_transaction::start() NOEXCEPT if (started()) return; - SUBSCRIBE_BITCOIND(handle_create_raw_transaction, _1, _2, _3, _4, _5, _6); + SUBSCRIBE_BITCOIND(handle_create_raw_transaction, _1, _2, _3, _4, _5, _6, _7); SUBSCRIBE_BITCOIND(handle_decode_raw_transaction, _1, _2, _3, _4); SUBSCRIBE_BITCOIND(handle_get_raw_transaction, _1, _2, _3, _4, _5); SUBSCRIBE_BITCOIND(handle_send_raw_transaction, _1, _2, _3, _4, _5); @@ -62,7 +62,7 @@ void protocol_bitcoind_transaction::start() NOEXCEPT SUBSCRIBE_BITCOIND(handle_analyze_psbt, _1, _2, _3); SUBSCRIBE_BITCOIND(handle_combine_psbt, _1, _2, _3); SUBSCRIBE_BITCOIND(handle_convert_to_psbt, _1, _2, _3, _4, _5, _6); - SUBSCRIBE_BITCOIND(handle_create_psbt, _1, _2, _3, _4, _5, _6, _7); + SUBSCRIBE_BITCOIND(handle_create_psbt, _1, _2, _3, _4, _5, _6, _7, _8); SUBSCRIBE_BITCOIND(handle_decode_psbt, _1, _2, _3); SUBSCRIBE_BITCOIND(handle_finalize_psbt, _1, _2, _3, _4); SUBSCRIBE_BITCOIND(handle_join_psbts, _1, _2, _3); @@ -247,13 +247,19 @@ bool protocol_bitcoind_transaction::handle_test_mempool_accept(const code& ec, // Shared by createrawtransaction and createpsbt. code protocol_bitcoind_transaction::build_transaction(chain::transaction& out, - const array_t& inputs, const object_t& outputs, double locktime, - bool replaceable) const NOEXCEPT + const array_t& inputs, const value_t& outputs, double locktime, + bool replaceable, double version) const NOEXCEPT { uint32_t lock_time{}; if (!to_integer(lock_time, locktime)) return error::bitcoind::invalid_parameter; + // bitcoind bounds the version to the maximum standard (currently 3). + uint32_t tx_version{}; + if (!to_integer(tx_version, version) || is_zero(tx_version) || + (tx_version > 3u)) + return error::bitcoind::invalid_parameter; + using namespace chain; const auto sequence = replaceable ? messages::peer::bip125_sequence : (is_zero(lock_time) ? max_input_sequence : sub1(max_input_sequence)); @@ -280,58 +286,96 @@ code protocol_bitcoind_transaction::build_transaction(chain::transaction& out, !to_integer(vout, std::get(vout_it->second.value()))) return error::bitcoind::invalid_parameter; - ins->push_back(to_shared(point{ hash, vout }, script{}, sequence)); + // An explicit sequence overrides the derived default. + auto sequenced = sequence; + const auto sequence_it = fields.find("sequence"); + if (sequence_it != fields.end() && + (!std::holds_alternative(sequence_it->second.value()) || + !to_integer(sequenced, + std::get(sequence_it->second.value())))) + return error::bitcoind::invalid_parameter; + + ins->push_back(to_shared(point{ hash, vout }, script{}, + sequenced)); } script script{}; uint64_t satoshi{}; const auto outs = std::make_shared(); - outs->reserve(outputs.size()); - for (const auto& pair: outputs) + + // Appends one address or data output from a name/value pair. + const auto append = [&](const std::string& name, + const value_t& item) NOEXCEPT -> code { // A data output carries a null data script and no value. - if (pair.first == "data") + if (name == "data") { data_chunk data{}; - if (!std::holds_alternative(pair.second.value()) || - !decode_base16(data, std::get(pair.second.value())) || + if (!std::holds_alternative(item.value()) || + !decode_base16(data, std::get(item.value())) || data.size() > max_null_data_size) return error::bitcoind::invalid_parameter; outs->push_back(to_shared(zero, chain::script{ script::to_pay_null_data_pattern(data) })); - continue; + return error::bitcoind::success; } - if (!std::holds_alternative(pair.second.value())) + if (!std::holds_alternative(item.value())) return error::bitcoind::type_error; - if (output_script(script, pair.first, p2kh_, p2sh_, witness_)) + if (output_script(script, name, p2kh_, p2sh_, witness_)) return error::bitcoind::invalid_address_or_key; - const auto btc = std::get(pair.second.value()); + const auto btc = std::get(item.value()); if (!to_integer(satoshi, btc * satoshi_per_bitcoin, true) || satoshi > system_settings().max_money()) return error::bitcoind::type_error; outs->push_back(to_shared(satoshi, std::move(script))); + return error::bitcoind::success; + }; + + // bitcoind accepts outputs as one object or an array of objects (which + // permits address repetition). + if (std::holds_alternative(outputs.value())) + { + for (const auto& pair: std::get(outputs.value())) + if (const auto fault = append(pair.first, pair.second)) + return fault; + } + else if (std::holds_alternative(outputs.value())) + { + for (const auto& element: std::get(outputs.value())) + { + if (!std::holds_alternative(element.value())) + return error::bitcoind::type_error; + + for (const auto& pair: std::get(element.value())) + if (const auto fault = append(pair.first, pair.second)) + return fault; + } + } + else + { + return error::bitcoind::type_error; } - out = { 1, ins, outs, lock_time }; + out = { tx_version, ins, outs, lock_time }; return error::bitcoind::success; } bool protocol_bitcoind_transaction::handle_create_raw_transaction( const code& ec, rpc_interface::create_raw_transaction, - const array_t& inputs, const object_t& outputs, double locktime, - bool replaceable) NOEXCEPT + const array_t& inputs, const value_t& outputs, double locktime, + bool replaceable, double version) NOEXCEPT { if (stopped(ec)) return false; chain::transaction tx{}; if (const auto fault = build_transaction(tx, inputs, outputs, locktime, - replaceable)) + replaceable, version)) { send_error(fault); return true; @@ -807,7 +851,7 @@ bool protocol_bitcoind_transaction::handle_convert_to_psbt(const code& ec, bool protocol_bitcoind_transaction::handle_create_psbt(const code& ec, rpc_interface::create_psbt, const array_t& inputs, - const object_t& outputs, double locktime, bool replaceable, + const value_t& outputs, double locktime, bool replaceable, double version, double psbt_version) NOEXCEPT { if (stopped(ec)) @@ -822,7 +866,7 @@ bool protocol_bitcoind_transaction::handle_create_psbt(const code& ec, chain::transaction tx{}; if (const auto fault = build_transaction(tx, inputs, outputs, locktime, - replaceable)) + replaceable, version)) { send_error(fault); return true; diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index df723da0..a800e1c0 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -495,23 +495,56 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__decoderawtransaction__created__round_trips) // The input sequence encodes locktime enforceability and replaceability: a // final sequence disables locktime and near-final does not signal bip125. -BOOST_AUTO_TEST_CASE(bitcoind_rpc__createrawtransaction__default__final_sequence) +BOOST_AUTO_TEST_CASE(bitcoind_rpc__createrawtransaction__default__replaceable_sequence) { const auto txid = encode_hash(test::block1.transactions_ptr()->front()->hash(false)); const auto created = rpc("createrawtransaction", "[[{\"txid\":\"" + txid + "\",\"vout\":0}], {\"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa\": 0.001}]"); const auto response = rpc("decoderawtransaction", "[\"" + as_text(created.at("result")) + "\"]"); + BOOST_REQUIRE_EQUAL(response.at("result").at("version").as_int64(), 2); + BOOST_REQUIRE_EQUAL(response.at("result").at("vin").at(0).at("sequence").as_int64(), 4294967293); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__createrawtransaction__not_replaceable__final_sequence) +{ + const auto txid = encode_hash(test::block1.transactions_ptr()->front()->hash(false)); + const auto created = rpc("createrawtransaction", "[[{\"txid\":\"" + txid + "\",\"vout\":0}], {\"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa\": 0.001}, 0, false]"); + const auto response = rpc("decoderawtransaction", "[\"" + as_text(created.at("result")) + "\"]"); BOOST_REQUIRE_EQUAL(response.at("result").at("vin").at(0).at("sequence").as_int64(), 4294967295); } -BOOST_AUTO_TEST_CASE(bitcoind_rpc__createrawtransaction__locktime__near_final_sequence) +BOOST_AUTO_TEST_CASE(bitcoind_rpc__createrawtransaction__locktime_not_replaceable__near_final_sequence) { const auto txid = encode_hash(test::block1.transactions_ptr()->front()->hash(false)); - const auto created = rpc("createrawtransaction", "[[{\"txid\":\"" + txid + "\",\"vout\":0}], {\"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa\": 0.001}, 500]"); + const auto created = rpc("createrawtransaction", "[[{\"txid\":\"" + txid + "\",\"vout\":0}], {\"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa\": 0.001}, 500, false]"); const auto response = rpc("decoderawtransaction", "[\"" + as_text(created.at("result")) + "\"]"); BOOST_REQUIRE_EQUAL(response.at("result").at("locktime").as_int64(), 500); BOOST_REQUIRE_EQUAL(response.at("result").at("vin").at(0).at("sequence").as_int64(), 4294967294); } +BOOST_AUTO_TEST_CASE(bitcoind_rpc__createrawtransaction__explicit_sequence__overrides) +{ + const auto txid = encode_hash(test::block1.transactions_ptr()->front()->hash(false)); + const auto created = rpc("createrawtransaction", "[[{\"txid\":\"" + txid + "\",\"vout\":0,\"sequence\":42}], {\"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa\": 0.001}]"); + const auto response = rpc("decoderawtransaction", "[\"" + as_text(created.at("result")) + "\"]"); + BOOST_REQUIRE_EQUAL(response.at("result").at("vin").at(0).at("sequence").as_int64(), 42); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__createrawtransaction__array_outputs__repeated_address) +{ + const auto txid = encode_hash(test::block1.transactions_ptr()->front()->hash(false)); + const auto created = rpc("createrawtransaction", "[[{\"txid\":\"" + txid + "\",\"vout\":0}], [{\"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa\": 0.001}, {\"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa\": 0.002}]]"); + const auto response = rpc("decoderawtransaction", "[\"" + as_text(created.at("result")) + "\"]"); + BOOST_REQUIRE_EQUAL(response.at("result").at("vout").as_array().size(), 2u); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__createrawtransaction__version_three__decodes) +{ + const auto txid = encode_hash(test::block1.transactions_ptr()->front()->hash(false)); + const auto created = rpc("createrawtransaction", "[[{\"txid\":\"" + txid + "\",\"vout\":0}], {\"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa\": 0.001}, 0, true, 3]"); + const auto response = rpc("decoderawtransaction", "[\"" + as_text(created.at("result")) + "\"]"); + BOOST_REQUIRE_EQUAL(response.at("result").at("version").as_int64(), 3); +} + BOOST_AUTO_TEST_CASE(bitcoind_rpc__createrawtransaction__replaceable__bip125_sequence) { const auto txid = encode_hash(test::block1.transactions_ptr()->front()->hash(false)); From e94c5527c84bbd7c96fe3354a20bbc63a9defec2 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 00:26:44 -0400 Subject: [PATCH 13/23] Serve rest headers from the active chain only. --- src/protocols/bitcoind/protocol_bitcoind_rest.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_rest.cpp b/src/protocols/bitcoind/protocol_bitcoind_rest.cpp index 02105c32..d91d883d 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_rest.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_rest.cpp @@ -270,9 +270,12 @@ bool protocol_bitcoind_rest::handle_get_block_headers(const code& ec, return true; } + // bitcoind serves headers only for a hash on the active chain. const auto& query = archive(); + const auto link = query.to_header(*hash); size_t height{}; - if (!query.get_height(height, query.to_header(*hash))) + if (!query.get_height(height, link) || + (query.to_confirmed(height) != link)) { send_not_found(); return true; From d1bf6039b4d15961c7b5709f6946501c726902cb Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 00:26:44 -0400 Subject: [PATCH 14/23] Add the rest tx endpoint. --- .../server/interfaces/bitcoind_rest.hpp | 6 ++- .../protocols/protocol_bitcoind_rest.hpp | 2 + src/parsers/bitcoind_target.cpp | 21 +++++++++ .../bitcoind/protocol_bitcoind_rest.cpp | 45 +++++++++++++++++++ test/parsers/bitcoind_target.cpp | 12 +++++ test/protocols/bitcoind/bitcoind_rest.cpp | 14 ++++++ 6 files changed, 99 insertions(+), 1 deletion(-) diff --git a/include/bitcoin/server/interfaces/bitcoind_rest.hpp b/include/bitcoin/server/interfaces/bitcoind_rest.hpp index d43cdefc..b0e13aae 100644 --- a/include/bitcoin/server/interfaces/bitcoind_rest.hpp +++ b/include/bitcoin/server/interfaces/bitcoind_rest.hpp @@ -53,7 +53,10 @@ struct bitcoind_rest_methods // info (json only) method<"chain_information">{}, method<"mempool_information">{}, - method<"fork_information", nullable>{ "hash" } + method<"fork_information", nullable>{ "hash" }, + + // transactions + method<"tx", uint8_t, system::hash_cptr>{ "media", "hash" } }; template @@ -77,6 +80,7 @@ struct bitcoind_rest_methods using chain_information = at<11>; using mempool_information = at<12>; using fork_information = at<13>; + using tx = at<14>; }; } // namespace interface diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_rest.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_rest.hpp index 1fa50e97..692aace0 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_rest.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_rest.hpp @@ -58,6 +58,8 @@ class BCS_API protocol_bitcoind_rest /// REST interface handlers. bool handle_get_block(const code& ec, rest_interface::block, uint8_t media, const system::hash_cptr& hash) NOEXCEPT; + bool handle_get_tx(const code& ec, rest_interface::tx, + uint8_t media, const system::hash_cptr& hash) NOEXCEPT; bool handle_get_block_hash(const code& ec, rest_interface::block_hash, uint8_t media, uint32_t height) NOEXCEPT; bool handle_get_block_txs(const code& ec, rest_interface::block_txs, diff --git a/src/parsers/bitcoind_target.cpp b/src/parsers/bitcoind_target.cpp index 3b264c0e..876ad565 100644 --- a/src/parsers/bitcoind_target.cpp +++ b/src/parsers/bitcoind_target.cpp @@ -161,6 +161,27 @@ code bitcoind_target(request_t& out, const std::string_view& path) NOEXCEPT return error::success; } + // /rest/tx/. + if (target == "tx") + { + if (segment == segments.size()) + return error::missing_hash; + + std::string name{}; + uint8_t media{}; + if (!split_leaf(name, media, segments[segment++])) + return error::invalid_target; + + const auto hash = to_hash(name); + if (!hash) + return error::invalid_hash; + + method = "tx"; + params["media"] = media; + params["hash"] = hash; + return error::success; + } + // /rest/blockhashbyheight/. if (target == "blockhashbyheight") { diff --git a/src/protocols/bitcoind/protocol_bitcoind_rest.cpp b/src/protocols/bitcoind/protocol_bitcoind_rest.cpp index d91d883d..44b6b473 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_rest.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_rest.cpp @@ -60,6 +60,7 @@ void protocol_bitcoind_rest::start() NOEXCEPT SUBSCRIBE_BITCOIND(handle_get_block_filter, _1, _2, _3, _4, _5); SUBSCRIBE_BITCOIND(handle_get_block_filter_headers, _1, _2, _3, _4, _5); SUBSCRIBE_BITCOIND(handle_get_chain_information, _1, _2); + SUBSCRIBE_BITCOIND(handle_get_tx, _1, _2, _3, _4); SUBSCRIBE_CHANNEL(get, handle_receive_get, _1, _2); network::protocol::start(); } @@ -185,6 +186,50 @@ bool protocol_bitcoind_rest::handle_get_block(const code& ec, return true; } +bool protocol_bitcoind_rest::handle_get_tx(const code& ec, + rest_interface::tx, uint8_t media, const hash_cptr& hash) NOEXCEPT +{ + if (stopped(ec)) + return false; + + if (!hash) + { + send_not_found(); + return true; + } + + constexpr auto witness = true; + const auto& query = archive(); + const auto link = query.to_tx(*hash); + const auto tx = query.get_transaction(link, witness); + if (!tx) + { + send_not_found(); + return true; + } + + const auto size = tx->serialized_size(witness); + switch (media) + { + case data: + send_data(to_data(*tx, size, witness)); + return true; + case text: + send_text(to_text(*tx, size, witness)); + return true; + case json: + { + auto model = value_from(bitcoind(*tx)); + inject_tx_context(model.as_object(), query, link); + send_json(std::move(model), two * size); + return true; + } + } + + send_not_found(); + return true; +} + bool protocol_bitcoind_rest::handle_get_block_hash(const code& ec, rest_interface::block_hash, uint8_t media, uint32_t height) NOEXCEPT { diff --git a/test/parsers/bitcoind_target.cpp b/test/parsers/bitcoind_target.cpp index 66b4214e..ce5d1a76 100644 --- a/test/parsers/bitcoind_target.cpp +++ b/test/parsers/bitcoind_target.cpp @@ -133,6 +133,18 @@ BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__block_json__expected) BOOST_REQUIRE_EQUAL(*hash_of(object), expected_hash); } +BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__tx_json__expected) +{ + request_t out{}; + BOOST_REQUIRE(!bitcoind_target(out, "/rest/tx/" + test_hash + ".json")); + BOOST_REQUIRE_EQUAL(out.method, "tx"); + + const auto& object = params_of(out); + BOOST_REQUIRE_EQUAL(object.size(), 2u); + BOOST_REQUIRE_EQUAL(media_of(object), to_value(media_type::application_json)); + BOOST_REQUIRE_EQUAL(*hash_of(object), expected_hash); +} + BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__block_media__mapped) { const std::vector> cases diff --git a/test/protocols/bitcoind/bitcoind_rest.cpp b/test/protocols/bitcoind/bitcoind_rest.cpp index 839a23f1..f1136b94 100644 --- a/test/protocols/bitcoind/bitcoind_rest.cpp +++ b/test/protocols/bitcoind/bitcoind_rest.cpp @@ -59,6 +59,20 @@ BOOST_AUTO_TEST_CASE(bitcoind_rest__block_json__block9_with_txs) BOOST_REQUIRE(result.at("tx").at(0).is_object()); } +BOOST_AUTO_TEST_CASE(bitcoind_rest__tx_json__coinbase_txid) +{ + const auto txid = encode_hash(test::block1.transactions_ptr()->front()->hash(false)); + const auto result = rest_json("/rest/tx/" + txid + ".json"); + BOOST_REQUIRE_EQUAL(as_text(result.at("txid")), txid); + BOOST_REQUIRE(result.at("vin").is_array()); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rest__tx_unknown__not_found) +{ + const auto txid = encode_hash(null_hash); + BOOST_REQUIRE_EQUAL(rest_status("/rest/tx/" + txid + ".json"), status::not_found); +} + BOOST_AUTO_TEST_CASE(bitcoind_rest__block_hex__hashes_to_block9) { const auto hex = rest_text("/rest/block/" + block9 + ".hex"); From 0a47f805458885204c35cf955750acc6b213ce6c Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 00:29:50 -0400 Subject: [PATCH 15/23] Distinguish rest bad request from not found. --- src/parsers/bitcoind_target.cpp | 2 +- src/protocols/bitcoind/protocol_bitcoind_rest.cpp | 13 +++++++++++-- test/parsers/bitcoind_target.cpp | 2 +- test/protocols/bitcoind/bitcoind_rest.cpp | 15 +++++++++++++++ 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/parsers/bitcoind_target.cpp b/src/parsers/bitcoind_target.cpp index 876ad565..1a861617 100644 --- a/src/parsers/bitcoind_target.cpp +++ b/src/parsers/bitcoind_target.cpp @@ -119,7 +119,7 @@ code bitcoind_target(request_t& out, const std::string_view& path) NOEXCEPT ++segment; if (segment == segments.size()) - return error::missing_target; + return error::invalid_target; const auto target = segments[segment++]; diff --git a/src/protocols/bitcoind/protocol_bitcoind_rest.cpp b/src/protocols/bitcoind/protocol_bitcoind_rest.cpp index 44b6b473..170a5b4e 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_rest.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_rest.cpp @@ -106,10 +106,19 @@ void protocol_bitcoind_rest::handle_receive_get(const code& ec, set_request(get); // Parse the REST url into a json-rpc model and dispatch to a handler. + // Malformed parameters are bad requests, unknown targets are not found. request_t model{}; - if (bitcoind_target(model, get->target())) + if (const auto fault = bitcoind_target(model, get->target())) { - send_not_found(); + if ((fault == error::invalid_hash) || + (fault == error::invalid_number) || + (fault == error::missing_hash) || + (fault == error::missing_height) || + (fault == error::missing_target)) + send_bad_request(*get); + else + send_not_found(); + return; } diff --git a/test/parsers/bitcoind_target.cpp b/test/parsers/bitcoind_target.cpp index ce5d1a76..775c56e6 100644 --- a/test/parsers/bitcoind_target.cpp +++ b/test/parsers/bitcoind_target.cpp @@ -66,7 +66,7 @@ BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__error_paths__expected) { { "", server::error::empty_path }, { "?foo=bar", server::error::empty_path }, - { "/rest", server::error::missing_target }, + { "/rest", server::error::invalid_target }, { "/rest/bogus", server::error::invalid_target }, { "/bogus", server::error::invalid_target }, { "/rest/block", server::error::missing_hash }, diff --git a/test/protocols/bitcoind/bitcoind_rest.cpp b/test/protocols/bitcoind/bitcoind_rest.cpp index f1136b94..5dc2af90 100644 --- a/test/protocols/bitcoind/bitcoind_rest.cpp +++ b/test/protocols/bitcoind/bitcoind_rest.cpp @@ -73,6 +73,21 @@ BOOST_AUTO_TEST_CASE(bitcoind_rest__tx_unknown__not_found) BOOST_REQUIRE_EQUAL(rest_status("/rest/tx/" + txid + ".json"), status::not_found); } +BOOST_AUTO_TEST_CASE(bitcoind_rest__malformed_hash__bad_request) +{ + BOOST_REQUIRE_EQUAL(rest_status("/rest/block/nothex.json"), status::bad_request); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rest__malformed_height__bad_request) +{ + BOOST_REQUIRE_EQUAL(rest_status("/rest/blockhashbyheight/abc.json"), status::bad_request); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rest__unknown_target__not_found) +{ + BOOST_REQUIRE_EQUAL(rest_status("/rest/bogus"), status::not_found); +} + BOOST_AUTO_TEST_CASE(bitcoind_rest__block_hex__hashes_to_block9) { const auto hex = rest_text("/rest/block/" + block9 + ".hex"); From 41eef6d31d41f12c4f04cbe78bd9023b923ada98 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 00:37:48 -0400 Subject: [PATCH 16/23] Declare missing bitcoind methods and serve rpc.discover. --- .../server/interfaces/bitcoind_control.hpp | 4 ++- .../interfaces/bitcoind_transaction.hpp | 6 ++++- .../server/interfaces/bitcoind_utility.hpp | 4 ++- .../protocols/protocol_bitcoind_control.hpp | 5 ++++ .../protocol_bitcoind_transaction.hpp | 4 +++ .../protocols/protocol_bitcoind_utility.hpp | 2 ++ .../bitcoind/protocol_bitcoind_control.cpp | 27 +++++++++++++++---- .../protocol_bitcoind_transaction.cpp | 19 +++++++++++++ .../bitcoind/protocol_bitcoind_utility.cpp | 10 +++++++ test/interfaces/bitcoind.cpp | 3 ++- test/protocols/bitcoind/bitcoind_rpc.cpp | 13 +++++++-- 11 files changed, 86 insertions(+), 11 deletions(-) diff --git a/include/bitcoin/server/interfaces/bitcoind_control.hpp b/include/bitcoin/server/interfaces/bitcoind_control.hpp index 3c65459c..92859b3c 100644 --- a/include/bitcoin/server/interfaces/bitcoind_control.hpp +++ b/include/bitcoin/server/interfaces/bitcoind_control.hpp @@ -36,7 +36,8 @@ struct bitcoind_control_methods method<"getopenrpcinfo", optional>{ "show_hidden" }, method<"getrpcinfo">{}, method<"logging", optional, optional>{ "include", "exclude" }, - method<"uptime">{} + method<"uptime">{}, + method<"rpc.discover", optional>{ "show_hidden" } }; template @@ -58,6 +59,7 @@ struct bitcoind_control_methods using get_rpc_info = at<4>; using logging = at<5>; using uptime = at<6>; + using rpc_discover = at<7>; }; } // namespace interface diff --git a/include/bitcoin/server/interfaces/bitcoind_transaction.hpp b/include/bitcoin/server/interfaces/bitcoind_transaction.hpp index 26a73f39..ce911c2c 100644 --- a/include/bitcoin/server/interfaces/bitcoind_transaction.hpp +++ b/include/bitcoin/server/interfaces/bitcoind_transaction.hpp @@ -46,7 +46,9 @@ struct bitcoind_transaction_methods method<"utxoupdatepsbt", string_t, optional>{ "psbt", "descriptors" }, method<"abortprivatebroadcast">{ unimplemented }, method<"getprivatebroadcastinfo">{ unimplemented }, - method<"submitpackage">{ unimplemented } + method<"submitpackage">{ unimplemented }, + method<"combinerawtransaction">{ unimplemented }, + method<"signrawtransactionwithkey">{ unimplemented } }; template @@ -78,6 +80,8 @@ struct bitcoind_transaction_methods using abort_private_broadcast = at<14>; using get_private_broadcast_info = at<15>; using submit_package = at<16>; + using combine_raw_transaction = at<17>; + using sign_raw_transaction_with_key = at<18>; }; } // namespace interface diff --git a/include/bitcoin/server/interfaces/bitcoind_utility.hpp b/include/bitcoin/server/interfaces/bitcoind_utility.hpp index cdcdf39e..617be12b 100644 --- a/include/bitcoin/server/interfaces/bitcoind_utility.hpp +++ b/include/bitcoin/server/interfaces/bitcoind_utility.hpp @@ -37,7 +37,8 @@ struct bitcoind_utility_methods method<"getdescriptorinfo", string_t>{ "descriptor" }, method<"verifymessage", string_t, string_t, string_t>{ "address", "signature", "message" }, method<"getindexinfo", optional<""_t>>{ "index_name" }, - method<"estimatesmartfee">{ unimplemented } + method<"estimatesmartfee">{ unimplemented }, + method<"signmessagewithprivkey">{ unimplemented } }; template @@ -60,6 +61,7 @@ struct bitcoind_utility_methods using verify_message = at<5>; using get_index_info = at<6>; using estimate_smart_fee = at<7>; + using sign_message_with_priv_key = at<8>; }; } // namespace interface diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_control.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_control.hpp index c73f4994..de3f41fd 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_control.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_control.hpp @@ -70,6 +70,11 @@ class BCS_API protocol_bitcoind_control const network::rpc::array_t& exclude) NOEXCEPT; bool handle_uptime(const code& ec, rpc_interface::uptime) NOEXCEPT; + bool handle_rpc_discover(const code& ec, + rpc_interface::rpc_discover, bool show_hidden) NOEXCEPT; + +private: + void send_openrpc() NOEXCEPT; }; } // namespace server diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_transaction.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_transaction.hpp index 1d85535a..744894dd 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_transaction.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_transaction.hpp @@ -110,6 +110,10 @@ class BCS_API protocol_bitcoind_transaction rpc_interface::get_private_broadcast_info) NOEXCEPT; bool handle_submit_package(const code& ec, rpc_interface::submit_package) NOEXCEPT; + bool handle_combine_raw_transaction(const code& ec, + rpc_interface::combine_raw_transaction) NOEXCEPT; + bool handle_sign_raw_transaction_with_key(const code& ec, + rpc_interface::sign_raw_transaction_with_key) NOEXCEPT; }; } // namespace server diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_utility.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_utility.hpp index f10260f3..2fc6f5d3 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_utility.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_utility.hpp @@ -77,6 +77,8 @@ class BCS_API protocol_bitcoind_utility rpc_interface::get_index_info, const std::string& index_name) NOEXCEPT; bool handle_estimate_smart_fee(const code& ec, rpc_interface::estimate_smart_fee) NOEXCEPT; + bool handle_sign_message_with_priv_key(const code& ec, + rpc_interface::sign_message_with_priv_key) NOEXCEPT; }; } // namespace server diff --git a/src/protocols/bitcoind/protocol_bitcoind_control.cpp b/src/protocols/bitcoind/protocol_bitcoind_control.cpp index c6406988..af18f1ab 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_control.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_control.cpp @@ -60,6 +60,7 @@ void protocol_bitcoind_control::start() NOEXCEPT SUBSCRIBE_BITCOIND(handle_get_rpc_info, _1, _2); SUBSCRIBE_BITCOIND(handle_logging, _1, _2, _3, _4); SUBSCRIBE_BITCOIND(handle_uptime, _1, _2); + SUBSCRIBE_BITCOIND(handle_rpc_discover, _1, _2, _3); protocol_bitcoind_dispatch::start(); } @@ -204,12 +205,8 @@ static void append_methods(array_t& out) NOEXCEPT } // There are no hidden methods (unimplemented rows are refusals, not hidden). -bool protocol_bitcoind_control::handle_get_openrpc_info(const code& ec, - rpc_interface::get_openrpc_info, bool) NOEXCEPT +void protocol_bitcoind_control::send_openrpc() NOEXCEPT { - if (stopped(ec)) - return false; - using namespace interface; array_t methods{}; append_methods(methods); @@ -234,6 +231,26 @@ bool protocol_bitcoind_control::handle_get_openrpc_info(const code& ec, } }, { "methods", std::move(methods) } }, size); +} + +bool protocol_bitcoind_control::handle_get_openrpc_info(const code& ec, + rpc_interface::get_openrpc_info, bool) NOEXCEPT +{ + if (stopped(ec)) + return false; + + send_openrpc(); + return true; +} + +// bitcoind's discovery alias for the openrpc document. +bool protocol_bitcoind_control::handle_rpc_discover(const code& ec, + rpc_interface::rpc_discover, bool) NOEXCEPT +{ + if (stopped(ec)) + return false; + + send_openrpc(); return true; } diff --git a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp index 86411aa1..58210db9 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp @@ -71,6 +71,8 @@ void protocol_bitcoind_transaction::start() NOEXCEPT SUBSCRIBE_BITCOIND(handle_abort_private_broadcast, _1, _2); SUBSCRIBE_BITCOIND(handle_get_private_broadcast_info, _1, _2); SUBSCRIBE_BITCOIND(handle_submit_package, _1, _2); + SUBSCRIBE_BITCOIND(handle_combine_raw_transaction, _1, _2); + SUBSCRIBE_BITCOIND(handle_sign_raw_transaction_with_key, _1, _2); protocol_bitcoind_dispatch::start(); } @@ -1038,6 +1040,23 @@ bool protocol_bitcoind_transaction::handle_submit_package(const code& ec, return true; } +bool protocol_bitcoind_transaction::handle_combine_raw_transaction( + const code& ec, rpc_interface::combine_raw_transaction) NOEXCEPT +{ + if (stopped(ec)) return false; + send_error(error::bitcoind::method_not_found); + return true; +} + +// Signing is a wallet function, keys never transit the server. +bool protocol_bitcoind_transaction::handle_sign_raw_transaction_with_key( + const code& ec, rpc_interface::sign_raw_transaction_with_key) NOEXCEPT +{ + if (stopped(ec)) return false; + send_error(error::bitcoind::method_not_found); + return true; +} + BC_POP_WARNING() BC_POP_WARNING() BC_POP_WARNING() diff --git a/src/protocols/bitcoind/protocol_bitcoind_utility.cpp b/src/protocols/bitcoind/protocol_bitcoind_utility.cpp index 75766915..30190816 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_utility.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_utility.cpp @@ -61,6 +61,7 @@ void protocol_bitcoind_utility::start() NOEXCEPT SUBSCRIBE_BITCOIND(handle_verify_message, _1, _2, _3, _4, _5); SUBSCRIBE_BITCOIND(handle_get_index_info, _1, _2, _3); SUBSCRIBE_BITCOIND(handle_estimate_smart_fee, _1, _2); + SUBSCRIBE_BITCOIND(handle_sign_message_with_priv_key, _1, _2); protocol_bitcoind_dispatch::start(); } @@ -431,6 +432,15 @@ bool protocol_bitcoind_utility::handle_estimate_smart_fee(const code& ec, return true; } +// Signing is a wallet function, keys never transit the server. +bool protocol_bitcoind_utility::handle_sign_message_with_priv_key( + const code& ec, rpc_interface::sign_message_with_priv_key) NOEXCEPT +{ + if (stopped(ec)) return false; + send_error(error::bitcoind::method_not_found); + return true; +} + BC_POP_WARNING() BC_POP_WARNING() BC_POP_WARNING() diff --git a/test/interfaces/bitcoind.cpp b/test/interfaces/bitcoind.cpp index 8709b05f..b91d4a34 100644 --- a/test/interfaces/bitcoind.cpp +++ b/test/interfaces/bitcoind.cpp @@ -121,7 +121,8 @@ static_assert(bitcoind_blockchain_methods::names == "getdeploymentinfo getdescriptoractivity getdifficulty scanblocks " "waitforblock waitforblockheight waitfornewblock"); static_assert(bitcoind_control_methods::names == - "help getmemoryinfo getopenrpcinfo getrpcinfo logging uptime"); + "help getmemoryinfo getopenrpcinfo getrpcinfo logging uptime " + "rpc.discover"); static_assert(bitcoind_mining_methods::names == "getnetworkhashps getmininginfo submitblock submitheader"); static_assert(bitcoind_network_methods::names == diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index a800e1c0..d03a1a22 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -62,7 +62,9 @@ const std::vector rejected_methods { "listbanned", -20 }, { "setban", -20 }, { "stop", -32601 }, - { "descriptorprocesspsbt", -32601 } + { "descriptorprocesspsbt", -32601 }, + { "signrawtransactionwithkey", -32601 }, + { "signmessagewithprivkey", -32601 } }; const std::vector wip_methods @@ -71,7 +73,8 @@ const std::vector wip_methods { "preciousblock", -32601 }, { "disconnectnode", -32601 }, { "exportasmap", -32601 }, - { "getaddednodeinfo", -24 } + { "getaddednodeinfo", -24 }, + { "combinerawtransaction", -32601 } }; std::string as_text(const boost::json::value& value) NOEXCEPT @@ -688,6 +691,12 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__createmultisig__excess_keys__invalid_paramete BOOST_REQUIRE_MESSAGE(has_code(response, -8), response); } +BOOST_AUTO_TEST_CASE(bitcoind_rpc__rpc_discover__default__openrpc_version) +{ + const auto response = rpc("rpc.discover"); + BOOST_REQUIRE_EQUAL(as_text(response.at("result").at("openrpc")), "1.2.6"); +} + 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 aeb33759bdbc9990438bbb895ca5b449f1c2d159 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 00:46:01 -0400 Subject: [PATCH 17/23] Style. --- .../protocols/protocol_bitcoind_blockchain.hpp | 2 +- src/protocols/bitcoind/protocol_bitcoind.cpp | 2 +- .../bitcoind/protocol_bitcoind_blockchain.cpp | 13 ++++++------- .../bitcoind/protocol_bitcoind_control.cpp | 1 - .../bitcoind/protocol_bitcoind_dispatch.cpp | 2 +- src/protocols/bitcoind/protocol_bitcoind_mining.cpp | 1 - .../bitcoind/protocol_bitcoind_network.cpp | 1 - .../bitcoind/protocol_bitcoind_notifications.cpp | 1 - src/protocols/bitcoind/protocol_bitcoind_rest.cpp | 5 +---- src/protocols/bitcoind/protocol_bitcoind_test.cpp | 1 - .../bitcoind/protocol_bitcoind_transaction.cpp | 5 ++--- .../bitcoind/protocol_bitcoind_utility.cpp | 1 - src/protocols/bitcoind/protocol_bitcoind_wallet.cpp | 1 - test/protocols/bitcoind/bitcoind_rpc.cpp | 6 +++--- 14 files changed, 15 insertions(+), 27 deletions(-) diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp index 4e93c48d..a9ea5d0c 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp @@ -163,7 +163,7 @@ class BCS_API protocol_bitcoind_blockchain void do_wait_event() NOEXCEPT; void handle_wait_timeout(const code& ec) NOEXCEPT; bool wait_done() const NOEXCEPT; - void send_tip() NOEXCEPT; + void send_top() NOEXCEPT; enum class set_hash : uint8_t { none, muhash, serialized }; diff --git a/src/protocols/bitcoind/protocol_bitcoind.cpp b/src/protocols/bitcoind/protocol_bitcoind.cpp index 2efb605d..3e492945 100644 --- a/src/protocols/bitcoind/protocol_bitcoind.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind.cpp @@ -325,7 +325,7 @@ http::request_cptr protocol_bitcoind::reset_rpc_request() NOEXCEPT return reset_request(); } -// utility (redundant with protocol_electrum) +// Utility (redundant with protocol_electrum). // ---------------------------------------------------------------------------- code protocol_bitcoind::validate_tx( diff --git a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp index a2c79df6..b74da855 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp @@ -28,7 +28,6 @@ #include namespace libbitcoin { - namespace server { #define CLASS protocol_bitcoind_blockchain @@ -140,7 +139,7 @@ bool protocol_bitcoind_blockchain::handle_get_best_block_hash(const code& ec, return false; const auto hash = archive().get_top_confirmed_hash(); - send_result(encode_hash(hash), two * system::hash_size); + send_result(encode_hash(hash), two * hash_size); return true; } @@ -1747,7 +1746,7 @@ bool protocol_bitcoind_blockchain::handle_wait_for_new_block(const code& ec, if (!current_tip.empty() && decode_hash(given, current_tip) && (given != archive().get_top_confirmed_hash())) { - send_tip(); + send_top(); return true; } @@ -1778,7 +1777,7 @@ bool protocol_bitcoind_blockchain::wait_done() const NOEXCEPT } } -void protocol_bitcoind_blockchain::send_tip() NOEXCEPT +void protocol_bitcoind_blockchain::send_top() NOEXCEPT { const auto& query = archive(); const auto top = query.get_top_confirmed(); @@ -1794,7 +1793,7 @@ void protocol_bitcoind_blockchain::arm_wait(double timeout) NOEXCEPT if (wait_done()) { wait_ = wait::none; - send_tip(); + send_top(); return; } @@ -1820,7 +1819,7 @@ void protocol_bitcoind_blockchain::do_wait_event() NOEXCEPT wait_ = wait::none; wait_timer_->stop(); - send_tip(); + send_top(); } void protocol_bitcoind_blockchain::handle_wait_timeout(const code& ec) NOEXCEPT @@ -1831,7 +1830,7 @@ void protocol_bitcoind_blockchain::handle_wait_timeout(const code& ec) NOEXCEPT return; wait_ = wait::none; - send_tip(); + send_top(); } // Chase events. diff --git a/src/protocols/bitcoind/protocol_bitcoind_control.cpp b/src/protocols/bitcoind/protocol_bitcoind_control.cpp index af18f1ab..53c8c774 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_control.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_control.cpp @@ -25,7 +25,6 @@ #include namespace libbitcoin { - namespace server { #define CLASS protocol_bitcoind_control diff --git a/src/protocols/bitcoind/protocol_bitcoind_dispatch.cpp b/src/protocols/bitcoind/protocol_bitcoind_dispatch.cpp index dd9421c6..d5e6a55f 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_dispatch.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_dispatch.cpp @@ -89,7 +89,7 @@ void CLASS::handle_receive_post(const code& ec, // Claim the request (informs the terminal responder). set_claimed(); - // The post is saved off during asynchonous handling and used in + // The post is saved off during asynchronous handling and used in // send_json to formulate response headers, isolating handlers from // http semantics. set_rpc_request(message.jsonrpc, message.id, post); diff --git a/src/protocols/bitcoind/protocol_bitcoind_mining.cpp b/src/protocols/bitcoind/protocol_bitcoind_mining.cpp index 0ce00e55..d8c3532d 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_mining.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_mining.cpp @@ -25,7 +25,6 @@ #include namespace libbitcoin { - namespace server { #define CLASS protocol_bitcoind_mining diff --git a/src/protocols/bitcoind/protocol_bitcoind_network.cpp b/src/protocols/bitcoind/protocol_bitcoind_network.cpp index d4a07bb3..b3b6d621 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_network.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_network.cpp @@ -25,7 +25,6 @@ #include namespace libbitcoin { - namespace server { #define CLASS protocol_bitcoind_network diff --git a/src/protocols/bitcoind/protocol_bitcoind_notifications.cpp b/src/protocols/bitcoind/protocol_bitcoind_notifications.cpp index 60e26e76..ecc0e90a 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_notifications.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_notifications.cpp @@ -25,7 +25,6 @@ #include namespace libbitcoin { - namespace server { #define CLASS protocol_bitcoind_notifications diff --git a/src/protocols/bitcoind/protocol_bitcoind_rest.cpp b/src/protocols/bitcoind/protocol_bitcoind_rest.cpp index 170a5b4e..14fbf362 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_rest.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_rest.cpp @@ -101,7 +101,7 @@ void protocol_bitcoind_rest::handle_receive_get(const code& ec, return; } - // The get is saved off during asynchonous handling and used in send_json + // The get is saved off during asynchronous handling and used in send_json // to formulate response headers, isolating handlers from http semantics. set_request(get); @@ -690,9 +690,6 @@ void protocol_bitcoind_rest::send_json(value&& model, SEND(std::move(message), handle_complete, _1, error::success); } -// private -// ---------------------------------------------------------------------------- - BC_POP_WARNING() BC_POP_WARNING() BC_POP_WARNING() diff --git a/src/protocols/bitcoind/protocol_bitcoind_test.cpp b/src/protocols/bitcoind/protocol_bitcoind_test.cpp index b77eb436..2126557c 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_test.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_test.cpp @@ -25,7 +25,6 @@ #include namespace libbitcoin { - namespace server { #define CLASS protocol_bitcoind_test diff --git a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp index 58210db9..4ffc2373 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp @@ -26,7 +26,6 @@ #include namespace libbitcoin { - namespace server { #define CLASS protocol_bitcoind_transaction @@ -193,7 +192,7 @@ bool protocol_bitcoind_transaction::handle_send_raw_transaction(const code& ec, return true; } - send_result(encode_hash(tx->hash(false)), two * system::hash_size); + send_result(encode_hash(tx->hash(false)), two * hash_size); return true; } @@ -998,7 +997,7 @@ bool protocol_bitcoind_transaction::handle_utxo_update_psbt(const code& ec, auto& in = doc.inputs().at(index); const auto& hash = version0 ? doc.unsigned_tx().inputs_ptr()->at(index)->point().hash() : - in.previous_txid.value_or(system::null_hash); + in.previous_txid.value_or(null_hash); const auto vout = version0 ? doc.unsigned_tx().inputs_ptr()->at(index)->point().index() : in.output_index.value_or(0); diff --git a/src/protocols/bitcoind/protocol_bitcoind_utility.cpp b/src/protocols/bitcoind/protocol_bitcoind_utility.cpp index 30190816..e0d0ec86 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_utility.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_utility.cpp @@ -25,7 +25,6 @@ #include namespace libbitcoin { - namespace server { #define CLASS protocol_bitcoind_utility diff --git a/src/protocols/bitcoind/protocol_bitcoind_wallet.cpp b/src/protocols/bitcoind/protocol_bitcoind_wallet.cpp index 12a98b2f..c3c04eb0 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_wallet.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_wallet.cpp @@ -25,7 +25,6 @@ #include namespace libbitcoin { - namespace server { #define CLASS protocol_bitcoind_wallet diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index d03a1a22..af5bfd9f 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -1618,7 +1618,7 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__submitheader__existing_header__null) // waitfor (all conditions immediately met or timing out on the fixture) -BOOST_AUTO_TEST_CASE(bitcoind_rpc__waitforblockheight__at_top__immediate_tip) +BOOST_AUTO_TEST_CASE(bitcoind_rpc__waitforblockheight__at_top__immediate_top) { const auto response = rpc("waitforblockheight", "[9]"); const auto& result = response.at("result"); @@ -1626,14 +1626,14 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__waitforblockheight__at_top__immediate_tip) BOOST_REQUIRE_EQUAL(as_text(result.at("hash")), block9); } -BOOST_AUTO_TEST_CASE(bitcoind_rpc__waitforblock__confirmed_hash__immediate_tip) +BOOST_AUTO_TEST_CASE(bitcoind_rpc__waitforblock__confirmed_hash__immediate_top) { const auto response = rpc("waitforblock", "[\"" + std::string{ block9 } + "\"]"); const auto& result = response.at("result"); BOOST_REQUIRE_EQUAL(result.at("height").as_int64(), 9); } -BOOST_AUTO_TEST_CASE(bitcoind_rpc__waitfornewblock__short_timeout__times_out_with_tip) +BOOST_AUTO_TEST_CASE(bitcoind_rpc__waitfornewblock__short_timeout__times_out_with_top) { const auto response = rpc("waitfornewblock", "[1]"); const auto& result = response.at("result"); From 6d6f7dc7658e34fc05f99374ed158e00117cd989 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 00:49:54 -0400 Subject: [PATCH 18/23] Comment style. --- src/parsers/bitcoind_script.cpp | 2 +- .../bitcoind/protocol_bitcoind_blockchain.cpp | 23 ++++++++----------- .../bitcoind/protocol_bitcoind_json.cpp | 3 +-- .../bitcoind/protocol_bitcoind_mining.cpp | 8 ++----- .../bitcoind/protocol_bitcoind_network.cpp | 3 +-- .../protocol_bitcoind_transaction.cpp | 3 +-- .../bitcoind/protocol_bitcoind_utility.cpp | 6 ++--- 7 files changed, 17 insertions(+), 31 deletions(-) diff --git a/src/parsers/bitcoind_script.cpp b/src/parsers/bitcoind_script.cpp index aa332255..efb53f1f 100644 --- a/src/parsers/bitcoind_script.cpp +++ b/src/parsers/bitcoind_script.cpp @@ -33,7 +33,7 @@ code output_script(script& out, const std::string& text, uint8_t p2kh, { using namespace wallet; - // The parses accept any prefix, so the configured ones are checks. + // The parsers accept any prefix, so the configured ones are checks. if (const payment_address payment{ text }; payment && ((payment.prefix() == p2kh) || (payment.prefix() == p2sh))) { diff --git a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp index b74da855..0bcfcc58 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp @@ -752,8 +752,8 @@ void protocol_bitcoind_blockchain::do_get_tx_out_set_info(set_hash type, if (ec) { - POST(complete_scan, error::bitcoind::internal_error, - std::move(result), zero); + POST(complete_scan, error::bitcoind::internal_error, std::move(result), + zero); return; } @@ -787,8 +787,7 @@ void protocol_bitcoind_blockchain::do_get_tx_out_set_info(set_hash type, if (type == set_hash::muhash) result.emplace("muhash", encode_hash(digest)); - POST(complete_scan, code{}, - std::move(result), 512); + POST(complete_scan, code{}, std::move(result), 512); } bool protocol_bitcoind_blockchain::handle_prune_block_chain(const code& ec, @@ -943,16 +942,16 @@ void protocol_bitcoind_blockchain::do_scan_tx_out_set( if (ec) { - POST(complete_scan, error::bitcoind::internal_error, - std::move(result), zero); + POST(complete_scan, error::bitcoind::internal_error, std::move(result), + zero); return; } // A reorganization across the pinned top voids the scan. if (!query.is_confirmed_block(link)) { - POST(complete_scan, error::bitcoind::internal_error, - std::move(result), zero); + POST(complete_scan, error::bitcoind::internal_error, std::move(result), + zero); return; } @@ -1002,8 +1001,7 @@ void protocol_bitcoind_blockchain::do_scan_tx_out_set( { "total_amount", to_floating(amount) / chain::satoshi_per_bitcoin } }; - POST(complete_scan, code{}, - std::move(result), size); + POST(complete_scan, code{}, std::move(result), size); } void protocol_bitcoind_blockchain::complete_scan(const code& ec, @@ -1229,10 +1227,7 @@ bool protocol_bitcoind_blockchain::handle_get_chain_states(const code& ec, if (!query.is_coalesced()) { - // The candidate chain is validated to the fork point (confirmed) plus - // the contiguously validated span above it. This does not imply - // confirmability, which is not determined until blocks are - // reorganized into the confirmed chain. + // Validated above the fork point does not imply confirmability. size_t fork{}; const auto span = query.get_validated_fork(fork, system_settings().top_checkpoint().height()); diff --git a/src/protocols/bitcoind/protocol_bitcoind_json.cpp b/src/protocols/bitcoind/protocol_bitcoind_json.cpp index 4fe39ecc..5211d2fb 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_json.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_json.cpp @@ -28,8 +28,7 @@ namespace server { using namespace system; -// bitcoind reports the median time past of the CHILD of the given block (its -// window includes the block's own timestamp). Reproduced for compatibility. +// bitcoind's mtp window includes the block's own timestamp (reproduced). uint32_t protocol_bitcoind::median_time_past(const node::query& query, const database::header_link& link) NOEXCEPT { diff --git a/src/protocols/bitcoind/protocol_bitcoind_mining.cpp b/src/protocols/bitcoind/protocol_bitcoind_mining.cpp index d8c3532d..eae93e00 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_mining.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_mining.cpp @@ -147,10 +147,7 @@ bool protocol_bitcoind_mining::handle_get_network_hash_ps(const code& ec, return true; } -// currentblockweight/currentblocktx are omitted (bitcoind omits them until a -// block is assembled, and there is no assembler). The tx pool is empty, so -// pooledtx is zero, and no packages are selected, so blockmintxfee is the -// maximum. +// bitcoind omits currentblockweight/currentblocktx until block assembly. bool protocol_bitcoind_mining::handle_get_mining_info(const code& ec, rpc_interface::get_mining_info) NOEXCEPT { @@ -234,8 +231,7 @@ bool protocol_bitcoind_mining::handle_submit_block(const code& ec, return true; } - // bitcoind reports an already-stored block as a duplicate result. A known - // header without its block still organizes (the submitheader flow). + // A known header without its block still organizes (the submitheader flow). const auto link = archive().to_header(block->hash()); if (!link.is_terminal() && archive().is_associated(link)) { diff --git a/src/protocols/bitcoind/protocol_bitcoind_network.cpp b/src/protocols/bitcoind/protocol_bitcoind_network.cpp index b3b6d621..10e9dc8f 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_network.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_network.cpp @@ -275,8 +275,7 @@ void protocol_bitcoind_network::do_send_nodes(const code& ec, send_result(std::move(out), size); } -// An injected ping would violate channel pong correlation, and there is no -// peer timing instrumentation to serve, so this is a no-op (null result). +// An injected ping would violate channel pong correlation (no-op). bool protocol_bitcoind_network::handle_ping(const code& ec, rpc_interface::ping) NOEXCEPT { diff --git a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp index 4ffc2373..9a7a24c1 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp @@ -337,8 +337,7 @@ code protocol_bitcoind_transaction::build_transaction(chain::transaction& out, return error::bitcoind::success; }; - // bitcoind accepts outputs as one object or an array of objects (which - // permits address repetition). + // outputs is one object or an array of objects (permits repeated address). if (std::holds_alternative(outputs.value())) { for (const auto& pair: std::get(outputs.value())) diff --git a/src/protocols/bitcoind/protocol_bitcoind_utility.cpp b/src/protocols/bitcoind/protocol_bitcoind_utility.cpp index e0d0ec86..641368da 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_utility.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_utility.cpp @@ -400,10 +400,8 @@ bool protocol_bitcoind_utility::handle_get_index_info(const code& ec, if (stopped(ec)) return false; - // Indexes track the confirmed chain only (no pool txs until v5 tx pool). - // tx lookup is always available (all txs are archived). - // synced: current chain (not a stale checkpoint) and confirmation has - // coalesced with the candidate top (no stronger blocks pending). + // Indexes track the confirmed chain only; all txs are archived (lookup). + // synced: current chain and confirmation coalesced with the candidate top. const auto& query = archive(); const object_t status { From 3070c06e84734e6da91b4b0b4dc66d85e5a45e4c Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 10:41:54 -0400 Subject: [PATCH 19/23] Match definition order to declaration order. --- .../bitcoind/protocol_bitcoind_blockchain.cpp | 200 +++---- .../bitcoind/protocol_bitcoind_json.cpp | 137 +++-- .../bitcoind/protocol_bitcoind_network.cpp | 174 +++---- .../protocol_bitcoind_transaction.cpp | 486 +++++++++--------- 4 files changed, 498 insertions(+), 499 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp index 0bcfcc58..02e05dcd 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp @@ -1,4 +1,4 @@ -/** +/** * Copyright (c) 2011-2026 libbitcoin developers * * This file is part of libbitcoin. @@ -1751,38 +1751,98 @@ bool protocol_bitcoind_blockchain::handle_wait_for_new_block(const code& ec, return true; } -// Wait machinery (strand). +bool protocol_bitcoind_blockchain::handle_get_mempool_ancestors(const code& ec, + rpc_interface::get_mempool_ancestors) NOEXCEPT +{ + if (stopped(ec)) return false; + send_error(error::bitcoind::client_mempool_disabled); + return true; +} + +bool protocol_bitcoind_blockchain::handle_get_mempool_cluster(const code& ec, + rpc_interface::get_mempool_cluster) NOEXCEPT +{ + if (stopped(ec)) return false; + send_error(error::bitcoind::client_mempool_disabled); + return true; +} + +bool protocol_bitcoind_blockchain::handle_get_mempool_descendants(const code& ec, + rpc_interface::get_mempool_descendants) NOEXCEPT +{ + if (stopped(ec)) return false; + send_error(error::bitcoind::client_mempool_disabled); + return true; +} + +bool protocol_bitcoind_blockchain::handle_get_mempool_entry(const code& ec, + rpc_interface::get_mempool_entry) NOEXCEPT +{ + if (stopped(ec)) return false; + send_error(error::bitcoind::client_mempool_disabled); + return true; +} + +bool protocol_bitcoind_blockchain::handle_get_mempool_info(const code& ec, + rpc_interface::get_mempool_info) NOEXCEPT +{ + if (stopped(ec)) return false; + send_error(error::bitcoind::client_mempool_disabled); + return true; +} + +bool protocol_bitcoind_blockchain::handle_get_raw_mempool(const code& ec, + rpc_interface::get_raw_mempool) NOEXCEPT +{ + if (stopped(ec)) return false; + send_error(error::bitcoind::client_mempool_disabled); + return true; +} + +bool protocol_bitcoind_blockchain::handle_get_tx_spending_prevout(const code& ec, + rpc_interface::get_tx_spending_prevout) NOEXCEPT +{ + if (stopped(ec)) return false; + send_error(error::bitcoind::client_mempool_disabled); + return true; +} + +bool protocol_bitcoind_blockchain::handle_import_mempool(const code& ec, + rpc_interface::import_mempool) NOEXCEPT +{ + if (stopped(ec)) return false; + send_error(error::bitcoind::client_mempool_disabled); + return true; +} + +// Chase events. // ---------------------------------------------------------------------------- -bool protocol_bitcoind_blockchain::wait_done() const NOEXCEPT +bool protocol_bitcoind_blockchain::handle_chase(const code&, + node::chase event_, node::event_value) NOEXCEPT { - const auto& query = archive(); - switch (wait_) + // Do not pass ec to stopped, it is not a call status. + if (stopped()) + return false; + + switch (event_) { - case wait::new_block: - case wait::height: - return query.get_top_confirmed() >= wait_height_; - case wait::block: + case node::chase::organized: + case node::chase::reorganized: { - const auto link = query.to_header(wait_hash_); - return !link.is_terminal() && query.is_confirmed_block(link); + POST(do_wait_event); + break; } default: - return false; + break; } -} -void protocol_bitcoind_blockchain::send_top() NOEXCEPT -{ - const auto& query = archive(); - const auto top = query.get_top_confirmed(); - send_result(object_t - { - { "hash", encode_hash(query.get_header_key(query.to_confirmed(top))) }, - { "height", top } - }, 128); + return true; } +// Wait machinery (strand). +// ---------------------------------------------------------------------------- + void protocol_bitcoind_blockchain::arm_wait(double timeout) NOEXCEPT { if (wait_done()) @@ -1828,93 +1888,33 @@ void protocol_bitcoind_blockchain::handle_wait_timeout(const code& ec) NOEXCEPT send_top(); } -// Chase events. -// ---------------------------------------------------------------------------- - -bool protocol_bitcoind_blockchain::handle_chase(const code&, - node::chase event_, node::event_value) NOEXCEPT +bool protocol_bitcoind_blockchain::wait_done() const NOEXCEPT { - // Do not pass ec to stopped, it is not a call status. - if (stopped()) - return false; - - switch (event_) + const auto& query = archive(); + switch (wait_) { - case node::chase::organized: - case node::chase::reorganized: + case wait::new_block: + case wait::height: + return query.get_top_confirmed() >= wait_height_; + case wait::block: { - POST(do_wait_event); - break; + const auto link = query.to_header(wait_hash_); + return !link.is_terminal() && query.is_confirmed_block(link); } default: - break; + return false; } - - return true; -} - -bool protocol_bitcoind_blockchain::handle_get_mempool_ancestors(const code& ec, - rpc_interface::get_mempool_ancestors) NOEXCEPT -{ - if (stopped(ec)) return false; - send_error(error::bitcoind::client_mempool_disabled); - return true; } -bool protocol_bitcoind_blockchain::handle_get_mempool_cluster(const code& ec, - rpc_interface::get_mempool_cluster) NOEXCEPT -{ - if (stopped(ec)) return false; - send_error(error::bitcoind::client_mempool_disabled); - return true; -} - -bool protocol_bitcoind_blockchain::handle_get_mempool_descendants(const code& ec, - rpc_interface::get_mempool_descendants) NOEXCEPT -{ - if (stopped(ec)) return false; - send_error(error::bitcoind::client_mempool_disabled); - return true; -} - -bool protocol_bitcoind_blockchain::handle_get_mempool_entry(const code& ec, - rpc_interface::get_mempool_entry) NOEXCEPT -{ - if (stopped(ec)) return false; - send_error(error::bitcoind::client_mempool_disabled); - return true; -} - -bool protocol_bitcoind_blockchain::handle_get_mempool_info(const code& ec, - rpc_interface::get_mempool_info) NOEXCEPT -{ - if (stopped(ec)) return false; - send_error(error::bitcoind::client_mempool_disabled); - return true; -} - -bool protocol_bitcoind_blockchain::handle_get_raw_mempool(const code& ec, - rpc_interface::get_raw_mempool) NOEXCEPT -{ - if (stopped(ec)) return false; - send_error(error::bitcoind::client_mempool_disabled); - return true; -} - -bool protocol_bitcoind_blockchain::handle_get_tx_spending_prevout(const code& ec, - rpc_interface::get_tx_spending_prevout) NOEXCEPT -{ - if (stopped(ec)) return false; - send_error(error::bitcoind::client_mempool_disabled); - return true; -} - -bool protocol_bitcoind_blockchain::handle_import_mempool(const code& ec, - rpc_interface::import_mempool) NOEXCEPT +void protocol_bitcoind_blockchain::send_top() NOEXCEPT { - if (stopped(ec)) return false; - send_error(error::bitcoind::client_mempool_disabled); - return true; + const auto& query = archive(); + const auto top = query.get_top_confirmed(); + send_result(object_t + { + { "hash", encode_hash(query.get_header_key(query.to_confirmed(top))) }, + { "height", top } + }, 128); } BC_POP_WARNING() diff --git a/src/protocols/bitcoind/protocol_bitcoind_json.cpp b/src/protocols/bitcoind/protocol_bitcoind_json.cpp index 5211d2fb..46b83cf2 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_json.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_json.cpp @@ -1,4 +1,4 @@ -/** +/** * Copyright (c) 2011-2026 libbitcoin developers * * This file is part of libbitcoin. @@ -28,6 +28,13 @@ namespace server { using namespace system; +// Clamped ratio of validated blocks to chain height. +double protocol_bitcoind::progress(size_t blocks, size_t headers) NOEXCEPT +{ + return is_zero(headers) ? 1.0 : + std::min(1.0, to_floating(blocks) / headers); +} + // bitcoind's mtp window includes the block's own timestamp (reproduced). uint32_t protocol_bitcoind::median_time_past(const node::query& query, const database::header_link& link) NOEXCEPT @@ -50,13 +57,6 @@ uint32_t protocol_bitcoind::median_time_past(const node::query& query, return times.empty() ? 0_u32 : times.at(to_half(times.size())); } -// Clamped ratio of validated blocks to chain height. -double protocol_bitcoind::progress(size_t blocks, size_t headers) NOEXCEPT -{ - return is_zero(headers) ? 1.0 : - std::min(1.0, to_floating(blocks) / headers); -} - // A getchainstates entry for candidate or confirmed at the link (top). network::rpc::object_t protocol_bitcoind::chain_states_entry( const node::query& query, const database::header_link& link, @@ -116,6 +116,29 @@ void protocol_bitcoind::inject_block_context(boost::json::object& out, query.get_header_key(query.to_confirmed(add1(height)))); } +void protocol_bitcoind::inject_tx_context(boost::json::object& out, + const node::query& query, const database::tx_link& link) NOEXCEPT +{ + size_t height{}; + if (!query.get_tx_height(height, link)) + { + out["confirmations"] = zero; + return; + } + + const auto block = query.to_confirmed(height); + const auto top = query.get_top_confirmed(); + const auto header = query.get_header(block); + out["blockhash"] = encode_hash(query.get_header_key(block)); + out["confirmations"] = add1(floored_subtract(top, height)); + out["in_active_chain"] = true; + if (header) + { + out["blocktime"] = header->timestamp(); + out["time"] = header->timestamp(); + } +} + // The tx must be populated (populate_without_metadata). void protocol_bitcoind::inject_tx_prevouts(boost::json::object& out, const node::query& query, const chain::transaction& tx) NOEXCEPT @@ -143,29 +166,6 @@ void protocol_bitcoind::inject_tx_prevouts(boost::json::object& out, }); } -void protocol_bitcoind::inject_tx_context(boost::json::object& out, - const node::query& query, const database::tx_link& link) NOEXCEPT -{ - size_t height{}; - if (!query.get_tx_height(height, link)) - { - out["confirmations"] = zero; - return; - } - - const auto block = query.to_confirmed(height); - const auto top = query.get_top_confirmed(); - const auto header = query.get_header(block); - out["blockhash"] = encode_hash(query.get_header_key(block)); - out["confirmations"] = add1(floored_subtract(top, height)); - out["in_active_chain"] = true; - if (header) - { - out["blocktime"] = header->timestamp(); - out["time"] = header->timestamp(); - } -} - boost::json::object protocol_bitcoind::header_to_bitcoind( const chain::header& header) NOEXCEPT { @@ -207,6 +207,43 @@ std::string protocol_bitcoind::chain_name(const node::query& query) NOEXCEPT return "unknown"; } +// Shared by the bitcoind blockchain subgroup and the btcd endpoint, which +// augments the result with bip9_softforks (required by lnd). +bool protocol_bitcoind::chain_info(network::rpc::object_t& out, + const node::query& query, bool pruned, bool current) NOEXCEPT +{ + const auto blocks = query.get_top_confirmed(); + const auto headers = query.get_top_candidate(); + const auto link = query.to_confirmed(blocks); + const auto header = query.get_header(link); + + uint256_t work{}; + if (!header || !query.get_branch_work(work, link)) + return false; + + const auto bits = header->bits(); + out = network::rpc::object_t + { + // bitcoind OB1 error ("blocks" wants height). + { "chain", chain_name(query) }, + { "blocks", blocks }, + { "headers", headers }, + { "bestblockhash", encode_hash(query.get_header_key(link)) }, + { "bits", encode_base16(to_big_endian(bits)) }, + { "target", encode_hash(from_uintx(chain::compact::expand(bits))) }, + { "difficulty", header->difficulty() }, + { "time", header->timestamp() }, + { "mediantime", median_time_past(query, link) }, + { "verificationprogress", progress(blocks, headers) }, + { "initialblockdownload", !current }, + { "chainwork", encode_hash(from_uintx(work)) }, + { "size_on_disk", query.store_size() }, + { "pruned", pruned }, + { "warnings", network::rpc::array_t{} } + }; + + return true; +} // The createmultisig result, empty if a key is invalid or the p2sh embedded // script exceeds one push element. An uncompressed key downgrades a segwit // address type to legacy with a warning (as bitcoind). @@ -336,43 +373,5 @@ std::string protocol_bitcoind::infer_descriptor( return body + "#" + descriptor_checksum(body); } -// Shared by the bitcoind blockchain subgroup and the btcd endpoint, which -// augments the result with bip9_softforks (required by lnd). -bool protocol_bitcoind::chain_info(network::rpc::object_t& out, - const node::query& query, bool pruned, bool current) NOEXCEPT -{ - const auto blocks = query.get_top_confirmed(); - const auto headers = query.get_top_candidate(); - const auto link = query.to_confirmed(blocks); - const auto header = query.get_header(link); - - uint256_t work{}; - if (!header || !query.get_branch_work(work, link)) - return false; - - const auto bits = header->bits(); - out = network::rpc::object_t - { - // bitcoind OB1 error ("blocks" wants height). - { "chain", chain_name(query) }, - { "blocks", blocks }, - { "headers", headers }, - { "bestblockhash", encode_hash(query.get_header_key(link)) }, - { "bits", encode_base16(to_big_endian(bits)) }, - { "target", encode_hash(from_uintx(chain::compact::expand(bits))) }, - { "difficulty", header->difficulty() }, - { "time", header->timestamp() }, - { "mediantime", median_time_past(query, link) }, - { "verificationprogress", progress(blocks, headers) }, - { "initialblockdownload", !current }, - { "chainwork", encode_hash(from_uintx(work)) }, - { "size_on_disk", query.store_size() }, - { "pruned", pruned }, - { "warnings", network::rpc::array_t{} } - }; - - return true; -} - } // namespace server } // namespace libbitcoin diff --git a/src/protocols/bitcoind/protocol_bitcoind_network.cpp b/src/protocols/bitcoind/protocol_bitcoind_network.cpp index 10e9dc8f..5e3a67d4 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_network.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_network.cpp @@ -1,4 +1,4 @@ -/** +/** * Copyright (c) 2011-2026 libbitcoin developers * * This file is part of libbitcoin. @@ -154,6 +154,92 @@ bool protocol_bitcoind_network::handle_get_network_info(const code& ec, return true; } +bool protocol_bitcoind_network::handle_clear_banned(const code& ec, + rpc_interface::clear_banned) NOEXCEPT +{ + if (stopped(ec)) return false; + send_error(error::bitcoind::database_error); + return true; +} + +bool protocol_bitcoind_network::handle_list_banned(const code& ec, + rpc_interface::list_banned) NOEXCEPT +{ + if (stopped(ec)) return false; + send_error(error::bitcoind::database_error); + return true; +} + +bool protocol_bitcoind_network::handle_set_ban(const code& ec, + rpc_interface::set_ban) NOEXCEPT +{ + if (stopped(ec)) return false; + send_error(error::bitcoind::database_error); + return true; +} + +// Removal requires manual session deregistration (not supported), and the +// transport is determined by the outbound privacy configuration. +bool protocol_bitcoind_network::handle_add_node(const code& ec, + rpc_interface::add_node, const std::string& node, + const std::string& command, bool v2transport) NOEXCEPT +{ + if (stopped(ec)) + return false; + + // bitcoind reports v2transport as invalid when not enabled. + if (v2transport) + { + send_error(error::bitcoind::invalid_parameter); + return true; + } + + if (command != "add" && command != "onetry") + { + send_error(command == "remove" ? error::bitcoind::client_node_not_added : + error::bitcoind::misc_error); + return true; + } + + // The endpoint parse throws on malformed input. + try + { + connect(network::config::endpoint{ node }); + } + catch (const std::exception&) + { + send_error(error::bitcoind::invalid_parameter); + return true; + } + + send_result(null_t{}, 8); + return true; +} + +bool protocol_bitcoind_network::handle_disconnect_node(const code& ec, + rpc_interface::disconnect_node) NOEXCEPT +{ + if (stopped(ec)) return false; + send_error(error::bitcoind::method_not_found); + return true; +} + +bool protocol_bitcoind_network::handle_export_asmap(const code& ec, + rpc_interface::export_asmap) NOEXCEPT +{ + if (stopped(ec)) return false; + send_error(error::bitcoind::method_not_found); + return true; +} + +bool protocol_bitcoind_network::handle_get_added_node_info(const code& ec, + rpc_interface::get_added_node_info) NOEXCEPT +{ + if (stopped(ec)) return false; + send_error(error::bitcoind::client_node_not_added); + return true; +} + // The pool has no tried table, so all addresses are reported as new. static object_t address_bucket(size_t count) NOEXCEPT { @@ -301,92 +387,6 @@ bool protocol_bitcoind_network::handle_set_network_active(const code& ec, return true; } -bool protocol_bitcoind_network::handle_clear_banned(const code& ec, - rpc_interface::clear_banned) NOEXCEPT -{ - if (stopped(ec)) return false; - send_error(error::bitcoind::database_error); - return true; -} - -bool protocol_bitcoind_network::handle_list_banned(const code& ec, - rpc_interface::list_banned) NOEXCEPT -{ - if (stopped(ec)) return false; - send_error(error::bitcoind::database_error); - return true; -} - -bool protocol_bitcoind_network::handle_set_ban(const code& ec, - rpc_interface::set_ban) NOEXCEPT -{ - if (stopped(ec)) return false; - send_error(error::bitcoind::database_error); - return true; -} - -// Removal requires manual session deregistration (not supported), and the -// transport is determined by the outbound privacy configuration. -bool protocol_bitcoind_network::handle_add_node(const code& ec, - rpc_interface::add_node, const std::string& node, - const std::string& command, bool v2transport) NOEXCEPT -{ - if (stopped(ec)) - return false; - - // bitcoind reports v2transport as invalid when not enabled. - if (v2transport) - { - send_error(error::bitcoind::invalid_parameter); - return true; - } - - if (command != "add" && command != "onetry") - { - send_error(command == "remove" ? error::bitcoind::client_node_not_added : - error::bitcoind::misc_error); - return true; - } - - // The endpoint parse throws on malformed input. - try - { - connect(network::config::endpoint{ node }); - } - catch (const std::exception&) - { - send_error(error::bitcoind::invalid_parameter); - return true; - } - - send_result(null_t{}, 8); - return true; -} - -bool protocol_bitcoind_network::handle_disconnect_node(const code& ec, - rpc_interface::disconnect_node) NOEXCEPT -{ - if (stopped(ec)) return false; - send_error(error::bitcoind::method_not_found); - return true; -} - -bool protocol_bitcoind_network::handle_export_asmap(const code& ec, - rpc_interface::export_asmap) NOEXCEPT -{ - if (stopped(ec)) return false; - send_error(error::bitcoind::method_not_found); - return true; -} - -bool protocol_bitcoind_network::handle_get_added_node_info(const code& ec, - rpc_interface::get_added_node_info) NOEXCEPT -{ - if (stopped(ec)) return false; - send_error(error::bitcoind::client_node_not_added); - return true; -} - // Peer channels only (client channels are not connections). bool protocol_bitcoind_network::handle_get_connection_count(const code& ec, rpc_interface::get_connection_count) NOEXCEPT diff --git a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp index 9a7a24c1..3850b1e1 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp @@ -1,4 +1,4 @@ -/** +/** * Copyright (c) 2011-2026 libbitcoin developers * * This file is part of libbitcoin. @@ -78,6 +78,176 @@ void protocol_bitcoind_transaction::start() NOEXCEPT // Raw transaction methods. // ---------------------------------------------------------------------------- +bool protocol_bitcoind_transaction::handle_create_raw_transaction( + const code& ec, rpc_interface::create_raw_transaction, + const array_t& inputs, const value_t& outputs, double locktime, + bool replaceable, double version) NOEXCEPT +{ + if (stopped(ec)) + return false; + + chain::transaction tx{}; + if (const auto fault = build_transaction(tx, inputs, outputs, locktime, + replaceable, version)) + { + send_error(fault); + return true; + } + + constexpr auto witness = false; + send_result(to_text(tx, tx.serialized_size(witness), witness), 400); + return true; +} + +// Shared by createrawtransaction and createpsbt. +code protocol_bitcoind_transaction::build_transaction(chain::transaction& out, + const array_t& inputs, const value_t& outputs, double locktime, + bool replaceable, double version) const NOEXCEPT +{ + uint32_t lock_time{}; + if (!to_integer(lock_time, locktime)) + return error::bitcoind::invalid_parameter; + + // bitcoind bounds the version to the maximum standard (currently 3). + uint32_t tx_version{}; + if (!to_integer(tx_version, version) || is_zero(tx_version) || + (tx_version > 3u)) + return error::bitcoind::invalid_parameter; + + using namespace chain; + const auto sequence = replaceable ? messages::peer::bip125_sequence : + (is_zero(lock_time) ? max_input_sequence : sub1(max_input_sequence)); + + const auto ins = to_shared(); + ins->reserve(inputs.size()); + hash_digest hash{}; + uint32_t vout{}; + + for (const auto& item: inputs) + { + if (!std::holds_alternative(item.value())) + return error::bitcoind::type_error; + + const auto& fields = std::get(item.value()); + const auto txid_it = fields.find("txid"); + const auto vout_it = fields.find("vout"); + if (txid_it == fields.end() || vout_it == fields.end() || + !std::holds_alternative(txid_it->second.value()) || + !std::holds_alternative(vout_it->second.value())) + return error::bitcoind::invalid_parameter; + + if (!decode_hash(hash, std::get(txid_it->second.value())) || + !to_integer(vout, std::get(vout_it->second.value()))) + return error::bitcoind::invalid_parameter; + + // An explicit sequence overrides the derived default. + auto sequenced = sequence; + const auto sequence_it = fields.find("sequence"); + if (sequence_it != fields.end() && + (!std::holds_alternative(sequence_it->second.value()) || + !to_integer(sequenced, + std::get(sequence_it->second.value())))) + return error::bitcoind::invalid_parameter; + + ins->push_back(to_shared(point{ hash, vout }, script{}, + sequenced)); + } + + script script{}; + uint64_t satoshi{}; + const auto outs = std::make_shared(); + + // Appends one address or data output from a name/value pair. + const auto append = [&](const std::string& name, + const value_t& item) NOEXCEPT -> code + { + // A data output carries a null data script and no value. + if (name == "data") + { + data_chunk data{}; + if (!std::holds_alternative(item.value()) || + !decode_base16(data, std::get(item.value())) || + data.size() > max_null_data_size) + return error::bitcoind::invalid_parameter; + + outs->push_back(to_shared(zero, + chain::script{ script::to_pay_null_data_pattern(data) })); + return error::bitcoind::success; + } + + if (!std::holds_alternative(item.value())) + return error::bitcoind::type_error; + + if (output_script(script, name, p2kh_, p2sh_, witness_)) + return error::bitcoind::invalid_address_or_key; + + const auto btc = std::get(item.value()); + if (!to_integer(satoshi, btc * satoshi_per_bitcoin, true) || + satoshi > system_settings().max_money()) + return error::bitcoind::type_error; + + outs->push_back(to_shared(satoshi, std::move(script))); + return error::bitcoind::success; + }; + + // outputs is one object or an array of objects (permits repeated address). + if (std::holds_alternative(outputs.value())) + { + for (const auto& pair: std::get(outputs.value())) + if (const auto fault = append(pair.first, pair.second)) + return fault; + } + else if (std::holds_alternative(outputs.value())) + { + for (const auto& element: std::get(outputs.value())) + { + if (!std::holds_alternative(element.value())) + return error::bitcoind::type_error; + + for (const auto& pair: std::get(element.value())) + if (const auto fault = append(pair.first, pair.second)) + return fault; + } + } + else + { + return error::bitcoind::type_error; + } + + out = { tx_version, ins, outs, lock_time }; + return error::bitcoind::success; +} + +bool protocol_bitcoind_transaction::handle_decode_raw_transaction(const code& ec, + rpc_interface::decode_raw_transaction, const std::string& hexstring, + const std::optional& iswitness) NOEXCEPT +{ + if (stopped(ec)) + return false; + + data_chunk data{}; + if (!decode_base16(data, hexstring)) + { + send_error(error::bitcoind::deserialization_error); + return true; + } + + // Absent the hint, witness deserialization is tried first (as bitcoind). + const auto witness = iswitness.value_or(true); + auto tx = chain::transaction{ data, witness }; + if (!iswitness.has_value() && !tx.is_valid()) + tx = chain::transaction{ data, false }; + + if (!tx.is_valid()) + { + send_error(error::bitcoind::deserialization_error); + return true; + } + + send_result(value_from(bitcoind(tx)), two * tx.serialized_size(true)); + return true; +} + // The hint is unused (not required). bool protocol_bitcoind_transaction::handle_get_raw_transaction(const code& ec, rpc_interface::get_raw_transaction, const std::string& txid, @@ -246,176 +416,6 @@ bool protocol_bitcoind_transaction::handle_test_mempool_accept(const code& ec, return true; } -// Shared by createrawtransaction and createpsbt. -code protocol_bitcoind_transaction::build_transaction(chain::transaction& out, - const array_t& inputs, const value_t& outputs, double locktime, - bool replaceable, double version) const NOEXCEPT -{ - uint32_t lock_time{}; - if (!to_integer(lock_time, locktime)) - return error::bitcoind::invalid_parameter; - - // bitcoind bounds the version to the maximum standard (currently 3). - uint32_t tx_version{}; - if (!to_integer(tx_version, version) || is_zero(tx_version) || - (tx_version > 3u)) - return error::bitcoind::invalid_parameter; - - using namespace chain; - const auto sequence = replaceable ? messages::peer::bip125_sequence : - (is_zero(lock_time) ? max_input_sequence : sub1(max_input_sequence)); - - const auto ins = to_shared(); - ins->reserve(inputs.size()); - hash_digest hash{}; - uint32_t vout{}; - - for (const auto& item: inputs) - { - if (!std::holds_alternative(item.value())) - return error::bitcoind::type_error; - - const auto& fields = std::get(item.value()); - const auto txid_it = fields.find("txid"); - const auto vout_it = fields.find("vout"); - if (txid_it == fields.end() || vout_it == fields.end() || - !std::holds_alternative(txid_it->second.value()) || - !std::holds_alternative(vout_it->second.value())) - return error::bitcoind::invalid_parameter; - - if (!decode_hash(hash, std::get(txid_it->second.value())) || - !to_integer(vout, std::get(vout_it->second.value()))) - return error::bitcoind::invalid_parameter; - - // An explicit sequence overrides the derived default. - auto sequenced = sequence; - const auto sequence_it = fields.find("sequence"); - if (sequence_it != fields.end() && - (!std::holds_alternative(sequence_it->second.value()) || - !to_integer(sequenced, - std::get(sequence_it->second.value())))) - return error::bitcoind::invalid_parameter; - - ins->push_back(to_shared(point{ hash, vout }, script{}, - sequenced)); - } - - script script{}; - uint64_t satoshi{}; - const auto outs = std::make_shared(); - - // Appends one address or data output from a name/value pair. - const auto append = [&](const std::string& name, - const value_t& item) NOEXCEPT -> code - { - // A data output carries a null data script and no value. - if (name == "data") - { - data_chunk data{}; - if (!std::holds_alternative(item.value()) || - !decode_base16(data, std::get(item.value())) || - data.size() > max_null_data_size) - return error::bitcoind::invalid_parameter; - - outs->push_back(to_shared(zero, - chain::script{ script::to_pay_null_data_pattern(data) })); - return error::bitcoind::success; - } - - if (!std::holds_alternative(item.value())) - return error::bitcoind::type_error; - - if (output_script(script, name, p2kh_, p2sh_, witness_)) - return error::bitcoind::invalid_address_or_key; - - const auto btc = std::get(item.value()); - if (!to_integer(satoshi, btc * satoshi_per_bitcoin, true) || - satoshi > system_settings().max_money()) - return error::bitcoind::type_error; - - outs->push_back(to_shared(satoshi, std::move(script))); - return error::bitcoind::success; - }; - - // outputs is one object or an array of objects (permits repeated address). - if (std::holds_alternative(outputs.value())) - { - for (const auto& pair: std::get(outputs.value())) - if (const auto fault = append(pair.first, pair.second)) - return fault; - } - else if (std::holds_alternative(outputs.value())) - { - for (const auto& element: std::get(outputs.value())) - { - if (!std::holds_alternative(element.value())) - return error::bitcoind::type_error; - - for (const auto& pair: std::get(element.value())) - if (const auto fault = append(pair.first, pair.second)) - return fault; - } - } - else - { - return error::bitcoind::type_error; - } - - out = { tx_version, ins, outs, lock_time }; - return error::bitcoind::success; -} - -bool protocol_bitcoind_transaction::handle_create_raw_transaction( - const code& ec, rpc_interface::create_raw_transaction, - const array_t& inputs, const value_t& outputs, double locktime, - bool replaceable, double version) NOEXCEPT -{ - if (stopped(ec)) - return false; - - chain::transaction tx{}; - if (const auto fault = build_transaction(tx, inputs, outputs, locktime, - replaceable, version)) - { - send_error(fault); - return true; - } - - constexpr auto witness = false; - send_result(to_text(tx, tx.serialized_size(witness), witness), 400); - return true; -} - -bool protocol_bitcoind_transaction::handle_decode_raw_transaction(const code& ec, - rpc_interface::decode_raw_transaction, const std::string& hexstring, - const std::optional& iswitness) NOEXCEPT -{ - if (stopped(ec)) - return false; - - data_chunk data{}; - if (!decode_base16(data, hexstring)) - { - send_error(error::bitcoind::deserialization_error); - return true; - } - - // Absent the hint, witness deserialization is tried first (as bitcoind). - const auto witness = iswitness.value_or(true); - auto tx = chain::transaction{ data, witness }; - if (!iswitness.has_value() && !tx.is_valid()) - tx = chain::transaction{ data, false }; - - if (!tx.is_valid()) - { - send_error(error::bitcoind::deserialization_error); - return true; - } - - send_result(value_from(bitcoind(tx)), two * tx.serialized_size(true)); - return true; -} - // PSBT methods. // ---------------------------------------------------------------------------- @@ -585,78 +585,6 @@ static object_t decode_psbt_output(const wallet::psbt::output& out) NOEXCEPT return entry; } -bool protocol_bitcoind_transaction::handle_decode_psbt(const code& ec, - rpc_interface::decode_psbt, const std::string& psbt) NOEXCEPT -{ - if (stopped(ec)) - return false; - - const psbt_tx doc(psbt); - if (!doc) - { - send_error(error::bitcoind::deserialization_error); - return true; - } - - object_t result{}; - const auto version0 = (doc.version() == psbt_tx::version_0); - if (version0) - result.emplace("tx", value_from(bitcoind(doc.unsigned_tx()))); - - if (!doc.xpubs().empty()) - { - array_t xpubs{}; - for (const auto& key: doc.xpubs()) - { - auto checked = key.key; - append_checksum(checked); - xpubs.emplace_back(object_t - { - { "xpub", encode_base58(checked) }, - { "master_fingerprint", encode_base16(to_little_endian( - key.origin.fingerprint)) }, - { "path", to_key_path(key.origin.path) } - }); - } - - result.emplace("global_xpubs", std::move(xpubs)); - } - - result.emplace("psbt_version", doc.version()); - - if (!version0) - { - result.emplace("tx_version", doc.tx_version()); - if (doc.fallback_locktime().has_value()) - result.emplace("fallback_locktime", - doc.fallback_locktime().value()); - - if (doc.tx_modifiable().has_value()) - result.emplace("tx_modifiable", doc.tx_modifiable().value()); - } - - if (!doc.others().empty()) - result.emplace("unknown", to_unknown(doc.others())); - - array_t ins{}; - for (const auto& in: doc.inputs()) - ins.emplace_back(decode_psbt_input(in)); - - array_t outs{}; - for (const auto& out: doc.outputs()) - outs.emplace_back(decode_psbt_output(out)); - - result.emplace("inputs", std::move(ins)); - result.emplace("outputs", std::move(outs)); - - if (const auto fee = doc.fee(); fee.has_value()) - result.emplace("fee", fee.value() / - to_floating(chain::satoshi_per_bitcoin)); - - send_result(std::move(result), 2048); - return true; -} - bool protocol_bitcoind_transaction::handle_analyze_psbt(const code& ec, rpc_interface::analyze_psbt, const std::string& psbt) NOEXCEPT { @@ -883,6 +811,78 @@ bool protocol_bitcoind_transaction::handle_create_psbt(const code& ec, return true; } +bool protocol_bitcoind_transaction::handle_decode_psbt(const code& ec, + rpc_interface::decode_psbt, const std::string& psbt) NOEXCEPT +{ + if (stopped(ec)) + return false; + + const psbt_tx doc(psbt); + if (!doc) + { + send_error(error::bitcoind::deserialization_error); + return true; + } + + object_t result{}; + const auto version0 = (doc.version() == psbt_tx::version_0); + if (version0) + result.emplace("tx", value_from(bitcoind(doc.unsigned_tx()))); + + if (!doc.xpubs().empty()) + { + array_t xpubs{}; + for (const auto& key: doc.xpubs()) + { + auto checked = key.key; + append_checksum(checked); + xpubs.emplace_back(object_t + { + { "xpub", encode_base58(checked) }, + { "master_fingerprint", encode_base16(to_little_endian( + key.origin.fingerprint)) }, + { "path", to_key_path(key.origin.path) } + }); + } + + result.emplace("global_xpubs", std::move(xpubs)); + } + + result.emplace("psbt_version", doc.version()); + + if (!version0) + { + result.emplace("tx_version", doc.tx_version()); + if (doc.fallback_locktime().has_value()) + result.emplace("fallback_locktime", + doc.fallback_locktime().value()); + + if (doc.tx_modifiable().has_value()) + result.emplace("tx_modifiable", doc.tx_modifiable().value()); + } + + if (!doc.others().empty()) + result.emplace("unknown", to_unknown(doc.others())); + + array_t ins{}; + for (const auto& in: doc.inputs()) + ins.emplace_back(decode_psbt_input(in)); + + array_t outs{}; + for (const auto& out: doc.outputs()) + outs.emplace_back(decode_psbt_output(out)); + + result.emplace("inputs", std::move(ins)); + result.emplace("outputs", std::move(outs)); + + if (const auto fee = doc.fee(); fee.has_value()) + result.emplace("fee", fee.value() / + to_floating(chain::satoshi_per_bitcoin)); + + send_result(std::move(result), 2048); + return true; +} + bool protocol_bitcoind_transaction::handle_finalize_psbt(const code& ec, rpc_interface::finalize_psbt, const std::string& psbt, bool extract) NOEXCEPT From 9f34ce86202c8771ad6ceab3306667c8bca1b648 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 11:01:13 -0400 Subject: [PATCH 20/23] Note bitcoind quoted-amount input deviation. --- include/bitcoin/server/interfaces/bitcoind_transaction.hpp | 1 + src/protocols/bitcoind/protocol_bitcoind_transaction.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/include/bitcoin/server/interfaces/bitcoind_transaction.hpp b/include/bitcoin/server/interfaces/bitcoind_transaction.hpp index ce911c2c..35701945 100644 --- a/include/bitcoin/server/interfaces/bitcoind_transaction.hpp +++ b/include/bitcoin/server/interfaces/bitcoind_transaction.hpp @@ -33,6 +33,7 @@ struct bitcoind_transaction_methods method<"createrawtransaction", array_t, value_t, optional<0.0>, optional, optional<2.0>>{ "inputs", "outputs", "locktime", "replaceable", "version" }, method<"decoderawtransaction", string_t, nullable>{ "hexstring", "iswitness" }, method<"getrawtransaction", string_t, optional<0.0>, optional<""_t>>{ "txid", "verbosity", "blockhash" }, + // bitcoind also accepts quoted amount arguments (not jrpc compliant). method<"sendrawtransaction", string_t, optional<0.1>, optional<0.0>>{ "hexstring", "maxfeerate", "maxburnamount" }, method<"testmempoolaccept", array_t, optional<0.1>>{ "rawtxs", "maxfeerate" }, method<"analyzepsbt", string_t>{ "psbt" }, diff --git a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp index 3850b1e1..33bd8b31 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp @@ -175,6 +175,7 @@ code protocol_bitcoind_transaction::build_transaction(chain::transaction& out, return error::bitcoind::success; } + // bitcoind also accepts a quoted amount (not jrpc compliant). if (!std::holds_alternative(item.value())) return error::bitcoind::type_error; From e9cdb385ada92f046c8d6adc19baebe38685647c Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 11:14:32 -0400 Subject: [PATCH 21/23] Reword quoted-amount comments. --- include/bitcoin/server/interfaces/bitcoind_transaction.hpp | 2 +- src/protocols/bitcoind/protocol_bitcoind_transaction.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/bitcoin/server/interfaces/bitcoind_transaction.hpp b/include/bitcoin/server/interfaces/bitcoind_transaction.hpp index 35701945..ebc257c5 100644 --- a/include/bitcoin/server/interfaces/bitcoind_transaction.hpp +++ b/include/bitcoin/server/interfaces/bitcoind_transaction.hpp @@ -33,7 +33,7 @@ struct bitcoind_transaction_methods method<"createrawtransaction", array_t, value_t, optional<0.0>, optional, optional<2.0>>{ "inputs", "outputs", "locktime", "replaceable", "version" }, method<"decoderawtransaction", string_t, nullable>{ "hexstring", "iswitness" }, method<"getrawtransaction", string_t, optional<0.0>, optional<""_t>>{ "txid", "verbosity", "blockhash" }, - // bitcoind also accepts quoted amount arguments (not jrpc compliant). + // bitcoind also accepts quoted amounts (slop; needs dispatch special case). method<"sendrawtransaction", string_t, optional<0.1>, optional<0.0>>{ "hexstring", "maxfeerate", "maxburnamount" }, method<"testmempoolaccept", array_t, optional<0.1>>{ "rawtxs", "maxfeerate" }, method<"analyzepsbt", string_t>{ "psbt" }, diff --git a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp index 33bd8b31..68906ba2 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp @@ -175,7 +175,7 @@ code protocol_bitcoind_transaction::build_transaction(chain::transaction& out, return error::bitcoind::success; } - // bitcoind also accepts a quoted amount (not jrpc compliant). + // bitcoind also accepts a quoted amount (slop; not special-cased). if (!std::holds_alternative(item.value())) return error::bitcoind::type_error; From d3e85548a3b0a29eb028c86b35f30e22f4d8c9b7 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 11:22:08 -0400 Subject: [PATCH 22/23] Report per-tx fee at getblock verbosity two. --- .../bitcoind/protocol_bitcoind_blockchain.cpp | 6 ++++-- test/protocols/bitcoind/bitcoind_rpc.cpp | 11 +++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp index 02e05dcd..c29cb011 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp @@ -190,7 +190,7 @@ bool protocol_bitcoind_blockchain::handle_get_block(const code& ec, inject_block_context(model.as_object(), query, link, block->header()); - if (level == block_verbosity::prevouts && + if (level >= block_verbosity::verbose && query.populate_without_metadata(*block)) { auto entry = model.as_object().at("tx").as_array().begin(); @@ -199,7 +199,9 @@ bool protocol_bitcoind_blockchain::handle_get_block(const code& ec, { if (!tx->is_coinbase()) { - inject_tx_prevouts(entry->as_object(), query, *tx); + if (level == block_verbosity::prevouts) + inject_tx_prevouts(entry->as_object(), query, *tx); + entry->as_object()["fee"] = tx->fee() / to_floating(chain::satoshi_per_bitcoin); } diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index af5bfd9f..ced8ab52 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -253,6 +253,17 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblock__block9_verbosity3__tx_objects) BOOST_REQUIRE(!tx.at(0).as_object().contains("fee")); } +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblock__spend_verbosity2__fee) +{ + 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)); + + const auto response = rpc("getblock", hash_param(test::mock_block11.hash(), "2")); + const auto& tx = response.at("result").at("tx"); + BOOST_REQUIRE(tx.at(0).as_object().contains("fee")); + BOOST_REQUIRE(!tx.at(0).at("vin").at(0).as_object().contains("prevout")); +} + BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblock__verbosity4__clamped_tx_objects) { const auto response = rpc("getblock", hash_param(test::block9_hash, "4")); From f2dc6fbb14133f8a99577d94cca142032892d81f Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 12:26:12 -0400 Subject: [PATCH 23/23] Suppress response objects for v2 notifications. --- src/protocols/bitcoind/protocol_bitcoind.cpp | 34 +++++++++++++++++-- test/protocols/bitcoind/bitcoind_rpc.cpp | 27 +++++++++++++++ .../bitcoind/bitcoind_setup_fixture.cpp | 22 ++++++++++++ .../bitcoind/bitcoind_setup_fixture.hpp | 6 ++++ 4 files changed, 86 insertions(+), 3 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind.cpp b/src/protocols/bitcoind/protocol_bitcoind.cpp index 3e492945..02ea943a 100644 --- a/src/protocols/bitcoind/protocol_bitcoind.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind.cpp @@ -121,9 +121,9 @@ void protocol_bitcoind::handle_receive_post(const code& ec, return; } - // Get the parsed json-rpc request object. - // v1 or v2 both supported, batch not yet supported. - // v1 null id and v2 missing id implies notification and no response. + // Get the parsed json-rpc request object (v1 or v2, singleton or batch). + // v2 missing id is a notification (no response object is sent). + // v1 null id is also a notification, but bitcoind answers (non-compliant). const auto& message = post->body().get().message; // Cache request context for response building (version + id). @@ -270,10 +270,26 @@ void protocol_bitcoind::send_rpc(response_t&& model, size_t size_hint, using namespace http; static const auto json = from_media_type(media_type::application_json); + // A v2 request without an id is a notification (no response object). + const auto notification = (model.jsonrpc == version::v2) && + !model.id.has_value(); + if (websocket()) { id_.reset(); version_ = version::undefined; + + // An unsent response does not restart the read cycle, so resume it. + if (notification) + { + if (close_reason) + stop(close_reason); + else + network::protocol::resume(); + + return; + } + http::response message{ status::ok, 11 }; message.set(field::content_type, json); message.body() = rpc::response @@ -286,6 +302,18 @@ void protocol_bitcoind::send_rpc(response_t&& model, size_t size_hint, } const auto request = reset_rpc_request(); + const auto& body = request->body().get(); + + // A batched notification is answered (response parts are sequenced). + if (notification && !body.batch && !body.changed) + { + http::response message{ status::no_content, request->version() }; + add_common_headers(message, *request); + add_access_control_headers(message, *request); + SEND(std::move(message), handle_complete, _1, close_reason); + return; + } + http::response message{ status::ok, request->version() }; add_common_headers(message, *request); add_access_control_headers(message, *request); diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index ced8ab52..716df47d 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -1202,6 +1202,33 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__batch__empty__dropped) REQUIRE_NO_THROW_TRUE(response.at("dropped").as_bool()); } +// notifications +// ---------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__notification__v2_missing_id__no_content) +{ + const auto result = rpc_body_status(R"({"jsonrpc":"2.0","method":"getblockcount","params":[]})"); + BOOST_REQUIRE(result == bitcoind_setup_fixture::status::no_content); +} + +// bitcoind answers a v1 null id notification (non-compliant, reproduced). +BOOST_AUTO_TEST_CASE(bitcoind_rpc__notification__v1_null_id__answered) +{ + const auto response = rpc_body(R"({"id":null,"method":"getblockcount","params":[]})"); + BOOST_REQUIRE(response.at("id").is_null()); + BOOST_REQUIRE_EQUAL(response.at("result").as_int64(), 9); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__notification__ws_v2_missing_id__no_response) +{ + BOOST_REQUIRE(!ws_upgrade()); + + ws_notify(R"({"jsonrpc":"2.0","method":"getblockcount","params":[]})"); + const auto response = ws_rpc("getblockcount"); + BOOST_REQUIRE_EQUAL(response.at("id").as_int64(), 0); + BOOST_REQUIRE_EQUAL(response.at("result").as_int64(), 9); +} + // websocket // ---------------------------------------------------------------------------- diff --git a/test/protocols/bitcoind/bitcoind_setup_fixture.cpp b/test/protocols/bitcoind/bitcoind_setup_fixture.cpp index 5208d5d9..0667317e 100644 --- a/test/protocols/bitcoind/bitcoind_setup_fixture.cpp +++ b/test/protocols/bitcoind/bitcoind_setup_fixture.cpp @@ -162,6 +162,19 @@ boost::json::value bitcoind_setup_fixture::rpc_body(std::string_view body) test::parse_json(response.body()); } +bitcoind_setup_fixture::status +bitcoind_setup_fixture::rpc_body_status(std::string_view body) +{ + http::write(socket_, create_post("/", body)); + + 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(); +} + bitcoind_setup_fixture::status bitcoind_setup_fixture::rpc_status(std::string_view method, const std::string& username, const std::string& password) @@ -261,6 +274,15 @@ boost::json::value bitcoind_setup_fixture::ws_rpc_dropped( test::parse_json(buffers_to_string(buffer.data())); } +void bitcoind_setup_fixture::ws_notify(std::string_view body) +{ + const std::string frame{ body }; + network::boost_code ec{}; + BOOST_CHECK(websocket_.has_value()); + websocket_.value().write(net::buffer(frame), ec); + BOOST_CHECK_MESSAGE(!ec, ec.message()); +} + 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 891bc927..bd240dbc 100644 --- a/test/protocols/bitcoind/bitcoind_setup_fixture.hpp +++ b/test/protocols/bitcoind/bitcoind_setup_fixture.hpp @@ -46,6 +46,9 @@ struct bitcoind_setup_fixture // the parsed json response, or {"dropped":true} if the channel dropped. boost::json::value rpc_body(std::string_view body); + // As rpc_body(), returning only the http status. + status rpc_body_status(std::string_view body); + // As rpc(), with basic authorization, returning only the http status. status rpc_status(std::string_view method, const std::string& username, const std::string& password); @@ -65,6 +68,9 @@ struct bitcoind_setup_fixture boost::json::value ws_rpc_dropped(std::string_view method, std::string_view params="[]"); + // Write a raw frame over the upgraded websocket without reading. + void ws_notify(std::string_view body); + // bitcoind REST over HTTP GET (target under "/rest/..."). status rest_status(std::string_view target);