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<BlockHash, uint256, FilterHeaderHasher>
         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<BlockFilterType>(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<int>(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<uint256> 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<uint32_t>::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<std::string>
     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("<B", f.read(1))[0]
+        self.start_height = struct.unpack("<I", f.read(4))[0]
+        self.stop_hash = deser_uint256(f)
+
+    def serialize(self):
+        r = b""
+        r += struct.pack("<B", self.filter_type)
+        r += struct.pack("<I", self.start_height)
+        r += ser_uint256(self.stop_hash)
+        return r
+
+    def __repr__(self):
+        return "msg_getcfheaders(filter_type={:#x}, start_height={}, stop_hash={:x})".format(
+            self.filter_type, self.start_height, self.stop_hash)
+
+
+class msg_cfheaders:
+    __slots__ = ("filter_type", "stop_hash", "prev_header", "hashes")
+    msgtype = b"cfheaders"
+
+    def __init__(self, filter_type=None, stop_hash=None,
+                 prev_header=None, hashes=None):
+        self.filter_type = filter_type
+        self.stop_hash = stop_hash
+        self.prev_header = prev_header
+        self.hashes = hashes
+
+    def deserialize(self, f):
+        self.filter_type = struct.unpack("<B", f.read(1))[0]
+        self.stop_hash = deser_uint256(f)
+        self.prev_header = deser_uint256(f)
+        self.hashes = deser_uint256_vector(f)
+
+    def serialize(self):
+        r = b""
+        r += struct.pack("<B", self.filter_type)
+        r += ser_uint256(self.stop_hash)
+        r += ser_uint256(self.prev_header)
+        r += ser_uint256_vector(self.hashes)
+        return r
+
+    def __repr__(self):
+        return "msg_cfheaders(filter_type={:#x}, stop_hash={:x})".format(
+            self.filter_type, self.stop_hash)
+
+
 class msg_getcfcheckpt:
     __slots__ = ("filter_type", "stop_hash")
     msgtype = b"getcfcheckpt"
diff --git a/test/functional/test_framework/mininode.py b/test/functional/test_framework/mininode.py
--- a/test/functional/test_framework/mininode.py
+++ b/test/functional/test_framework/mininode.py
@@ -31,6 +31,7 @@
     MSG_BLOCK,
     msg_blocktxn,
     msg_cfcheckpt,
+    msg_cfheaders,
     msg_cmpctblock,
     msg_feefilter,
     msg_filteradd,
@@ -69,6 +70,7 @@
     b"block": msg_block,
     b"blocktxn": msg_blocktxn,
     b"cfcheckpt": msg_cfcheckpt,
+    b"cfheaders": msg_cfheaders,
     b"cmpctblock": msg_cmpctblock,
     b"feefilter": msg_feefilter,
     b"filteradd": msg_filteradd,
@@ -374,6 +376,8 @@
 
     def on_cfcheckpt(self, message): pass
 
+    def on_cfheaders(self, message): pass
+
     def on_cmpctblock(self, message): pass
 
     def on_feefilter(self, message): pass