From accb1c59719a855b413be7eb8c5d39ee921521e3 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 19 Aug 2026 15:02:28 -0400 Subject: [PATCH 01/21] Support data outputs in transaction construction. --- .../protocol_bitcoind_transaction.hpp | 6 ++ .../protocol_bitcoind_transaction.cpp | 77 +++++++++++-------- test/protocols/bitcoind/bitcoind_rpc.cpp | 11 +++ 3 files changed, 60 insertions(+), 34 deletions(-) diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_transaction.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_transaction.hpp index 4e7a202a..9209a5e9 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_transaction.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_transaction.hpp @@ -96,6 +96,12 @@ 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; + + /// 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; }; } // namespace server diff --git a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp index ed01318b..6d9e272a 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp @@ -238,20 +238,14 @@ bool protocol_bitcoind_transaction::handle_test_mempool_accept(const code& ec, return true; } -bool protocol_bitcoind_transaction::handle_create_raw_transaction( - const code& ec, rpc_interface::create_raw_transaction, +// 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) NOEXCEPT + bool replaceable) const NOEXCEPT { - if (stopped(ec)) - return false; - uint32_t lock_time{}; if (!to_integer(lock_time, locktime)) - { - send_error(error::invalid_argument); - return true; - } + return error::invalid_argument; using namespace chain; const auto sequence = replaceable ? messages::peer::bip125_sequence : @@ -265,10 +259,7 @@ bool protocol_bitcoind_transaction::handle_create_raw_transaction( for (const auto& item: inputs) { if (!std::holds_alternative(item.value())) - { - send_error(error::invalid_argument); - return true; - } + return error::invalid_argument; const auto& fields = std::get(item.value()); const auto txid_it = fields.find("txid"); @@ -276,17 +267,11 @@ bool protocol_bitcoind_transaction::handle_create_raw_transaction( if (txid_it == fields.end() || vout_it == fields.end() || !std::holds_alternative(txid_it->second.value()) || !std::holds_alternative(vout_it->second.value())) - { - send_error(error::invalid_argument); - return true; - } + return error::invalid_argument; if (!decode_hash(hash, std::get(txid_it->second.value())) || !to_integer(vout, std::get(vout_it->second.value()))) - { - send_error(error::invalid_argument); - return true; - } + return error::invalid_argument; ins->push_back(to_shared(point{ hash, vout }, script{}, sequence)); } @@ -297,31 +282,55 @@ bool protocol_bitcoind_transaction::handle_create_raw_transaction( outs->reserve(outputs.size()); for (const auto& pair: outputs) { - if (!std::holds_alternative(pair.second.value())) + // A data output carries a null data script and no value. + if (pair.first == "data") { - send_error(error::invalid_argument); - return true; + data_chunk data{}; + if (!std::holds_alternative(pair.second.value()) || + !decode_base16(data, std::get(pair.second.value())) || + data.size() > max_null_data_size) + return error::invalid_argument; + + outs->push_back(to_shared(zero, + chain::script{ script::to_pay_null_data_pattern(data) })); + continue; } + if (!std::holds_alternative(pair.second.value())) + return error::invalid_argument; + if (const auto fault = output_script(script, pair.first, p2kh_, p2sh_, witness_)) - { - send_error(fault); - return true; - } + return fault; const auto btc = std::get(pair.second.value()); if (!to_integer(satoshi, btc * satoshi_per_bitcoin, false)) - { - send_error(error::invalid_argument); - return true; - } + return error::invalid_argument; outs->push_back(to_shared(satoshi, std::move(script))); } + out = { 1, ins, outs, lock_time }; + return error::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 +{ + if (stopped(ec)) + return false; + + chain::transaction tx{}; + if (const auto fault = build_transaction(tx, inputs, outputs, locktime, + replaceable)) + { + send_error(fault); + return true; + } + constexpr auto witness = false; - const transaction tx{ 1, ins, outs, lock_time }; send_result(to_text(tx, tx.serialized_size(witness), witness), 400); return true; } diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index a520bb5d..060d9cd1 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -1046,6 +1046,17 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__response__websocket__id_matches_request) BOOST_REQUIRE_EQUAL(response.at("id").as_int64(), 0); } + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__createrawtransaction__data_output__op_return) +{ + const auto txid = encode_hash(test::block1.transactions_ptr()->front()->hash(false)); + const auto created = rpc("createrawtransaction", "[[{\"txid\":\"" + txid + "\",\"vout\":0}], {\"data\": \"deadbeef\"}]"); + const auto response = rpc("decoderawtransaction", "[\"" + as_text(created.at("result")) + "\"]"); + const auto& out = response.at("result").at("vout").at(0); + BOOST_REQUIRE_EQUAL(out.at("value").as_double(), 0.0); + BOOST_REQUIRE_EQUAL(as_text(out.at("scriptPubKey").at("hex")), "6a04deadbeef"); +} + BOOST_AUTO_TEST_SUITE_END() // websocket authorization From a9f17421b50b3c23a8ffb7418d6f8ca2101d555b Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 19 Aug 2026 15:02:29 -0400 Subject: [PATCH 02/21] Implement psbt methods over wallet::psbt. --- .../interfaces/bitcoind_transaction.hpp | 16 +- .../protocol_bitcoind_transaction.hpp | 35 +- .../protocol_bitcoind_transaction.cpp | 569 ++++++++++++++++-- test/interfaces/bitcoind.cpp | 4 +- test/protocols/bitcoind/bitcoind_rpc.cpp | 109 ++++ 5 files changed, 674 insertions(+), 59 deletions(-) diff --git a/include/bitcoin/server/interfaces/bitcoind_transaction.hpp b/include/bitcoin/server/interfaces/bitcoind_transaction.hpp index ba3c856a..c28591b4 100644 --- a/include/bitcoin/server/interfaces/bitcoind_transaction.hpp +++ b/include/bitcoin/server/interfaces/bitcoind_transaction.hpp @@ -35,15 +35,15 @@ struct bitcoind_transaction_methods 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<"analyzepsbt">{ unimplemented }, - method<"combinepsbt">{ unimplemented }, - method<"converttopsbt">{ unimplemented }, - method<"createpsbt">{ unimplemented }, - method<"decodepsbt">{ unimplemented }, - method<"finalizepsbt">{ unimplemented }, - method<"joinpsbts">{ unimplemented }, + 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<"decodepsbt", string_t>{ "psbt" }, + method<"finalizepsbt", string_t, optional>{ "psbt", "extract" }, + method<"joinpsbts", array_t>{ "txs" }, method<"descriptorprocesspsbt">{ unimplemented }, - method<"utxoupdatepsbt">{ unimplemented }, + method<"utxoupdatepsbt", string_t, optional>{ "psbt", "descriptors" }, method<"abortprivatebroadcast">{ unimplemented }, method<"getprivatebroadcastinfo">{ unimplemented }, method<"submitpackage">{ unimplemented } diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_transaction.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_transaction.hpp index 9209a5e9..f1db5c20 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_transaction.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_transaction.hpp @@ -73,35 +73,42 @@ class BCS_API protocol_bitcoind_transaction rpc_interface::test_mempool_accept, const network::rpc::array_t& rawtxs, double maxfeerate) NOEXCEPT; bool handle_analyze_psbt(const code& ec, - rpc_interface::analyze_psbt) NOEXCEPT; + rpc_interface::analyze_psbt, const std::string& psbt) NOEXCEPT; bool handle_combine_psbt(const code& ec, - rpc_interface::combine_psbt) NOEXCEPT; + rpc_interface::combine_psbt, + const network::rpc::array_t& txs) NOEXCEPT; bool handle_convert_to_psbt(const code& ec, - rpc_interface::convert_to_psbt) NOEXCEPT; + rpc_interface::convert_to_psbt, const std::string& hexstring, + bool permitsigdata, const std::optional& iswitness) NOEXCEPT; bool handle_create_psbt(const code& ec, - rpc_interface::create_psbt) NOEXCEPT; + rpc_interface::create_psbt, const network::rpc::array_t& inputs, + const network::rpc::object_t& outputs, double locktime, + bool replaceable) NOEXCEPT; bool handle_decode_psbt(const code& ec, - rpc_interface::decode_psbt) NOEXCEPT; + rpc_interface::decode_psbt, const std::string& psbt) NOEXCEPT; bool handle_finalize_psbt(const code& ec, - rpc_interface::finalize_psbt) NOEXCEPT; + rpc_interface::finalize_psbt, const std::string& psbt, + bool extract) NOEXCEPT; bool handle_join_psbts(const code& ec, - rpc_interface::join_psbts) NOEXCEPT; + rpc_interface::join_psbts, + const network::rpc::array_t& txs) NOEXCEPT; bool handle_descriptor_process_psbt(const code& ec, rpc_interface::descriptor_process_psbt) NOEXCEPT; bool handle_utxo_update_psbt(const code& ec, - rpc_interface::utxo_update_psbt) NOEXCEPT; - bool handle_abort_private_broadcast(const code& ec, - rpc_interface::abort_private_broadcast) NOEXCEPT; - bool handle_get_private_broadcast_info(const code& ec, - rpc_interface::get_private_broadcast_info) NOEXCEPT; - bool handle_submit_package(const code& ec, - rpc_interface::submit_package) NOEXCEPT; + rpc_interface::utxo_update_psbt, const std::string& psbt, + const network::rpc::array_t& descriptors) NOEXCEPT; /// 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; + bool handle_abort_private_broadcast(const code& ec, + rpc_interface::abort_private_broadcast) NOEXCEPT; + bool handle_get_private_broadcast_info(const code& ec, + rpc_interface::get_private_broadcast_info) NOEXCEPT; + bool handle_submit_package(const code& ec, + rpc_interface::submit_package) NOEXCEPT; }; } // namespace server diff --git a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp index 6d9e272a..8341273d 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp @@ -59,15 +59,15 @@ void protocol_bitcoind_transaction::start() NOEXCEPT SUBSCRIBE_BITCOIND(handle_get_raw_transaction, _1, _2, _3, _4, _5); SUBSCRIBE_BITCOIND(handle_send_raw_transaction, _1, _2, _3, _4); SUBSCRIBE_BITCOIND(handle_test_mempool_accept, _1, _2, _3, _4); - SUBSCRIBE_BITCOIND(handle_analyze_psbt, _1, _2); - SUBSCRIBE_BITCOIND(handle_combine_psbt, _1, _2); - SUBSCRIBE_BITCOIND(handle_convert_to_psbt, _1, _2); - SUBSCRIBE_BITCOIND(handle_create_psbt, _1, _2); - SUBSCRIBE_BITCOIND(handle_decode_psbt, _1, _2); - SUBSCRIBE_BITCOIND(handle_finalize_psbt, _1, _2); - SUBSCRIBE_BITCOIND(handle_join_psbts, _1, _2); + 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_decode_psbt, _1, _2, _3); + SUBSCRIBE_BITCOIND(handle_finalize_psbt, _1, _2, _3, _4); + SUBSCRIBE_BITCOIND(handle_join_psbts, _1, _2, _3); SUBSCRIBE_BITCOIND(handle_descriptor_process_psbt, _1, _2); - SUBSCRIBE_BITCOIND(handle_utxo_update_psbt, _1, _2); + SUBSCRIBE_BITCOIND(handle_utxo_update_psbt, _1, _2, _3, _4); SUBSCRIBE_BITCOIND(handle_abort_private_broadcast, _1, _2); SUBSCRIBE_BITCOIND(handle_get_private_broadcast_info, _1, _2); SUBSCRIBE_BITCOIND(handle_submit_package, _1, _2); @@ -361,59 +361,515 @@ bool protocol_bitcoind_transaction::handle_decode_raw_transaction(const code& ec return true; } +// PSBT methods. +// ---------------------------------------------------------------------------- + +using psbt_tx = wallet::psbt::transaction; + +static std::string sighash_name(uint32_t type) NOEXCEPT +{ + std::string name{}; + switch (type & 0x03_u32) + { + case 0: name = "DEFAULT"; break; + case 1: name = "ALL"; break; + case 2: name = "NONE"; break; + default: name = "SINGLE"; + } + + if (to_bool(type & 0x80_u32)) + name += "|ANYONECANPAY"; + + return name; +} + +static std::string to_key_path(const std_vector& path) NOEXCEPT +{ + constexpr auto hardened = 0x80000000_u32; + std::string out{ "m" }; + for (const auto& index: path) + { + out += "/" + std::to_string(index & ~hardened); + if (to_bool(index & hardened)) + out += "'"; + } + + return out; +} + +static object_t to_unknown(const wallet::psbt::entry::list& entries) NOEXCEPT +{ + object_t out{}; + for (const auto& entry: entries) + out.emplace(encode_base16(entry.key), encode_base16(entry.value)); + + return out; +} + +static array_t to_derivations( + const wallet::psbt::derivation::list& derivations) NOEXCEPT +{ + array_t out{}; + for (const auto& derived: derivations) + { + out.emplace_back(object_t + { + { "pubkey", encode_base16(derived.point) }, + { "master_fingerprint", encode_base16(to_little_endian( + derived.origin.fingerprint)) }, + { "path", to_key_path(derived.origin.path) } + }); + } + + return out; +} + +static object_t decode_psbt_input(const wallet::psbt::input& in) NOEXCEPT +{ + using namespace chain; + object_t entry{}; + + if (in.non_witness_utxo) + entry.emplace("non_witness_utxo", + value_from(bitcoind(*in.non_witness_utxo))); + + if (in.witness_utxo) + { + entry.emplace("witness_utxo", object_t + { + { "amount", in.witness_utxo->value() / + to_floating(satoshi_per_bitcoin) }, + { "scriptPubKey", value_from(bitcoind(in.witness_utxo->script())) } + }); + } + + if (!in.partial_signatures.empty()) + { + object_t signatures{}; + for (const auto& signature: in.partial_signatures) + signatures.emplace(encode_base16(signature.keydata()), + encode_base16(signature.value)); + + entry.emplace("partial_signatures", std::move(signatures)); + } + + if (in.sighash_type.has_value()) + entry.emplace("sighash", sighash_name(in.sighash_type.value())); + + if (in.redeem_script) + entry.emplace("redeem_script", value_from(bitcoind(*in.redeem_script))); + + if (in.witness_script) + entry.emplace("witness_script", + value_from(bitcoind(*in.witness_script))); + + if (!in.derivations.empty()) + entry.emplace("bip32_derivs", to_derivations(in.derivations)); + + if (in.final_script_sig) + entry.emplace("final_scriptSig", + value_from(bitcoind(*in.final_script_sig))); + + if (in.final_script_witness) + { + array_t stack{}; + for (const auto& item: in.final_script_witness->stack()) + stack.emplace_back(encode_base16(*item)); + + entry.emplace("final_scriptwitness", std::move(stack)); + } + + if (in.previous_txid.has_value()) + entry.emplace("previous_txid", encode_hash(in.previous_txid.value())); + + if (in.output_index.has_value()) + entry.emplace("output_index", in.output_index.value()); + + if (in.sequence.has_value()) + entry.emplace("sequence", in.sequence.value()); + + if (in.required_time_locktime.has_value()) + entry.emplace("time_locktime", in.required_time_locktime.value()); + + if (in.required_height_locktime.has_value()) + entry.emplace("height_locktime", in.required_height_locktime.value()); + + if (!in.others.empty()) + entry.emplace("unknown", to_unknown(in.others)); + + return entry; +} + +static object_t decode_psbt_output(const wallet::psbt::output& out) NOEXCEPT +{ + using namespace chain; + object_t entry{}; + + if (out.redeem_script) + entry.emplace("redeem_script", + value_from(bitcoind(*out.redeem_script))); + + if (out.witness_script) + entry.emplace("witness_script", + value_from(bitcoind(*out.witness_script))); + + if (!out.derivations.empty()) + entry.emplace("bip32_derivs", to_derivations(out.derivations)); + + if (out.amount.has_value()) + entry.emplace("amount", out.amount.value() / + to_floating(satoshi_per_bitcoin)); + + if (out.script) + entry.emplace("script", value_from(bitcoind(*out.script))); + + if (!out.others.empty()) + entry.emplace("unknown", to_unknown(out.others)); + + 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::invalid_argument); + 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) NOEXCEPT + rpc_interface::analyze_psbt, const std::string& psbt) NOEXCEPT { - if (stopped(ec)) return false; - send_error(error::not_implemented); + if (stopped(ec)) + return false; + + const psbt_tx doc(psbt); + if (!doc) + { + send_error(error::invalid_argument); + return true; + } + + auto missing_utxo = false; + array_t ins{}; + for (size_t index = 0; index < doc.inputs().size(); ++index) + { + const auto& in = doc.inputs().at(index); + const auto utxo = !!doc.prevout(index); + missing_utxo |= !utxo; + + object_t entry + { + { "has_utxo", utxo }, + { "is_final", in.is_final() } + }; + + if (!in.is_final()) + { + array_t unsigned_keys{}; + for (const auto& derived: in.derivations) + { + const auto match = [&](const auto& signature) NOEXCEPT + { + return signature.keydata() == derived.point; + }; + + if (std::none_of(in.partial_signatures.begin(), + in.partial_signatures.end(), match)) + unsigned_keys.emplace_back(encode_base16(derived.point)); + } + + if (!unsigned_keys.empty()) + entry.emplace("missing", object_t + { + { "signatures", std::move(unsigned_keys) } + }); + + entry.emplace("next", std::string{ utxo ? "signer" : "updater" }); + } + + ins.emplace_back(std::move(entry)); + } + + object_t result{ { "inputs", std::move(ins) } }; + + if (const auto fee = doc.fee(); fee.has_value()) + result.emplace("fee", fee.value() / + to_floating(chain::satoshi_per_bitcoin)); + + if (doc.is_final()) + { + const auto tx = doc.extract(); + const auto vsize = ceilinged_divide(tx.weight(), + chain::light_weight_factor); + result.emplace("estimated_vsize", vsize); + + if (const auto fee = doc.fee(); fee.has_value() && !is_zero(vsize)) + result.emplace("estimated_feerate", (fee.value() * 1000u) / + to_floating(chain::satoshi_per_bitcoin) / vsize); + + result.emplace("next", std::string{ "extractor" }); + } + else + { + result.emplace("next", std::string{ missing_utxo ? "updater" : "signer" }); + } + + send_result(std::move(result), 1024); return true; } bool protocol_bitcoind_transaction::handle_combine_psbt(const code& ec, - rpc_interface::combine_psbt) NOEXCEPT + rpc_interface::combine_psbt, const array_t& txs) NOEXCEPT { - if (stopped(ec)) return false; - send_error(error::not_implemented); + if (stopped(ec)) + return false; + + psbt_tx combined{}; + for (const auto& item: txs) + { + if (!std::holds_alternative(item.value())) + { + send_error(error::invalid_argument); + return true; + } + + psbt_tx doc(std::get(item.value())); + if (!doc || (combined && !combined.combine(doc))) + { + send_error(error::invalid_argument); + return true; + } + + if (!combined) + combined = std::move(doc); + } + + if (!combined) + { + send_error(error::invalid_argument); + return true; + } + + send_result(combined.encoded(), 1024); return true; } bool protocol_bitcoind_transaction::handle_convert_to_psbt(const code& ec, - rpc_interface::convert_to_psbt) NOEXCEPT + rpc_interface::convert_to_psbt, const std::string& hexstring, + bool permitsigdata, const std::optional& iswitness) NOEXCEPT { - if (stopped(ec)) return false; - send_error(error::not_implemented); + if (stopped(ec)) + return false; + + data_chunk data{}; + if (!decode_base16(data, hexstring)) + { + send_error(error::invalid_argument); + return true; + } + + // Absent the hint, witness deserialization is tried first (as bitcoind). + auto tx = chain::transaction{ data, iswitness.value_or(true) }; + if (!iswitness.has_value() && !tx.is_valid()) + tx = chain::transaction{ data, false }; + + if (!tx.is_valid()) + { + send_error(error::invalid_argument); + return true; + } + + const auto is_signed = [](const auto& in) NOEXCEPT + { + return !in->script().ops().empty() || !in->witness().stack().empty(); + }; + + const auto& ins = *tx.inputs_ptr(); + if (std::any_of(ins.begin(), ins.end(), is_signed)) + { + if (!permitsigdata) + { + send_error(error::invalid_argument); + return true; + } + + // Strip signature data for the unsigned psbt transaction. + const auto stripped = to_shared(); + stripped->reserve(ins.size()); + for (const auto& in: ins) + stripped->push_back(to_shared(in->point(), + chain::script{}, chain::witness{}, in->sequence())); + + tx = { tx.version(), stripped, tx.outputs_ptr(), tx.locktime() }; + } + + const psbt_tx doc(tx); + if (!doc) + { + send_error(error::invalid_argument); + return true; + } + + send_result(doc.encoded(), 1024); return true; } bool protocol_bitcoind_transaction::handle_create_psbt(const code& ec, - rpc_interface::create_psbt) NOEXCEPT + rpc_interface::create_psbt, const array_t& inputs, + const object_t& outputs, double locktime, bool replaceable) NOEXCEPT { - if (stopped(ec)) return false; - send_error(error::not_implemented); - return true; -} + if (stopped(ec)) + return false; -bool protocol_bitcoind_transaction::handle_decode_psbt(const code& ec, - rpc_interface::decode_psbt) NOEXCEPT -{ - if (stopped(ec)) return false; - send_error(error::not_implemented); + chain::transaction tx{}; + if (const auto fault = build_transaction(tx, inputs, outputs, locktime, + replaceable)) + { + send_error(fault); + return true; + } + + const psbt_tx doc(tx); + if (!doc) + { + send_error(error::invalid_argument); + return true; + } + + send_result(doc.encoded(), 1024); return true; } bool protocol_bitcoind_transaction::handle_finalize_psbt(const code& ec, - rpc_interface::finalize_psbt) NOEXCEPT + rpc_interface::finalize_psbt, const std::string& psbt, + bool extract) NOEXCEPT { - if (stopped(ec)) return false; - send_error(error::not_implemented); + if (stopped(ec)) + return false; + + psbt_tx doc(psbt); + if (!doc) + { + send_error(error::invalid_argument); + return true; + } + + const auto complete = doc.finalize(); + object_t result{}; + if (complete && extract) + { + constexpr auto witness = true; + const auto tx = doc.extract(); + result.emplace("hex", encode_base16(tx.to_data(witness))); + } + else + { + result.emplace("psbt", doc.encoded()); + } + + result.emplace("complete", complete); + send_result(std::move(result), 1024); return true; } bool protocol_bitcoind_transaction::handle_join_psbts(const code& ec, - rpc_interface::join_psbts) NOEXCEPT + rpc_interface::join_psbts, const array_t& txs) NOEXCEPT { - if (stopped(ec)) return false; - send_error(error::not_implemented); + if (stopped(ec)) + return false; + + if (txs.size() < 2u) + { + send_error(error::invalid_argument); + return true; + } + + psbt_tx joined{}; + for (const auto& item: txs) + { + if (!std::holds_alternative(item.value())) + { + send_error(error::invalid_argument); + return true; + } + + psbt_tx doc(std::get(item.value())); + if (!doc || (joined && !joined.join(doc))) + { + send_error(error::invalid_argument); + return true; + } + + if (!joined) + joined = std::move(doc); + } + + send_result(joined.encoded(), 1024); return true; } @@ -426,10 +882,51 @@ bool protocol_bitcoind_transaction::handle_descriptor_process_psbt(const code& e } bool protocol_bitcoind_transaction::handle_utxo_update_psbt(const code& ec, - rpc_interface::utxo_update_psbt) NOEXCEPT + rpc_interface::utxo_update_psbt, const std::string& psbt, + const array_t& descriptors) NOEXCEPT { - if (stopped(ec)) return false; - send_error(error::not_implemented); + if (stopped(ec)) + return false; + + // Descriptor expansion requires the descriptor engine (pending). + if (!descriptors.empty()) + { + send_error(error::not_implemented); + return true; + } + + psbt_tx doc(psbt); + if (!doc) + { + send_error(error::invalid_argument); + return true; + } + + const auto& query = archive(); + const auto version0 = (doc.version() == psbt_tx::version_0); + for (size_t index = 0; index < doc.inputs().size(); ++index) + { + if (doc.prevout(index)) + continue; + + 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); + const auto vout = version0 ? + doc.unsigned_tx().inputs_ptr()->at(index)->point().index() : + in.output_index.value_or(0); + + const auto out = query.get_output(query.to_tx(hash), vout); + if (!out) + continue; + + // Only witness utxos are populated (as bitcoind). + if (chain::script::is_pay_witness_pattern(out->script().ops())) + in.witness_utxo = out; + } + + send_result(doc.encoded(), 1024); return true; } diff --git a/test/interfaces/bitcoind.cpp b/test/interfaces/bitcoind.cpp index 69b03e95..a594baad 100644 --- a/test/interfaces/bitcoind.cpp +++ b/test/interfaces/bitcoind.cpp @@ -128,7 +128,9 @@ static_assert(bitcoind_notifications_methods::names == "getzmqnotifications"); static_assert(bitcoind_test_methods::names == ""); static_assert(bitcoind_transaction_methods::names == "createrawtransaction decoderawtransaction getrawtransaction " - "sendrawtransaction testmempoolaccept"); + "sendrawtransaction testmempoolaccept analyzepsbt combinepsbt " + "converttopsbt createpsbt decodepsbt finalizepsbt joinpsbts " + "utxoupdatepsbt"); static_assert(bitcoind_utility_methods::names == "decodescript validateaddress createmultisig verifymessage getindexinfo"); static_assert(bitcoind_wallet_methods::names == ""); diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 060d9cd1..d5a87803 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -1047,6 +1047,15 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__response__websocket__id_matches_request) } +// psbt (vectors from bip174) + +#define PSBT_UPDATER "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHAQQiACCMI1MXN0O1ld+0oHtyuo5C43l9p06H/n2ddJfjsgKJAwEFR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuIgYCOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnMQ2QxqTwAAAIAAAACAAwAAgCIGAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcENkMak8AAACAAAAAgAIAAIAAIgIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1Ptnuylh3EQ2QxqTwAAAIAAAACABAAAgAAiAgJ/Y5l1fS7/VaE2rQLGhLGDi2VW5fG2s0KCqUtrUAUQlhDZDGpPAAAAgAAAAIAFAACAAA==" +#define PSBT_SIGNER_A "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU210gwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gEBAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohyICAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBAQMEAQAAAAEEIgAgjCNTFzdDtZXftKB7crqOQuN5fadOh/59nXSX47ICiQMBBUdSIQMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3CECOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnNSriIGAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zENkMak8AAACAAAAAgAMAAIAiBgMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3BDZDGpPAAAAgAAAAIACAACAACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=" +#define PSBT_SIGNER_B "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgf0cwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMASICAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAQEDBAEAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHIgIDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtxHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwEiAgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc0cwRAIgZfRbpZmLWaJ//hp77QFq8fH5DVSzqo90UKpfVqJRA70CIH9yRwOtHtuWaAsoS1bU/8uI9/t1nqu+CKow8puFE4PSAQEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA" +#define PSBT_COMBINED "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgf0cwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMASICAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAQEDBAEAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHIgIDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtxHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwEiAgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc0cwRAIgZfRbpZmLWaJ//hp77QFq8fH5DVSzqo90UKpfVqJRA70CIH9yRwOtHtuWaAsoS1bU/8uI9/t1nqu+CKow8puFE4PSAQEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA" +#define PSBT_FINALIZED "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABB9oARzBEAiB0AYrUGACXuHMyPAAVcgs2hMyBI4kQSOfbzZtVrWecmQIgc9Npt0Dj61Pc76M4I8gHBRTKVafdlUTxV8FnkTJhEYwBSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAUdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSrgABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEHIyIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQjaBABHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwFHMEQCIGX0W6WZi1mif/4ae+0BavHx+Q1Us6qPdFCqX1aiUQO9AiB/ckcDrR7blmgLKEtW1P/LiPf7dZ6rvgiqMPKbhROD0gFHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4AIgIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1Ptnuylh3EQ2QxqTwAAAIAAAACABAAAgAAiAgJ/Y5l1fS7/VaE2rQLGhLGDi2VW5fG2s0KCqUtrUAUQlhDZDGpPAAAAgAAAAIAFAACAAA==" +#define PSBT_EXTRACTED_TX "0200000000010258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7500000000da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752aeffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d01000000232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00000000" + BOOST_AUTO_TEST_CASE(bitcoind_rpc__createrawtransaction__data_output__op_return) { const auto txid = encode_hash(test::block1.transactions_ptr()->front()->hash(false)); @@ -1057,6 +1066,106 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__createrawtransaction__data_output__op_return) BOOST_REQUIRE_EQUAL(as_text(out.at("scriptPubKey").at("hex")), "6a04deadbeef"); } +BOOST_AUTO_TEST_CASE(bitcoind_rpc__createpsbt__data_output__decodes) +{ + const auto txid = encode_hash(test::block1.transactions_ptr()->front()->hash(false)); + const auto created = rpc("createpsbt", "[[{\"txid\":\"" + txid + "\",\"vout\":0}], {\"data\": \"deadbeef\"}]"); + const auto response = rpc("decodepsbt", "[\"" + as_text(created.at("result")) + "\"]"); + const auto& result = response.at("result"); + BOOST_REQUIRE_EQUAL(result.at("psbt_version").as_int64(), 0); + const auto& out = result.at("tx").at("vout").at(0); + BOOST_REQUIRE_EQUAL(as_text(out.at("scriptPubKey").at("hex")), "6a04deadbeef"); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__decodepsbt__updater__expected_scripts) +{ + const auto response = rpc("decodepsbt", "[\"" PSBT_UPDATER "\"]"); + const auto& result = response.at("result"); + BOOST_REQUIRE_EQUAL(result.at("psbt_version").as_int64(), 0); + BOOST_REQUIRE_EQUAL(result.at("inputs").as_array().size(), 2u); + BOOST_REQUIRE(result.at("inputs").at(0).as_object().contains("non_witness_utxo")); + BOOST_REQUIRE(result.at("inputs").at(0).as_object().contains("redeem_script")); + BOOST_REQUIRE(result.at("inputs").at(1).as_object().contains("witness_utxo")); + BOOST_REQUIRE_EQUAL(result.at("outputs").as_array().size(), 2u); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__combinepsbt__two_signed__combined) +{ + const auto response = rpc("combinepsbt", "[[\"" PSBT_SIGNER_A "\", \"" PSBT_SIGNER_B "\"]]"); + const auto decoded = rpc("decodepsbt", "[\"" + as_text(response.at("result")) + "\"]"); + const auto& sigs = decoded.at("result").at("inputs").at(0).at("partial_signatures"); + BOOST_REQUIRE_EQUAL(sigs.as_object().size(), 2u); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__finalizepsbt__combined__extracted_transaction) +{ + const auto response = rpc("finalizepsbt", "[\"" PSBT_COMBINED "\"]"); + const auto& result = response.at("result"); + BOOST_REQUIRE(result.at("complete").as_bool()); + BOOST_REQUIRE_EQUAL(as_text(result.at("hex")), PSBT_EXTRACTED_TX); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__finalizepsbt__no_extract__psbt) +{ + const auto response = rpc("finalizepsbt", "[\"" PSBT_COMBINED "\", false]"); + const auto& result = response.at("result"); + BOOST_REQUIRE(result.at("complete").as_bool()); + BOOST_REQUIRE(result.as_object().contains("psbt")); + BOOST_REQUIRE(!result.as_object().contains("hex")); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__finalizepsbt__unsigned__incomplete) +{ + const auto response = rpc("finalizepsbt", "[\"" PSBT_UPDATER "\"]"); + const auto& result = response.at("result"); + BOOST_REQUIRE(!result.at("complete").as_bool()); + BOOST_REQUIRE(result.as_object().contains("psbt")); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__analyzepsbt__finalized__extractor_next) +{ + const auto response = rpc("analyzepsbt", "[\"" PSBT_FINALIZED "\"]"); + const auto& result = response.at("result"); + BOOST_REQUIRE_EQUAL(as_text(result.at("next")), "extractor"); + BOOST_REQUIRE(result.as_object().contains("estimated_vsize")); + BOOST_REQUIRE(result.at("inputs").at(0).at("is_final").as_bool()); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__analyzepsbt__updater__signer_next) +{ + const auto response = rpc("analyzepsbt", "[\"" PSBT_UPDATER "\"]"); + const auto& result = response.at("result"); + BOOST_REQUIRE_EQUAL(as_text(result.at("next")), "signer"); + BOOST_REQUIRE(result.at("inputs").at(0).at("has_utxo").as_bool()); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__joinpsbts__single__invalid) +{ + const auto response = rpc("joinpsbts", "[[\"" PSBT_UPDATER "\"]]"); + REQUIRE_NO_THROW_TRUE(response.as_object().contains("error")); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__converttopsbt__created_raw__psbt) +{ + 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("converttopsbt", "[\"" + as_text(created.at("result")) + "\"]"); + const auto decoded = rpc("decodepsbt", "[\"" + as_text(response.at("result")) + "\"]"); + BOOST_REQUIRE_EQUAL(decoded.at("result").at("inputs").as_array().size(), 1u); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__utxoupdatepsbt__descriptors__not_implemented) +{ + const auto response = rpc("utxoupdatepsbt", "[\"" PSBT_UPDATER "\", [\"wpkh(abc)\"]]"); + REQUIRE_NO_THROW_TRUE(response.as_object().contains("error")); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__utxoupdatepsbt__no_matching_utxos__round_trips) +{ + const auto response = rpc("utxoupdatepsbt", "[\"" PSBT_UPDATER "\"]"); + BOOST_REQUIRE_EQUAL(as_text(response.at("result")), PSBT_UPDATER); +} + BOOST_AUTO_TEST_SUITE_END() // websocket authorization From 0fa26f517fa41469c4b5cf5cea59e145ce66af91 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 19 Aug 2026 15:16:09 -0400 Subject: [PATCH 03/21] Add scriptPubKey to validateaddress. --- .../bitcoind/protocol_bitcoind_utility.cpp | 3 +++ test/protocols/bitcoind/bitcoind_rpc.cpp | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/protocols/bitcoind/protocol_bitcoind_utility.cpp b/src/protocols/bitcoind/protocol_bitcoind_utility.cpp index 01156b05..4d15c39c 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_utility.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_utility.cpp @@ -153,6 +153,8 @@ bool protocol_bitcoind_utility::handle_validate_address(const code& ec, { { "isvalid", true }, { "address", base58.encoded() }, + { "scriptPubKey", encode_base16(base58.output_script(p2kh_, + p2sh_).to_data(false)) }, { "isscript", base58.prefix() == p2sh_ }, { "iswitness", false } }, 128); @@ -169,6 +171,7 @@ bool protocol_bitcoind_utility::handle_validate_address(const code& ec, { { "isvalid", true }, { "address", witness.encoded() }, + { "scriptPubKey", encode_base16(witness.script().to_data(false)) }, { "isscript", version0_p2sh }, { "iswitness", true }, { "witness_version", witness.version() }, diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index d5a87803..9bea5687 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -455,6 +455,19 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__validateaddress__genesis__valid) REQUIRE_NO_THROW_TRUE(response.at("result").at("isvalid").as_bool()); } +BOOST_AUTO_TEST_CASE(bitcoind_rpc__validateaddress__genesis__expected_script) +{ + const auto response = rpc("validateaddress", "[\"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa\"]"); + BOOST_REQUIRE_EQUAL(as_text(response.at("result").at("scriptPubKey")), "76a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1888ac"); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__validateaddress__witness__expected_script) +{ + const auto response = rpc("validateaddress", "[\"bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\"]"); + REQUIRE_NO_THROW_TRUE(response.at("result").at("iswitness").as_bool()); + BOOST_REQUIRE_EQUAL(as_text(response.at("result").at("scriptPubKey")), "0014751e76e8199196d454941c45d1b3a323f1433bd6"); +} + BOOST_AUTO_TEST_CASE(bitcoind_rpc__validateaddress__garbage__invalid) { const auto response = rpc("validateaddress", "[\"notanaddress\"]"); From 4028164eac057386017434b33d0034ca8129d50a Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 19 Aug 2026 15:26:17 -0400 Subject: [PATCH 04/21] Add size_on_disk to getblockchaininfo. --- src/protocols/bitcoind/protocol_bitcoind_json.cpp | 1 + test/protocols/bitcoind/bitcoind_rpc.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/src/protocols/bitcoind/protocol_bitcoind_json.cpp b/src/protocols/bitcoind/protocol_bitcoind_json.cpp index 72420e08..7b40e6ee 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_json.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_json.cpp @@ -268,6 +268,7 @@ bool protocol_bitcoind::chain_info(network::rpc::object_t& out, { "verificationprogress", progress(blocks, headers) }, { "initialblockdownload", !current }, { "chainwork", encode_hash(from_uintx(work)) }, + { "size_on_disk", query.store_size() }, { "pruned", pruned }, { "warnings", std::string{} } }; diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 9bea5687..772175ab 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -273,6 +273,7 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblockchaininfo__ten_block_store__expected) BOOST_REQUIRE(result.at("warnings").is_string()); BOOST_REQUIRE(result.at("initialblockdownload").is_bool()); BOOST_REQUIRE(result.at("chainwork").is_string()); + BOOST_REQUIRE(result.at("size_on_disk").as_int64() > 0); } // Ten blocks at minimum difficulty: cumulative work is 10 * 0x0100010001. From f0403ae1cc127eaba046012bf5a4997312eb6a6e Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 19 Aug 2026 15:32:37 -0400 Subject: [PATCH 05/21] Add iswitness hint to decoderawtransaction. --- .../server/interfaces/bitcoind_transaction.hpp | 2 +- .../protocols/protocol_bitcoind_transaction.hpp | 4 ++-- .../bitcoind/protocol_bitcoind_transaction.cpp | 16 ++++++++++------ test/protocols/bitcoind/bitcoind_rpc.cpp | 9 +++++++++ 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/include/bitcoin/server/interfaces/bitcoind_transaction.hpp b/include/bitcoin/server/interfaces/bitcoind_transaction.hpp index c28591b4..497c3e37 100644 --- a/include/bitcoin/server/interfaces/bitcoind_transaction.hpp +++ b/include/bitcoin/server/interfaces/bitcoind_transaction.hpp @@ -31,7 +31,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<"decoderawtransaction", string_t>{ "hexstring" }, + 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" }, diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_transaction.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_transaction.hpp index f1db5c20..399eed9f 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_transaction.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_transaction.hpp @@ -61,8 +61,8 @@ class BCS_API protocol_bitcoind_transaction const network::rpc::object_t& outputs, double locktime, bool replaceable) NOEXCEPT; bool handle_decode_raw_transaction(const code& ec, - rpc_interface::decode_raw_transaction, - const std::string& hexstring) NOEXCEPT; + rpc_interface::decode_raw_transaction, const std::string& hexstring, + const std::optional& iswitness) NOEXCEPT; bool handle_get_raw_transaction(const code& ec, rpc_interface::get_raw_transaction, const std::string& txid, double verbose, const std::string& blockhash) NOEXCEPT; diff --git a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp index 8341273d..61207f7b 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp @@ -55,7 +55,7 @@ void protocol_bitcoind_transaction::start() NOEXCEPT return; SUBSCRIBE_BITCOIND(handle_create_raw_transaction, _1, _2, _3, _4, _5, _6); - SUBSCRIBE_BITCOIND(handle_decode_raw_transaction, _1, _2, _3); + 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_test_mempool_accept, _1, _2, _3, _4); @@ -336,8 +336,8 @@ bool protocol_bitcoind_transaction::handle_create_raw_transaction( } bool protocol_bitcoind_transaction::handle_decode_raw_transaction(const code& ec, - rpc_interface::decode_raw_transaction, - const std::string& hexstring) NOEXCEPT + rpc_interface::decode_raw_transaction, const std::string& hexstring, + const std::optional& iswitness) NOEXCEPT { if (stopped(ec)) return false; @@ -349,15 +349,19 @@ bool protocol_bitcoind_transaction::handle_decode_raw_transaction(const code& ec return true; } - constexpr auto witness = true; - const chain::transaction tx{ data, witness }; + // 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::invalid_argument); return true; } - send_result(value_from(bitcoind(tx)), two * tx.serialized_size(witness)); + send_result(value_from(bitcoind(tx)), two * tx.serialized_size(true)); return true; } diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 772175ab..8f574711 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -407,6 +407,15 @@ 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__decoderawtransaction__iswitness_false__round_trips) +{ + 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")) + "\", false]"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + BOOST_REQUIRE_EQUAL(response.at("result").at("vin").as_array().size(), 1u); +} + BOOST_AUTO_TEST_CASE(bitcoind_rpc__decoderawtransaction__created__round_trips) { const auto txid = encode_hash(test::block1.transactions_ptr()->front()->hash(false)); From 1b8a407791f75cbca8ea5266e0371b102e855a0b Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 19 Aug 2026 15:36:03 -0400 Subject: [PATCH 06/21] Add descriptor and segwit forms to decodescript. --- .../bitcoind/protocol_bitcoind_utility.cpp | 28 ++++++++++++++++++- test/protocols/bitcoind/bitcoind_rpc.cpp | 19 +++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_utility.cpp b/src/protocols/bitcoind/protocol_bitcoind_utility.cpp index 4d15c39c..6ac71d5a 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_utility.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_utility.cpp @@ -115,11 +115,19 @@ bool protocol_bitcoind_utility::handle_decode_script(const code& ec, return true; } + // Inference is pending the descriptor engine; raw is always correct. + const auto raw_descriptor = [](const chain::script& target) NOEXCEPT + { + const auto body = "raw(" + encode_base16(target.to_data(false)) + ")"; + return body + "#" + descriptor_checksum(body); + }; + using namespace wallet; const auto pattern = script.output_pattern(); object_t result { { "asm", script.to_string(flags::all_rules, true) }, + { "desc", raw_descriptor(script) }, { "type", to_script_type(pattern) } }; @@ -135,7 +143,25 @@ bool protocol_bitcoind_utility::handle_decode_script(const code& ec, if (pay) result.emplace("p2sh", pay.encoded()); - send_result(std::move(result), 256); + // Witness-embeddable scripts carry the version 0 program forms. + if (!chain::script::is_pay_witness_pattern(script.ops()) && + !chain::script::is_pay_null_data_pattern(script.ops())) + { + const chain::script wsh{ chain::script::to_pay_witness_pattern(0, + sha256_hash(script.to_data(false))) }; + + result.emplace("segwit", object_t + { + { "asm", wsh.to_string(flags::all_rules, true) }, + { "hex", encode_base16(wsh.to_data(false)) }, + { "type", to_script_type(script_pattern::pay_witness_script_hash) }, + { "address", witness_address{ script, witness_ }.encoded() }, + { "desc", raw_descriptor(wsh) }, + { "p2sh-segwit", payment_address{ wsh, p2sh_ }.encoded() } + }); + } + + send_result(std::move(result), 512); return true; } diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 8f574711..64c13084 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -459,6 +459,25 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__decodescript__p2kh__pubkeyhash) BOOST_REQUIRE_EQUAL(as_text(response.at("result").at("type")), "pubkeyhash"); } +BOOST_AUTO_TEST_CASE(bitcoind_rpc__decodescript__p2kh__descriptor_and_segwit) +{ + const auto response = rpc("decodescript", "[\"76a914000000000000000000000000000000000000000088ac\"]"); + const auto& result = response.at("result"); + BOOST_REQUIRE_EQUAL(as_text(result.at("desc")), "raw(76a914000000000000000000000000000000000000000088ac)#" + descriptor_checksum("raw(76a914000000000000000000000000000000000000000088ac)")); + const auto& segwit = result.at("segwit"); + BOOST_REQUIRE_EQUAL(as_text(segwit.at("type")), "witness_v0_scripthash"); + BOOST_REQUIRE(as_text(segwit.at("address")).starts_with("bc1q")); + BOOST_REQUIRE(segwit.as_object().contains("p2sh-segwit")); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__decodescript__witness_program__no_segwit) +{ + const auto response = rpc("decodescript", "[\"0014751e76e8199196d454941c45d1b3a323f1433bd6\"]"); + const auto& result = response.at("result"); + BOOST_REQUIRE_EQUAL(as_text(result.at("type")), "witness_v0_keyhash"); + BOOST_REQUIRE(!result.as_object().contains("segwit")); +} + BOOST_AUTO_TEST_CASE(bitcoind_rpc__validateaddress__genesis__valid) { const auto response = rpc("validateaddress", "[\"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa\"]"); From 4899fe3332eff0c586eb184d63649a8fe217e6b6 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 19 Aug 2026 15:43:14 -0400 Subject: [PATCH 07/21] Add getblock verbosity 3 (prevout context). --- .../server/protocols/protocol_bitcoind.hpp | 3 +++ .../bitcoind/protocol_bitcoind_blockchain.cpp | 26 ++++++++++++++++-- .../bitcoind/protocol_bitcoind_json.cpp | 27 +++++++++++++++++++ .../protocol_bitcoind_transaction.cpp | 23 +--------------- test/protocols/bitcoind/bitcoind_rpc.cpp | 16 +++++++++++ 5 files changed, 71 insertions(+), 24 deletions(-) diff --git a/include/bitcoin/server/protocols/protocol_bitcoind.hpp b/include/bitcoin/server/protocols/protocol_bitcoind.hpp index 855288cb..bf5e2363 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind.hpp @@ -97,6 +97,9 @@ class BCS_API protocol_bitcoind const system::chain::header& header) NOEXCEPT; static void inject_tx_context(boost::json::object& out, const node::query& query, const database::tx_link& link) NOEXCEPT; + static void inject_tx_prevouts(boost::json::object& out, + const node::query& query, + const system::chain::transaction& tx) NOEXCEPT; static boost::json::object header_to_bitcoind( const system::chain::header& header) NOEXCEPT; static std::string chain_name(const node::query& query) NOEXCEPT; diff --git a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp index c3ab340b..84982636 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp @@ -52,7 +52,10 @@ enum block_verbosity : size_t hashed = 1, /// Block object embedding full tx objects. - verbose = 2 + verbose = 2, + + /// Adds per-input prevout context and per-tx fee. + prevouts = 3 }; // bitcoind defines only the "basic" (neutrino) block filter type. @@ -142,7 +145,7 @@ bool protocol_bitcoind_blockchain::handle_get_block(const code& ec, } size_t level{}; - if (!to_integer(level, verbosity) || level > block_verbosity::verbose) + if (!to_integer(level, verbosity) || level > block_verbosity::prevouts) { send_error(error::invalid_argument); return true; @@ -169,6 +172,25 @@ bool protocol_bitcoind_blockchain::handle_get_block(const code& ec, value_from(bitcoind_verbose(*block)); inject_block_context(model.as_object(), query, link, block->header()); + + if (level == block_verbosity::prevouts && + query.populate_without_metadata(*block)) + { + auto entry = model.as_object().at("tx").as_array().begin(); + std::ranges::for_each(*block->transactions_ptr(), + [&](const auto& tx) NOEXCEPT + { + if (!tx->is_coinbase()) + { + inject_tx_prevouts(entry->as_object(), query, *tx); + entry->as_object()["fee"] = + tx->fee() / to_floating(chain::satoshi_per_bitcoin); + } + + ++entry; + }); + } + send_result(std::move(model), two * block->serialized_size(witness)); return true; } diff --git a/src/protocols/bitcoind/protocol_bitcoind_json.cpp b/src/protocols/bitcoind/protocol_bitcoind_json.cpp index 7b40e6ee..99b673db 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_json.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_json.cpp @@ -100,6 +100,33 @@ void protocol_bitcoind::inject_block_context(boost::json::object& out, query.get_header_key(query.to_confirmed(add1(height)))); } +// 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 +{ + size_t height{}; + auto entry = out.at("vin").as_array().begin(); + std::ranges::for_each(*tx.inputs_ptr(), [&](const auto& in) NOEXCEPT + { + const auto spent = query.to_tx(in->point().hash()); + if (query.get_tx_height(height, spent)) + { + auto put = value_from(bitcoind(*in->prevout)).as_object(); + boost::json::object prevout + { + { "generated", query.is_coinbase(spent) }, + { "height", height }, + { "value", put.at("value") }, + { "scriptPubKey", std::move(put.at("scriptPubKey")) } + }; + + entry->as_object()["prevout"] = std::move(prevout); + } + + ++entry; + }); +} + void protocol_bitcoind::inject_tx_context(boost::json::object& out, const node::query& query, const database::tx_link& link) NOEXCEPT { diff --git a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp index 61207f7b..0a7e5ffd 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp @@ -127,28 +127,7 @@ bool protocol_bitcoind_transaction::handle_get_raw_transaction(const code& ec, if (level == verbosity::json_verbose && !tx->is_coinbase() && query.populate_without_metadata(*tx)) { - size_t height{}; - auto entry = model.as_object().at("vin").as_array().begin(); - std::ranges::for_each(*tx->inputs_ptr(), [&](const auto& in) NOEXCEPT - { - const auto spent = query.to_tx(in->point().hash()); - if (query.get_tx_height(height, spent)) - { - auto out = value_from(bitcoind(*in->prevout)).as_object(); - boost::json::object prevout - { - { "generated", query.is_coinbase(spent) }, - { "height", height }, - { "value", out.at("value") }, - { "scriptPubKey", std::move(out.at("scriptPubKey")) } - }; - - entry->as_object()["prevout"] = std::move(prevout); - } - - ++entry; - }); - + inject_tx_prevouts(model.as_object(), query, *tx); model.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 64c13084..b2a53d28 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -261,6 +261,22 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblock__block9_verbosity2__tx_objects) BOOST_REQUIRE(tx.at(0).as_object().contains("txid")); } +// Coinbase-only blocks carry no prevout context (no fee, no prevouts). +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblock__block9_verbosity3__tx_objects) +{ + const auto response = rpc("getblock", hash_param(test::block9_hash, "3")); + const auto& tx = response.at("result").at("tx"); + BOOST_REQUIRE(tx.is_array()); + BOOST_REQUIRE(tx.at(0).as_object().contains("txid")); + BOOST_REQUIRE(!tx.at(0).as_object().contains("fee")); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblock__verbosity4__invalid) +{ + const auto response = rpc("getblock", hash_param(test::block9_hash, "4")); + REQUIRE_NO_THROW_TRUE(response.as_object().contains("error")); +} + BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblockchaininfo__ten_block_store__expected) { const auto response = rpc("getblockchaininfo"); From 37fbc60d5f23d0593022b0d34965961007286e99 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 19 Aug 2026 15:46:35 -0400 Subject: [PATCH 08/21] Compute getnetworkhashps over the nblocks window. --- .../bitcoind/protocol_bitcoind_mining.cpp | 61 ++++++++++++++++--- test/protocols/bitcoind/bitcoind_rpc.cpp | 13 ++++ 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_mining.cpp b/src/protocols/bitcoind/protocol_bitcoind_mining.cpp index 9f85d013..2160bd76 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_mining.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_mining.cpp @@ -67,7 +67,8 @@ void protocol_bitcoind_mining::start() NOEXCEPT // ---------------------------------------------------------------------------- bool protocol_bitcoind_mining::handle_get_network_hash_ps(const code& ec, - rpc_interface::get_network_hash_ps, double, double height) NOEXCEPT + rpc_interface::get_network_hash_ps, double nblocks, + double height) NOEXCEPT { if (stopped(ec)) return false; @@ -91,22 +92,66 @@ bool protocol_bitcoind_mining::handle_get_network_hash_ps(const code& ec, target = std::min(target, top); } - const auto header = query.get_header(query.to_confirmed(target)); - if (!header) + // A non-positive window selects the span since the last retarget. + size_t window{}; + if (nblocks <= 0) + { + window = add1(target % system_settings().retargeting_interval()); + } + else if (!to_integer(window, nblocks)) + { + send_error(error::invalid_argument); + return true; + } + + window = std::min(window, target); + if (is_zero(window)) + { + send_result(zero, 20); + return true; + } + + // The window timespan is bounded by its observed timestamps. + const auto first = target - window; + auto minimum = max_uint32; + auto maximum = min_uint32; + for (auto index = first; index <= target; ++index) + { + const auto header = query.get_header(query.to_confirmed(index)); + if (!header) + { + send_error(database::error::integrity); + return true; + } + + minimum = std::min(minimum, header->timestamp()); + maximum = std::max(maximum, header->timestamp()); + } + + if (minimum == maximum) + { + send_result(zero, 20); + return true; + } + + uint256_t start_work{}; + uint256_t end_work{}; + if (!query.get_branch_work(start_work, query.to_confirmed(first)) || + !query.get_branch_work(end_work, query.to_confirmed(target))) { send_error(database::error::integrity); return true; } - const auto period = system_settings().block_spacing_seconds; - const auto span = to_floating(power2(32u)); - send_result(header->difficulty() * span / period, 20); + const auto work = (end_work - start_work).convert_to(); + send_result(work / (maximum - minimum), 20); return true; } // currentblockweight/currentblocktx are omitted (bitcoind omits them until a -// block is assembled, and there is no assembler). The tx pool is empty and no -// packages are selected, so pooledtx and blockmintxfee are zero. +// 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. bool protocol_bitcoind_mining::handle_get_mining_info(const code& ec, rpc_interface::get_mining_info) NOEXCEPT { diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index b2a53d28..2e3a992f 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -401,6 +401,19 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__getnetworkhashps__default__number) BOOST_REQUIRE(response.at("result").is_double() || response.at("result").is_int64()); } +// Work over the window divided by its timestamp span (early 2009 blocks). +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getnetworkhashps__nine_block_window__positive) +{ + const auto response = rpc("getnetworkhashps", "[9, 9]"); + BOOST_REQUIRE(response.at("result").as_double() > 0.0); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getnetworkhashps__genesis_height__zero_window) +{ + const auto response = rpc("getnetworkhashps", "[120, 0]"); + BOOST_REQUIRE_EQUAL(response.at("result").as_int64(), 0); +} + // currentblockweight/currentblocktx omitted (no block ever assembled). BOOST_AUTO_TEST_CASE(bitcoind_rpc__getmininginfo__ten_block_store__expected) { From 47e03aa7eb0556a724aea2cd35381683b6e43ea5 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 19 Aug 2026 15:52:30 -0400 Subject: [PATCH 09/21] Fill out getnetworkinfo services and network detail. --- .../bitcoind/protocol_bitcoind_network.cpp | 60 +++++++++++++++++-- test/protocols/bitcoind/bitcoind_rpc.cpp | 6 +- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_network.cpp b/src/protocols/bitcoind/protocol_bitcoind_network.cpp index 30609479..7501c68d 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_network.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_network.cpp @@ -74,6 +74,28 @@ void protocol_bitcoind_network::start() NOEXCEPT // Network methods. // ---------------------------------------------------------------------------- +// bitcoind's service name for each advertised service bit. +static array_t to_service_names(uint64_t services) NOEXCEPT +{ + using service = messages::peer::service; + static const std::vector> names + { + { service::node_network, "NETWORK" }, + { service::node_bloom, "BLOOM" }, + { service::node_witness, "WITNESS" }, + { service::node_client_filters, "COMPACT_FILTERS" }, + { service::node_network_limited, "NETWORK_LIMITED" }, + { service::node_encrypted_transport, "P2P_V2" } + }; + + array_t out{}; + for (const auto& [bit, name]: names) + if (to_bool(services & bit)) + out.emplace_back(name); + + return out; +} + bool protocol_bitcoind_network::handle_get_network_info(const code& ec, rpc_interface::get_network_info) NOEXCEPT { @@ -85,21 +107,51 @@ bool protocol_bitcoind_network::handle_get_network_info(const code& ec, const auto& segments = settings.version.segments(); const auto version = 10'000 * segments[0] + 100 * segments[1] + segments[2]; + // Proxied networks are not configurable (onion/i2p/cjdns unreachable). + const auto network = [](const std::string& name) NOEXCEPT + { + return object_t + { + { "name", name }, + { "limited", false }, + { "reachable", true }, + { "proxy", std::string{} }, + { "proxy_randomize_credentials", false } + }; + }; + + array_t locals{}; + for (const auto& self: network_settings().inbound.selfs) + locals.emplace_back(object_t + { + { "address", self.to_host() }, + { "port", self.port() }, + { "score", 1 } + }); + + const auto services = node_settings().services_provided(); + const auto connections = channel_count(); + const auto inbound = inbound_channel_count(); + send_result(object_t { { "version", version }, { "subversion", settings.subversion }, { "protocolversion", network_settings().protocol_maximum }, + { "localservices", encode_base16(to_big_endian(services)) }, + { "localservicesnames", to_service_names(services) }, { "localrelay", network_settings().enable_relay }, { "timeoffset", 0 }, - { "connections", channel_count() }, + { "connections", connections }, + { "connections_in", inbound }, + { "connections_out", floored_subtract(connections, inbound) }, { "networkactive", true }, - { "networks", array_t{} }, + { "networks", array_t{ network("ipv4"), network("ipv6") } }, { "relayfee", node_settings().minimum_fee_rate }, { "incrementalfee", node_settings().minimum_bump_rate }, - { "localaddresses", array_t{} }, + { "localaddresses", std::move(locals) }, { "warnings", std::string{} } - }, 256); + }, 512); return true; } diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 2e3a992f..10001b44 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -610,7 +610,11 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__getnetworkinfo__fields) BOOST_REQUIRE(result.as_object().contains("version")); BOOST_REQUIRE(result.at("subversion").is_string()); BOOST_REQUIRE(result.as_object().contains("protocolversion")); - BOOST_REQUIRE(result.at("networks").is_array()); + BOOST_REQUIRE_EQUAL(as_text(result.at("networks").at(0).at("name")), "ipv4"); + BOOST_REQUIRE_EQUAL(as_text(result.at("localservices")).size(), 16u); + BOOST_REQUIRE(result.at("localservicesnames").is_array()); + BOOST_REQUIRE(result.at("connections_in").is_int64()); + BOOST_REQUIRE(result.at("connections_out").is_int64()); } // not implemented From f08b305d24c8fcdd5c131e7a9123e173053b1a00 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sat, 22 Aug 2026 22:59:38 -0400 Subject: [PATCH 10/21] Update wip method list for psbt implementations. --- test/protocols/bitcoind/bitcoind_rpc.cpp | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 10001b44..392ddc79 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -70,15 +70,7 @@ const std::vector wip_methods "waitforblock", "waitforblockheight", "waitfornewblock", - "analyzepsbt", - "combinepsbt", - "converttopsbt", - "createpsbt", - "decodepsbt", - "finalizepsbt", - "joinpsbts", "descriptorprocesspsbt", - "utxoupdatepsbt", "submitblock", "submitheader", "addnode", From ab5be82649a7949747e6ff23042fbf9dfbbdee18 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sat, 22 Aug 2026 23:18:35 -0400 Subject: [PATCH 11/21] Implement waitforblock, waitforblockheight, waitfornewblock. --- .../server/interfaces/bitcoind_blockchain.hpp | 6 +- .../protocol_bitcoind_blockchain.hpp | 32 +++- .../bitcoind/protocol_bitcoind_blockchain.cpp | 161 ++++++++++++++++-- test/interfaces/bitcoind.cpp | 3 +- test/protocols/bitcoind/bitcoind_rpc.cpp | 28 ++- 5 files changed, 207 insertions(+), 23 deletions(-) diff --git a/include/bitcoin/server/interfaces/bitcoind_blockchain.hpp b/include/bitcoin/server/interfaces/bitcoind_blockchain.hpp index 4b3a1b8b..e7caf085 100644 --- a/include/bitcoin/server/interfaces/bitcoind_blockchain.hpp +++ b/include/bitcoin/server/interfaces/bitcoind_blockchain.hpp @@ -57,9 +57,9 @@ struct bitcoind_blockchain_methods method<"getdifficulty">{}, method<"preciousblock">{ unimplemented }, method<"scanblocks">{ unimplemented }, - method<"waitforblock">{ unimplemented }, - method<"waitforblockheight">{ unimplemented }, - method<"waitfornewblock">{ unimplemented }, + 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<"getmempoolancestors">{ unimplemented }, method<"getmempoolcluster">{ unimplemented }, method<"getmempooldescendants">{ unimplemented }, diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp index e9aefbdb..60983068 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp @@ -47,11 +47,14 @@ class BCS_API protocol_bitcoind_blockchain const network::channel::ptr& channel, const options_t& options) NOEXCEPT : protocol_bitcoind_dispatch(session, channel, options), - network::tracker(session->log) + network::tracker(session->log), + wait_timer_(std::make_shared(session->log, + channel->strand())) { } void start() NOEXCEPT override; + void stopping(const code& ec) NOEXCEPT override; protected: /// Handlers. @@ -117,11 +120,13 @@ class BCS_API protocol_bitcoind_blockchain bool handle_scan_blocks(const code& ec, rpc_interface::scan_blocks) NOEXCEPT; bool handle_wait_for_block(const code& ec, - rpc_interface::wait_for_block) NOEXCEPT; + rpc_interface::wait_for_block, const std::string& blockhash, + double timeout) NOEXCEPT; bool handle_wait_for_block_height(const code& ec, - rpc_interface::wait_for_block_height) NOEXCEPT; + 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) NOEXCEPT; + rpc_interface::wait_for_new_block, double timeout) NOEXCEPT; bool handle_get_mempool_ancestors(const code& ec, rpc_interface::get_mempool_ancestors) NOEXCEPT; bool handle_get_mempool_cluster(const code& ec, @@ -138,6 +143,25 @@ class BCS_API protocol_bitcoind_blockchain rpc_interface::get_tx_spending_prevout) NOEXCEPT; bool handle_import_mempool(const code& ec, rpc_interface::import_mempool) NOEXCEPT; + + /// Chase events (block wait long polling). + bool handle_chase(const code& ec, node::chase event_, + node::event_value value) NOEXCEPT; + +private: + enum class wait : uint8_t { none, new_block, block, height }; + + void arm_wait(double timeout) NOEXCEPT; + void do_wait_event() NOEXCEPT; + void handle_wait_timeout(const code& ec) NOEXCEPT; + bool wait_done() const NOEXCEPT; + void send_tip() NOEXCEPT; + + // These are protected by strand. + wait wait_{ wait::none }; + size_t wait_height_{}; + system::hash_digest wait_hash_{}; + network::deadline::ptr wait_timer_; }; } // namespace server diff --git a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp index 84982636..b69b20e2 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp @@ -102,9 +102,9 @@ void protocol_bitcoind_blockchain::start() NOEXCEPT SUBSCRIBE_BITCOIND(handle_get_difficulty, _1, _2); SUBSCRIBE_BITCOIND(handle_precious_block, _1, _2); SUBSCRIBE_BITCOIND(handle_scan_blocks, _1, _2); - SUBSCRIBE_BITCOIND(handle_wait_for_block, _1, _2); - SUBSCRIBE_BITCOIND(handle_wait_for_block_height, _1, _2); - SUBSCRIBE_BITCOIND(handle_wait_for_new_block, _1, _2); + 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_get_mempool_ancestors, _1, _2); SUBSCRIBE_BITCOIND(handle_get_mempool_cluster, _1, _2); SUBSCRIBE_BITCOIND(handle_get_mempool_descendants, _1, _2); @@ -113,9 +113,18 @@ void protocol_bitcoind_blockchain::start() NOEXCEPT SUBSCRIBE_BITCOIND(handle_get_raw_mempool, _1, _2); SUBSCRIBE_BITCOIND(handle_get_tx_spending_prevout, _1, _2); SUBSCRIBE_BITCOIND(handle_import_mempool, _1, _2); + subscribe_chase(BIND(handle_chase, _1, _2, _3)); protocol_bitcoind_dispatch::start(); } +void protocol_bitcoind_blockchain::stopping(const code& ec) NOEXCEPT +{ + BC_ASSERT(stranded()); + unsubscribe_chase(); + wait_timer_->stop(); + protocol_bitcoind_dispatch::stopping(ec); +} + // Blockchain methods. // ---------------------------------------------------------------------------- @@ -1019,27 +1028,155 @@ bool protocol_bitcoind_blockchain::handle_scan_blocks(const code& ec, return true; } +// The response defers to the chase event or the timeout (long poll). The +// saved request context carries the deferred send (see dispatch). bool protocol_bitcoind_blockchain::handle_wait_for_block(const code& ec, - rpc_interface::wait_for_block) NOEXCEPT + rpc_interface::wait_for_block, const std::string& blockhash, + double timeout) NOEXCEPT { - if (stopped(ec)) return false; - send_error(error::not_implemented); + if (stopped(ec)) + return false; + + if (!decode_hash(wait_hash_, blockhash)) + { + send_error(error::invalid_argument); + return true; + } + + wait_ = wait::block; + arm_wait(timeout); return true; } bool protocol_bitcoind_blockchain::handle_wait_for_block_height(const code& ec, - rpc_interface::wait_for_block_height) NOEXCEPT + rpc_interface::wait_for_block_height, const double height, + double timeout) NOEXCEPT { - if (stopped(ec)) return false; - send_error(error::not_implemented); + if (stopped(ec)) + return false; + + if (!to_integer(wait_height_, height)) + { + send_error(error::invalid_argument); + return true; + } + + wait_ = wait::height; + arm_wait(timeout); return true; } bool protocol_bitcoind_blockchain::handle_wait_for_new_block(const code& ec, - rpc_interface::wait_for_new_block) NOEXCEPT + rpc_interface::wait_for_new_block, double timeout) NOEXCEPT { - if (stopped(ec)) return false; - send_error(error::not_implemented); + if (stopped(ec)) + return false; + + wait_ = wait::new_block; + wait_height_ = add1(archive().get_top_confirmed()); + arm_wait(timeout); + return true; +} + +// Wait machinery (strand). +// ---------------------------------------------------------------------------- + +bool protocol_bitcoind_blockchain::wait_done() const NOEXCEPT +{ + const auto& query = archive(); + switch (wait_) + { + case wait::new_block: + case wait::height: + return query.get_top_confirmed() >= wait_height_; + case wait::block: + { + const auto link = query.to_header(wait_hash_); + return !link.is_terminal() && query.is_confirmed_block(link); + } + default: + return false; + } +} + +void protocol_bitcoind_blockchain::send_tip() 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); +} + +void protocol_bitcoind_blockchain::arm_wait(double timeout) NOEXCEPT +{ + if (wait_done()) + { + wait_ = wait::none; + send_tip(); + return; + } + + // A zero timeout waits indefinitely (as bitcoind). + uint64_t span{}; + if (!to_integer(span, timeout)) + { + wait_ = wait::none; + send_error(error::invalid_argument); + return; + } + + if (!is_zero(span)) + wait_timer_->start(BIND(handle_wait_timeout, _1), + network::milliseconds(span)); +} + +void protocol_bitcoind_blockchain::do_wait_event() NOEXCEPT +{ + BC_ASSERT(stranded()); + if (wait_ == wait::none || !wait_done()) + return; + + wait_ = wait::none; + wait_timer_->stop(); + send_tip(); +} + +void protocol_bitcoind_blockchain::handle_wait_timeout(const code& ec) NOEXCEPT +{ + BC_ASSERT(stranded()); + if (stopped() || ec == network::error::operation_canceled || + wait_ == wait::none) + return; + + wait_ = wait::none; + send_tip(); +} + +// Chase events. +// ---------------------------------------------------------------------------- + +bool protocol_bitcoind_blockchain::handle_chase(const code&, + node::chase event_, node::event_value) NOEXCEPT +{ + // Do not pass ec to stopped, it is not a call status. + if (stopped()) + return false; + + switch (event_) + { + case node::chase::organized: + case node::chase::reorganized: + { + network::protocol::post(&CLASS::do_wait_event); + break; + } + default: + break; + } + return true; } diff --git a/test/interfaces/bitcoind.cpp b/test/interfaces/bitcoind.cpp index a594baad..6d6360d8 100644 --- a/test/interfaces/bitcoind.cpp +++ b/test/interfaces/bitcoind.cpp @@ -117,7 +117,8 @@ static_assert(bitcoind_blockchain_methods::names == "getbestblockhash getblock getblockchaininfo getblockcount " "getblockfilter getblockhash getblockheader getblockstats " "getchaintxstats gettxout verifychain gettxoutproof verifytxoutproof " - "getchainstates getchaintips getdeploymentinfo getdifficulty"); + "getchainstates getchaintips getdeploymentinfo getdifficulty " + "waitforblock waitforblockheight waitfornewblock"); static_assert(bitcoind_control_methods::names == "help getmemoryinfo getrpcinfo logging uptime"); static_assert(bitcoind_mining_methods::names == diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 392ddc79..4988df75 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -67,9 +67,6 @@ const std::vector wip_methods "getdescriptoractivity", "preciousblock", "scanblocks", - "waitforblock", - "waitforblockheight", - "waitfornewblock", "descriptorprocesspsbt", "submitblock", "submitheader", @@ -1114,6 +1111,31 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__response__websocket__id_matches_request) } +// waitfor (all conditions immediately met or timing out on the fixture) + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__waitforblockheight__at_top__immediate_tip) +{ + const auto response = rpc("waitforblockheight", "[9]"); + const auto& result = response.at("result"); + BOOST_REQUIRE_EQUAL(result.at("height").as_int64(), 9); + BOOST_REQUIRE_EQUAL(as_text(result.at("hash")), block9); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__waitforblock__confirmed_hash__immediate_tip) +{ + 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) +{ + const auto response = rpc("waitfornewblock", "[1]"); + const auto& result = response.at("result"); + BOOST_REQUIRE_EQUAL(result.at("height").as_int64(), 9); + BOOST_REQUIRE_EQUAL(as_text(result.at("hash")), block9); +} + // psbt (vectors from bip174) #define PSBT_UPDATER "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHAQQiACCMI1MXN0O1ld+0oHtyuo5C43l9p06H/n2ddJfjsgKJAwEFR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuIgYCOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnMQ2QxqTwAAAIAAAACAAwAAgCIGAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcENkMak8AAACAAAAAgAIAAIAAIgIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1Ptnuylh3EQ2QxqTwAAAIAAAACABAAAgAAiAgJ/Y5l1fS7/VaE2rQLGhLGDi2VW5fG2s0KCqUtrUAUQlhDZDGpPAAAAgAAAAIAFAACAAA==" From d36b3ab88ac09e3c75f77b3d18d3714c04e6c7d7 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sat, 22 Aug 2026 23:35:27 -0400 Subject: [PATCH 12/21] Implement submitblock and submitheader. --- .../server/interfaces/bitcoind_mining.hpp | 4 +- .../protocols/protocol_bitcoind_mining.hpp | 12 ++- .../bitcoind/protocol_bitcoind_mining.cpp | 101 ++++++++++++++++-- test/interfaces/bitcoind.cpp | 2 +- test/protocols/bitcoind/bitcoind_rpc.cpp | 24 ++++- 5 files changed, 128 insertions(+), 15 deletions(-) diff --git a/include/bitcoin/server/interfaces/bitcoind_mining.hpp b/include/bitcoin/server/interfaces/bitcoind_mining.hpp index 4ff0978a..a6b6f68a 100644 --- a/include/bitcoin/server/interfaces/bitcoind_mining.hpp +++ b/include/bitcoin/server/interfaces/bitcoind_mining.hpp @@ -32,8 +32,8 @@ struct bitcoind_mining_methods { method<"getnetworkhashps", optional<120.0>, optional<-1.0>>{ "nblocks", "height" }, method<"getmininginfo">{}, - method<"submitblock">{ unimplemented }, - method<"submitheader">{ unimplemented }, + method<"submitblock", string_t, optional<""_t>>{ "hexdata", "dummy" }, + method<"submitheader", string_t>{ "hexdata" }, method<"getblocktemplate">{ unimplemented }, method<"getprioritisedtransactions">{ unimplemented }, method<"prioritisetransaction">{ unimplemented } diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_mining.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_mining.hpp index 95341b07..9edbb459 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_mining.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_mining.hpp @@ -61,15 +61,23 @@ class BCS_API protocol_bitcoind_mining bool handle_get_mining_info(const code& ec, rpc_interface::get_mining_info) NOEXCEPT; bool handle_submit_block(const code& ec, - rpc_interface::submit_block) NOEXCEPT; + rpc_interface::submit_block, const std::string& hexdata, + const std::string& dummy) NOEXCEPT; bool handle_submit_header(const code& ec, - rpc_interface::submit_header) NOEXCEPT; + rpc_interface::submit_header, const std::string& hexdata) NOEXCEPT; bool handle_get_block_template(const code& ec, rpc_interface::get_block_template) NOEXCEPT; bool handle_get_prioritised_transactions(const code& ec, rpc_interface::get_prioritised_transactions) NOEXCEPT; bool handle_prioritise_transaction(const code& ec, rpc_interface::prioritise_transaction) NOEXCEPT; + +private: + /// Organize completions (bounced to the channel strand). + void handle_organize_block(const code& ec, size_t height) NOEXCEPT; + void handle_organize_header(const code& ec, size_t height) NOEXCEPT; + void do_submit_block(const code& ec) NOEXCEPT; + void do_submit_header(const code& ec) NOEXCEPT; }; } // namespace server diff --git a/src/protocols/bitcoind/protocol_bitcoind_mining.cpp b/src/protocols/bitcoind/protocol_bitcoind_mining.cpp index 2160bd76..27c0d909 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_mining.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_mining.cpp @@ -55,8 +55,8 @@ void protocol_bitcoind_mining::start() NOEXCEPT SUBSCRIBE_BITCOIND(handle_get_network_hash_ps, _1, _2, _3, _4); SUBSCRIBE_BITCOIND(handle_get_mining_info, _1, _2); - SUBSCRIBE_BITCOIND(handle_submit_block, _1, _2); - SUBSCRIBE_BITCOIND(handle_submit_header, _1, _2); + SUBSCRIBE_BITCOIND(handle_submit_block, _1, _2, _3, _4); + SUBSCRIBE_BITCOIND(handle_submit_header, _1, _2, _3); SUBSCRIBE_BITCOIND(handle_get_block_template, _1, _2); SUBSCRIBE_BITCOIND(handle_get_prioritised_transactions, _1, _2); SUBSCRIBE_BITCOIND(handle_prioritise_transaction, _1, _2); @@ -212,22 +212,107 @@ bool protocol_bitcoind_mining::handle_get_mining_info(const code& ec, return true; } +// The response defers to organize completion (see dispatch). bool protocol_bitcoind_mining::handle_submit_block(const code& ec, - rpc_interface::submit_block) NOEXCEPT + rpc_interface::submit_block, const std::string& hexdata, + const std::string&) NOEXCEPT { - if (stopped(ec)) return false; - send_error(error::not_implemented); + if (stopped(ec)) + return false; + + data_chunk data{}; + if (!decode_base16(data, hexdata)) + { + send_error(error::invalid_argument); + return true; + } + + constexpr auto witness = true; + const auto block = to_shared(data, witness); + if (!block->is_valid()) + { + send_error(error::invalid_argument); + return true; + } + + // bitcoind reports an already-stored block as a duplicate result. + if (!archive().to_header(block->hash()).is_terminal()) + { + send_result(std::string{ "duplicate" }, 32); + return true; + } + + organize(block, BIND(handle_organize_block, _1, _2)); return true; } bool protocol_bitcoind_mining::handle_submit_header(const code& ec, - rpc_interface::submit_header) NOEXCEPT + rpc_interface::submit_header, const std::string& hexdata) NOEXCEPT { - if (stopped(ec)) return false; - send_error(error::not_implemented); + if (stopped(ec)) + return false; + + data_chunk data{}; + if (!decode_base16(data, hexdata)) + { + send_error(error::invalid_argument); + return true; + } + + const auto header = to_shared(data); + if (!header->is_valid()) + { + send_error(error::invalid_argument); + return true; + } + + if (!archive().to_header(header->hash()).is_terminal()) + { + send_result(null_t{}, 8); + return true; + } + + organize(header, BIND(handle_organize_header, _1, _2)); return true; } +void protocol_bitcoind_mining::handle_organize_block(const code& ec, + size_t) NOEXCEPT +{ + if (stopped()) + return; + + network::protocol::post(&CLASS::do_submit_block, ec); +} + +void protocol_bitcoind_mining::handle_organize_header(const code& ec, + size_t) NOEXCEPT +{ + if (stopped()) + return; + + network::protocol::post(&CLASS::do_submit_header, ec); +} + +// bitcoind returns null on acceptance and a reason string on rejection. +void protocol_bitcoind_mining::do_submit_block(const code& ec) NOEXCEPT +{ + BC_ASSERT(stranded()); + if (ec) + send_result(ec.message(), 64); + else + send_result(null_t{}, 8); +} + +void protocol_bitcoind_mining::do_submit_header(const code& ec) NOEXCEPT +{ + BC_ASSERT(stranded()); + if (ec) + send_error(ec); + else + send_result(null_t{}, 8); +} + bool protocol_bitcoind_mining::handle_get_block_template(const code& ec, rpc_interface::get_block_template) NOEXCEPT { diff --git a/test/interfaces/bitcoind.cpp b/test/interfaces/bitcoind.cpp index 6d6360d8..fedfea08 100644 --- a/test/interfaces/bitcoind.cpp +++ b/test/interfaces/bitcoind.cpp @@ -122,7 +122,7 @@ static_assert(bitcoind_blockchain_methods::names == static_assert(bitcoind_control_methods::names == "help getmemoryinfo getrpcinfo logging uptime"); static_assert(bitcoind_mining_methods::names == - "getnetworkhashps getmininginfo"); + "getnetworkhashps getmininginfo submitblock submitheader"); static_assert(bitcoind_network_methods::names == "getnetworkinfo getconnectioncount getnettotals"); static_assert(bitcoind_notifications_methods::names == "getzmqnotifications"); diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 4988df75..3a8695b6 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -68,8 +68,6 @@ const std::vector wip_methods "preciousblock", "scanblocks", "descriptorprocesspsbt", - "submitblock", - "submitheader", "addnode", "disconnectnode", "exportasmap", @@ -1111,6 +1109,28 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__response__websocket__id_matches_request) } +// submit + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__submitblock__existing_block__duplicate) +{ + const auto block = encode_base16(test::block9.to_data(true)); + const auto response = rpc("submitblock", "[\"" + block + "\"]"); + BOOST_REQUIRE_EQUAL(as_text(response.at("result")), "duplicate"); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__submitblock__garbage__invalid) +{ + const auto response = rpc("submitblock", "[\"deadbeef\"]"); + REQUIRE_NO_THROW_TRUE(response.as_object().contains("error")); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__submitheader__existing_header__null) +{ + const auto header = encode_base16(test::block9.header().to_data()); + const auto response = rpc("submitheader", "[\"" + header + "\"]"); + BOOST_REQUIRE(response.at("result").is_null()); +} + // waitfor (all conditions immediately met or timing out on the fixture) BOOST_AUTO_TEST_CASE(bitcoind_rpc__waitforblockheight__at_top__immediate_tip) From d5907ad0ee9d068fba8b789e109fd3a5eb1ee098 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sat, 22 Aug 2026 23:49:14 -0400 Subject: [PATCH 13/21] Implement getnodeaddresses, getaddrmaninfo, ping, setnetworkactive. --- .../server/interfaces/bitcoind_network.hpp | 8 +- .../protocols/protocol_bitcoind_network.hpp | 27 ++- .../bitcoind/protocol_bitcoind_network.cpp | 187 ++++++++++++++---- test/interfaces/bitcoind.cpp | 3 +- test/protocols/bitcoind/bitcoind_rpc.cpp | 42 +++- 5 files changed, 217 insertions(+), 50 deletions(-) diff --git a/include/bitcoin/server/interfaces/bitcoind_network.hpp b/include/bitcoin/server/interfaces/bitcoind_network.hpp index b5a4dd99..7e12361f 100644 --- a/include/bitcoin/server/interfaces/bitcoind_network.hpp +++ b/include/bitcoin/server/interfaces/bitcoind_network.hpp @@ -38,13 +38,13 @@ struct bitcoind_network_methods method<"disconnectnode">{ unimplemented }, method<"exportasmap">{ unimplemented }, method<"getaddednodeinfo">{ unimplemented }, - method<"getaddrmaninfo">{ unimplemented }, + method<"getaddrmaninfo">{}, method<"getconnectioncount">{}, method<"getnettotals">{}, - method<"getnodeaddresses">{ unimplemented }, + method<"getnodeaddresses", optional<1.0>, optional<""_t>>{ "count", "network" }, method<"getpeerinfo">{ unimplemented }, - method<"ping">{ unimplemented }, - method<"setnetworkactive">{ unimplemented } + method<"ping">{}, + method<"setnetworkactive", boolean_t>{ "state" } }; template diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_network.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_network.hpp index fe841baf..91ddfe01 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_network.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_network.hpp @@ -73,18 +73,33 @@ class BCS_API protocol_bitcoind_network rpc_interface::get_added_node_info) NOEXCEPT; bool handle_get_addrman_info(const code& ec, rpc_interface::get_addrman_info) NOEXCEPT; + bool handle_get_node_addresses(const code& ec, + rpc_interface::get_node_addresses, double count, + const std::string& network) NOEXCEPT; + bool handle_ping(const code& ec, rpc_interface::ping) NOEXCEPT; + bool handle_set_network_active(const code& ec, + rpc_interface::set_network_active, bool state) NOEXCEPT; bool handle_get_connection_count(const code& ec, rpc_interface::get_connection_count) NOEXCEPT; bool handle_get_net_totals(const code& ec, rpc_interface::get_net_totals) NOEXCEPT; - bool handle_get_node_addresses(const code& ec, - rpc_interface::get_node_addresses) NOEXCEPT; bool handle_get_peer_info(const code& ec, rpc_interface::get_peer_info) NOEXCEPT; - bool handle_ping(const code& ec, - rpc_interface::ping) NOEXCEPT; - bool handle_set_network_active(const code& ec, - rpc_interface::set_network_active) NOEXCEPT; + +private: + /// Address fetch completions (bounced to the channel strand). + void handle_fetch_nodes(const code& ec, + const network::address_cptr& message) NOEXCEPT; + void handle_fetch_info(const code& ec, + const network::address_cptr& message) NOEXCEPT; + void do_send_nodes(const code& ec, + const network::address_cptr& message) NOEXCEPT; + void do_send_info(const code& ec, + const network::address_cptr& message) NOEXCEPT; + + // These are protected by strand. + size_t node_count_{}; + std::string node_network_{}; }; } // namespace server diff --git a/src/protocols/bitcoind/protocol_bitcoind_network.cpp b/src/protocols/bitcoind/protocol_bitcoind_network.cpp index 7501c68d..f255098c 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_network.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_network.cpp @@ -64,10 +64,10 @@ void protocol_bitcoind_network::start() NOEXCEPT SUBSCRIBE_BITCOIND(handle_get_addrman_info, _1, _2); SUBSCRIBE_BITCOIND(handle_get_connection_count, _1, _2); SUBSCRIBE_BITCOIND(handle_get_net_totals, _1, _2); - SUBSCRIBE_BITCOIND(handle_get_node_addresses, _1, _2); + SUBSCRIBE_BITCOIND(handle_get_node_addresses, _1, _2, _3, _4); SUBSCRIBE_BITCOIND(handle_get_peer_info, _1, _2); SUBSCRIBE_BITCOIND(handle_ping, _1, _2); - SUBSCRIBE_BITCOIND(handle_set_network_active, _1, _2); + SUBSCRIBE_BITCOIND(handle_set_network_active, _1, _2, _3); protocol_bitcoind_dispatch::start(); } @@ -155,6 +155,157 @@ bool protocol_bitcoind_network::handle_get_network_info(const code& ec, return true; } +// The pool has no tried table, so all addresses are reported as new. +static object_t address_bucket(size_t count) NOEXCEPT +{ + return object_t + { + { "new", count }, + { "tried", zero }, + { "total", count } + }; +} + +bool protocol_bitcoind_network::handle_get_addrman_info(const code& ec, + rpc_interface::get_addrman_info) NOEXCEPT +{ + if (stopped(ec)) + return false; + + fetch_addresses(BIND(handle_fetch_info, _1, _2)); + return true; +} + +void protocol_bitcoind_network::handle_fetch_info(const code& ec, + const network::address_cptr& message) NOEXCEPT +{ + if (stopped()) + return; + + network::protocol::post(&CLASS::do_send_info, ec, message); +} + +// An empty or unavailable pool is reported as empty (as bitcoind). +void protocol_bitcoind_network::do_send_info(const code& ec, + const network::address_cptr& message) NOEXCEPT +{ + BC_ASSERT(stranded()); + const auto empty = (ec || !message); + + size_t v4{}; + if (!empty) + for (const auto& item: message->addresses) + if (network::config::address{ item }.is_v4()) + ++v4; + + const auto total = empty ? zero : message->addresses.size(); + send_result(object_t + { + { "ipv4", address_bucket(v4) }, + { "ipv6", address_bucket(total - v4) }, + { "onion", address_bucket(zero) }, + { "i2p", address_bucket(zero) }, + { "cjdns", address_bucket(zero) }, + { "all_networks", address_bucket(total) } + }, 512); +} + +bool protocol_bitcoind_network::handle_get_node_addresses(const code& ec, + rpc_interface::get_node_addresses, double count, + const std::string& network) NOEXCEPT +{ + if (stopped(ec)) + return false; + + if (!to_integer(node_count_, count) || + (!network.empty() && network != "ipv4" && network != "ipv6")) + { + send_error(error::invalid_argument); + return true; + } + + node_network_ = network; + fetch_addresses(BIND(handle_fetch_nodes, _1, _2)); + return true; +} + +void protocol_bitcoind_network::handle_fetch_nodes(const code& ec, + const network::address_cptr& message) NOEXCEPT +{ + if (stopped()) + return; + + network::protocol::post(&CLASS::do_send_nodes, ec, message); +} + +void protocol_bitcoind_network::do_send_nodes(const code& ec, + const network::address_cptr& message) NOEXCEPT +{ + BC_ASSERT(stranded()); + + // An empty or unavailable pool is reported as empty (as bitcoind). + if (ec || !message) + { + send_result(array_t{}, 16); + return; + } + + // A zero count returns all addresses (as bitcoind). + array_t out{}; + for (const auto& item: message->addresses) + { + if (!is_zero(node_count_) && out.size() >= node_count_) + break; + + const network::config::address address{ item }; + const auto name = address.is_v4() ? "ipv4" : "ipv6"; + if (!node_network_.empty() && node_network_ != name) + continue; + + out.emplace_back(object_t + { + { "time", item.timestamp }, + { "services", item.services }, + { "address", address.to_host() }, + { "port", item.port }, + { "network", std::string{ name } } + }); + } + + const auto size = 128 * out.size(); + send_result(std::move(out), size); +} + +// The nonce is discarded (pong correlation is a channel concern). +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; +} + +bool protocol_bitcoind_network::handle_set_network_active(const code& ec, + rpc_interface::set_network_active, bool state) NOEXCEPT +{ + if (stopped(ec)) + return false; + + if (state) + node::protocol::resume(); + else + node::protocol::suspend(network::error::service_suspended); + + send_result(state, 8); + return true; +} + bool protocol_bitcoind_network::handle_clear_banned(const code& ec, rpc_interface::clear_banned) NOEXCEPT { @@ -211,14 +362,6 @@ bool protocol_bitcoind_network::handle_get_added_node_info(const code& ec, return true; } -bool protocol_bitcoind_network::handle_get_addrman_info(const code& ec, - rpc_interface::get_addrman_info) NOEXCEPT -{ - if (stopped(ec)) return false; - send_error(error::not_implemented); - 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 @@ -258,14 +401,6 @@ bool protocol_bitcoind_network::handle_get_net_totals(const code& ec, return true; } -bool protocol_bitcoind_network::handle_get_node_addresses(const code& ec, - rpc_interface::get_node_addresses) NOEXCEPT -{ - if (stopped(ec)) return false; - send_error(error::not_implemented); - return true; -} - bool protocol_bitcoind_network::handle_get_peer_info(const code& ec, rpc_interface::get_peer_info) NOEXCEPT { @@ -274,22 +409,6 @@ bool protocol_bitcoind_network::handle_get_peer_info(const code& ec, return true; } -bool protocol_bitcoind_network::handle_ping(const code& ec, - rpc_interface::ping) NOEXCEPT -{ - if (stopped(ec)) return false; - send_error(error::not_implemented); - return true; -} - -bool protocol_bitcoind_network::handle_set_network_active(const code& ec, - rpc_interface::set_network_active) NOEXCEPT -{ - if (stopped(ec)) return false; - send_error(error::not_implemented); - return true; -} - BC_POP_WARNING() BC_POP_WARNING() BC_POP_WARNING() diff --git a/test/interfaces/bitcoind.cpp b/test/interfaces/bitcoind.cpp index fedfea08..7038af55 100644 --- a/test/interfaces/bitcoind.cpp +++ b/test/interfaces/bitcoind.cpp @@ -124,7 +124,8 @@ static_assert(bitcoind_control_methods::names == static_assert(bitcoind_mining_methods::names == "getnetworkhashps getmininginfo submitblock submitheader"); static_assert(bitcoind_network_methods::names == - "getnetworkinfo getconnectioncount getnettotals"); + "getnetworkinfo getaddrmaninfo getconnectioncount getnettotals " + "getnodeaddresses ping setnetworkactive"); static_assert(bitcoind_notifications_methods::names == "getzmqnotifications"); static_assert(bitcoind_test_methods::names == ""); static_assert(bitcoind_transaction_methods::names == diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 3a8695b6..43bde428 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -72,11 +72,6 @@ const std::vector wip_methods "disconnectnode", "exportasmap", "getaddednodeinfo", - "getaddrmaninfo", - "getnodeaddresses", - "getpeerinfo", - "ping", - "setnetworkactive", "deriveaddresses", "getdescriptorinfo", "getopenrpcinfo" @@ -1109,6 +1104,43 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__response__websocket__id_matches_request) } +// network group + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__ping__always__null) +{ + const auto response = rpc("ping"); + BOOST_REQUIRE(response.at("result").is_null()); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getnodeaddresses__empty_pool__empty) +{ + const auto response = rpc("getnodeaddresses"); + BOOST_REQUIRE(response.at("result").is_array()); + BOOST_REQUIRE(response.at("result").as_array().empty()); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getnodeaddresses__bad_network__invalid) +{ + const auto response = rpc("getnodeaddresses", "[0, \"onion\"]"); + REQUIRE_NO_THROW_TRUE(response.as_object().contains("error")); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getaddrmaninfo__empty_pool__zero_buckets) +{ + const auto response = rpc("getaddrmaninfo"); + const auto& result = response.at("result"); + BOOST_REQUIRE_EQUAL(result.at("all_networks").at("total").as_int64(), 0); + BOOST_REQUIRE_EQUAL(result.at("ipv4").at("tried").as_int64(), 0); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__setnetworkactive__toggle__round_trips) +{ + const auto off = rpc("setnetworkactive", "[false]"); + BOOST_REQUIRE(!off.at("result").as_bool()); + const auto on = rpc("setnetworkactive", "[true]"); + BOOST_REQUIRE(on.at("result").as_bool()); +} + // submit BOOST_AUTO_TEST_CASE(bitcoind_rpc__submitblock__existing_block__duplicate) From 4c75816de77ca18e7594f9c089cc94350f6fee68 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sat, 22 Aug 2026 23:57:47 -0400 Subject: [PATCH 14/21] Implement addnode add and onetry. --- .../server/interfaces/bitcoind_network.hpp | 2 +- .../protocols/protocol_bitcoind_network.hpp | 3 +- .../bitcoind/protocol_bitcoind_network.cpp | 31 ++++++++++++++++--- test/interfaces/bitcoind.cpp | 2 +- test/protocols/bitcoind/bitcoind_rpc.cpp | 13 +++++++- 5 files changed, 43 insertions(+), 8 deletions(-) diff --git a/include/bitcoin/server/interfaces/bitcoind_network.hpp b/include/bitcoin/server/interfaces/bitcoind_network.hpp index 7e12361f..66bcb7fc 100644 --- a/include/bitcoin/server/interfaces/bitcoind_network.hpp +++ b/include/bitcoin/server/interfaces/bitcoind_network.hpp @@ -34,7 +34,7 @@ struct bitcoind_network_methods method<"clearbanned">{ unimplemented }, method<"listbanned">{ unimplemented }, method<"setban">{ unimplemented }, - method<"addnode">{ unimplemented }, + method<"addnode", string_t, string_t, optional>{ "node", "command", "v2transport" }, method<"disconnectnode">{ unimplemented }, method<"exportasmap">{ unimplemented }, method<"getaddednodeinfo">{ unimplemented }, diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_network.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_network.hpp index 91ddfe01..3066d60a 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_network.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_network.hpp @@ -64,7 +64,8 @@ class BCS_API protocol_bitcoind_network bool handle_set_ban(const code& ec, rpc_interface::set_ban) NOEXCEPT; bool handle_add_node(const code& ec, - rpc_interface::add_node) NOEXCEPT; + rpc_interface::add_node, const std::string& node, + const std::string& command, bool v2transport) NOEXCEPT; bool handle_disconnect_node(const code& ec, rpc_interface::disconnect_node) NOEXCEPT; bool handle_export_asmap(const code& ec, diff --git a/src/protocols/bitcoind/protocol_bitcoind_network.cpp b/src/protocols/bitcoind/protocol_bitcoind_network.cpp index f255098c..488f409e 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_network.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_network.cpp @@ -57,7 +57,7 @@ void protocol_bitcoind_network::start() NOEXCEPT SUBSCRIBE_BITCOIND(handle_clear_banned, _1, _2); SUBSCRIBE_BITCOIND(handle_list_banned, _1, _2); SUBSCRIBE_BITCOIND(handle_set_ban, _1, _2); - SUBSCRIBE_BITCOIND(handle_add_node, _1, _2); + SUBSCRIBE_BITCOIND(handle_add_node, _1, _2, _3, _4, _5); SUBSCRIBE_BITCOIND(handle_disconnect_node, _1, _2); SUBSCRIBE_BITCOIND(handle_export_asmap, _1, _2); SUBSCRIBE_BITCOIND(handle_get_added_node_info, _1, _2); @@ -330,11 +330,34 @@ bool protocol_bitcoind_network::handle_set_ban(const code& ec, 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) NOEXCEPT + rpc_interface::add_node, const std::string& node, + const std::string& command, bool) NOEXCEPT { - if (stopped(ec)) return false; - send_error(error::not_implemented); + if (stopped(ec)) + return false; + + if (command != "add" && command != "onetry") + { + send_error(command == "remove" ? error::not_implemented : + error::invalid_argument); + return true; + } + + // The endpoint parse throws on malformed input. + try + { + connect(network::config::endpoint{ node }); + } + catch (const std::exception&) + { + send_error(error::invalid_argument); + return true; + } + + send_result(null_t{}, 8); return true; } diff --git a/test/interfaces/bitcoind.cpp b/test/interfaces/bitcoind.cpp index 7038af55..76e1808b 100644 --- a/test/interfaces/bitcoind.cpp +++ b/test/interfaces/bitcoind.cpp @@ -124,7 +124,7 @@ static_assert(bitcoind_control_methods::names == static_assert(bitcoind_mining_methods::names == "getnetworkhashps getmininginfo submitblock submitheader"); static_assert(bitcoind_network_methods::names == - "getnetworkinfo getaddrmaninfo getconnectioncount getnettotals " + "getnetworkinfo addnode getaddrmaninfo getconnectioncount getnettotals " "getnodeaddresses ping setnetworkactive"); static_assert(bitcoind_notifications_methods::names == "getzmqnotifications"); static_assert(bitcoind_test_methods::names == ""); diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 43bde428..48ea3c0c 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -68,7 +68,6 @@ const std::vector wip_methods "preciousblock", "scanblocks", "descriptorprocesspsbt", - "addnode", "disconnectnode", "exportasmap", "getaddednodeinfo", @@ -1106,6 +1105,18 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__response__websocket__id_matches_request) // network group +BOOST_AUTO_TEST_CASE(bitcoind_rpc__addnode__remove__not_implemented) +{ + const auto response = rpc("addnode", "[\"127.0.0.1:8333\", \"remove\"]"); + REQUIRE_NO_THROW_TRUE(response.as_object().contains("error")); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__addnode__bad_command__invalid) +{ + const auto response = rpc("addnode", "[\"127.0.0.1:8333\", \"nonsense\"]"); + REQUIRE_NO_THROW_TRUE(response.as_object().contains("error")); +} + BOOST_AUTO_TEST_CASE(bitcoind_rpc__ping__always__null) { const auto response = rpc("ping"); From 068ebb1fb799f9d6942366829b0bc73f863b3f59 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sun, 23 Aug 2026 00:44:45 -0400 Subject: [PATCH 15/21] Implement deriveaddresses and getdescriptorinfo. --- .../server/interfaces/bitcoind_utility.hpp | 4 +- .../protocols/protocol_bitcoind_utility.hpp | 10 +- .../bitcoind/protocol_bitcoind_utility.cpp | 126 ++++++++++++++++-- test/interfaces/bitcoind.cpp | 3 +- test/protocols/bitcoind/bitcoind_rpc.cpp | 40 +++++- 5 files changed, 168 insertions(+), 15 deletions(-) diff --git a/include/bitcoin/server/interfaces/bitcoind_utility.hpp b/include/bitcoin/server/interfaces/bitcoind_utility.hpp index f674f76e..4472f2d5 100644 --- a/include/bitcoin/server/interfaces/bitcoind_utility.hpp +++ b/include/bitcoin/server/interfaces/bitcoind_utility.hpp @@ -33,8 +33,8 @@ struct bitcoind_utility_methods method<"decodescript", string_t>{ "hex" }, method<"validateaddress", string_t>{ "address" }, method<"createmultisig", number_t, array_t, optional<"legacy"_t>>{ "nrequired", "keys", "address_type" }, - method<"deriveaddresses">{ unimplemented }, - method<"getdescriptorinfo">{ unimplemented }, + method<"deriveaddresses", string_t, nullable>{ "descriptor", "range" }, + 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 } diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_utility.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_utility.hpp index a4e5bfd0..096fcf84 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_utility.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_utility.hpp @@ -65,9 +65,11 @@ class BCS_API protocol_bitcoind_utility const network::rpc::array_t& keys, const std::string& address_type) NOEXCEPT; bool handle_derive_addresses(const code& ec, - rpc_interface::derive_addresses) NOEXCEPT; + rpc_interface::derive_addresses, const std::string& expression, + const std::optional& range) NOEXCEPT; bool handle_get_descriptor_info(const code& ec, - rpc_interface::get_descriptor_info) NOEXCEPT; + rpc_interface::get_descriptor_info, + const std::string& expression) NOEXCEPT; bool handle_verify_message(const code& ec, rpc_interface::verify_message, const std::string& address, const std::string& signature, const std::string& message) NOEXCEPT; @@ -75,6 +77,10 @@ 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; + + /// The address of a singular output script (empty if unaddressable). + std::string to_address( + const system::chain::script& script) const NOEXCEPT; }; } // namespace server diff --git a/src/protocols/bitcoind/protocol_bitcoind_utility.cpp b/src/protocols/bitcoind/protocol_bitcoind_utility.cpp index 6ac71d5a..a7529a3b 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_utility.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_utility.cpp @@ -56,8 +56,8 @@ void protocol_bitcoind_utility::start() NOEXCEPT SUBSCRIBE_BITCOIND(handle_decode_script, _1, _2, _3); SUBSCRIBE_BITCOIND(handle_validate_address, _1, _2, _3); SUBSCRIBE_BITCOIND(handle_create_multisig, _1, _2, _3, _4, _5); - SUBSCRIBE_BITCOIND(handle_derive_addresses, _1, _2); - SUBSCRIBE_BITCOIND(handle_get_descriptor_info, _1, _2); + SUBSCRIBE_BITCOIND(handle_derive_addresses, _1, _2, _3, _4); + SUBSCRIBE_BITCOIND(handle_get_descriptor_info, _1, _2, _3); 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); @@ -245,19 +245,129 @@ bool protocol_bitcoind_utility::handle_create_multisig(const code& ec, return true; } +// The address of a singular output script (empty if unaddressable). +std::string protocol_bitcoind_utility::to_address( + const chain::script& script) const NOEXCEPT +{ + using namespace chain; + using namespace wallet; + + const auto& ops = script.ops(); + if (chain::script::is_pay_witness_pattern(ops)) + { + const auto code = ops.front().code(); + const auto version = (code == opcode::push_size_0) ? 0_u8 : + operation::opcode_to_positive(code); + + return witness_address{ ops.at(1).data(), version, + witness_ }.encoded(); + } + + const auto pay = payment_address::extract_output(script, p2kh_, p2sh_); + return pay ? pay.encoded() : std::string{}; +} + bool protocol_bitcoind_utility::handle_derive_addresses(const code& ec, - rpc_interface::derive_addresses) NOEXCEPT + rpc_interface::derive_addresses, const std::string& expression, + const std::optional& range) NOEXCEPT { - if (stopped(ec)) return false; - send_error(error::not_implemented); + if (stopped(ec)) + return false; + + const wallet::descriptor parsed{ expression }; + if (!parsed || parsed.ranged() != range.has_value()) + { + send_error(error::invalid_argument); + return true; + } + + // The range is an end index or a [begin, end] pair (as bitcoind). + uint32_t begin{}; + uint32_t end{}; + if (range.has_value()) + { + const auto& value = range.value().value(); + if (std::holds_alternative(value)) + { + if (!to_integer(end, std::get(value))) + { + send_error(error::invalid_argument); + return true; + } + } + else if (std::holds_alternative(value)) + { + const auto& pair = std::get(value); + if (pair.size() != 2u || + !std::holds_alternative(pair.front().value()) || + !std::holds_alternative(pair.back().value()) || + !to_integer(begin, std::get(pair.front().value())) || + !to_integer(end, std::get(pair.back().value())) || + end < begin) + { + send_error(error::invalid_argument); + return true; + } + } + else + { + send_error(error::invalid_argument); + return true; + } + } + + // bitcoind's derivation range limit. + constexpr uint32_t maximum_range = 10'000; + if (floored_subtract(end, begin) >= maximum_range) + { + send_error(error::invalid_argument); + return true; + } + + array_t out{}; + for (auto index = begin; index <= end; ++index) + { + const auto scripts = parsed.scripts(index); + std::string address{}; + if (is_one(scripts.size())) + address = to_address(scripts.front()); + + if (address.empty()) + { + send_error(error::invalid_argument); + return true; + } + + out.emplace_back(std::move(address)); + } + + const auto size = 64 * out.size(); + send_result(std::move(out), size); return true; } bool protocol_bitcoind_utility::handle_get_descriptor_info(const code& ec, - rpc_interface::get_descriptor_info) NOEXCEPT + rpc_interface::get_descriptor_info, + const std::string& expression) NOEXCEPT { - if (stopped(ec)) return false; - send_error(error::not_implemented); + if (stopped(ec)) + return false; + + const wallet::descriptor parsed{ expression }; + if (!parsed) + { + send_error(error::invalid_argument); + return true; + } + + send_result(object_t + { + { "descriptor", parsed.encoded() }, + { "checksum", parsed.checksum() }, + { "isrange", parsed.ranged() }, + { "issolvable", parsed.solvable() }, + { "hasprivatekeys", parsed.has_private_keys() } + }, 256); return true; } diff --git a/test/interfaces/bitcoind.cpp b/test/interfaces/bitcoind.cpp index 76e1808b..d08314c9 100644 --- a/test/interfaces/bitcoind.cpp +++ b/test/interfaces/bitcoind.cpp @@ -134,5 +134,6 @@ static_assert(bitcoind_transaction_methods::names == "converttopsbt createpsbt decodepsbt finalizepsbt joinpsbts " "utxoupdatepsbt"); static_assert(bitcoind_utility_methods::names == - "decodescript validateaddress createmultisig verifymessage getindexinfo"); + "decodescript validateaddress createmultisig deriveaddresses " + "getdescriptorinfo verifymessage getindexinfo"); static_assert(bitcoind_wallet_methods::names == ""); diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 48ea3c0c..d85837e0 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -71,8 +71,6 @@ const std::vector wip_methods "disconnectnode", "exportasmap", "getaddednodeinfo", - "deriveaddresses", - "getdescriptorinfo", "getopenrpcinfo" }; @@ -1103,6 +1101,44 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__response__websocket__id_matches_request) } +// descriptors + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getdescriptorinfo__wpkh__expected) +{ + const auto response = rpc("getdescriptorinfo", "[\"wpkh([d34db33f/84h/0h/0h]xpub6DJ2dNUysrn5Vt36jH2KLBT2i1auw1tTSSomg8PhqNiUtx8QX2SvC9nrHu81fT41fvDUnhMjEzQgXnQjKEu3oaqMSzhSrHMxyyoEAmUHQbY/0/*)\"]"); + const auto& result = response.at("result"); + BOOST_REQUIRE_EQUAL(as_text(result.at("checksum")), "cjjspncu"); + BOOST_REQUIRE(result.at("isrange").as_bool()); + BOOST_REQUIRE(result.at("issolvable").as_bool()); + BOOST_REQUIRE(!result.at("hasprivatekeys").as_bool()); +} + +// The derived address round-trips to the bip386 vector script. +BOOST_AUTO_TEST_CASE(bitcoind_rpc__deriveaddresses__tr_bip386__expected) +{ + const auto response = rpc("deriveaddresses", "[\"tr(a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)\"]"); + const auto& result = response.at("result"); + BOOST_REQUIRE_EQUAL(result.as_array().size(), 1u); + const auto address = as_text(result.at(0)); + BOOST_REQUIRE(address.starts_with("bc1p")); + const auto validated = rpc("validateaddress", "[\"" + address + "\"]"); + BOOST_REQUIRE_EQUAL(as_text(validated.at("result").at("scriptPubKey")), "512077aab6e066f8a7419c5ab714c12c67d25007ed55a43cadcacb4d7a970a093f11"); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__deriveaddresses__ranged_without_range__invalid) +{ + const auto response = rpc("deriveaddresses", "[\"pkh(xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1/*)\"]"); + REQUIRE_NO_THROW_TRUE(response.as_object().contains("error")); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__deriveaddresses__range_pair__two_addresses) +{ + const auto response = rpc("deriveaddresses", "[\"pkh(xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1/*)\", [3, 4]]"); + const auto& result = response.at("result"); + BOOST_REQUIRE_EQUAL(result.as_array().size(), 2u); + BOOST_REQUIRE(as_text(result.at(0)).starts_with("1")); +} + // network group BOOST_AUTO_TEST_CASE(bitcoind_rpc__addnode__remove__not_implemented) From 348435e9d5871389d1c093444933bd239d3b0efe Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sun, 23 Aug 2026 00:47:33 -0400 Subject: [PATCH 16/21] Serve per-method usage from help. --- .../bitcoind/protocol_bitcoind_control.cpp | 59 +++++++++++++++++-- test/protocols/bitcoind/bitcoind_rpc.cpp | 14 +++++ 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_control.cpp b/src/protocols/bitcoind/protocol_bitcoind_control.cpp index d4063365..eb5861fd 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_control.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_control.cpp @@ -66,15 +66,66 @@ void protocol_bitcoind_control::start() NOEXCEPT // Control methods. // ---------------------------------------------------------------------------- +// One usage line synthesized from the interface metadata (not bitcoind's +// narrative help text). Optional parameters are parenthesized. +template +static void append_usage(std::string& out, const Method& entry, + const std::string& command) NOEXCEPT +{ + if (!entry.implemented() || entry.name != command) + return; + + out = command; + const auto& names = entry.parameter_names(); + [&](std::index_sequence) NOEXCEPT + { + ((out += is_optional> ? + " ( " + std::string{ names.at(Index) } + " )" : + " " + std::string{ names.at(Index) }), ...); + }(std::make_index_sequence{}); +} + +template +static void find_usage(std::string& out, const std::string& command) NOEXCEPT +{ + std::apply([&](const auto&... entries) NOEXCEPT + { + (append_usage(out, entries, command), ...); + }, Methods::methods); +} + bool protocol_bitcoind_control::handle_help(const code& ec, rpc_interface::help, - const std::string&) NOEXCEPT + const std::string& command) NOEXCEPT { if (stopped(ec)) return false; - auto names = help_names(); - const auto size = two * names.size(); - send_result(std::move(names), size); + if (command.empty()) + { + auto names = help_names(); + const auto size = two * names.size(); + send_result(std::move(names), size); + return true; + } + + using namespace interface; + std::string usage{}; + find_usage(usage, command); + find_usage(usage, command); + find_usage(usage, command); + find_usage(usage, command); + find_usage(usage, command); + find_usage(usage, command); + find_usage(usage, command); + find_usage(usage, command); + find_usage(usage, command); + + // bitcoind reports an unknown command in the result text. + if (usage.empty()) + usage = "help: unknown command: " + command; + + send_result(std::move(usage), 128); return true; } diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index d85837e0..9106dda1 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -1101,6 +1101,20 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__response__websocket__id_matches_request) } +// help + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__help__getblock__usage_line) +{ + const auto response = rpc("help", "[\"getblock\"]"); + BOOST_REQUIRE_EQUAL(as_text(response.at("result")), "getblock blockhash ( verbosity )"); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__help__unknown__reported) +{ + const auto response = rpc("help", "[\"nonsense\"]"); + BOOST_REQUIRE_EQUAL(as_text(response.at("result")), "help: unknown command: nonsense"); +} + // descriptors BOOST_AUTO_TEST_CASE(bitcoind_rpc__getdescriptorinfo__wpkh__expected) From ab048d45b79a222955f72dba77dc3e265695e062 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sun, 23 Aug 2026 00:55:30 -0400 Subject: [PATCH 17/21] Implement scanblocks over neutrino filters. --- .../server/interfaces/bitcoind_blockchain.hpp | 2 +- .../protocol_bitcoind_blockchain.hpp | 4 +- .../bitcoind/protocol_bitcoind_blockchain.cpp | 154 +++++++++++++++++- test/interfaces/bitcoind.cpp | 2 +- test/protocols/bitcoind/bitcoind_rpc.cpp | 16 +- 5 files changed, 170 insertions(+), 8 deletions(-) diff --git a/include/bitcoin/server/interfaces/bitcoind_blockchain.hpp b/include/bitcoin/server/interfaces/bitcoind_blockchain.hpp index e7caf085..a93c9202 100644 --- a/include/bitcoin/server/interfaces/bitcoind_blockchain.hpp +++ b/include/bitcoin/server/interfaces/bitcoind_blockchain.hpp @@ -56,7 +56,7 @@ struct bitcoind_blockchain_methods method<"getdescriptoractivity">{ unimplemented }, method<"getdifficulty">{}, method<"preciousblock">{ unimplemented }, - method<"scanblocks">{ unimplemented }, + method<"scanblocks", string_t, optional, optional<0.0>, optional<-1.0>, optional<"basic"_t>>{ "action", "scanobjects", "start_height", "stop_height", "filtertype" }, method<"waitforblock", string_t, optional<0.0>>{ "blockhash", "timeout" }, method<"waitforblockheight", number_t, optional<0.0>>{ "height", "timeout" }, method<"waitfornewblock", optional<0.0>>{ "timeout" }, diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp index 60983068..b7405d60 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp @@ -118,7 +118,9 @@ class BCS_API protocol_bitcoind_blockchain bool handle_precious_block(const code& ec, rpc_interface::precious_block) NOEXCEPT; bool handle_scan_blocks(const code& ec, - rpc_interface::scan_blocks) NOEXCEPT; + 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; bool handle_wait_for_block(const code& ec, rpc_interface::wait_for_block, const std::string& blockhash, double timeout) NOEXCEPT; diff --git a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp index b69b20e2..e7575493 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp @@ -101,7 +101,7 @@ void protocol_bitcoind_blockchain::start() NOEXCEPT SUBSCRIBE_BITCOIND(handle_get_descriptor_activity, _1, _2); SUBSCRIBE_BITCOIND(handle_get_difficulty, _1, _2); SUBSCRIBE_BITCOIND(handle_precious_block, _1, _2); - SUBSCRIBE_BITCOIND(handle_scan_blocks, _1, _2); + SUBSCRIBE_BITCOIND(handle_scan_blocks, _1, _2, _3, _4, _5, _6, _7); 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); @@ -1020,11 +1020,157 @@ bool protocol_bitcoind_blockchain::handle_precious_block(const code& ec, return true; } +// A scan object is a descriptor string or { "desc", "range" } object. +static bool expand_scan_object(chain::scripts& out, + const value_t& item) NOEXCEPT +{ + std::string expression{}; + uint32_t begin{}; + uint32_t end{}; + + // bitcoind's default range for ranged descriptors. + constexpr uint32_t default_range = 1'000; + constexpr uint32_t maximum_range = 10'000; + + if (std::holds_alternative(item.value())) + { + expression = std::get(item.value()); + end = default_range; + } + else if (std::holds_alternative(item.value())) + { + const auto& fields = std::get(item.value()); + const auto desc = fields.find("desc"); + if (desc == fields.end() || + !std::holds_alternative(desc->second.value())) + return false; + + expression = std::get(desc->second.value()); + end = default_range; + const auto range = fields.find("range"); + if (range != fields.end()) + { + const auto& value = range->second.value(); + if (std::holds_alternative(value)) + { + if (!to_integer(end, std::get(value))) + return false; + } + else if (std::holds_alternative(value)) + { + const auto& pair = std::get(value); + if (pair.size() != 2u || + !std::holds_alternative(pair.front().value()) || + !std::holds_alternative(pair.back().value()) || + !to_integer(begin, + std::get(pair.front().value())) || + !to_integer(end, + std::get(pair.back().value())) || + end < begin) + return false; + } + else + { + return false; + } + } + } + else + { + return false; + } + + const wallet::descriptor parsed{ expression }; + if (!parsed || floored_subtract(end, begin) >= maximum_range) + return false; + + if (!parsed.ranged()) + end = begin; + + for (auto index = begin; index <= end; ++index) + { + const auto derived = parsed.scripts(index); + if (derived.empty()) + return false; + + out.insert(out.end(), derived.begin(), derived.end()); + } + + return true; +} + bool protocol_bitcoind_blockchain::handle_scan_blocks(const code& ec, - rpc_interface::scan_blocks) NOEXCEPT + rpc_interface::scan_blocks, const std::string& action, + const array_t& scanobjects, double start_height, double stop_height, + const std::string& filtertype) NOEXCEPT { - if (stopped(ec)) return false; - send_error(error::not_implemented); + if (stopped(ec)) + return false; + + if (action != "start" || filtertype != basic_filter || + scanobjects.empty()) + { + send_error(error::invalid_argument); + return true; + } + + const auto& query = archive(); + if (!query.filter_enabled()) + { + send_error(error::not_implemented); + return true; + } + + const auto top = query.get_top_confirmed(); + size_t from{}; + auto to = top; + if (!to_integer(from, start_height) || + (stop_height >= 0 && !to_integer(to, stop_height))) + { + send_error(error::invalid_argument); + return true; + } + + to = std::min(to, top); + if (from > to) + { + send_error(error::invalid_argument); + return true; + } + + chain::scripts scripts{}; + for (const auto& item: scanobjects) + { + if (!expand_scan_object(scripts, item)) + { + send_error(error::invalid_argument); + return true; + } + } + + array_t relevant{}; + for (auto height = from; height <= to; ++height) + { + const auto link = query.to_confirmed(height); + const auto hash = query.get_header_key(link); + neutrino::block_filter filter{ hash, {} }; + if (!query.get_filter_body(filter.filter, link)) + { + send_error(database::error::integrity); + return true; + } + + if (neutrino::match_filter(filter, scripts)) + relevant.emplace_back(encode_hash(hash)); + } + + send_result(object_t + { + { "from_height", from }, + { "to_height", to }, + { "relevant_blocks", std::move(relevant) }, + { "completed", true } + }, 1024); return true; } diff --git a/test/interfaces/bitcoind.cpp b/test/interfaces/bitcoind.cpp index d08314c9..07194700 100644 --- a/test/interfaces/bitcoind.cpp +++ b/test/interfaces/bitcoind.cpp @@ -118,7 +118,7 @@ static_assert(bitcoind_blockchain_methods::names == "getblockfilter getblockhash getblockheader getblockstats " "getchaintxstats gettxout verifychain gettxoutproof verifytxoutproof " "getchainstates getchaintips getdeploymentinfo getdifficulty " - "waitforblock waitforblockheight waitfornewblock"); + "scanblocks waitforblock waitforblockheight waitfornewblock"); static_assert(bitcoind_control_methods::names == "help getmemoryinfo getrpcinfo logging uptime"); static_assert(bitcoind_mining_methods::names == diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 9106dda1..7747442b 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -66,7 +66,6 @@ const std::vector wip_methods "getblockfrompeer", "getdescriptoractivity", "preciousblock", - "scanblocks", "descriptorprocesspsbt", "disconnectnode", "exportasmap", @@ -1101,6 +1100,21 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__response__websocket__id_matches_request) } +// scanblocks + +// The fixture runs with block filters disabled (as getblockfilter). +BOOST_AUTO_TEST_CASE(bitcoind_rpc__scanblocks__filters_disabled__error) +{ + const auto response = rpc("scanblocks", "[\"start\", [\"pk(04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f)\"]]"); + BOOST_REQUIRE(has_error(response)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__scanblocks__bad_action__invalid) +{ + const auto response = rpc("scanblocks", "[\"status\", []]"); + REQUIRE_NO_THROW_TRUE(response.as_object().contains("error")); +} + // help BOOST_AUTO_TEST_CASE(bitcoind_rpc__help__getblock__usage_line) From 71c22aa296165c531375563ca46eff3c4da2f9ef Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sun, 23 Aug 2026 08:28:53 -0400 Subject: [PATCH 18/21] Implement getdescriptoractivity. --- .../server/interfaces/bitcoind_blockchain.hpp | 2 +- .../protocol_bitcoind_blockchain.hpp | 5 +- .../bitcoind/protocol_bitcoind_blockchain.cpp | 116 +++++++++++++++++- test/interfaces/bitcoind.cpp | 5 +- test/protocols/bitcoind/bitcoind_rpc.cpp | 22 +++- 5 files changed, 140 insertions(+), 10 deletions(-) diff --git a/include/bitcoin/server/interfaces/bitcoind_blockchain.hpp b/include/bitcoin/server/interfaces/bitcoind_blockchain.hpp index a93c9202..3b5d4ad0 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">{ unimplemented }, + method<"getdescriptoractivity", optional, optional, optional>{ "blockhashes", "scanobjects", "include_spent" }, 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 b7405d60..d2448684 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp @@ -112,7 +112,10 @@ class BCS_API protocol_bitcoind_blockchain rpc_interface::get_deployment_info, const std::string& blockhash) NOEXCEPT; bool handle_get_descriptor_activity(const code& ec, - rpc_interface::get_descriptor_activity) NOEXCEPT; + rpc_interface::get_descriptor_activity, + const network::rpc::array_t& blockhashes, + const network::rpc::array_t& scanobjects, + bool include_spent) 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 e7575493..68c94188 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp @@ -61,6 +61,9 @@ enum block_verbosity : size_t // bitcoind defines only the "basic" (neutrino) block filter type. constexpr auto basic_filter = "basic"; +static bool expand_scan_object(chain::scripts& out, + const value_t& item) NOEXCEPT; + BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) BC_PUSH_WARNING(SMART_PTR_NOT_NEEDED) BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR) @@ -98,7 +101,7 @@ void protocol_bitcoind_blockchain::start() NOEXCEPT SUBSCRIBE_BITCOIND(handle_get_chain_states, _1, _2); SUBSCRIBE_BITCOIND(handle_get_chain_tips, _1, _2); SUBSCRIBE_BITCOIND(handle_get_deployment_info, _1, _2, _3); - SUBSCRIBE_BITCOIND(handle_get_descriptor_activity, _1, _2); + 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); @@ -985,11 +988,114 @@ bool protocol_bitcoind_blockchain::handle_get_deployment_info(const code& ec, return true; } -bool protocol_bitcoind_blockchain::handle_get_descriptor_activity(const code& ec, - rpc_interface::get_descriptor_activity) NOEXCEPT +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 { - if (stopped(ec)) return false; - send_error(error::not_implemented); + if (stopped(ec)) + return false; + + chain::scripts derived{}; + for (const auto& item: scanobjects) + { + if (!expand_scan_object(derived, item)) + { + send_error(error::invalid_argument); + return true; + } + } + + std::unordered_set watch{}; + for (const auto& script: derived) + watch.insert(encode_base16(script.to_data(false))); + + const auto& query = archive(); + array_t activity{}; + for (const auto& item: blockhashes) + { + hash_digest hash{}; + if (!std::holds_alternative(item.value()) || + !decode_hash(hash, std::get(item.value()))) + { + send_error(error::invalid_argument); + return true; + } + + constexpr auto witness = true; + const auto link = query.to_header(hash); + const auto block = query.get_block(link, witness); + size_t height{}; + if (!block || !query.get_height(height, link)) + { + send_error(error::not_found); + return true; + } + + const auto encoded = encode_hash(hash); + const auto populated = include_spent && + query.populate_without_metadata(*block); + + for (const auto& tx: *block->transactions_ptr()) + { + const auto txid = encode_hash(tx->hash(false)); + uint32_t index{}; + for (const auto& out: *tx->outputs_ptr()) + { + const auto script = encode_base16( + out->script().to_data(false)); + if (watch.contains(script)) + { + activity.emplace_back(object_t + { + { "type", std::string{ "receive" } }, + { "amount", out->value() / + to_floating(chain::satoshi_per_bitcoin) }, + { "blockhash", encoded }, + { "height", height }, + { "txid", txid }, + { "vout", index }, + { "output_spk", value_from(bitcoind(out->script())) } + }); + } + + ++index; + } + + if (!populated || tx->is_coinbase()) + continue; + + uint32_t spend{}; + for (const auto& in: *tx->inputs_ptr()) + { + const auto& prevout = *in->prevout; + const auto script = encode_base16( + prevout.script().to_data(false)); + if (watch.contains(script)) + { + activity.emplace_back(object_t + { + { "type", std::string{ "spend" } }, + { "amount", prevout.value() / + to_floating(chain::satoshi_per_bitcoin) }, + { "blockhash", encoded }, + { "height", height }, + { "spend_txid", txid }, + { "spend_vout", spend }, + { "prevout_txid", encode_hash(in->point().hash()) }, + { "prevout_vout", in->point().index() }, + { "prevout_spk", value_from(bitcoind( + prevout.script())) } + }); + } + + ++spend; + } + } + } + + const auto size = 256 * activity.size(); + send_result(object_t{ { "activity", std::move(activity) } }, size); return true; } diff --git a/test/interfaces/bitcoind.cpp b/test/interfaces/bitcoind.cpp index 07194700..8b3f2c52 100644 --- a/test/interfaces/bitcoind.cpp +++ b/test/interfaces/bitcoind.cpp @@ -117,8 +117,9 @@ static_assert(bitcoind_blockchain_methods::names == "getbestblockhash getblock getblockchaininfo getblockcount " "getblockfilter getblockhash getblockheader getblockstats " "getchaintxstats gettxout verifychain gettxoutproof verifytxoutproof " - "getchainstates getchaintips getdeploymentinfo getdifficulty " - "scanblocks waitforblock waitforblockheight waitfornewblock"); + "getchainstates getchaintips getdeploymentinfo getdescriptoractivity " + "getdifficulty scanblocks waitforblock waitforblockheight " + "waitfornewblock"); static_assert(bitcoind_control_methods::names == "help getmemoryinfo getrpcinfo logging uptime"); static_assert(bitcoind_mining_methods::names == diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 7747442b..646a56dc 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -64,7 +64,6 @@ const std::vector rejected_methods const std::vector wip_methods { "getblockfrompeer", - "getdescriptoractivity", "preciousblock", "descriptorprocesspsbt", "disconnectnode", @@ -1100,6 +1099,27 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__response__websocket__id_matches_request) } +// descriptor activity + +// Block one's coinbase output is watched via its raw script descriptor. +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getdescriptoractivity__block1_coinbase__one_receive) +{ + const auto hash = encode_hash(test::block1.hash()); + const auto script = encode_base16(test::block1.transactions_ptr()->front()->outputs_ptr()->front()->script().to_data(false)); + const auto response = rpc("getdescriptoractivity", "[[\"" + hash + "\"], [\"raw(" + script + ")\"]]"); + const auto& activity = response.at("result").at("activity"); + BOOST_REQUIRE_EQUAL(activity.as_array().size(), 1u); + BOOST_REQUIRE_EQUAL(as_text(activity.at(0).at("type")), "receive"); + BOOST_REQUIRE_EQUAL(activity.at(0).at("height").as_int64(), 1); + BOOST_REQUIRE_EQUAL(activity.at(0).at("amount").as_double(), 50.0); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getdescriptoractivity__unknown_block__not_found) +{ + const auto response = rpc("getdescriptoractivity", "[[\"0000000000000000000000000000000000000000000000000000000000000001\"], []]"); + REQUIRE_NO_THROW_TRUE(response.as_object().contains("error")); +} + // scanblocks // The fixture runs with block filters disabled (as getblockfilter). From 202ebeacb6b2684294306ac2253a81deff9617eb Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sun, 23 Aug 2026 08:34:50 -0400 Subject: [PATCH 19/21] Implement getopenrpcinfo from interface metadata. --- .../server/interfaces/bitcoind_control.hpp | 2 +- .../bitcoind/protocol_bitcoind_control.cpp | 63 ++++++++++++++++++- test/interfaces/bitcoind.cpp | 2 +- test/protocols/bitcoind/bitcoind_rpc.cpp | 12 +++- 4 files changed, 74 insertions(+), 5 deletions(-) diff --git a/include/bitcoin/server/interfaces/bitcoind_control.hpp b/include/bitcoin/server/interfaces/bitcoind_control.hpp index 31a17a5f..4ae9bb90 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">{ unimplemented }, + method<"getopenrpcinfo">{}, method<"getrpcinfo">{}, method<"logging", optional, optional>{ "include", "exclude" }, method<"uptime">{} diff --git a/src/protocols/bitcoind/protocol_bitcoind_control.cpp b/src/protocols/bitcoind/protocol_bitcoind_control.cpp index eb5861fd..f2e05238 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_control.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_control.cpp @@ -169,11 +169,70 @@ bool protocol_bitcoind_control::handle_get_memory_info(const code& ec, return true; } +template +static void append_method(array_t& out, const Method& entry) NOEXCEPT +{ + if (!entry.implemented()) + return; + + array_t parameters{}; + const auto& names = entry.parameter_names(); + [&](std::index_sequence) NOEXCEPT + { + (parameters.emplace_back(object_t + { + { "name", std::string{ names.at(Index) } }, + { "required", !is_optional> } + }), ...); + }(std::make_index_sequence{}); + + out.emplace_back(object_t + { + { "name", std::string{ entry.name } }, + { "params", std::move(parameters) } + }); +} + +template +static void append_methods(array_t& out) NOEXCEPT +{ + std::apply([&](const auto&... entries) NOEXCEPT + { + (append_method(out, entries), ...); + }, Methods::methods); +} + bool protocol_bitcoind_control::handle_get_openrpc_info(const code& ec, rpc_interface::get_openrpc_info) NOEXCEPT { - if (stopped(ec)) return false; - send_error(error::not_implemented); + if (stopped(ec)) + return false; + + using namespace interface; + array_t methods{}; + append_methods(methods); + append_methods(methods); + append_methods(methods); + append_methods(methods); + append_methods(methods); + append_methods(methods); + append_methods(methods); + append_methods(methods); + append_methods(methods); + + const auto& settings = server_settings().bitcoind; + const auto size = 64 * methods.size(); + send_result(object_t + { + { "openrpc", std::string{ "1.2.6" } }, + { "info", object_t + { + { "title", settings.subversion }, + { "version", settings.version.to_string() } + } }, + { "methods", std::move(methods) } + }, size); return true; } diff --git a/test/interfaces/bitcoind.cpp b/test/interfaces/bitcoind.cpp index 8b3f2c52..e3f4d678 100644 --- a/test/interfaces/bitcoind.cpp +++ b/test/interfaces/bitcoind.cpp @@ -121,7 +121,7 @@ static_assert(bitcoind_blockchain_methods::names == "getdifficulty scanblocks waitforblock waitforblockheight " "waitfornewblock"); static_assert(bitcoind_control_methods::names == - "help getmemoryinfo getrpcinfo logging uptime"); + "help getmemoryinfo getopenrpcinfo getrpcinfo logging uptime"); 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 646a56dc..5beaf8ce 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -69,7 +69,6 @@ const std::vector wip_methods "disconnectnode", "exportasmap", "getaddednodeinfo", - "getopenrpcinfo" }; std::string as_text(const boost::json::value& value) NOEXCEPT @@ -1135,6 +1134,17 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__scanblocks__bad_action__invalid) REQUIRE_NO_THROW_TRUE(response.as_object().contains("error")); } +// openrpc + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getopenrpcinfo__always__document) +{ + const auto response = rpc("getopenrpcinfo"); + const auto& result = response.at("result"); + BOOST_REQUIRE_EQUAL(as_text(result.at("openrpc")), "1.2.6"); + BOOST_REQUIRE(result.at("methods").is_array()); + BOOST_REQUIRE(!result.at("methods").as_array().empty()); +} + // help BOOST_AUTO_TEST_CASE(bitcoind_rpc__help__getblock__usage_line) From ef2b445675285e9b7f6671d98e2a9ad536e82840 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sun, 23 Aug 2026 08:36:49 -0400 Subject: [PATCH 20/21] Infer decodescript descriptors from script patterns. --- .../bitcoind/protocol_bitcoind_utility.cpp | 33 ++++++++++++++++--- test/protocols/bitcoind/bitcoind_rpc.cpp | 3 +- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_utility.cpp b/src/protocols/bitcoind/protocol_bitcoind_utility.cpp index a7529a3b..8c405de9 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_utility.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_utility.cpp @@ -115,10 +115,33 @@ bool protocol_bitcoind_utility::handle_decode_script(const code& ec, return true; } - // Inference is pending the descriptor engine; raw is always correct. - const auto raw_descriptor = [](const chain::script& target) NOEXCEPT + // Inferred where a pattern is expressible, otherwise raw. + const auto infer_descriptor = [&](const chain::script& target) NOEXCEPT { - const auto body = "raw(" + encode_base16(target.to_data(false)) + ")"; + std::string body{}; + const auto& ops = target.ops(); + if (chain::script::is_pay_public_key_pattern(ops)) + { + body = "pk(" + encode_base16(ops.front().data()) + ")"; + } + else if (chain::script::is_pay_multisig_pattern(ops)) + { + body = "multi(" + std::to_string( + chain::operation::opcode_to_positive(ops.front().code())); + for (auto op = std::next(ops.begin()); + op != std::prev(ops.end(), 2); ++op) + body += "," + encode_base16(op->data()); + + body += ")"; + } + else + { + const auto address = to_address(target); + body = address.empty() ? + "raw(" + encode_base16(target.to_data(false)) + ")" : + "addr(" + address + ")"; + } + return body + "#" + descriptor_checksum(body); }; @@ -127,7 +150,7 @@ bool protocol_bitcoind_utility::handle_decode_script(const code& ec, object_t result { { "asm", script.to_string(flags::all_rules, true) }, - { "desc", raw_descriptor(script) }, + { "desc", infer_descriptor(script) }, { "type", to_script_type(pattern) } }; @@ -156,7 +179,7 @@ bool protocol_bitcoind_utility::handle_decode_script(const code& ec, { "hex", encode_base16(wsh.to_data(false)) }, { "type", to_script_type(script_pattern::pay_witness_script_hash) }, { "address", witness_address{ script, witness_ }.encoded() }, - { "desc", raw_descriptor(wsh) }, + { "desc", infer_descriptor(wsh) }, { "p2sh-segwit", payment_address{ wsh, p2sh_ }.encoded() } }); } diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 5beaf8ce..0031ad04 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -468,7 +468,8 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__decodescript__p2kh__descriptor_and_segwit) { const auto response = rpc("decodescript", "[\"76a914000000000000000000000000000000000000000088ac\"]"); const auto& result = response.at("result"); - BOOST_REQUIRE_EQUAL(as_text(result.at("desc")), "raw(76a914000000000000000000000000000000000000000088ac)#" + descriptor_checksum("raw(76a914000000000000000000000000000000000000000088ac)")); + const auto address = as_text(result.at("address")); + BOOST_REQUIRE_EQUAL(as_text(result.at("desc")), "addr(" + address + ")#" + descriptor_checksum("addr(" + address + ")")); const auto& segwit = result.at("segwit"); BOOST_REQUIRE_EQUAL(as_text(segwit.at("type")), "witness_v0_scripthash"); BOOST_REQUIRE(as_text(segwit.at("address")).starts_with("bc1q")); From 361f0e3c168657e2c024dfd6ed72e34107cd600a Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sun, 23 Aug 2026 21:34:14 -0400 Subject: [PATCH 21/21] Use "embedded" script terminology. --- src/protocols/bitcoind/protocol_bitcoind_transaction.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp index 0a7e5ffd..866541ad 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp @@ -439,8 +439,9 @@ static object_t decode_psbt_input(const wallet::psbt::input& in) NOEXCEPT if (in.sighash_type.has_value()) entry.emplace("sighash", sighash_name(in.sighash_type.value())); - if (in.redeem_script) - entry.emplace("redeem_script", value_from(bitcoind(*in.redeem_script))); + if (in.embedded_script) + entry.emplace("redeem_script", + value_from(bitcoind(*in.embedded_script))); if (in.witness_script) entry.emplace("witness_script", @@ -488,9 +489,9 @@ static object_t decode_psbt_output(const wallet::psbt::output& out) NOEXCEPT using namespace chain; object_t entry{}; - if (out.redeem_script) + if (out.embedded_script) entry.emplace("redeem_script", - value_from(bitcoind(*out.redeem_script))); + value_from(bitcoind(*out.embedded_script))); if (out.witness_script) entry.emplace("witness_script",