diff --git a/console/executor.hpp b/console/executor.hpp index 8c01166b..80ed873e 100644 --- a/console/executor.hpp +++ b/console/executor.hpp @@ -73,7 +73,7 @@ class executor void dump_records() const; void dump_buckets() const; void dump_progress() const; - void dump_collisions() const; + void dump_load_factors() const; // Warnings. void warn_hardware(system::string_list& out) const; diff --git a/console/executor_commands.cpp b/console/executor_commands.cpp index 31c33695..2289327a 100644 --- a/console/executor_commands.cpp +++ b/console/executor_commands.cpp @@ -135,7 +135,7 @@ bool executor::do_information() dump_body_sizes(); dump_records(); dump_buckets(); - dump_collisions(); + dump_load_factors(); // This one can take a few seconds on cold iron. logger(BS_INFORMATION_PROGRESS_START); diff --git a/console/executor_dumps.cpp b/console/executor_dumps.cpp index a95bd5fb..44d1ceed 100644 --- a/console/executor_dumps.cpp +++ b/console/executor_dumps.cpp @@ -147,8 +147,6 @@ void executor::dump_body_sizes() const query_.output_body_size(), query_.ins_body_size(), query_.outs_body_size(), - query_.candidate_body_size(), - query_.confirmed_body_size(), query_.ecdsa0_body_size(), query_.ecdsa1_body_size(), query_.schnorr0_body_size(), @@ -166,7 +164,6 @@ void executor::dump_body_sizes() const query_.strong_body_size(), query_.scan_body_size(), query_.scan_prefix_body_size(), - query_.state_body_size(), query_.pool_body_size(), query_.wtxid_body_size(), query_.spends_body_size(), @@ -181,8 +178,6 @@ void executor::dump_records() const query_.tx_records(), query_.ins_records(), query_.outs_records(), - query_.candidate_records(), - query_.confirmed_records(), query_.ecdsa0_records(), query_.ecdsa1_records(), query_.schnorr0_records(), @@ -199,7 +194,6 @@ void executor::dump_records() const query_.strong_records(), query_.scan_records(), query_.scan_prefix_records(), - query_.state_records(), query_.pool_records(), query_.wtxid_records(), query_.spends_records(), @@ -214,44 +208,53 @@ void executor::dump_buckets() const query_.tx_buckets(), query_.ins_buckets(), query_.outs_buckets(), + query_.candidate_buckets(), + query_.confirmed_buckets(), query_.prevout_buckets(), query_.duplicate_buckets(), query_.strong_buckets(), + query_.scan_buckets(), + query_.state_buckets(), query_.pool_buckets(), query_.wtxid_buckets(), query_.filter_bk_buckets(), query_.filter_tx_buckets())); } -void executor::dump_collisions() const +void executor::dump_load_factors() const { - const auto rate = [](size_t records, size_t buckets) + const auto load = [](size_t records, size_t buckets) { return is_zero(buckets) ? 0.0 : to_double(records) / buckets; }; - const auto header = rate(query_.header_records(), query_.header_buckets()); - const auto tx = rate(query_.tx_records(), query_.tx_buckets()); - const auto ins = rate(query_.ins_records(), query_.ins_buckets()); - const auto strong = rate(query_.strong_records(), query_.strong_buckets()); - const auto pool = rate(query_.pool_records(), query_.pool_buckets()); - const auto wtxid = rate(query_.wtxid_records(), query_.wtxid_buckets()); + const auto header = load(query_.header_records(), query_.header_buckets()); + const auto tx = load(query_.tx_records(), query_.tx_buckets()); + const auto ins = load(query_.ins_records(), query_.ins_buckets()); + const auto strong = load(query_.strong_records(), query_.strong_buckets()); + const auto pool = load(query_.pool_records(), query_.pool_buckets()); + const auto wtxid = load(query_.wtxid_records(), query_.wtxid_buckets()); if (query_.address_enabled()) { - const auto outs = rate(query_.outs_records(), query_.outs_buckets()); - logger(std::format(BS_INFORMATION_COLLISION_RATES_OUTS, + const auto outs = load(query_.outs_records(), query_.outs_buckets()); + logger(std::format(BS_INFORMATION_LOAD_FACTORS_OUTS, header, tx, ins, outs, strong, pool, wtxid)); return; } - logger(std::format(BS_INFORMATION_COLLISION_RATES, + logger(std::format(BS_INFORMATION_LOAD_FACTORS, header, tx, ins, strong, pool, wtxid)); } void executor::dump_progress() const { using namespace system; + const auto wire = [](size_t size) + { + return size == max_size_t ? std::string{ BS_INFORMATION_INCOMPLETE } : + std::to_string(size); + }; logger(std::format(BS_INFORMATION_PROGRESS, query_.get_fork(), @@ -261,8 +264,8 @@ void executor::dump_progress() const encode_hash(query_.get_top_candidate_hash()), query_.get_top_associated(), (query_.get_top_candidate() - query_.get_unassociated_count()), - query_.get_confirmed_size(), - query_.get_candidate_size())); + wire(query_.get_confirmed_size()), + wire(query_.get_candidate_size()))); } } // namespace server diff --git a/console/executor_options.cpp b/console/executor_options.cpp index 235e58b3..ef7aa01d 100644 --- a/console/executor_options.cpp +++ b/console/executor_options.cpp @@ -179,7 +179,7 @@ void executor::do_info() const dump_body_sizes(); dump_records(); dump_buckets(); - dump_collisions(); + dump_load_factors(); } // [m]enu diff --git a/console/executor_scans.cpp b/console/executor_scans.cpp index 53b5e3ca..cb14e8dd 100644 --- a/console/executor_scans.cpp +++ b/console/executor_scans.cpp @@ -51,7 +51,7 @@ void executor::scan_flags() const const auto link = query_.to_candidate(height); if (!query_.get_context(ctx, link) || (ctx.height != height)) { - logger(std::format("Error: {}", error)); + logger(std::format(BS_FLAGS_ERROR, error)); return; } @@ -59,8 +59,8 @@ void executor::scan_flags() const { const binary prev{ flag_bits, to_big_endian(flags) }; const binary next{ flag_bits, to_big_endian(ctx.flags) }; - logger(std::format("Forked from [{}] to [{}] at [{}:{}]", - prev.encoded(), next.encoded(), + logger(std::format(BS_FLAGS_FORKED, prev.encoded(), + next.encoded(), encode_hash(query_.get_header_key(link)), height)); flags = ctx.flags; } @@ -70,8 +70,7 @@ void executor::scan_flags() const logger(BS_OPERATION_CANCELED); const auto span = duration_cast(logger::now() - start); - logger(std::format("Scanned {} headers for rule forks in {} ms.", height, - span.count())); + logger(std::format(BS_FLAGS_SCANNED, height, span.count())); } // Link keys are narrower than hashes, other keys are hashed. @@ -84,6 +83,24 @@ constexpr bool is_linked() NOEXCEPT return false; } +// Null points (coinbase) and null search keys (unwritten rows) are excluded. +template +constexpr bool is_nullable() NOEXCEPT +{ + return is_same_type || database::keys::is_search; +} + +template +constexpr bool is_null(const Key& key) NOEXCEPT +{ + if constexpr (is_same_type) + return key.index() == chain::point::null_index; + else if constexpr (database::keys::is_search) + return key.value == null_hash; + else + return false; +} + static std::string to_grouped(size_t value) { auto text = std::to_string(value); @@ -116,17 +133,16 @@ void executor::scan_buckets(const std::string& name, const Table& table, const auto load = to_double(records) / buckets; const auto expect = linked ? std::min(load, 1.0) : 1.0 - std::exp(-load); const auto span = duration_cast(logger::now() - start); - logger(std::format("{:<10} {:>15} {:>15} {:>9.6f} {:>8.6f} {:>8.6f} " - "{:>6}", name, to_grouped(bucket), to_grouped(records), load, - (to_double(filled) / std::max(bucket, one)), expect, span.count())); + logger(std::format(BS_BUCKETS_ROW, name, to_grouped(bucket), + to_grouped(records), load, (to_double(filled) / std::max(bucket, one)), + expect, span.count())); } // hashmap bucket fill rates, all heads. void executor::scan_buckets() const { logger(BS_OPERATION_INTERRUPT); - logger("table buckets records load " - "fill expect secs"); + logger(BS_BUCKETS_HEADER); scan_buckets("header", store_.header, query_.header_records()); scan_buckets("tx", store_.tx, query_.tx_records()); @@ -156,25 +172,18 @@ void executor::scan_collisions(const std::string& name, size_t records, constexpr auto rows = 20_size; constexpr auto filtered = !filter_t::disabled; constexpr auto empty = unmask_right(m); - constexpr auto points = is_same_type; + constexpr auto nullable = is_nullable(); constexpr auto linked = is_linked(); if (is_zero(buckets)) return; - const auto kind = linked ? "link" : "hash"; + const auto kind = linked ? BS_COLLISIONS_LINK : BS_COLLISIONS_HASH; if constexpr (filtered) - { - logger(std::format("\n{} ({} keyed, filter m {} k {} k_max {})", - name, kind, m, k, Table::filter_k_max)); - logger(" keys load fp model " - "walk model secs"); - } + logger(std::format(BS_COLLISIONS_FILTERED, name, kind, m, k, + Table::filter_k_max)); else - { - logger(std::format("\n{} ({} keyed, unfiltered)", name, kind)); - logger(" keys load secs"); - } + logger(std::format(BS_COLLISIONS_UNFILTERED, name, kind)); std_vector lengths(buckets, zero); std_vector screens(filtered ? buckets : zero, empty); @@ -208,8 +217,8 @@ void executor::scan_collisions(const std::string& name, size_t records, const auto cost = Table::expected_walk(k, middle); fp_model += rate * window; walk_model += cost * window; - logger(std::format("{:>15} {:>9.6f} {:>10.7f} {:>10.7f} " - "{:>10.7f} {:>10.7f} {:>6}", to_grouped(keyed), load, + logger(std::format(BS_COLLISIONS_FILTERED_ROW, + to_grouped(keyed), load, (to_double(window_collisions) / window), rate, (to_double(window_walk) / window), cost, secs.count())); @@ -218,8 +227,8 @@ void executor::scan_collisions(const std::string& name, size_t records, } else { - logger(std::format("{:>15} {:>9.6f} {:>6}", to_grouped(keyed), - load, secs.count())); + logger(std::format(BS_COLLISIONS_UNFILTERED_ROW, + to_grouped(keyed), load, secs.count())); } window_start = keyed; @@ -231,13 +240,10 @@ void executor::scan_collisions(const std::string& name, size_t records, progress(); const auto key = get_key(index); - if constexpr (points) + if (is_null(key)) { - if (key.index() == chain::point::null_index) - { - ++nulls; - continue; - } + ++nulls; + continue; } const auto bucket = keys::bucket(key, count); @@ -289,7 +295,7 @@ void executor::scan_collisions(const std::string& name, size_t records, return 0.0; }; - logger(" length buckets expect"); + logger(BS_COLLISIONS_LENGTHS); auto poisson = std::exp(-load); auto squares = 0.0; auto hits = 0.0; @@ -307,13 +313,14 @@ void executor::scan_collisions(const std::string& name, size_t records, squares += to_double(occurrences) * size * size; hits += to_double(occurrences) * size * (size + 1.0) / 2.0; longest = length; - logger(std::format("{:>15} {:>15} {:>15.0f}", length, - to_grouped(occurrences), (linked ? perfect(length) : - poisson * buckets))); + const auto expect = linked ? perfect(length) : poisson * buckets; + logger(std::format(BS_COLLISIONS_LENGTHS_ROW, length, + to_grouped(occurrences), + to_grouped(to_integer(std::round(expect))))); } - if constexpr (points) - logger(std::format("{:>15} null points excluded", to_grouped(nulls))); + if constexpr (nullable) + logger(std::format(BS_COLLISIONS_NULLS, to_grouped(nulls))); const auto divisor = to_double(std::max(keyed, one)); const auto empty_load = is_zero(keyed); @@ -329,8 +336,7 @@ void executor::scan_collisions(const std::string& name, size_t records, ((1.0 - part) * whole * (whole + 1.0) / 2.0 + part * (whole + 1.0) * (whole + 2.0) / 2.0) / load; - distributions.push_back(std::format("{:<10} {:>15} {:>15} {:>9.6f} " - "{:>8.6f} {:>8.6f} {:>7.4f} {:>7.4f} {:>7.4f} {:>7.4f} {:>4}", + distributions.push_back(std::format(BS_COLLISIONS_DISTRIBUTION_ROW, name, to_grouped(buckets), to_grouped(keyed), load, filled, fill_expect, dispersion, dispersion_expect, walked, walk_expect, longest)); @@ -338,8 +344,7 @@ void executor::scan_collisions(const std::string& name, size_t records, if constexpr (filtered) { const auto optimal = Table::optimal_k(keyed, buckets); - filters.push_back(std::format("{:<10} {:>3} {:>3} {:>3} {:>10.7f} " - "{:>10.7f} {:>10.7f} {:>10.7f} {:>10.7f} {:>10.7f}", name, m, k, + filters.push_back(std::format(BS_COLLISIONS_FILTER_ROW, name, m, k, optimal, (to_double(collisions) / divisor), (fp_model / divisor), (to_double(walk) / divisor), (walk_model / divisor), Table::false_positive(k, load), @@ -389,15 +394,27 @@ void executor::scan_collisions() ins_link::integer>(index)); }); - logger("\nDistribution (measured, expected)..."); - logger("table buckets keys load " - "fill expect disp expect walk expect max"); + scan_collisions("outs", query_.outs_records(), + query_.outs_buckets(), store_.outs.filter_k(), distributions, filters, + [&](size_t index) + { + return table::outs::key{ query_.get_outs_key(possible_narrow_cast< + outs_link::integer>(index)) }; + }); + + scan_collisions("wtxid", query_.wtxid_records(), + query_.wtxid_buckets(), store_.wtxid.filter_k(), distributions, + filters, [&](size_t index) + { + return table::wtxid::key{ query_.get_wtxid_key( + possible_narrow_cast(index)) }; + }); + + logger(BS_COLLISIONS_DISTRIBUTION); for (const auto& row: distributions) logger(row); - logger("\nFilter (measured vs model over growth, fp at load by k)..."); - logger("table m k opt fp model walk " - "model fp(k) fp(opt)"); + logger(BS_COLLISIONS_FILTER); for (const auto& row: filters) logger(row); } diff --git a/console/localize.hpp b/console/localize.hpp index bc768d5f..854687bf 100644 --- a/console/localize.hpp +++ b/console/localize.hpp @@ -139,8 +139,6 @@ " output :{}\n" \ " ins :{}\n" \ " outs :{}\n" \ - " candidate :{}\n" \ - " confirmed :{}\n" \ " ecdsa0 :{}\n" \ " ecdsa1 :{}\n" \ " schnorr0 :{}\n" \ @@ -158,7 +156,6 @@ " strong :{}\n" \ " scan :{}\n" \ " scan_prefix :{}\n" \ - " state :{}\n" \ " pool :{}\n" \ " wtxid :{}\n" \ " spends :{}\n" \ @@ -170,8 +167,6 @@ " tx :{}\n" \ " ins :{}\n" \ " outs :{}\n" \ - " candidate :{}\n" \ - " confirmed :{}\n" \ " ecdsa0 :{}\n" \ " ecdsa1 :{}\n" \ " schnorr0 :{}\n" \ @@ -188,7 +183,6 @@ " strong :{}\n" \ " scan :{}\n" \ " scan_prefix :{}\n" \ - " state :{}\n" \ " pool :{}\n" \ " wtxid :{}\n" \ " spends :{}\n" \ @@ -200,23 +194,27 @@ " tx :{}\n" \ " ins :{}\n" \ " outs :{}\n" \ + " candidate :{}\n" \ + " confirmed :{}\n" \ " prevout :{}\n" \ " duplicate :{}\n" \ " strong :{}\n" \ + " scan :{}\n" \ + " state :{}\n" \ " pool :{}\n" \ " wtxid :{}\n" \ " filter_bk :{}\n" \ " filter_tx :{}" -#define BS_INFORMATION_COLLISION_RATES \ - "Collision rates...\n" \ +#define BS_INFORMATION_LOAD_FACTORS \ + "Load factors...\n" \ " header :{}\n" \ " tx :{}\n" \ " ins :{}\n" \ " strong :{}\n" \ " pool :{}\n" \ " wtxid :{}" -#define BS_INFORMATION_COLLISION_RATES_OUTS \ - "Collision rates...\n" \ +#define BS_INFORMATION_LOAD_FACTORS_OUTS \ + "Load factors...\n" \ " header :{}\n" \ " tx :{}\n" \ " ins :{}\n" \ @@ -226,15 +224,69 @@ " wtxid :{}" #define BS_INFORMATION_PROGRESS_START \ "Thinking..." +#define BS_INFORMATION_INCOMPLETE \ + "incomplete" #define BS_INFORMATION_PROGRESS \ "Chain progress...\n" \ - " fork pt :{}\n" \ - " top conf :{}:{}\n" \ - " top cand :{}:{}\n" \ - " top assoc :{}\n" \ - " associated:{}\n" \ - " wire conf :{}\n" \ - " wire cand :{}" + " fork pt :{}\n" \ + " top conf :{}:{}\n" \ + " top cand :{}:{}\n" \ + " top assoc :{}\n" \ + " associated :{}\n" \ + " wire conf :{}\n" \ + " wire cand :{}" + +// --flags +#define BS_FLAGS_ERROR \ + "Error: {}" +#define BS_FLAGS_FORKED \ + "Forked from [{}] to [{}] at [{}:{}]" +#define BS_FLAGS_SCANNED \ + "Scanned {} headers for rule forks in {} ms." + +// --buckets +#define BS_BUCKETS_HEADER \ + "table buckets records load fill expect " \ + " secs" +#define BS_BUCKETS_ROW \ + "{:<10} {:>15} {:>15} {:>9.6f} {:>8.6f} {:>8.6f} {:>6}" + +// --collisions +#define BS_COLLISIONS_HASH \ + "hash" +#define BS_COLLISIONS_LINK \ + "link" +#define BS_COLLISIONS_FILTERED \ + "\n{} ({} keyed, filter m {} k {} k_max {})\n" \ + " keys load fp model walk model " \ + "secs" +#define BS_COLLISIONS_FILTERED_ROW \ + "{:>15} {:>9.6f} {:>10.7f} {:>10.7f} {:>10.7f} {:>10.7f} {:>6}" +#define BS_COLLISIONS_UNFILTERED \ + "\n{} ({} keyed, unfiltered)\n" \ + " keys load secs" +#define BS_COLLISIONS_UNFILTERED_ROW \ + "{:>15} {:>9.6f} {:>6}" +#define BS_COLLISIONS_LENGTHS \ + " length buckets expect" +#define BS_COLLISIONS_LENGTHS_ROW \ + "{:>15} {:>15} {:>15}" +#define BS_COLLISIONS_NULLS \ + "{:>15} null keys excluded" +#define BS_COLLISIONS_DISTRIBUTION \ + "\nDistribution (measured, expected)...\n" \ + "table buckets keys load fill expect " \ + " disp expect walk expect max" +#define BS_COLLISIONS_DISTRIBUTION_ROW \ + "{:<10} {:>15} {:>15} {:>9.6f} {:>8.6f} {:>8.6f} {:>7.4f} {:>7.4f} " \ + "{:>7.4f} {:>7.4f} {:>4}" +#define BS_COLLISIONS_FILTER \ + "\nFilter (measured vs model over growth, fp at load by k)...\n" \ + "table m k opt fp model walk model " \ + "fp(k) fp(opt)" +#define BS_COLLISIONS_FILTER_ROW \ + "{:<10} {:>3} {:>3} {:>3} {:>10.7f} {:>10.7f} {:>10.7f} {:>10.7f} " \ + "{:>10.7f} {:>10.7f}" // --read #define BS_READ_ROW \ diff --git a/include/bitcoin/server/protocols/protocol_native.hpp b/include/bitcoin/server/protocols/protocol_native.hpp index 8bc19bb3..4539c3bb 100644 --- a/include/bitcoin/server/protocols/protocol_native.hpp +++ b/include/bitcoin/server/protocols/protocol_native.hpp @@ -230,8 +230,8 @@ class BCS_API protocol_native const system::hash_cptr& hash, const gate_t::ptr& gate) NOEXCEPT; void do_get_address_confirmed(uint8_t media, bool turbo, const system::hash_cptr& hash, const gate_t::ptr& gate) NOEXCEPT; - ////void do_get_address_unconfirmed(uint8_t media, bool turbo, - //// const system::hash_cptr& hash) NOEXCEPT; + void do_get_address_unconfirmed(uint8_t media, bool turbo, + const system::hash_cptr& hash, const gate_t::ptr& gate) NOEXCEPT; void complete_get_address(const code& ec, uint8_t media, const database::outpoints& set, const gate_t::ptr& gate) NOEXCEPT; diff --git a/include/bitcoin/server/serializers/bitcoind_json.hpp b/include/bitcoin/server/serializers/bitcoind_json.hpp index 61ea0558..3d768066 100644 --- a/include/bitcoin/server/serializers/bitcoind_json.hpp +++ b/include/bitcoin/server/serializers/bitcoind_json.hpp @@ -72,6 +72,13 @@ BCS_API void inject_activity(network::rpc::array_t& out, const std::unordered_set& watch, uint8_t p2kh, uint8_t p2sh, const std::string& witness, uint32_t flags) NOEXCEPT; +/// As above for one tx, with its block location (empty if unconfirmed). +BCS_API void inject_activity(network::rpc::array_t& out, + const system::chain::transaction& tx, + const network::rpc::object_t& location, + const std::unordered_set& watch, uint8_t p2kh, + uint8_t p2sh, const std::string& witness, uint32_t flags) NOEXCEPT; + /// The bitcoind chain name, resolved from the genesis block. BCS_API 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 1410b042..1909252f 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp @@ -549,9 +549,10 @@ bool protocol_bitcoind_blockchain::handle_get_chain_tx_stats(const code& ec, return true; } +// The mempool is the set of pooled and confirmable txs. bool protocol_bitcoind_blockchain::handle_get_tx_out(const code& ec, rpc_interface::get_tx_out, const std::string& txid, double n, - bool) NOEXCEPT + bool include_mempool) NOEXCEPT { if (stopped(ec)) return false; @@ -567,7 +568,8 @@ bool protocol_bitcoind_blockchain::handle_get_tx_out(const code& ec, // Return only unspent output. const auto& query = archive(); const auto output_link = query.to_output(hash, index); - if (output_link.is_terminal() || query.is_confirmed_spent(output_link)) + if (output_link.is_terminal() || query.is_confirmed_spent(output_link) || + (include_mempool && query.is_confirmable_spent(output_link))) { send_result(null_t{}); return true; @@ -583,15 +585,19 @@ bool protocol_bitcoind_blockchain::handle_get_tx_out(const code& ec, return true; } - // Return only confirmed output. - size_t height{}; - if (!query.get_tx_height(height, tx_link)) + // Return only confirmed output, or mempool output if included. + size_t depth{}; + if (size_t height{}; query.get_tx_height(height, tx_link)) + { + depth = add1(floored_subtract(top, height)); + } + else if (!include_mempool || !query.is_pooled(tx_link) || + !query.is_confirmable_tx(tx_link)) { send_result(null_t{}); return true; } - const auto depth = add1(floored_subtract(top, height)); const auto coins = to_floating(output->value()) / chain::satoshi_per_bitcoin; @@ -1256,7 +1262,8 @@ bool protocol_bitcoind_blockchain::handle_get_deployment_info(const code& ec, // The mempool option is meaningless here (no mempool), always applied false. bool protocol_bitcoind_blockchain::handle_get_descriptor_activity( const code& ec, rpc_interface::get_descriptor_activity, - const array_t& blockhashes, const array_t& scanobjects, bool) NOEXCEPT + const array_t& blockhashes, const array_t& scanobjects, + bool include_mempool) NOEXCEPT { if (stopped(ec)) return false; @@ -1314,6 +1321,29 @@ bool protocol_bitcoind_blockchain::handle_get_descriptor_activity( p2kh_, p2sh_, witness_, flags_); } + if (include_mempool) + { + database::tx_links links{}; + if (query.get_confirmable_txs(links)) + { + send_error(error::bitcoind::internal_error); + return true; + } + + for (const auto& link: links) + { + const auto tx = query.get_transaction(link, true); + if (!tx || !query.populate_without_metadata(*tx)) + { + send_error(error::bitcoind::internal_error); + return true; + } + + inject_activity(activity, *tx, {}, watch, p2kh_, p2sh_, witness_, + flags_); + } + } + send_result(object_t{ { "activity", std::move(activity) } }); return true; } diff --git a/src/protocols/native/protocol_native_address.cpp b/src/protocols/native/protocol_native_address.cpp index 3d14617d..8f3254be 100644 --- a/src/protocols/native/protocol_native_address.cpp +++ b/src/protocols/native/protocol_native_address.cpp @@ -140,19 +140,37 @@ void protocol_native::do_get_address_confirmed(uint8_t media, bool turbo, // ---------------------------------------------------------------------------- bool protocol_native::handle_get_address_unconfirmed(const code& ec, - interface::address_unconfirmed, uint8_t, uint8_t, - const hash_cptr&, bool) NOEXCEPT + interface::address_unconfirmed, uint8_t, uint8_t media, + const hash_cptr& hash, bool turbo) NOEXCEPT { BC_ASSERT(stranded()); if (stopped(ec)) return false; - // TODO: there are currently no unconfirmed txs. - send_not_implemented(); + if (!archive().address_enabled()) + { + send_not_implemented(); + return true; + } + + PARALLEL(do_get_address_unconfirmed, media, turbo && turbo_, hash, gate()); return true; } +// private +void protocol_native::do_get_address_unconfirmed(uint8_t media, bool turbo, + const hash_cptr& hash, const gate_t::ptr& gate) NOEXCEPT +{ + BC_ASSERT(!stranded()); + + database::outpoints set{}; + const auto& query = archive(); + auto ec = query.get_unconfirmed_unspent_outpoints(stopping_, set, *hash, + turbo); + POST(complete_get_address, ec, media, std::move(set), gate); +} + // handle_get_address_balance // ---------------------------------------------------------------------------- diff --git a/src/serializers/bitcoind_json.cpp b/src/serializers/bitcoind_json.cpp index 75f7116a..2852546f 100644 --- a/src/serializers/bitcoind_json.cpp +++ b/src/serializers/bitcoind_json.cpp @@ -189,65 +189,79 @@ void inject_activity(network::rpc::array_t& out, const chain::block& block, const std::unordered_set& watch, uint8_t p2kh, uint8_t p2sh, const std::string& witness, uint32_t flags) NOEXCEPT { - using namespace network::rpc; + const network::rpc::object_t location + { + { "blockhash", blockhash }, + { "height", height } + }; + for (const auto& tx: *block.transactions_ptr()) + inject_activity(out, *tx, location, watch, p2kh, p2sh, witness, flags); +} + +void inject_activity(network::rpc::array_t& out, const chain::transaction& tx, + const network::rpc::object_t& location, + const std::unordered_set& watch, uint8_t p2kh, + uint8_t p2sh, const std::string& witness, uint32_t flags) NOEXCEPT +{ + using namespace network::rpc; + const auto txid = encode_hash(tx.hash(false)); + uint32_t index{}; + for (const auto& put: *tx.outputs_ptr()) { - const auto txid = encode_hash(tx->hash(false)); - uint32_t index{}; - for (const auto& put: *tx->outputs_ptr()) + const auto script = encode_base16(put->script().to_data(false)); + if (watch.contains(script)) { - const auto script = encode_base16(put->script().to_data(false)); - if (watch.contains(script)) + auto output = to_script_public_key(put->script(), p2kh, p2sh, + witness, flags); + + object_t entry { - auto output = to_script_public_key(put->script(), p2kh, p2sh, - witness, flags); - - out.emplace_back(object_t - { - { "type", std::string{ "receive" } }, - { "amount", put->value() / - to_floating(chain::satoshi_per_bitcoin) }, - { "blockhash", blockhash }, - { "height", height }, - { "txid", txid }, - { "vout", index }, - { "output_spk", std::move(output) } - }); - } - - ++index; + { "type", std::string{ "receive" } }, + { "amount", put->value() / + to_floating(chain::satoshi_per_bitcoin) }, + { "txid", txid }, + { "vout", index }, + { "output_spk", std::move(output) } + }; + + entry.insert(location.cbegin(), location.cend()); + out.emplace_back(std::move(entry)); } - if (tx->is_coinbase()) - continue; + ++index; + } + + if (tx.is_coinbase()) + return; - uint32_t spend{}; - for (const auto& in: *tx->inputs_ptr()) + 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)) { - const auto& prevout = *in->prevout; - const auto script = encode_base16(prevout.script().to_data(false)); - if (watch.contains(script)) + auto output = to_script_public_key(prevout.script(), p2kh, p2sh, + witness, flags); + + object_t entry { - auto output = to_script_public_key(prevout.script(), p2kh, p2sh, - witness, flags); - - out.emplace_back(object_t - { - { "type", std::string{ "spend" } }, - { "amount", prevout.value() / - to_floating(chain::satoshi_per_bitcoin) }, - { "blockhash", blockhash }, - { "height", height }, - { "spend_txid", txid }, - { "spend_vin", spend }, - { "prevout_txid", encode_hash(in->point().hash()) }, - { "prevout_vout", in->point().index() }, - { "prevout_spk", std::move(output) } - }); - } - - ++spend; + { "type", std::string{ "spend" } }, + { "amount", prevout.value() / + to_floating(chain::satoshi_per_bitcoin) }, + { "spend_txid", txid }, + { "spend_vin", spend }, + { "prevout_txid", encode_hash(in->point().hash()) }, + { "prevout_vout", in->point().index() }, + { "prevout_spk", std::move(output) } + }; + + entry.insert(location.cbegin(), location.cend()); + out.emplace_back(std::move(entry)); } + + ++spend; } } diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 597598c7..c82cff7a 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -3976,6 +3976,47 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__getmempoolinfo__broadcast__expected) BOOST_REQUIRE(result.at("maxmempool").as_int64() > 0); } +BOOST_AUTO_TEST_CASE(bitcoind_rpc__gettxout__broadcast_spent__null) +{ + const auto sent = rpc("sendrawtransaction", "[\"" + encode_base16(test::tx1c.to_data(true)) + "\"]"); + BOOST_REQUIRE_MESSAGE(sent.is_object() && sent.as_object().contains("result"), serialize(sent)); + + const auto prevout = test::block1c.transactions_ptr()->front()->hash(false); + const auto included = rpc("gettxout", hash_param(prevout, "0")); + const auto excluded = rpc("gettxout", hash_param(prevout, "0, false")); + REQUIRE_NO_THROW_TRUE(included.at("result").is_null()); + REQUIRE_NO_THROW_TRUE(excluded.at("result").is_object()); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__gettxout__broadcast_output__zero_confirmations) +{ + const auto sent = rpc("sendrawtransaction", "[\"" + encode_base16(test::tx1c.to_data(true)) + "\"]"); + BOOST_REQUIRE_MESSAGE(sent.is_object() && sent.as_object().contains("result"), serialize(sent)); + + const auto included = rpc("gettxout", hash_param(test::tx1c.hash(false), "0")); + const auto excluded = rpc("gettxout", hash_param(test::tx1c.hash(false), "0, false")); + BOOST_REQUIRE_EQUAL(included.at("result").at("confirmations").as_int64(), 0); + BOOST_REQUIRE_EQUAL(included.at("result").at("value").as_double(), 90.0 / chain::satoshi_per_bitcoin); + REQUIRE_NO_THROW_TRUE(excluded.at("result").is_null()); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getdescriptoractivity__broadcast__mempool_receive) +{ + const auto sent = rpc("sendrawtransaction", "[\"" + encode_base16(test::tx1c.to_data(true)) + "\"]"); + BOOST_REQUIRE_MESSAGE(sent.is_object() && sent.as_object().contains("result"), serialize(sent)); + + const auto script = encode_base16(test::tx1c.outputs_ptr()->front()->script().to_data(false)); + const auto included = rpc("getdescriptoractivity", "[[], [\"raw(" + script + ")\"]]"); + const auto excluded = rpc("getdescriptoractivity", "[[], [\"raw(" + script + ")\"], false]"); + const auto& activity = included.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(as_text(activity.at(0).at("txid")), encode_hash(test::tx1c.hash(false))); + BOOST_REQUIRE(!activity.at(0).as_object().contains("blockhash")); + BOOST_REQUIRE(!activity.at(0).as_object().contains("height")); + BOOST_REQUIRE(excluded.at("result").at("activity").as_array().empty()); +} + BOOST_AUTO_TEST_CASE(bitcoind_rpc__getrawmempool__broadcast__txid) { const auto sent = rpc("sendrawtransaction", "[\"" + encode_base16(test::tx1c.to_data(true)) + "\"]"); diff --git a/test/protocols/native/native_address.cpp b/test/protocols/native/native_address.cpp index f72ae09f..ae1e344e 100644 --- a/test/protocols/native/native_address.cpp +++ b/test/protocols/native/native_address.cpp @@ -78,9 +78,9 @@ BOOST_AUTO_TEST_CASE(native__address_confirmed__unfunded__not_found) // address/unconfirmed // ---------------------------------------------------------------------------- -BOOST_AUTO_TEST_CASE(native__address_unconfirmed__any__not_implemented) +BOOST_AUTO_TEST_CASE(native__address_unconfirmed__confirmed__not_found) { - BOOST_REQUIRE_EQUAL(get_status("/v1/address/" + funded + "/unconfirmed?format=data"), http::status::not_implemented); + BOOST_REQUIRE_EQUAL(get_status("/v1/address/" + funded + "/unconfirmed?format=data"), http::status::not_found); } // address/balance @@ -151,3 +151,25 @@ BOOST_AUTO_TEST_CASE(native__configuration__no_address__disabled) } BOOST_AUTO_TEST_SUITE_END() + +// address/unconfirmed (unconfirmed) +// ---------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_SUITE(native_unconfirmed_tests, native_unconfirmed_setup_fixture) + +static const auto& output4 = *test::tx4.outputs_ptr()->front(); +static const std::string unconfirmed = encode_hash(sha256_hash(output4.script().to_data(false))); +static const chain::outpoint outpoint4{ chain::point{ test::tx4.hash(false), 0 }, 0x08 }; +static const chain::outpoint outpoint5{ chain::point{ test::tx5.hash(false), 0 }, 0x85 }; + +BOOST_AUTO_TEST_CASE(native__address_unconfirmed__json__unconfirmed_outputs) +{ + const auto response = get_json("/v1/address/" + unconfirmed + "/unconfirmed?format=json"); + BOOST_REQUIRE(response.is_array()); + + const auto& outputs = response.as_array(); + BOOST_REQUIRE(contains(outputs, value_from(outpoint4))); + BOOST_REQUIRE(contains(outputs, value_from(outpoint5))); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/protocols/native/native_setup_fixture.hpp b/test/protocols/native/native_setup_fixture.hpp index 688a96c4..f30e1030 100644 --- a/test/protocols/native/native_setup_fixture.hpp +++ b/test/protocols/native/native_setup_fixture.hpp @@ -114,6 +114,18 @@ struct native_address_setup_fixture } }; +struct native_unconfirmed_setup_fixture + : native_setup_fixture +{ + inline native_unconfirmed_setup_fixture() + : native_setup_fixture([](test::query_t& query) + { + return test::setup_three_block_unconfirmed_address_store(query); + }) + { + } +}; + struct native_unassociated_setup_fixture : native_setup_fixture {