diff --git a/include/bitcoin/node/protocols/protocol_block_out_106.hpp b/include/bitcoin/node/protocols/protocol_block_out_106.hpp index 7c254ce7..d648006c 100644 --- a/include/bitcoin/node/protocols/protocol_block_out_106.hpp +++ b/include/bitcoin/node/protocols/protocol_block_out_106.hpp @@ -41,6 +41,8 @@ class BCN_API protocol_block_out_106 node_pruned_(session->node_settings().limited_blocks), node_witness_(session->node_settings().provide_witness), allow_overlapped_(session->node_settings().allow_overlapped), + not_found_allowed_(negotiated_version() >= + network::messages::peer::not_found::version_minimum), network::tracker(session->log) { } @@ -73,10 +75,12 @@ class BCN_API protocol_block_out_106 private: using inventory = network::messages::peer::inventory; + using not_found = network::messages::peer::not_found; using inventory_item = network::messages::peer::inventory_item; using inventory_items = network::messages::peer::inventory_items; bool is_under_checkpoint(const database::header_link& link) NOEXCEPT; + void send_not_found() NOEXCEPT; inventory create_inventory(const get_blocks& locator) const NOEXCEPT; void merge_inventory(const inventory_items& items) NOEXCEPT; @@ -85,6 +89,7 @@ class BCN_API protocol_block_out_106 const bool node_pruned_; const bool node_witness_; const bool allow_overlapped_; + const bool not_found_allowed_; // This is protected by strand. std::deque backlog_{}; diff --git a/src/protocols/protocol_block_out_106.cpp b/src/protocols/protocol_block_out_106.cpp index 51495482..193a67ad 100644 --- a/src/protocols/protocol_block_out_106.cpp +++ b/src/protocols/protocol_block_out_106.cpp @@ -201,10 +201,13 @@ bool protocol_block_out_106::handle_receive_get_data(const code& ec, void protocol_block_out_106::send_block(const code& ec) NOEXCEPT { BC_ASSERT(stranded()); + if (stopped(ec)) return; - if (backlog_.empty()) return; + if (backlog_.empty()) + return; + const auto& item = backlog_.front(); const auto witness = item.is_witness_type(); if (witness && !node_witness_) @@ -216,11 +219,23 @@ void protocol_block_out_106::send_block(const code& ec) NOEXCEPT const auto& query = archive(); const auto link = query.to_header(item.hash); + + // The checkpoint height query faults the store on a terminal link. + if (link.is_terminal()) + { + LOGR("Requested block " << encode_hash(item.hash) << " from [" + << opposite() << "] not stored."); + + send_not_found(); + return; + } + if (node_pruned_ && (is_under_checkpoint(link) || query.is_milestone(link))) { LOGR("Requested pruned block " << encode_hash(item.hash) << " from [" << opposite() << "]."); - stop(system::error::not_found); + + send_not_found(); return; } @@ -229,14 +244,13 @@ void protocol_block_out_106::send_block(const code& ec) NOEXCEPT { { query.get_wire_block(link, witness), witness } }; + if (!out.block.is_valid()) { LOGR("Requested block " << encode_hash(item.hash) << " from [" << opposite() << "] not found."); - // This block could not have been advertised to the peer. - // TODO: send not_found message in protocol override. - stop(system::error::not_found); + send_not_found(); return; } @@ -248,6 +262,23 @@ void protocol_block_out_106::send_block(const code& ec) NOEXCEPT // utilities // ---------------------------------------------------------------------------- +// This protocol is also attached below bip130, and when headers-first is +// disabled, so the peer may be below bip37, where not_found is undefined. +void protocol_block_out_106::send_not_found() NOEXCEPT +{ + BC_ASSERT(stranded()); + + if (!not_found_allowed_) + { + stop(system::error::not_found); + return; + } + + not_found out{ { backlog_.front() } }; + backlog_.pop_front(); + SEND(std::move(out), send_block, _1); +} + void protocol_block_out_106::merge_inventory( const inventory_items& items) NOEXCEPT { diff --git a/src/protocols/protocol_transaction_out_106.cpp b/src/protocols/protocol_transaction_out_106.cpp index 9f2423f3..c1dbbd9d 100644 --- a/src/protocols/protocol_transaction_out_106.cpp +++ b/src/protocols/protocol_transaction_out_106.cpp @@ -202,8 +202,9 @@ void protocol_transaction_out_106::send_transaction(const code& ec, LOGR("Requested tx " << encode_hash(item.hash) << " from [" << opposite() << "] not found."); - // This tx could not have been advertised to the peer. - stop(system::error::not_found); + // The protocol is attached above bip37, where not_found is defined. + BC_ASSERT(negotiated_version() >= level::bip37); + SEND(not_found{ { item } }, send_transaction, _1, add1(index), message); return; } diff --git a/test/functional/p2p.cpp b/test/functional/p2p.cpp index 0d244a86..8a47ca66 100644 --- a/test/functional/p2p.cpp +++ b/test/functional/p2p.cpp @@ -59,4 +59,102 @@ BOOST_AUTO_TEST_CASE(functional_p2p__get_data__genesis_block__expected_bytes) BOOST_REQUIRE(payload == expected); } +BOOST_AUTO_TEST_CASE(functional_p2p__get_data__unknown_block__not_found) +{ + BOOST_REQUIRE(handshake()); + + const get_data get{ { { inventory_item::type_id::block, system::one_hash } } }; + send(get, node_version->value); + + const auto payload = receive(not_found::command); + const auto message = not_found::deserialize(node_version->value, payload); + BOOST_REQUIRE(message); + BOOST_REQUIRE_EQUAL(message->items.size(), one); + BOOST_REQUIRE(message->items.front().hash == system::one_hash); +} + +// A limited node answers for a block it has pruned, and remains unfaulted. +struct p2p_limited_setup_fixture + : p2p_setup_fixture +{ + p2p_limited_setup_fixture() NOEXCEPT + : p2p_setup_fixture({}, [](configuration& config) NOEXCEPT + { + config.node.limited_blocks = true; + }) + { + } +}; + +BOOST_FIXTURE_TEST_CASE(functional_p2p__get_data__pruned_block__not_found, p2p_limited_setup_fixture) +{ + BOOST_REQUIRE(handshake()); + + const system::chain::block& genesis = config_.bitcoin.genesis_block; + const get_data get{ { { inventory_item::type_id::block, genesis.hash() } } }; + send(get, node_version->value); + + const auto payload = receive(not_found::command); + const auto message = not_found::deserialize(node_version->value, payload); + BOOST_REQUIRE(message); + BOOST_REQUIRE_EQUAL(message->items.size(), one); +} + +// A header is archived before its block is associated, so the header link +// resolves while the block remains absent from the archive. This is the +// steady state of headers-first sync. +struct p2p_unassociated_setup_fixture + : p2p_setup_fixture +{ + static system::chain::header unassociated() NOEXCEPT + { + const system::settings bitcoin{ system::chain::selection::mainnet }; + return + { + 1u, + bitcoin.genesis_block.hash(), + system::null_hash, + 0u, + 0u, + 0u + }; + } + + p2p_unassociated_setup_fixture() NOEXCEPT + : p2p_setup_fixture([](node::query& query) NOEXCEPT + { + return query.set(unassociated(), database::context{}, false); + }) + { + } +}; + +BOOST_FIXTURE_TEST_CASE(functional_p2p__get_data__unassociated_block__not_found, + p2p_unassociated_setup_fixture) +{ + BOOST_REQUIRE(handshake()); + + const auto hash = unassociated().hash(); + const get_data get{ { { inventory_item::type_id::block, hash } } }; + send(get, node_version->value); + + const auto payload = receive(not_found::command); + const auto message = not_found::deserialize(node_version->value, payload); + BOOST_REQUIRE(message); + BOOST_REQUIRE_EQUAL(message->items.size(), one); + BOOST_REQUIRE(message->items.front().hash == hash); +} + +// not_found is undefined below bip37, so the channel is stopped instead. +BOOST_AUTO_TEST_CASE(functional_p2p__get_data__unknown_block_below_bip37__stopped) +{ + BOOST_REQUIRE(handshake(0, level::bip35)); + + const get_data get{ { { inventory_item::type_id::block, system::one_hash } } }; + send(get, level::bip35); + + // The channel is stopped, so the socket closes without a not_found. + BOOST_REQUIRE_THROW(receive(not_found::command), boost::system::system_error); +} + BOOST_AUTO_TEST_SUITE_END()