diff --git a/include/bitcoin/server/interfaces/bitcoind_blockchain.hpp b/include/bitcoin/server/interfaces/bitcoind_blockchain.hpp index 3b5d4ad0..90296866 100644 --- a/include/bitcoin/server/interfaces/bitcoind_blockchain.hpp +++ b/include/bitcoin/server/interfaces/bitcoind_blockchain.hpp @@ -40,10 +40,10 @@ struct bitcoind_blockchain_methods method<"getblockstats", value_t, optional>{ "hash_or_height", "stats" }, method<"getchaintxstats", optional<-1.0>, optional<""_t>>{ "nblocks", "blockhash" }, method<"gettxout", string_t, number_t, optional>{ "txid", "n", "include_mempool" }, - method<"gettxoutsetinfo">{ unimplemented }, + method<"gettxoutsetinfo", optional<"none"_t>>{ "hash_type" }, method<"pruneblockchain", number_t>{ unimplemented, "height" }, method<"savemempool">{ unimplemented }, - method<"scantxoutset", string_t, optional>{ unimplemented, "action", "scanobjects" }, + method<"scantxoutset", string_t, optional>{ "action", "scanobjects" }, method<"verifychain", optional<4.0>, optional<288.0>>{ "checklevel", "nblocks" }, method<"dumptxoutset">{ unimplemented }, method<"loadtxoutset">{ unimplemented }, diff --git a/include/bitcoin/server/parsers/block_stats.hpp b/include/bitcoin/server/parsers/block_stats.hpp index ff725e17..4137540a 100644 --- a/include/bitcoin/server/parsers/block_stats.hpp +++ b/include/bitcoin/server/parsers/block_stats.hpp @@ -26,8 +26,7 @@ namespace server { /// All bitcoind getblockstats statistics of a prevout-populated block. BCS_API network::rpc::object_t block_stats(const system::chain::block& block, - size_t height, uint32_t median_time_past, uint64_t subsidy, - bool repeat) NOEXCEPT; + size_t height, uint32_t median_time_past, uint64_t subsidy) NOEXCEPT; } // namespace server } // namespace libbitcoin diff --git a/include/bitcoin/server/protocols/protocol.hpp b/include/bitcoin/server/protocols/protocol.hpp index c8cc6db6..078cbe09 100644 --- a/include/bitcoin/server/protocols/protocol.hpp +++ b/include/bitcoin/server/protocols/protocol.hpp @@ -64,4 +64,12 @@ class BCS_API protocol // For use with secondary (e.g. notification) strands. #define POST_NOTIFY(method, ...) notify(&CLASS::method, __VA_ARGS__) +// Qualified, as http protocols alias post as the request type. +#undef POST +#undef PARALLEL +#define POST(method, ...) \ + network::protocol::post(&CLASS::method __VA_OPT__(,) __VA_ARGS__) +#define PARALLEL(method, ...) \ + network::protocol::parallel(&CLASS::method __VA_OPT__(,) __VA_ARGS__) + #endif diff --git a/include/bitcoin/server/protocols/protocol_bitcoind.hpp b/include/bitcoin/server/protocols/protocol_bitcoind.hpp index bf5e2363..10534356 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind.hpp @@ -115,6 +115,14 @@ class BCS_API protocol_bitcoind const network::rpc::array_t& keys, const std::string& address_type) const NOEXCEPT; + /// The address of a singular output script (empty if unaddressable). + std::string to_address( + const system::chain::script& script) const NOEXCEPT; + + /// Inferred where a pattern is expressible, otherwise raw. + std::string infer_descriptor( + const system::chain::script& script) const NOEXCEPT; + /// Senders. close_reason (if truthy) stops the channel only once the /// write has completed, so the error reaches the client first. void send_error(const code& ec) NOEXCEPT; diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp index d2448684..bba36d48 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp @@ -83,7 +83,7 @@ class BCS_API protocol_bitcoind_blockchain bool handle_get_tx_out(const code& ec, rpc_interface::get_tx_out, const std::string&, double, bool) NOEXCEPT; bool handle_get_tx_out_set_info(const code& ec, - rpc_interface::get_tx_out_set_info) NOEXCEPT; + rpc_interface::get_tx_out_set_info, const std::string&) NOEXCEPT; bool handle_prune_block_chain(const code& ec, rpc_interface::prune_block_chain, double) NOEXCEPT; bool handle_save_mempool(const code& ec, @@ -162,6 +162,15 @@ class BCS_API protocol_bitcoind_blockchain bool wait_done() const NOEXCEPT; void send_tip() NOEXCEPT; + void do_get_tx_out_set_info() NOEXCEPT; + void do_scan_tx_out_set( + const std::shared_ptr& scripts) NOEXCEPT; + void complete_scan(const code& ec, network::rpc::object_t& result, + size_t size) NOEXCEPT; + + // This is thread safe. + std::atomic_bool stopping_{}; + // These are protected by strand. wait wait_{ wait::none }; size_t wait_height_{}; diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_utility.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_utility.hpp index 096fcf84..62f76443 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_utility.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_utility.hpp @@ -77,10 +77,6 @@ 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/parsers/block_stats.cpp b/src/parsers/block_stats.cpp index 14ce1778..1a5b5504 100644 --- a/src/parsers/block_stats.cpp +++ b/src/parsers/block_stats.cpp @@ -75,7 +75,7 @@ static std::array fee_rate_percentiles( } object_t block_stats(const chain::block& block, size_t height, - uint32_t median_time_past, uint64_t subsidy, bool repeat) NOEXCEPT + uint32_t median_time_past, uint64_t subsidy) NOEXCEPT { const auto& txs = *block.transactions_ptr(); @@ -95,10 +95,9 @@ object_t block_stats(const chain::block& block, size_t height, { tx_total_output += out->value(); - // Genesis and repeated coinbases do not add to the utxo set, - // and unspendable outputs are not included in it. - if (is_zero(height) || (repeat && tx->is_coinbase()) || - out->script().is_unspendable()) + // The genesis coinbase does not add to the utxo set, and + // unspendable outputs are not included in it. + if (is_zero(height) || out->script().is_unspendable()) continue; ++utxos; diff --git a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp index 68c94188..5c4d6cb3 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp @@ -88,7 +88,7 @@ void protocol_bitcoind_blockchain::start() NOEXCEPT SUBSCRIBE_BITCOIND(handle_get_block_stats, _1, _2, _3, _4); SUBSCRIBE_BITCOIND(handle_get_chain_tx_stats, _1, _2, _3, _4); SUBSCRIBE_BITCOIND(handle_get_tx_out, _1, _2, _3, _4, _5); - SUBSCRIBE_BITCOIND(handle_get_tx_out_set_info, _1, _2); + SUBSCRIBE_BITCOIND(handle_get_tx_out_set_info, _1, _2, _3); SUBSCRIBE_BITCOIND(handle_prune_block_chain, _1, _2, _3); SUBSCRIBE_BITCOIND(handle_save_mempool, _1, _2); SUBSCRIBE_BITCOIND(handle_scan_tx_out_set, _1, _2, _3, _4); @@ -123,6 +123,7 @@ void protocol_bitcoind_blockchain::start() NOEXCEPT void protocol_bitcoind_blockchain::stopping(const code& ec) NOEXCEPT { BC_ASSERT(stranded()); + stopping_.store(true); unsubscribe_chase(); wait_timer_->stop(); protocol_bitcoind_dispatch::stopping(ec); @@ -399,13 +400,8 @@ bool protocol_bitcoind_blockchain::handle_get_block_stats(const code& ec, settings.subsidy_interval_blocks, settings.initial_subsidy(), settings.forks.bip42); - // The duplicated-coinbase blocks (bip30 exceptions) do not add to the - // utxo set. - const auto repeat = chain::chain_state::is_bip30_exception(block->hash(), - height); - auto result = block_stats(*block, height, median_time_past(query, link), - subsidy, repeat); + subsidy); // An empty selection returns all statistics, otherwise the named subset. if (stats.empty()) @@ -549,14 +545,14 @@ bool protocol_bitcoind_blockchain::handle_get_tx_out(const code& ec, // (is_spent would also count unconfirmed/conflicting/invalid-block spenders). if (output_link.is_terminal() || query.is_confirmed_spent(output_link)) { - send_result({}, 42); + send_result(null_t{}, 42); return true; } const auto output = query.get_output(output_link); if (!output) { - send_result({}, 42); + send_result(null_t{}, 42); return true; } @@ -589,14 +585,78 @@ bool protocol_bitcoind_blockchain::handle_get_tx_out(const code& ec, return true; } +// The response defers to completion of the store scan (see dispatch). This is +// an administrative query, expected to run long, performed off the strand. bool protocol_bitcoind_blockchain::handle_get_tx_out_set_info(const code& ec, - rpc_interface::get_tx_out_set_info) NOEXCEPT + rpc_interface::get_tx_out_set_info, const std::string& hash_type) NOEXCEPT { - if (stopped(ec)) return false; - send_error(error::not_implemented); + if (stopped(ec)) + return false; + + // Utxo set commitments have no consumer here (no assumeutxo). + if (hash_type != "none") + { + send_error(error::invalid_argument); + return true; + } + + monitor(true); + PARALLEL(do_get_tx_out_set_info); return true; } +void protocol_bitcoind_blockchain::do_get_tx_out_set_info() NOEXCEPT +{ + BC_ASSERT(!stranded()); + + object_t result{}; + const auto& query = archive(); + const auto top = query.get_top_confirmed(); + const auto link = query.to_confirmed(top); + + // The pinned ancestry excludes genesis, absent from the set (as bitcoind). + database::header_links branch{}; + if (!query.get_ancestry(branch, link, top)) + { + POST(complete_scan, database::error::integrity, std::move(result), + zero); + return; + } + + database::unspent_totals totals{}; + const auto ec = query.get_unspent_totals(stopping_, totals, branch, + database_settings().turbo); + if (ec) + { + POST(complete_scan, ec, + std::move(result), zero); + return; + } + + // A reorganization across the pinned top voids the scan. + if (!query.is_confirmed_block(link)) + { + POST(complete_scan, error::server_error, std::move(result), zero); + return; + } + + result = object_t + { + { "height", top }, + { "bestblock", encode_hash(query.get_header_key(link)) }, + { "transactions", totals.transactions }, + { "txouts", totals.outputs }, + + // bitcoind's per-utxo accounting fiction (50 byte overhead). + { "bogosize", 50u * totals.outputs + totals.script_bytes }, + { "total_amount", to_floating(totals.value) / + chain::satoshi_per_bitcoin } + }; + + POST(complete_scan, code{}, + std::move(result), 512); +} + bool protocol_bitcoind_blockchain::handle_prune_block_chain(const code& ec, rpc_interface::prune_block_chain, double) NOEXCEPT { @@ -613,15 +673,131 @@ bool protocol_bitcoind_blockchain::handle_save_mempool(const code& ec, return true; } +// The response defers to completion of the indexed scan (see dispatch). bool protocol_bitcoind_blockchain::handle_scan_tx_out_set(const code& ec, - rpc_interface::scan_tx_out_set, const std::string&, - const array_t&) NOEXCEPT + rpc_interface::scan_tx_out_set, const std::string& action, + const array_t& scanobjects) NOEXCEPT { - if (stopped(ec)) return false; - send_error(error::not_implemented); + if (stopped(ec)) + return false; + + // Each scan completes with its response, so there is never one to report. + if (action == "status") + { + send_result(null_t{}, 8); + return true; + } + + if (action == "abort") + { + send_result(value{ false }, 8); + return true; + } + + if (action != "start" || scanobjects.empty()) + { + send_error(error::invalid_argument); + return true; + } + + if (!archive().address_enabled()) + { + send_error(error::not_implemented); + return true; + } + + const auto scripts = std::make_shared(); + for (const auto& item: scanobjects) + { + if (!expand_scan_object(*scripts, item)) + { + send_error(error::invalid_argument); + return true; + } + } + + monitor(true); + PARALLEL(do_scan_tx_out_set, scripts); return true; } +void protocol_bitcoind_blockchain::do_scan_tx_out_set( + const std::shared_ptr& scripts) NOEXCEPT +{ + BC_ASSERT(!stranded()); + + uint64_t amount{}; + array_t unspents{}; + object_t result{}; + const auto& query = archive(); + const auto top = query.get_top_confirmed(); + + for (const auto& script: *scripts) + { + database::unspents outs{}; + const auto data = script.to_data(false); + auto ec = query.get_confirmed_unspent(stopping_, outs, + sha256_hash(data)); + if (ec) + { + POST(complete_scan, ec, + std::move(result), zero); + return; + } + + const auto hex = encode_base16(data); + const auto desc = infer_descriptor(script); + for (const auto& utxo: outs) + { + const auto& target = utxo.out.point(); + unspents.emplace_back(object_t + { + { "txid", encode_hash(target.hash()) }, + { "vout", target.index() }, + { "scriptPubKey", hex }, + { "desc", desc }, + { "amount", to_floating(utxo.out.value()) / + chain::satoshi_per_bitcoin }, + { "coinbase", query.is_coinbase(query.to_tx(target.hash())) }, + { "height", utxo.height }, + { "blockhash", encode_hash(query.get_header_key( + query.to_confirmed(utxo.height))) }, + { "confirmations", add1(floored_subtract(top, utxo.height)) } + }); + + amount += utxo.out.value(); + } + } + + const auto size = add1(unspents.size()) * 384u; + result = object_t + { + { "success", true }, + { "height", top }, + { "bestblock", encode_hash(query.get_header_key( + query.to_confirmed(top))) }, + { "unspents", std::move(unspents) }, + { "total_amount", to_floating(amount) / chain::satoshi_per_bitcoin } + }; + + POST(complete_scan, code{}, + std::move(result), size); +} + +void protocol_bitcoind_blockchain::complete_scan(const code& ec, + object_t& result, size_t size) NOEXCEPT +{ + BC_ASSERT(stranded()); + monitor(false); + if (stopped()) + return; + + if (ec) + send_error(ec); + else + send_result(std::move(result), size); +} + bool protocol_bitcoind_blockchain::handle_verify_chain(const code& ec, rpc_interface::verify_chain, double, double) NOEXCEPT { @@ -1422,7 +1598,7 @@ bool protocol_bitcoind_blockchain::handle_chase(const code&, case node::chase::organized: case node::chase::reorganized: { - network::protocol::post(&CLASS::do_wait_event); + POST(do_wait_event); break; } default: diff --git a/src/protocols/bitcoind/protocol_bitcoind_json.cpp b/src/protocols/bitcoind/protocol_bitcoind_json.cpp index 99b673db..de2abfa1 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_json.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_json.cpp @@ -265,6 +265,61 @@ network::rpc::object_t protocol_bitcoind::create_multisig(uint8_t required, return result; } +// The address of a singular output script (empty if unaddressable). +std::string protocol_bitcoind::to_address( + const chain::script& script) const NOEXCEPT +{ + using namespace wallet; + + const auto& ops = script.ops(); + if (chain::script::is_pay_witness_pattern(ops)) + { + // TODO: this should be an extractor (don't parse scripts). + const auto code = ops.front().code(); + const auto& program = ops.at(1).data(); + const auto version = chain::operation::opcode_to_nonnegative(code); + return witness_address{ program, version, witness_ }.encoded(); + } + + const auto pay = payment_address::extract_output(script, p2kh_, p2sh_); + return pay ? pay.encoded() : std::string{}; +} + +// Inferred where a pattern is expressible, otherwise raw. +std::string protocol_bitcoind::infer_descriptor( + const chain::script& script) const NOEXCEPT +{ + std::string body{}; + + const auto& ops = script.ops(); + if (chain::script::is_pay_public_key_pattern(ops)) + { + // TODO: this should be an extractor (don't parse scripts). + body = "pk(" + encode_base16(ops.front().data()) + ")"; + } + else if (chain::script::is_pay_multisig_pattern(ops)) + { + // TODO: this should be an extractor (don't parse scripts). + using namespace chain; + const auto code = ops.front().code(); + body = "multi(" + std::to_string(operation::opcode_to_positive(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(script); + body = address.empty() ? + "raw(" + encode_base16(script.to_data(false)) + ")" : + "addr(" + address + ")"; + } + + return body + "#" + descriptor_checksum(body); +} + // Shared by the bitcoind blockchain subgroup and the btcd endpoint, which // augments the result with bip9_softforks (required by lnd). bool protocol_bitcoind::chain_info(network::rpc::object_t& out, diff --git a/src/protocols/bitcoind/protocol_bitcoind_mining.cpp b/src/protocols/bitcoind/protocol_bitcoind_mining.cpp index 27c0d909..688db09d 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_mining.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_mining.cpp @@ -282,7 +282,7 @@ void protocol_bitcoind_mining::handle_organize_block(const code& ec, if (stopped()) return; - network::protocol::post(&CLASS::do_submit_block, ec); + POST(do_submit_block, ec); } void protocol_bitcoind_mining::handle_organize_header(const code& ec, @@ -291,7 +291,7 @@ void protocol_bitcoind_mining::handle_organize_header(const code& ec, if (stopped()) return; - network::protocol::post(&CLASS::do_submit_header, ec); + POST(do_submit_header, ec); } // bitcoind returns null on acceptance and a reason string on rejection. diff --git a/src/protocols/bitcoind/protocol_bitcoind_network.cpp b/src/protocols/bitcoind/protocol_bitcoind_network.cpp index 488f409e..f83779ee 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_network.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_network.cpp @@ -182,7 +182,7 @@ void protocol_bitcoind_network::handle_fetch_info(const code& ec, if (stopped()) return; - network::protocol::post(&CLASS::do_send_info, ec, message); + POST(do_send_info, ec, message); } // An empty or unavailable pool is reported as empty (as bitcoind). @@ -235,7 +235,7 @@ void protocol_bitcoind_network::handle_fetch_nodes(const code& ec, if (stopped()) return; - network::protocol::post(&CLASS::do_send_nodes, ec, message); + POST(do_send_nodes, ec, message); } void protocol_bitcoind_network::do_send_nodes(const code& ec, diff --git a/src/protocols/bitcoind/protocol_bitcoind_utility.cpp b/src/protocols/bitcoind/protocol_bitcoind_utility.cpp index 8c405de9..2391e34a 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_utility.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_utility.cpp @@ -115,36 +115,6 @@ bool protocol_bitcoind_utility::handle_decode_script(const code& ec, return true; } - // Inferred where a pattern is expressible, otherwise raw. - const auto infer_descriptor = [&](const chain::script& target) NOEXCEPT - { - 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); - }; - using namespace wallet; const auto pattern = script.output_pattern(); object_t result @@ -268,28 +238,6 @@ 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, const std::string& expression, const std::optional& range) NOEXCEPT diff --git a/test/interfaces/bitcoind.cpp b/test/interfaces/bitcoind.cpp index e3f4d678..8709b05f 100644 --- a/test/interfaces/bitcoind.cpp +++ b/test/interfaces/bitcoind.cpp @@ -70,10 +70,10 @@ constexpr bool bitcoind_unserved(const std::string_view& name) NOEXCEPT // These are dispatchable but answer not_implemented (see protocol). static_assert(bitcoind_served("getchaintxstats")); -static_assert(bitcoind_unserved("gettxoutsetinfo")); +static_assert(bitcoind_served("gettxoutsetinfo")); static_assert(bitcoind_unserved("pruneblockchain")); static_assert(bitcoind_unserved("savemempool")); -static_assert(bitcoind_unserved("scantxoutset")); +static_assert(bitcoind_served("scantxoutset")); // no-op that returns true (store is reliable, see protocol). static_assert(bitcoind_served("verifychain")); @@ -116,10 +116,10 @@ static_assert(!declared("getindexinfo", true)); static_assert(bitcoind_blockchain_methods::names == "getbestblockhash getblock getblockchaininfo getblockcount " "getblockfilter getblockhash getblockheader getblockstats " - "getchaintxstats gettxout verifychain gettxoutproof verifytxoutproof " - "getchainstates getchaintips getdeploymentinfo getdescriptoractivity " - "getdifficulty scanblocks waitforblock waitforblockheight " - "waitfornewblock"); + "getchaintxstats gettxout gettxoutsetinfo scantxoutset verifychain " + "gettxoutproof verifytxoutproof getchainstates getchaintips " + "getdeploymentinfo getdescriptoractivity getdifficulty scanblocks " + "waitforblock waitforblockheight waitfornewblock"); static_assert(bitcoind_control_methods::names == "help getmemoryinfo getopenrpcinfo getrpcinfo logging uptime"); static_assert(bitcoind_mining_methods::names == diff --git a/test/parsers/block_stats.cpp b/test/parsers/block_stats.cpp index 1d017207..e0432c85 100644 --- a/test/parsers/block_stats.cpp +++ b/test/parsers/block_stats.cpp @@ -49,7 +49,7 @@ static block make_block(transactions&& txs) NOEXCEPT BOOST_AUTO_TEST_CASE(block_stats__coinbase_only__no_fees) { const auto block = make_block({ make_coinbase() }); - const auto stats = server::block_stats(block, 1, 40, test_subsidy, false); + const auto stats = server::block_stats(block, 1, 40, test_subsidy); BOOST_REQUIRE_EQUAL(std::get(stats.at("txs").value()), 1u); BOOST_REQUIRE_EQUAL(std::get(stats.at("ins").value()), 0u); @@ -66,7 +66,7 @@ BOOST_AUTO_TEST_CASE(block_stats__two_paying__fee_statistics) { const auto block = make_block({ make_coinbase(), make_paying(100'000, 90'000), make_paying(50'000, 48'000) }); - const auto stats = server::block_stats(block, 2, 40, test_subsidy, false); + const auto stats = server::block_stats(block, 2, 40, test_subsidy); BOOST_REQUIRE_EQUAL(std::get(stats.at("txs").value()), 3u); BOOST_REQUIRE_EQUAL(std::get(stats.at("ins").value()), 2u); @@ -95,17 +95,7 @@ BOOST_AUTO_TEST_CASE(block_stats__unspendable_output__excluded_from_actual) { const script unspendable{ operations{ operation{ opcode::op_return } } }; const auto block = make_block({ make_coinbase(unspendable) }); - const auto stats = server::block_stats(block, 1, 40, test_subsidy, false); - - BOOST_REQUIRE_EQUAL(std::get(stats.at("utxo_increase").value()), 1); - BOOST_REQUIRE_EQUAL(std::get(stats.at("utxo_increase_actual").value()), 0); -} - -// A bip30 repeat coinbase does not add to the actual utxo statistics. -BOOST_AUTO_TEST_CASE(block_stats__repeat__coinbase_excluded_from_actual) -{ - const auto block = make_block({ make_coinbase() }); - const auto stats = server::block_stats(block, 1, 40, test_subsidy, true); + const auto stats = server::block_stats(block, 1, 40, test_subsidy); BOOST_REQUIRE_EQUAL(std::get(stats.at("utxo_increase").value()), 1); BOOST_REQUIRE_EQUAL(std::get(stats.at("utxo_increase_actual").value()), 0); diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 0031ad04..20cfe59f 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -58,14 +58,14 @@ const std::vector rejected_methods "clearbanned", "listbanned", "setban", - "stop" + "stop", + "descriptorprocesspsbt" }; const std::vector wip_methods { "getblockfrompeer", "preciousblock", - "descriptorprocesspsbt", "disconnectnode", "exportasmap", "getaddednodeinfo", @@ -368,7 +368,7 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__help__default__implemented_method_list) const auto response = rpc("help"); REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); BOOST_REQUIRE_NE(as_text(response.at("result")).find("getblockcount"), std::string::npos); - BOOST_REQUIRE_EQUAL(as_text(response.at("result")).find("gettxoutsetinfo"), std::string::npos); + BOOST_REQUIRE_EQUAL(as_text(response.at("result")).find("pruneblockchain"), std::string::npos); } BOOST_AUTO_TEST_CASE(bitcoind_rpc__getnetworkhashps__default__number) @@ -601,8 +601,6 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__not_implemented__error) { const std::vector> methods { - { "gettxoutsetinfo", "[]" }, - { "scantxoutset", "[\"start\", []]" }, { "pruneblockchain", "[1]" }, { "savemempool", "[]" } }; @@ -1135,6 +1133,73 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__scanblocks__bad_action__invalid) REQUIRE_NO_THROW_TRUE(response.as_object().contains("error")); } +// gettxoutsetinfo + +// The genesis output is excluded from the utxo set (as bitcoind). +BOOST_AUTO_TEST_CASE(bitcoind_rpc__gettxoutsetinfo__default__expected) +{ + const auto response = rpc("gettxoutsetinfo"); + const auto& result = response.at("result"); + BOOST_REQUIRE(result.is_object()); + BOOST_REQUIRE_EQUAL(result.at("height").as_int64(), 9); + BOOST_REQUIRE_EQUAL(as_text(result.at("bestblock")), block9); + BOOST_REQUIRE_EQUAL(result.at("transactions").as_int64(), 9); + BOOST_REQUIRE_EQUAL(result.at("txouts").as_int64(), 9); + BOOST_REQUIRE_EQUAL(result.at("bogosize").as_int64(), 9 * (50 + 67)); + BOOST_REQUIRE_EQUAL(result.at("total_amount").as_double(), 450.0); +} + +// Utxo set commitments have no consumer here (no assumeutxo). +BOOST_AUTO_TEST_CASE(bitcoind_rpc__gettxoutsetinfo__hash_type__invalid) +{ + const auto response = rpc("gettxoutsetinfo", "[\"muhash\"]"); + BOOST_REQUIRE(has_error(response)); +} + +// scantxoutset + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__scantxoutset__status__null) +{ + const auto response = rpc("scantxoutset", "[\"status\"]"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_null()); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__scantxoutset__abort__false) +{ + const auto response = rpc("scantxoutset", "[\"abort\"]"); + REQUIRE_NO_THROW_TRUE(!response.at("result").as_bool()); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__scantxoutset__empty_scanobjects__invalid) +{ + const auto response = rpc("scantxoutset", "[\"start\", []]"); + BOOST_REQUIRE(has_error(response)); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__scantxoutset__genesis_pk__expected) +{ + const auto response = rpc("scantxoutset", "[\"start\", [\"pk(04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f)\"]]"); + const auto& result = response.at("result"); + BOOST_REQUIRE(result.is_object()); + BOOST_REQUIRE(result.at("success").as_bool()); + BOOST_REQUIRE_EQUAL(result.at("height").as_int64(), 9); + BOOST_REQUIRE_EQUAL(as_text(result.at("bestblock")), block9); + BOOST_REQUIRE_EQUAL(result.at("total_amount").as_double(), 50.0); + + const auto& unspents = result.at("unspents").as_array(); + BOOST_REQUIRE_EQUAL(unspents.size(), 1u); + const auto& unspent = unspents.front(); + BOOST_REQUIRE_EQUAL(as_text(unspent.at("txid")), "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"); + BOOST_REQUIRE_EQUAL(unspent.at("vout").as_int64(), 0); + BOOST_REQUIRE_EQUAL(as_text(unspent.at("scriptPubKey")), "4104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac"); + BOOST_REQUIRE_EQUAL(as_text(unspent.at("desc")).find("pk(04678afd"), 0u); + BOOST_REQUIRE_EQUAL(unspent.at("amount").as_double(), 50.0); + BOOST_REQUIRE(unspent.at("coinbase").as_bool()); + BOOST_REQUIRE_EQUAL(unspent.at("height").as_int64(), 0); + BOOST_REQUIRE_EQUAL(as_text(unspent.at("blockhash")), block0); + BOOST_REQUIRE_EQUAL(unspent.at("confirmations").as_int64(), 10); +} + // openrpc BOOST_AUTO_TEST_CASE(bitcoind_rpc__getopenrpcinfo__always__document)