diff --git a/src/index/blockfilterindex.h b/src/index/blockfilterindex.h --- a/src/index/blockfilterindex.h +++ b/src/index/blockfilterindex.h @@ -40,6 +40,10 @@ size_t WriteFilterToDisk(FlatFilePos &pos, const BlockFilter &filter); Mutex m_cs_headers_cache; + /** + * Cache of block hash to filter header, to avoid disk access when + * responding to getcfcheckpt. + */ std::unordered_map m_headers_cache GUARDED_BY(m_cs_headers_cache); diff --git a/src/net_processing.cpp b/src/net_processing.cpp --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -165,6 +165,11 @@ * Maximum feefilter broadcast delay after significant change. */ static constexpr unsigned int MAX_FEEFILTER_CHANGE_DELAY = 5 * 60; +/** + * Maximum number of cf hashes that may be requested with one getcfheaders. See + * BIP 157. + */ +static constexpr uint32_t MAX_GETCFHEADERS_SIZE = 2000; // Internal stuff namespace { @@ -2240,21 +2245,22 @@ * * @param[in] pfrom The peer that we received the request from * @param[in] chain_params Chain parameters - * @param[in] filter_type The filter type the request is for. Must be + * @param[in] filter_type The filter type the request is for. Must be * basic filters. + * @param[in] start_height The start height for the request * @param[in] stop_hash The stop_hash for the request + * @param[in] max_height_diff The maximum number of items permitted to + * request, as specified in BIP 157 * @param[out] stop_index The CBlockIndex for the stop_hash block, if the * request can be serviced. - * @param[out] filter_index The filter index, if the request can be + * @param[out] filter_index The filter index, if the request can be * serviced. * @return True if the request can be serviced. */ -static bool PrepareBlockFilterRequest(CNode *pfrom, - const CChainParams &chain_params, - BlockFilterType filter_type, - const BlockHash &stop_hash, - const CBlockIndex *&stop_index, - BlockFilterIndex *&filter_index) { +static bool PrepareBlockFilterRequest( + CNode *pfrom, const CChainParams &chain_params, BlockFilterType filter_type, + uint32_t start_height, const BlockHash &stop_hash, uint32_t max_height_diff, + const CBlockIndex *&stop_index, BlockFilterIndex *&filter_index) { const bool supported_filter_type = (filter_type == BlockFilterType::BASIC && gArgs.GetBoolArg("-peerblockfilters", DEFAULT_PEERBLOCKFILTERS)); @@ -2281,6 +2287,26 @@ } } + uint32_t stop_height = stop_index->nHeight; + if (start_height > stop_height) { + LogPrint( + BCLog::NET, + "peer %d sent invalid getcfilters/getcfheaders with " /* Continued + */ + "start height %d and stop height %d\n", + pfrom->GetId(), start_height, stop_height); + pfrom->fDisconnect = true; + return false; + } + if (stop_height - start_height >= max_height_diff) { + LogPrint(BCLog::NET, + "peer %d requested too many cfilters/cfheaders: %d / %d\n", + pfrom->GetId(), stop_height - start_height + 1, + max_height_diff); + pfrom->fDisconnect = true; + return false; + } + filter_index = GetBlockFilterIndex(filter_type); if (!filter_index) { LogPrint(BCLog::NET, "Filter index for supported type %s not found\n", @@ -2291,6 +2317,68 @@ return true; } +/** + * Handle a cfheaders request. + * + * May disconnect from the peer in the case of a bad request. + * + * @param[in] pfrom The peer that we received the request from + * @param[in] vRecv The raw message received + * @param[in] chain_params Chain parameters + * @param[in] connman Pointer to the connection manager + */ +static void ProcessGetCFHeaders(CNode *pfrom, CDataStream &vRecv, + const CChainParams &chain_params, + CConnman *connman) { + uint8_t filter_type_ser; + uint32_t start_height; + BlockHash stop_hash; + + vRecv >> filter_type_ser >> start_height >> stop_hash; + + const BlockFilterType filter_type = + static_cast(filter_type_ser); + + const CBlockIndex *stop_index; + BlockFilterIndex *filter_index; + if (!PrepareBlockFilterRequest( + pfrom, chain_params, filter_type, start_height, stop_hash, + MAX_GETCFHEADERS_SIZE, stop_index, filter_index)) { + return; + } + + uint256 prev_header; + if (start_height > 0) { + const CBlockIndex *const prev_block = + stop_index->GetAncestor(static_cast(start_height - 1)); + if (!filter_index->LookupFilterHeader(prev_block, prev_header)) { + LogPrint(BCLog::NET, + "Failed to find block filter header in index: " + "filter_type=%s, block_hash=%s\n", + BlockFilterTypeName(filter_type), + prev_block->GetBlockHash().ToString()); + return; + } + } + + std::vector filter_hashes; + if (!filter_index->LookupFilterHashRange(start_height, stop_index, + filter_hashes)) { + LogPrint(BCLog::NET, + "Failed to find block filter hashes in index: filter_type=%s, " + "start_height=%d, stop_hash=%s\n", + BlockFilterTypeName(filter_type), start_height, + stop_hash.ToString()); + return; + } + + CSerializedNetMsg msg = + CNetMsgMaker(pfrom->GetSendVersion()) + .Make(NetMsgType::CFHEADERS, filter_type_ser, + stop_index->GetBlockHash(), prev_header, filter_hashes); + connman->PushMessage(pfrom, std::move(msg)); +} + /** * Handle a getcfcheckpt request. * @@ -2314,8 +2402,10 @@ const CBlockIndex *stop_index; BlockFilterIndex *filter_index; - if (!PrepareBlockFilterRequest(pfrom, chain_params, filter_type, stop_hash, - stop_index, filter_index)) { + if (!PrepareBlockFilterRequest( + pfrom, chain_params, filter_type, /*start_height=*/0, stop_hash, + /*max_height_diff=*/std::numeric_limits::max(), + stop_index, filter_index)) { return; } @@ -3965,6 +4055,11 @@ return true; } + if (msg_type == NetMsgType::GETCFHEADERS) { + ProcessGetCFHeaders(pfrom, vRecv, chainparams, connman); + return true; + } + if (msg_type == NetMsgType::GETCFCHECKPT) { ProcessGetCFCheckPt(pfrom, vRecv, chainparams, connman); return true; diff --git a/src/protocol.h b/src/protocol.h --- a/src/protocol.h +++ b/src/protocol.h @@ -251,6 +251,20 @@ * @since protocol version 70014 as described by BIP 152 */ extern const char *BLOCKTXN; +/** + * getcfheaders requests a compact filter header and the filter hashes for a + * range of blocks, which can then be used to reconstruct the filter headers + * for those blocks. + * Only available with service bit NODE_COMPACT_FILTERS as described by + * BIP 157 & 158. + */ +extern const char *GETCFHEADERS; +/** + * cfheaders is a response to a getcfheaders request containing a filter header + * and a vector of filter hashes for each subsequent block in the requested + * range. + */ +extern const char *CFHEADERS; /** * getcfcheckpt requests evenly spaced compact filter headers, enabling * parallelized download and validation of the headers between them. @@ -261,8 +275,6 @@ /** * cfcheckpt is a response to a getcfcheckpt request containing a vector of * evenly spaced filter headers for blocks on the requested chain. - * Only available with service bit NODE_COMPACT_FILTERS as described by - * BIP 157 & 158. */ extern const char *CFCHECKPT; /** diff --git a/src/protocol.cpp b/src/protocol.cpp --- a/src/protocol.cpp +++ b/src/protocol.cpp @@ -43,6 +43,8 @@ const char *CMPCTBLOCK = "cmpctblock"; const char *GETBLOCKTXN = "getblocktxn"; const char *BLOCKTXN = "blocktxn"; +const char *GETCFHEADERS = "getcfheaders"; +const char *CFHEADERS = "cfheaders"; const char *GETCFCHECKPT = "getcfcheckpt"; const char *CFCHECKPT = "cfcheckpt"; const char *AVAPOLL = "avapoll"; @@ -60,15 +62,16 @@ * above and in protocol.h. */ static const std::string allNetMessageTypes[] = { - NetMsgType::VERSION, NetMsgType::VERACK, NetMsgType::ADDR, - NetMsgType::INV, NetMsgType::GETDATA, NetMsgType::MERKLEBLOCK, - NetMsgType::GETBLOCKS, NetMsgType::GETHEADERS, NetMsgType::TX, - NetMsgType::HEADERS, NetMsgType::BLOCK, NetMsgType::GETADDR, - NetMsgType::MEMPOOL, NetMsgType::PING, NetMsgType::PONG, - NetMsgType::NOTFOUND, NetMsgType::FILTERLOAD, NetMsgType::FILTERADD, - NetMsgType::FILTERCLEAR, NetMsgType::SENDHEADERS, NetMsgType::FEEFILTER, - NetMsgType::SENDCMPCT, NetMsgType::CMPCTBLOCK, NetMsgType::GETBLOCKTXN, - NetMsgType::BLOCKTXN, NetMsgType::GETCFCHECKPT, NetMsgType::CFCHECKPT, + NetMsgType::VERSION, NetMsgType::VERACK, NetMsgType::ADDR, + NetMsgType::INV, NetMsgType::GETDATA, NetMsgType::MERKLEBLOCK, + NetMsgType::GETBLOCKS, NetMsgType::GETHEADERS, NetMsgType::TX, + NetMsgType::HEADERS, NetMsgType::BLOCK, NetMsgType::GETADDR, + NetMsgType::MEMPOOL, NetMsgType::PING, NetMsgType::PONG, + NetMsgType::NOTFOUND, NetMsgType::FILTERLOAD, NetMsgType::FILTERADD, + NetMsgType::FILTERCLEAR, NetMsgType::SENDHEADERS, NetMsgType::FEEFILTER, + NetMsgType::SENDCMPCT, NetMsgType::CMPCTBLOCK, NetMsgType::GETBLOCKTXN, + NetMsgType::BLOCKTXN, NetMsgType::GETCFHEADERS, NetMsgType::CFHEADERS, + NetMsgType::GETCFCHECKPT, NetMsgType::CFCHECKPT, }; static const std::vector allNetMessageTypesVec(allNetMessageTypes, diff --git a/test/functional/p2p_blockfilters.py b/test/functional/p2p_blockfilters.py --- a/test/functional/p2p_blockfilters.py +++ b/test/functional/p2p_blockfilters.py @@ -5,12 +5,16 @@ """Tests NODE_COMPACT_FILTERS (BIP 157/158). Tests that a node configured with -blockfilterindex and -peerblockfilters can serve -cfcheckpts. +cfheaders and cfcheckpts. """ from test_framework.messages import ( FILTER_TYPE_BASIC, + hash256, msg_getcfcheckpt, + msg_getcfheaders, + ser_uint256, + uint256_from_str, ) from test_framework.mininode import P2PInterface from test_framework.test_framework import BitcoinTestFramework @@ -104,6 +108,34 @@ [int(header, 16) for header in (stale_cfcheckpt,)] ) + self.log.info("Check that peers can fetch cfheaders on active chain.") + request = msg_getcfheaders( + filter_type=FILTER_TYPE_BASIC, + start_height=1, + stop_hash=int(main_block_hash, 16) + ) + node0.send_and_ping(request) + response = node0.last_message['cfheaders'] + assert_equal(len(response.hashes), 1000) + assert_equal( + compute_last_header(response.prev_header, response.hashes), + int(main_cfcheckpt, 16) + ) + + self.log.info("Check that peers can fetch cfheaders on stale chain.") + request = msg_getcfheaders( + filter_type=FILTER_TYPE_BASIC, + start_height=1, + stop_hash=int(stale_block_hash, 16) + ) + node0.send_and_ping(request) + response = node0.last_message['cfheaders'] + assert_equal(len(response.hashes), 1000) + assert_equal( + compute_last_header(response.prev_header, response.hashes), + int(stale_cfcheckpt, 16) + ) + self.log.info( "Requests to node 1 without NODE_COMPACT_FILTERS results in disconnection.") requests = [ @@ -111,6 +143,11 @@ filter_type=FILTER_TYPE_BASIC, stop_hash=int(main_block_hash, 16) ), + msg_getcfheaders( + filter_type=FILTER_TYPE_BASIC, + start_height=1000, + stop_hash=int(main_block_hash, 16) + ), ] for request in requests: node1 = self.nodes[1].add_p2p_connection(P2PInterface()) @@ -119,6 +156,12 @@ self.log.info("Check that invalid requests result in disconnection.") requests = [ + # Requesting too many filter headers results in disconnection. + msg_getcfheaders( + filter_type=FILTER_TYPE_BASIC, + start_height=0, + stop_hash=int(tip_hash, 16) + ), # Requesting unknown filter type results in disconnection. msg_getcfcheckpt( filter_type=255, @@ -136,5 +179,13 @@ node0.wait_for_disconnect() +def compute_last_header(prev_header, hashes): + """Compute the last filter header from a starting header and a sequence of filter hashes.""" + header = ser_uint256(prev_header) + for filter_hash in hashes: + header = hash256(ser_uint256(filter_hash) + header) + return uint256_from_str(header) + + if __name__ == '__main__': CompactFiltersTest().main() diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -1471,6 +1471,62 @@ repr(self.block_transactions)) +class msg_getcfheaders: + __slots__ = ("filter_type", "start_height", "stop_hash") + msgtype = b"getcfheaders" + + def __init__(self, filter_type, start_height, stop_hash): + self.filter_type = filter_type + self.start_height = start_height + self.stop_hash = stop_hash + + def deserialize(self, f): + self.filter_type = struct.unpack("