diff --git a/include/bitcoin/network/messages/peer/detail/get_data.hpp b/include/bitcoin/network/messages/peer/detail/get_data.hpp index 92ea1f77b..492c1356f 100644 --- a/include/bitcoin/network/messages/peer/detail/get_data.hpp +++ b/include/bitcoin/network/messages/peer/detail/get_data.hpp @@ -34,7 +34,9 @@ namespace peer { struct BCT_API get_data { typedef std::shared_ptr cptr; + typedef inventory_item::selector selector; typedef inventory_item::type_id type_id; + typedef inventory_item item; static const identifier id; static const std::string command; @@ -54,9 +56,11 @@ struct BCT_API get_data size_t size(uint32_t version) const NOEXCEPT; // inventory implements the same methods. - auto view(type_id type) const NOEXCEPT; + inline auto view(type_id type) const NOEXCEPT; + inventory_items select(selector types) const NOEXCEPT; inventory_items filter(type_id type) const NOEXCEPT; system::hashes to_hashes(type_id type) const NOEXCEPT; + size_t count(selector type) const NOEXCEPT; size_t count(type_id type) const NOEXCEPT; bool any(type_id type) const NOEXCEPT; bool any_transaction() const NOEXCEPT; @@ -68,9 +72,9 @@ struct BCT_API get_data inline auto get_data::view(type_id type) const NOEXCEPT { - const auto is_type = [type](const auto& item) NOEXCEPT + const auto is_type = [type](const inventory_item& item) NOEXCEPT { - return item.type == type; + return item.is_type(type); }; return std::ranges::filter_view(items, is_type); diff --git a/include/bitcoin/network/messages/peer/detail/inventory.hpp b/include/bitcoin/network/messages/peer/detail/inventory.hpp index da9cc61d1..510aa83ef 100644 --- a/include/bitcoin/network/messages/peer/detail/inventory.hpp +++ b/include/bitcoin/network/messages/peer/detail/inventory.hpp @@ -34,7 +34,9 @@ namespace peer { struct BCT_API inventory { typedef std::shared_ptr cptr; + typedef inventory_item::selector selector; typedef inventory_item::type_id type_id; + typedef inventory_item item; static const identifier id; static const std::string command; @@ -60,9 +62,11 @@ struct BCT_API inventory size_t size(uint32_t version) const NOEXCEPT; // get_data implements the same methods. - auto view(type_id type) const NOEXCEPT; + inline auto view(type_id type) const NOEXCEPT; + inventory_items select(selector types) const NOEXCEPT; inventory_items filter(type_id type) const NOEXCEPT; system::hashes to_hashes(type_id type) const NOEXCEPT; + size_t count(selector type) const NOEXCEPT; size_t count(type_id type) const NOEXCEPT; bool any(type_id type) const NOEXCEPT; bool any_transaction() const NOEXCEPT; @@ -74,9 +78,9 @@ struct BCT_API inventory inline auto inventory::view(type_id type) const NOEXCEPT { - const auto is_type = [type](const auto& item) NOEXCEPT + const auto is_type = [type](const inventory_item& item) NOEXCEPT { - return item.type == type; + return item.is_type(type); }; return std::ranges::filter_view(items, is_type); diff --git a/include/bitcoin/network/messages/peer/detail/inventory_item.hpp b/include/bitcoin/network/messages/peer/detail/inventory_item.hpp index 4d0f1d9a3..72b0a57af 100644 --- a/include/bitcoin/network/messages/peer/detail/inventory_item.hpp +++ b/include/bitcoin/network/messages/peer/detail/inventory_item.hpp @@ -32,17 +32,27 @@ struct BCT_API inventory_item { typedef std::shared_ptr cptr; + /// Logical type selection. + enum class selector + { + txids, + wtxids, + blocks, + filters + }; + + /// Serializeable type id. enum class type_id : uint32_t { - // It's so dumb that these are not flags (so use class enum). - error = 0, - transaction = 1, - block = 2, - filtered = 3, - compact = 4, - wtxid = 5, + // It's so dumb that these are not flags. + error = 0, + transaction = 1, + block = 2, + filtered = 3, + compact = 4, + wtxid = 5, - // And yet this is a flag, but excludes wtxid. + // But witness is a flag. witness = system::bit_right(30), witness_tx = witness | transaction, witness_block = witness | block, @@ -59,9 +69,18 @@ struct BCT_API inventory_item system::reader& source) NOEXCEPT; void serialize(uint32_t version, system::writer& sink) const NOEXCEPT; + /// These indicate relevance to a specific object identifier. + bool is_txid() const NOEXCEPT; + bool is_wtxid() const NOEXCEPT; + bool is_block() const NOEXCEPT; + bool is_filter() const NOEXCEPT; + bool is_selected(selector types) const NOEXCEPT; + + /// These indicate context, not object identifier. bool is_block_type() const NOEXCEPT; bool is_transaction_type() const NOEXCEPT; bool is_witness_type() const NOEXCEPT; + bool is_type(type_id id) const NOEXCEPT; type_id type; system::hash_digest hash; diff --git a/src/messages/peer/detail/get_data.cpp b/src/messages/peer/detail/get_data.cpp index 1263960c5..34ca483af 100644 --- a/src/messages/peer/detail/get_data.cpp +++ b/src/messages/peer/detail/get_data.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -55,11 +56,11 @@ get_data get_data::deserialize(uint32_t version, reader& source) NOEXCEPT source.invalidate(); const auto size = source.read_size(max_inventory); - get_data get; + get_data get{}; get.items.reserve(size); - for (size_t item = 0; item < size; ++item) - get.items.push_back(inventory_item::deserialize(version, source)); + for (size_t item{}; item < size; ++item) + get.items.push_back(item::deserialize(version, source)); return get; } @@ -76,9 +77,9 @@ bool get_data::serialize(uint32_t version, void get_data::serialize(uint32_t version, writer& sink) const NOEXCEPT { BC_DEBUG_ONLY(const auto bytes = size(version);) - BC_DEBUG_ONLY(const auto start = sink.get_write_position();) + BC_DEBUG_ONLY(const auto start = sink.get_write_position();) - sink.write_variable(items.size()); + sink.write_variable(items.size()); for (const auto& item: items) item.serialize(version, sink); @@ -89,12 +90,25 @@ void get_data::serialize(uint32_t version, writer& sink) const NOEXCEPT size_t get_data::size(uint32_t version) const NOEXCEPT { return variable_size(items.size()) + - (items.size() * inventory_item::size(version)); + (items.size() * item::size(version)); +} + +// Populated in reverse order for efficient removals. +inventory_items get_data::select(selector types) const NOEXCEPT +{ + inventory_items out{}; + out.reserve(count(types)); + + for (const auto& item: std::views::reverse(items)) + if (item.is_selected(types)) + out.push_back(item); + + return out; } inventory_items get_data::filter(type_id type) const NOEXCEPT { - inventory_items out; + inventory_items out{}; out.reserve(count(type)); for (const auto& item: items) @@ -106,7 +120,7 @@ inventory_items get_data::filter(type_id type) const NOEXCEPT hashes get_data::to_hashes(type_id type) const NOEXCEPT { - hashes out; + hashes out{}; out.reserve(count(type)); for (const auto& item: items) @@ -116,11 +130,21 @@ hashes get_data::to_hashes(type_id type) const NOEXCEPT return out; } +size_t get_data::count(selector type) const NOEXCEPT +{ + const auto is_selected = [type](const item& item) + { + return item.is_selected(type); + }; + + return std::count_if(items.begin(), items.end(), is_selected); +} + size_t get_data::count(type_id type) const NOEXCEPT { - const auto is_type = [type](const inventory_item& item) + const auto is_type = [type](const item& item) { - return item.type == type; + return item.is_type(type); }; return std::count_if(items.begin(), items.end(), is_type); @@ -128,9 +152,9 @@ size_t get_data::count(type_id type) const NOEXCEPT bool get_data::any(type_id type) const NOEXCEPT { - const auto is_type = [type](const inventory_item& item) + const auto is_type = [type](const item& item) { - return item.type == type; + return item.is_type(type); }; return std::any_of(items.begin(), items.end(), is_type); @@ -138,7 +162,7 @@ bool get_data::any(type_id type) const NOEXCEPT bool get_data::any_transaction() const NOEXCEPT { - const auto is_transaction = [](const inventory_item& item) + const auto is_transaction = [](const item& item) { return item.is_transaction_type(); }; @@ -148,7 +172,7 @@ bool get_data::any_transaction() const NOEXCEPT bool get_data::any_block() const NOEXCEPT { - const auto is_block = [](const inventory_item& item) + const auto is_block = [](const item& item) { return item.is_block_type(); }; @@ -158,7 +182,7 @@ bool get_data::any_block() const NOEXCEPT bool get_data::any_witness() const NOEXCEPT { - const auto is_witness = [](const inventory_item& item) + const auto is_witness = [](const item& item) { return item.is_witness_type(); }; diff --git a/src/messages/peer/detail/inventory.cpp b/src/messages/peer/detail/inventory.cpp index 0192909dd..9f2a9bc62 100644 --- a/src/messages/peer/detail/inventory.cpp +++ b/src/messages/peer/detail/inventory.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -48,7 +49,7 @@ inventory inventory::factory(hashes&& hashes, type_id type) NOEXCEPT std::transform(hashes.begin(), hashes.end(), items.begin(), [=](hash_digest& hash) NOEXCEPT { - return inventory_item{ type, std::move(hash) }; + return item{ type, std::move(hash) }; }); return { items }; @@ -62,7 +63,7 @@ inventory inventory::factory(const hashes& hashes, type_id type) NOEXCEPT std::transform(hashes.begin(), hashes.end(), items.begin(), [=](const hash_digest& hash) NOEXCEPT { - return inventory_item{ type, hash }; + return item{ type, hash }; }); return { items }; @@ -88,8 +89,8 @@ inventory inventory::deserialize(uint32_t version, reader& source) NOEXCEPT inventory_items items; items.reserve(size); - for (size_t item = 0; item < size; ++item) - items.push_back(inventory_item::deserialize(version, source)); + for (size_t item{}; item < size; ++item) + items.push_back(item::deserialize(version, source)); return { items }; } @@ -119,12 +120,25 @@ void inventory::serialize(uint32_t version, writer& sink) const NOEXCEPT size_t inventory::size(uint32_t version) const NOEXCEPT { return variable_size(items.size()) + - (items.size() * inventory_item::size(version)); + (items.size() * item::size(version)); +} + +// Populated in reverse order for efficient removals. +inventory_items inventory::select(selector types) const NOEXCEPT +{ + inventory_items out{}; + out.reserve(count(types)); + + for (const auto& item: std::views::reverse(items)) + if (item.is_selected(types)) + out.push_back(item); + + return out; } inventory_items inventory::filter(type_id type) const NOEXCEPT { - inventory_items out; + inventory_items out{}; out.reserve(count(type)); for (const auto& item: items) @@ -136,7 +150,7 @@ inventory_items inventory::filter(type_id type) const NOEXCEPT hashes inventory::to_hashes(type_id type) const NOEXCEPT { - hashes out; + hashes out{}; out.reserve(count(type)); for (const auto& item: items) @@ -146,11 +160,21 @@ hashes inventory::to_hashes(type_id type) const NOEXCEPT return out; } +size_t inventory::count(selector type) const NOEXCEPT +{ + const auto is_selected = [type](const inventory_item& item) + { + return item.is_selected(type); + }; + + return std::count_if(items.begin(), items.end(), is_selected); +} + size_t inventory::count(type_id type) const NOEXCEPT { - const auto is_type = [type](const inventory_item& item) + const auto is_type = [type](const item& item) { - return item.type == type; + return item.is_type(type); }; return std::count_if(items.begin(), items.end(), is_type); @@ -158,9 +182,9 @@ size_t inventory::count(type_id type) const NOEXCEPT bool inventory::any(type_id type) const NOEXCEPT { - const auto is_type = [type](const inventory_item& item) + const auto is_type = [type](const item& item) { - return item.type == type; + return item.is_type(type); }; return std::any_of(items.begin(), items.end(), is_type); @@ -168,7 +192,7 @@ bool inventory::any(type_id type) const NOEXCEPT bool inventory::any_transaction() const NOEXCEPT { - const auto is_transaction = [](const inventory_item& item) + const auto is_transaction = [](const item& item) { return item.is_transaction_type(); }; @@ -178,7 +202,7 @@ bool inventory::any_transaction() const NOEXCEPT bool inventory::any_block() const NOEXCEPT { - const auto is_block = [](const inventory_item& item) + const auto is_block = [](const item& item) { return item.is_block_type(); }; @@ -188,7 +212,7 @@ bool inventory::any_block() const NOEXCEPT bool inventory::any_witness() const NOEXCEPT { - const auto is_witness = [](const inventory_item& item) + const auto is_witness = [](const item& item) { return item.is_witness_type(); }; diff --git a/src/messages/peer/detail/inventory_item.cpp b/src/messages/peer/detail/inventory_item.cpp index 1e11a5c22..6e4707acd 100644 --- a/src/messages/peer/detail/inventory_item.cpp +++ b/src/messages/peer/detail/inventory_item.cpp @@ -95,32 +95,71 @@ void inventory_item::serialize(uint32_t BC_DEBUG_ONLY(version), BC_ASSERT(sink && sink.get_write_position() - start == bytes); } +bool inventory_item::is_txid() const NOEXCEPT +{ + return is_type(type_id::witness_tx) + || is_type(type_id::transaction); +} + +bool inventory_item::is_wtxid() const NOEXCEPT +{ + return is_type(type_id::wtxid); +} + +bool inventory_item::is_block() const NOEXCEPT +{ + return is_type(type_id::block) + || is_type(type_id::witness_block); +} + +bool inventory_item::is_filter() const NOEXCEPT +{ + return is_type(type_id::filtered) + || is_type(type_id::witness_filtered); +} + +bool inventory_item::is_selected(selector types) const NOEXCEPT +{ + switch (types) + { + case selector::txids: return is_txid(); + case selector::wtxids: return is_wtxid(); + case selector::blocks: return is_block(); + case selector::filters: return is_filter(); + default: return false; + }; +} + bool inventory_item::is_block_type() const NOEXCEPT { - // Filtered are bip37, effectively deprecated by bip111. - return type == type_id::witness_block - || type == type_id::witness_compact - || type == type_id::witness_filtered - || type == type_id::block - || type == type_id::compact - || type == type_id::filtered; + return is_type(type_id::witness_block) + || is_type(type_id::witness_compact) + || is_type(type_id::witness_filtered) + || is_type(type_id::block) + || is_type(type_id::compact) + || is_type(type_id::filtered); } bool inventory_item::is_transaction_type() const NOEXCEPT { // Only wtxid corresponds to a witness hash. - return type == type_id::witness_tx - || type == type_id::transaction - || type == type_id::wtxid; + return is_type(type_id::witness_tx) + || is_type(type_id::transaction) + || is_type(type_id::wtxid); } bool inventory_item::is_witness_type() const NOEXCEPT { // wtxid excluded - return type == type_id::witness_tx - || type == type_id::witness_block - || type == type_id::witness_compact - || type == type_id::witness_filtered; + return is_type(type_id::witness_tx) + || is_type(type_id::witness_block) + || is_type(type_id::witness_compact) + || is_type(type_id::witness_filtered); +} + +bool inventory_item::is_type(type_id id) const NOEXCEPT +{ + return type == id; } bool operator==(const inventory_item& left, const inventory_item& right) NOEXCEPT diff --git a/test/messages/peer/detail/get_data.cpp b/test/messages/peer/detail/get_data.cpp index 88ffb81be..f20e8ef09 100644 --- a/test/messages/peer/detail/get_data.cpp +++ b/test/messages/peer/detail/get_data.cpp @@ -20,8 +20,36 @@ BOOST_AUTO_TEST_SUITE(p2p_get_data_tests) +using namespace system; using namespace network::messages::peer; +constexpr auto hash_tx = base16_hash("0000000000000000000000000000000000000000000000000000000000000001"); +constexpr auto hash_block = base16_hash("0000000000000000000000000000000000000000000000000000000000000002"); +constexpr auto hash_witness_tx = base16_hash("0000000000000000000000000000000000000000000000000000000000000003"); +constexpr auto hash_witness_block = base16_hash("0000000000000000000000000000000000000000000000000000000000000004"); +constexpr auto hash_filtered = base16_hash("0000000000000000000000000000000000000000000000000000000000000005"); +constexpr auto hash_wtxid = base16_hash("0000000000000000000000000000000000000000000000000000000000000006"); +constexpr auto hash_compact = base16_hash("0000000000000000000000000000000000000000000000000000000000000007"); + +static get_data make_mixed_inventory() NOEXCEPT +{ + static get_data value + { + inventory_items + { + { inventory_item::type_id::transaction, hash_tx }, + { inventory_item::type_id::block, hash_block }, + { inventory_item::type_id::witness_tx, hash_witness_tx }, + { inventory_item::type_id::witness_block, hash_witness_block }, + { inventory_item::type_id::filtered, hash_filtered }, + { inventory_item::type_id::wtxid, hash_wtxid }, + { inventory_item::type_id::compact, hash_compact } + } + }; + + return value; +} + BOOST_AUTO_TEST_CASE(get_data__properties__always__expected) { BOOST_REQUIRE_EQUAL(get_data::command, "getdata"); @@ -38,4 +66,151 @@ BOOST_AUTO_TEST_CASE(get_data__size__default__expected) BOOST_REQUIRE_EQUAL(get_data{}.size(level::canonical), expected); } +BOOST_AUTO_TEST_CASE(get_data__select__selectors__expected_items) +{ + const auto inv = make_mixed_inventory(); + + const auto txs = inv.select(inventory_item::selector::txids); + BOOST_REQUIRE_EQUAL(txs.size(), 2u); + BOOST_CHECK(txs[0].type == inventory_item::type_id::witness_tx); + BOOST_CHECK(txs[1].type == inventory_item::type_id::transaction); + + const auto wtxs = inv.select(inventory_item::selector::wtxids); + BOOST_REQUIRE_EQUAL(wtxs.size(), 1u); + BOOST_CHECK(wtxs[0].type == inventory_item::type_id::wtxid); + + const auto blocks = inv.select(inventory_item::selector::blocks); + BOOST_REQUIRE_EQUAL(blocks.size(), 2u); + BOOST_CHECK(blocks[0].type == inventory_item::type_id::witness_block); + BOOST_CHECK(blocks[1].type == inventory_item::type_id::block); + + const auto filters = inv.select(inventory_item::selector::filters); + BOOST_REQUIRE_EQUAL(filters.size(), 1u); + BOOST_CHECK(filters[0].type == inventory_item::type_id::filtered); +} + +BOOST_AUTO_TEST_CASE(get_data__filter__types__expected_subset) +{ + const auto inv = make_mixed_inventory(); + + const auto txs = inv.filter(inventory_item::type_id::transaction); + BOOST_REQUIRE_EQUAL(txs.size(), 1u); + BOOST_CHECK(txs[0].type == inventory_item::type_id::transaction); + BOOST_CHECK(txs[0].hash == hash_tx); + + const auto witness_txs = inv.filter(inventory_item::type_id::witness_tx); + BOOST_REQUIRE_EQUAL(witness_txs.size(), 1u); + BOOST_CHECK(witness_txs[0].type == inventory_item::type_id::witness_tx); + BOOST_CHECK(witness_txs[0].hash == hash_witness_tx); + + const auto witness_blocks = inv.filter(inventory_item::type_id::witness_block); + BOOST_REQUIRE_EQUAL(witness_blocks.size(), 1u); + BOOST_CHECK(witness_blocks[0].hash == hash_witness_block); + + const auto none = inv.filter(inventory_item::type_id::error); + BOOST_CHECK(none.empty()); +} + +BOOST_AUTO_TEST_CASE(get_data__to_hashes__specific_type__expected_hashes) +{ + const auto inv = make_mixed_inventory(); + + const auto tx_hashes = inv.to_hashes(inventory_item::type_id::transaction); + BOOST_REQUIRE_EQUAL(tx_hashes.size(), 1u); + BOOST_CHECK(tx_hashes[0] == hash_tx); + + const auto block_hashes = inv.to_hashes(inventory_item::type_id::block); + BOOST_REQUIRE_EQUAL(block_hashes.size(), 1u); + BOOST_CHECK(block_hashes[0] == hash_block); + + const auto error_hashes = inv.to_hashes(inventory_item::type_id::error); + BOOST_CHECK(error_hashes.empty()); +} + +BOOST_AUTO_TEST_CASE(get_data__count__selector__expected_counts) +{ + const auto inv = make_mixed_inventory(); + + BOOST_CHECK_EQUAL(inv.count(inventory_item::selector::txids), 2u); + BOOST_CHECK_EQUAL(inv.count(inventory_item::selector::wtxids), 1u); + BOOST_CHECK_EQUAL(inv.count(inventory_item::selector::blocks), 2u); + BOOST_CHECK_EQUAL(inv.count(inventory_item::selector::filters), 1u); +} + +BOOST_AUTO_TEST_CASE(get_data__count__type_id__expected_counts) +{ + const auto inv = make_mixed_inventory(); + + BOOST_CHECK_EQUAL(inv.count(inventory_item::type_id::transaction), 1u); + BOOST_CHECK_EQUAL(inv.count(inventory_item::type_id::block), 1u); + BOOST_CHECK_EQUAL(inv.count(inventory_item::type_id::witness_tx), 1u); + BOOST_CHECK_EQUAL(inv.count(inventory_item::type_id::witness_block), 1u); + BOOST_CHECK_EQUAL(inv.count(inventory_item::type_id::filtered), 1u); + BOOST_CHECK_EQUAL(inv.count(inventory_item::type_id::wtxid), 1u); + BOOST_CHECK_EQUAL(inv.count(inventory_item::type_id::compact), 1u); + BOOST_CHECK_EQUAL(inv.count(inventory_item::type_id::error), 0u); +} + +BOOST_AUTO_TEST_CASE(get_data__any__type_id__expected_presence) +{ + const auto inv = make_mixed_inventory(); + + BOOST_CHECK(inv.any(inventory_item::type_id::transaction)); + BOOST_CHECK(inv.any(inventory_item::type_id::block)); + BOOST_CHECK(inv.any(inventory_item::type_id::witness_tx)); + BOOST_CHECK(inv.any(inventory_item::type_id::witness_block)); + BOOST_CHECK(!inv.any(inventory_item::type_id::error)); +} + +BOOST_AUTO_TEST_CASE(get_data__any_transaction__mixed_items__expected) +{ + auto inv = make_mixed_inventory(); + BOOST_CHECK(inv.any_transaction()); + + inv.items.clear(); + BOOST_CHECK(!inv.any_transaction()); + + inv.items.emplace_back(inventory_item{ inventory_item::type_id::witness_tx, hash_tx }); + BOOST_CHECK(inv.any_transaction()); +} + +BOOST_AUTO_TEST_CASE(get_data__any_block__mixed_items__expected) +{ + auto inv = make_mixed_inventory(); + BOOST_CHECK(inv.any_block()); + + inv.items.clear(); + BOOST_CHECK(!inv.any_block()); + + inv.items.emplace_back(inventory_item{ inventory_item::type_id::block, hash_block }); + BOOST_CHECK(inv.any_block()); +} + +BOOST_AUTO_TEST_CASE(get_data__any_witness__mixed_items__expected) +{ + auto inv = make_mixed_inventory(); + BOOST_CHECK(inv.any_witness()); + + inv.items.clear(); + BOOST_CHECK(!inv.any_witness()); + + inv.items.emplace_back(inventory_item{ inventory_item::type_id::witness_tx, hash_witness_tx }); + BOOST_CHECK(inv.any_witness()); +} + +// std::ranges::distance requires non-const view (due to potential underlying caching). +BOOST_AUTO_TEST_CASE(get_data__view__specific_type__filtered_view) +{ + const auto inv = make_mixed_inventory(); + + auto tx_view = inv.view(inventory_item::type_id::transaction); + BOOST_CHECK(is_one(std::ranges::distance(tx_view))); + + auto block_view = inv.view(inventory_item::type_id::block); + BOOST_CHECK(is_one(std::ranges::distance(block_view))); + + auto error_view = inv.view(inventory_item::type_id::error); + BOOST_CHECK(is_zero(std::ranges::distance(error_view))); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/messages/peer/detail/inventory.cpp b/test/messages/peer/detail/inventory.cpp index 11071e2e0..ca197ce32 100644 --- a/test/messages/peer/detail/inventory.cpp +++ b/test/messages/peer/detail/inventory.cpp @@ -20,12 +20,35 @@ BOOST_AUTO_TEST_SUITE(p2p_inventory_tests) +using namespace system; using namespace network::messages::peer; -// factory move/copy -// filter -// to_hashes -// count +constexpr auto hash_tx = base16_hash("0000000000000000000000000000000000000000000000000000000000000001"); +constexpr auto hash_block = base16_hash("0000000000000000000000000000000000000000000000000000000000000002"); +constexpr auto hash_witness_tx = base16_hash("0000000000000000000000000000000000000000000000000000000000000003"); +constexpr auto hash_witness_block = base16_hash("0000000000000000000000000000000000000000000000000000000000000004"); +constexpr auto hash_filtered = base16_hash("0000000000000000000000000000000000000000000000000000000000000005"); +constexpr auto hash_wtxid = base16_hash("0000000000000000000000000000000000000000000000000000000000000006"); +constexpr auto hash_compact = base16_hash("0000000000000000000000000000000000000000000000000000000000000007"); + +static inventory make_mixed_inventory() NOEXCEPT +{ + static inventory value + { + inventory_items + { + { inventory_item::type_id::transaction, hash_tx }, + { inventory_item::type_id::block, hash_block }, + { inventory_item::type_id::witness_tx, hash_witness_tx }, + { inventory_item::type_id::witness_block, hash_witness_block }, + { inventory_item::type_id::filtered, hash_filtered }, + { inventory_item::type_id::wtxid, hash_wtxid }, + { inventory_item::type_id::compact, hash_compact } + } + }; + + return value; +} BOOST_AUTO_TEST_CASE(inventory__properties__always__expected) { @@ -41,4 +64,151 @@ BOOST_AUTO_TEST_CASE(inventory__size__default__expected) BOOST_REQUIRE_EQUAL(inventory{}.size(level::canonical), expected); } +BOOST_AUTO_TEST_CASE(inventory__select__selectors__expected_items) +{ + const auto inv = make_mixed_inventory(); + + const auto txs = inv.select(inventory_item::selector::txids); + BOOST_REQUIRE_EQUAL(txs.size(), 2u); + BOOST_CHECK(txs[0].type == inventory_item::type_id::witness_tx); + BOOST_CHECK(txs[1].type == inventory_item::type_id::transaction); + + const auto wtxs = inv.select(inventory_item::selector::wtxids); + BOOST_REQUIRE_EQUAL(wtxs.size(), 1u); + BOOST_CHECK(wtxs[0].type == inventory_item::type_id::wtxid); + + const auto blocks = inv.select(inventory_item::selector::blocks); + BOOST_REQUIRE_EQUAL(blocks.size(), 2u); + BOOST_CHECK(blocks[0].type == inventory_item::type_id::witness_block); + BOOST_CHECK(blocks[1].type == inventory_item::type_id::block); + + const auto filters = inv.select(inventory_item::selector::filters); + BOOST_REQUIRE_EQUAL(filters.size(), 1u); + BOOST_CHECK(filters[0].type == inventory_item::type_id::filtered); +} + +BOOST_AUTO_TEST_CASE(inventory__filter__types__expected_subset) +{ + const auto inv = make_mixed_inventory(); + + const auto txs = inv.filter(inventory_item::type_id::transaction); + BOOST_REQUIRE_EQUAL(txs.size(), 1u); + BOOST_CHECK(txs[0].type == inventory_item::type_id::transaction); + BOOST_CHECK(txs[0].hash == hash_tx); + + const auto witness_txs = inv.filter(inventory_item::type_id::witness_tx); + BOOST_REQUIRE_EQUAL(witness_txs.size(), 1u); + BOOST_CHECK(witness_txs[0].type == inventory_item::type_id::witness_tx); + BOOST_CHECK(witness_txs[0].hash == hash_witness_tx); + + const auto witness_blocks = inv.filter(inventory_item::type_id::witness_block); + BOOST_REQUIRE_EQUAL(witness_blocks.size(), 1u); + BOOST_CHECK(witness_blocks[0].hash == hash_witness_block); + + const auto none = inv.filter(inventory_item::type_id::error); + BOOST_CHECK(none.empty()); +} + +BOOST_AUTO_TEST_CASE(inventory__to_hashes__specific_type__expected_hashes) +{ + const auto inv = make_mixed_inventory(); + + const auto tx_hashes = inv.to_hashes(inventory_item::type_id::transaction); + BOOST_REQUIRE_EQUAL(tx_hashes.size(), 1u); + BOOST_CHECK(tx_hashes[0] == hash_tx); + + const auto block_hashes = inv.to_hashes(inventory_item::type_id::block); + BOOST_REQUIRE_EQUAL(block_hashes.size(), 1u); + BOOST_CHECK(block_hashes[0] == hash_block); + + const auto error_hashes = inv.to_hashes(inventory_item::type_id::error); + BOOST_CHECK(error_hashes.empty()); +} + +BOOST_AUTO_TEST_CASE(inventory__count__selector__expected_counts) +{ + const auto inv = make_mixed_inventory(); + + BOOST_CHECK_EQUAL(inv.count(inventory_item::selector::txids), 2u); + BOOST_CHECK_EQUAL(inv.count(inventory_item::selector::wtxids), 1u); + BOOST_CHECK_EQUAL(inv.count(inventory_item::selector::blocks), 2u); + BOOST_CHECK_EQUAL(inv.count(inventory_item::selector::filters), 1u); +} + +BOOST_AUTO_TEST_CASE(inventory__count__type_id__expected_counts) +{ + const auto inv = make_mixed_inventory(); + + BOOST_CHECK_EQUAL(inv.count(inventory_item::type_id::transaction), 1u); + BOOST_CHECK_EQUAL(inv.count(inventory_item::type_id::block), 1u); + BOOST_CHECK_EQUAL(inv.count(inventory_item::type_id::witness_tx), 1u); + BOOST_CHECK_EQUAL(inv.count(inventory_item::type_id::witness_block), 1u); + BOOST_CHECK_EQUAL(inv.count(inventory_item::type_id::filtered), 1u); + BOOST_CHECK_EQUAL(inv.count(inventory_item::type_id::wtxid), 1u); + BOOST_CHECK_EQUAL(inv.count(inventory_item::type_id::compact), 1u); + BOOST_CHECK_EQUAL(inv.count(inventory_item::type_id::error), 0u); +} + +BOOST_AUTO_TEST_CASE(inventory__any__type_id__expected_presence) +{ + const auto inv = make_mixed_inventory(); + + BOOST_CHECK(inv.any(inventory_item::type_id::transaction)); + BOOST_CHECK(inv.any(inventory_item::type_id::block)); + BOOST_CHECK(inv.any(inventory_item::type_id::witness_tx)); + BOOST_CHECK(inv.any(inventory_item::type_id::witness_block)); + BOOST_CHECK(!inv.any(inventory_item::type_id::error)); +} + +BOOST_AUTO_TEST_CASE(inventory__any_transaction__mixed_items__expected) +{ + auto inv = make_mixed_inventory(); + BOOST_CHECK(inv.any_transaction()); + + inv.items.clear(); + BOOST_CHECK(!inv.any_transaction()); + + inv.items.emplace_back(inventory_item{ inventory_item::type_id::witness_tx, hash_tx }); + BOOST_CHECK(inv.any_transaction()); +} + +BOOST_AUTO_TEST_CASE(inventory__any_block__mixed_items__expected) +{ + auto inv = make_mixed_inventory(); + BOOST_CHECK(inv.any_block()); + + inv.items.clear(); + BOOST_CHECK(!inv.any_block()); + + inv.items.emplace_back(inventory_item{ inventory_item::type_id::block, hash_block }); + BOOST_CHECK(inv.any_block()); +} + +BOOST_AUTO_TEST_CASE(inventory__any_witness__mixed_items__expected) +{ + auto inv = make_mixed_inventory(); + BOOST_CHECK(inv.any_witness()); + + inv.items.clear(); + BOOST_CHECK(!inv.any_witness()); + + inv.items.emplace_back(inventory_item{ inventory_item::type_id::witness_tx, hash_witness_tx }); + BOOST_CHECK(inv.any_witness()); +} + +// std::ranges::distance requires non-const view (due to potential underlying caching). +BOOST_AUTO_TEST_CASE(inventory__view__specific_type__filtered_view) +{ + const auto inv = make_mixed_inventory(); + + auto tx_view = inv.view(inventory_item::type_id::transaction); + BOOST_CHECK(is_one(std::ranges::distance(tx_view))); + + auto block_view = inv.view(inventory_item::type_id::block); + BOOST_CHECK(is_one(std::ranges::distance(block_view))); + + auto error_view = inv.view(inventory_item::type_id::error); + BOOST_CHECK(is_zero(std::ranges::distance(error_view))); +} + BOOST_AUTO_TEST_SUITE_END()