Repository: stacks-network/stacks-core (language: python)

Issue:
[Network] Inventory cache incoherency 
For reasons that are not yet clear to me, nodes' cached tenure data will eventually become inconsistent with tenures stored in the Nakamoto chain state.  This, in turn, causes them to produce invalid `NakamotoInv` messages, which prevents downstream peers from finding and downloading tenures.


Discussion / hints:

Fix patch:
diff --git a/stackslib/src/chainstate/nakamoto/mod.rs b/stackslib/src/chainstate/nakamoto/mod.rs
index 6a850e6d35..a8708b0a8d 100644
--- a/stackslib/src/chainstate/nakamoto/mod.rs
+++ b/stackslib/src/chainstate/nakamoto/mod.rs
@@ -2533,6 +2533,18 @@ impl NakamotoChainState {
         Ok(result)
     }
 
+    /// Load a consensus hash for a Nakamoto header
+    pub fn get_block_header_nakamoto_tenure_id(
+        chainstate_conn: &Connection,
+        index_block_hash: &StacksBlockId,
+    ) -> Result<Option<ConsensusHash>, ChainstateError> {
+        let sql = "SELECT consensus_hash FROM nakamoto_block_headers WHERE index_block_hash = ?1";
+        let result = query_row_panic(chainstate_conn, sql, &[&index_block_hash], || {
+            "FATAL: multiple rows for the same block hash".to_string()
+        })?;
+        Ok(result)
+    }
+
     /// Load an epoch2 header
     pub fn get_block_header_epoch2(
         chainstate_conn: &Connection,
diff --git a/stackslib/src/chainstate/nakamoto/staging_blocks.rs b/stackslib/src/chainstate/nakamoto/staging_blocks.rs
index 91aad5a325..382c708850 100644
--- a/stackslib/src/chainstate/nakamoto/staging_blocks.rs
+++ b/stackslib/src/chainstate/nakamoto/staging_blocks.rs
@@ -271,7 +271,7 @@ impl<'a> NakamotoStagingBlocksConnRef<'a> {
             .optional()?)
     }
 
-    /// Get the rowid of a Nakamoto block
+    /// Get the rowid of a staging Nakamoto block
     pub fn get_nakamoto_block_rowid(
         &self,
         index_block_hash: &StacksBlockId,
@@ -282,6 +282,26 @@ impl<'a> NakamotoStagingBlocksConnRef<'a> {
         Ok(res)
     }
 
+    /// Get the tenure and parent block ID of a staging block.
+    /// Used for downloads
+    pub fn get_tenure_and_parent_block_id(
+        &self,
+        index_block_hash: &StacksBlockId,
+    ) -> Result<Option<(ConsensusHash, StacksBlockId)>, ChainstateError> {
+        let sql = "SELECT consensus_hash,parent_block_id FROM nakamoto_staging_blocks WHERE index_block_hash = ?1";
+        let args = params![index_block_hash];
+
+        let mut stmt = self.deref().prepare(sql)?;
+        Ok(stmt
+            .query_row(args, |row| {
+                let ch: ConsensusHash = row.get(0)?;
+                let parent_id: StacksBlockId = row.get(1)?;
+
+                Ok((ch, parent_id))
+            })
+            .optional()?)
+    }
+
     /// Get a Nakamoto block by index block hash, as well as its size.
     /// Verifies its integrity.
     /// Returns Ok(Some(block, size)) if the block was present
diff --git a/stackslib/src/main.rs b/stackslib/src/main.rs
index 029ee16c6b..98315cffa8 100644
--- a/stackslib/src/main.rs
+++ b/stackslib/src/main.rs
@@ -36,6 +36,8 @@ use std::collections::{BTreeMap, HashMap, HashSet};
 use std::fs::File;
 use std::io::prelude::*;
 use std::io::BufReader;
+use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
+use std::time::Duration;
 use std::{env, fs, io, process, thread};
 
 use blockstack_lib::burnchains::bitcoin::{spv, BitcoinNetworkType};
@@ -62,10 +64,12 @@ use blockstack_lib::clarity::vm::ClarityVersion;
 use blockstack_lib::core::{MemPoolDB, *};
 use blockstack_lib::cost_estimates::metrics::UnitMetric;
 use blockstack_lib::cost_estimates::UnitEstimator;
+use blockstack_lib::net::api::getinfo::RPCPeerInfoData;
 use blockstack_lib::net::db::LocalPeer;
+use blockstack_lib::net::httpcore::{send_http_request, StacksHttpRequest};
 use blockstack_lib::net::p2p::PeerNetwork;
 use blockstack_lib::net::relay::Relayer;
-use blockstack_lib::net::StacksMessage;
+use blockstack_lib::net::{GetNakamotoInvData, HandshakeData, StacksMessage, StacksMessageType};
 use blockstack_lib::util_lib::db::sqlite_open;
 use blockstack_lib::util_lib::strings::UrlString;
 use blockstack_lib::{clarity_cli, cli};
@@ -76,7 +80,7 @@ use stacks_common::codec::{read_next, StacksMessageCodec};
 use stacks_common::types::chainstate::{
     BlockHeaderHash, BurnchainHeaderHash, StacksAddress, StacksBlockId,
 };
-use stacks_common::types::net::PeerAddress;
+use stacks_common::types::net::{PeerAddress, PeerHost};
 use stacks_common::types::sqlite::NO_PARAMS;
 use stacks_common::types::MempoolCollectionBehavior;
 use stacks_common::util::hash::{hex_bytes, to_hex, Hash160};
@@ -85,6 +89,164 @@ use stacks_common::util::secp256k1::{Secp256k1PrivateKey, Secp256k1PublicKey};
 use stacks_common::util::vrf::VRFProof;
 use stacks_common::util::{get_epoch_time_ms, sleep_ms};
 
+struct P2PSession {
+    pub local_peer: LocalPeer,
+    peer_info: RPCPeerInfoData,
+    burn_block_hash: BurnchainHeaderHash,
+    stable_burn_block_hash: BurnchainHeaderHash,
+    tcp_socket: TcpStream,
+    seq: u32,
+}
+
+impl P2PSession {
+    /// Make a StacksMessage.  Sign it and set a sequence number.
+    fn make_peer_message(&mut self, payload: StacksMessageType) -> Result<StacksMessage, String> {
+        let mut msg = StacksMessage::new(
+            self.peer_info.peer_version,
+            self.peer_info.network_id,
+            self.peer_info.burn_block_height,
+            &self.burn_block_hash,
+            self.peer_info.stable_burn_block_height,
+            &self.stable_burn_block_hash,
+            payload,
+        );
+
+        msg.sign(self.seq, &self.local_peer.private_key)
+            .map_err(|e| format!("Failed to sign message {:?}: {:?}", &msg, &e))?;
+        self.seq = self.seq.wrapping_add(1);
+
+        Ok(msg)
+    }
+
+    /// Send a p2p message.
+    /// Returns error text on failure.
+    fn send_peer_message(&mut self, msg: StacksMessage) -> Result<(), String> {
+        msg.consensus_serialize(&mut self.tcp_socket)
+            .map_err(|e| format!("Failed to send message {:?}: {:?}", &msg, &e))
+    }
+
+    /// Receive a p2p message.
+    /// Returns error text on failure.
+    fn recv_peer_message(&mut self) -> Result<StacksMessage, String> {
+        let msg: StacksMessage = read_next(&mut self.tcp_socket)
+            .map_err(|e| format!("Failed to receive message: {:?}", &e))?;
+        Ok(msg)
+    }
+
+    /// Begin a p2p session.
+    /// Synthesizes a LocalPeer from the remote peer's responses to /v2/info and /v2/pox.
+    /// Performs the initial handshake for you.
+    ///
+    /// Returns the session handle on success.
+    /// Returns error text on failure.
+    pub fn begin(peer_addr: SocketAddr, data_port: u16) -> Result<Self, String> {
+        let mut data_addr = peer_addr.clone();
+        data_addr.set_port(data_port);
+
+        // get /v2/info
+        let peer_info = send_http_request(
+            &format!("{}", data_addr.ip()),
+            data_addr.port(),
+            StacksHttpRequest::new_getinfo(PeerHost::from(data_addr.clone()), None)
+                .with_header("Connection".to_string(), "close".to_string()),
+            Duration::from_secs(60),
+        )
+        .map_err(|e| format!("Failed to query /v2/info: {:?}", &e))?
+        .decode_peer_info()
+        .map_err(|e| format!("Failed to decode response from /v2/info: {:?}", &e))?;
+
+        // convert `pox_consensus` and `stable_pox_consensus` into their respective burn block
+        // hashes
+        let sort_info = send_http_request(
+            &format!("{}", data_addr.ip()),
+            data_addr.port(),
+            StacksHttpRequest::new_get_sortition_consensus(
+                PeerHost::from(data_addr.clone()),
+                &peer_info.pox_consensus,
+            )
+            .with_header("Connection".to_string(), "close".to_string()),
+            Duration::from_secs(60),
+        )
+        .map_err(|e| format!("Failed to query /v3/sortitions: {:?}", &e))?
+        .decode_sortition_info()
+        .map_err(|e| format!("Failed to decode response from /v3/sortitions: {:?}", &e))?
+        .pop()
+        .ok_or_else(|| format!("No sortition returned for {}", &peer_info.pox_consensus))?;
+
+        let stable_sort_info = send_http_request(
+            &format!("{}", data_addr.ip()),
+            data_addr.port(),
+            StacksHttpRequest::new_get_sortition_consensus(
+                PeerHost::from(data_addr.clone()),
+                &peer_info.stable_pox_consensus,
+            )
+            .with_header("Connection".to_string(), "close".to_string()),
+            Duration::from_secs(60),
+        )
+        .map_err(|e| format!("Failed to query stable /v3/sortitions: {:?}", &e))?
+        .decode_sortition_info()
+        .map_err(|e| {
+            format!(
+                "Failed to decode response from stable /v3/sortitions: {:?}",
+                &e
+            )
+        })?
+        .pop()
+        .ok_or_else(|| {
+            format!(
+                "No sortition returned for {}",
+                &peer_info.stable_pox_consensus
+            )
+        })?;
+
+        let burn_block_hash = sort_info.burn_block_hash;
+        let stable_burn_block_hash = stable_sort_info.burn_block_hash;
+
+        let local_peer = LocalPeer::new(
+            peer_info.network_id,
+            peer_info.parent_network_id,
+            PeerAddress::from_socketaddr(&peer_addr),
+            peer_addr.port(),
+            Some(StacksPrivateKey::new()),
+            u64::MAX,
+            UrlString::try_from(format!("http://127.0.0.1:{}", data_port).as_str()).unwrap(),
+            vec![],
+        );
+
+        let tcp_socket = TcpStream::connect(&peer_addr)
+            .map_err(|e| format!("Failed to open {:?}: {:?}", &peer_addr, &e))?;
+
+        let mut session = Self {
+            local_peer,
+            peer_info,
+            burn_block_hash,
+            stable_burn_block_hash,
+            tcp_socket,
+            seq: 0,
+        };
+
+        // perform the handshake
+        let handshake_data =
+            StacksMessageType::Handshake(HandshakeData::from_local_peer(&session.local_peer));
+        let handshake = session.make_peer_message(handshake_data)?;
+        session.send_peer_message(handshake)?;
+
+        let resp = session.recv_peer_message()?;
+        match resp.payload {
+            StacksMessageType::HandshakeAccept(..)
+            | StacksMessageType::StackerDBHandshakeAccept(..) => {}
+            x => {
+                return Err(format!(
+                    "Peer returned unexpected message (expected HandshakeAccept variant): {:?}",
+                    &x
+                ));
+            }
+        }
+
+        Ok(session)
+    }
+}
+
 #[cfg_attr(test, mutants::skip)]
 fn main() {
     let mut argv: Vec<String> = env::args().collect();
@@ -974,6 +1136,36 @@ simulating a miner.
         process::exit(1);
     }
 
+    if argv[1] == "getnakamotoinv" {
+        if argv.len() < 5 {
+            eprintln!(
+                "Usage: {} getnakamotoinv HOST:PORT DATA_PORT CONSENSUS_HASH",
+                &argv[0]
+            );
+            process::exit(1);
+        }
+
+        let peer_addr: SocketAddr = argv[2].to_socket_addrs().unwrap().next().unwrap();
+        let data_port: u16 = argv[3].parse().unwrap();
+        let ch = ConsensusHash::from_hex(&argv[4]).unwrap();
+
+        let mut session = P2PSession::begin(peer_addr, data_port).unwrap();
+
+        // send getnakamotoinv
+        let get_nakamoto_inv =
+            StacksMessageType::GetNakamotoInv(GetNakamotoInvData { consensus_hash: ch });
+
+        let msg = session.make_peer_message(get_nakamoto_inv).unwrap();
+        session.send_peer_message(msg).unwrap();
+        let resp = session.recv_peer_message().unwrap();
+
+        let StacksMessageType::NakamotoInv(inv) = &resp.payload else {
+            panic!("Got spurious message: {:?}", &resp);
+        };
+
+        println!("{:?}", inv);
+    }
+
     if argv[1] == "replay-chainstate" {
         if argv.len() < 7 {
             eprintln!("Usage: {} OLD_CHAINSTATE_PATH OLD_SORTITION_DB_PATH OLD_BURNCHAIN_DB_PATH NEW_CHAINSTATE_PATH NEW_BURNCHAIN_DB_PATH", &argv[0]);
diff --git a/stackslib/src/net/api/getblock_v3.rs b/stackslib/src/net/api/getblock_v3.rs
index 0279d9dc0c..56e8063dda 100644
--- a/stackslib/src/net/api/getblock_v3.rs
+++ b/stackslib/src/net/api/getblock_v3.rs
@@ -181,20 +181,13 @@ impl RPCRequestHandler for RPCNakamotoBlockRequestHandler {
 
         let stream_res =
             node.with_node_state(|_network, _sortdb, chainstate, _mempool, _rpc_args| {
-                let Some(header) =
-                    NakamotoChainState::get_block_header_nakamoto(chainstate.db(), &block_id)?
+                let Some((tenure_id, parent_block_id)) = chainstate
+                    .nakamoto_blocks_db()
+                    .get_tenure_and_parent_block_id(&block_id)?
                 else {
                     return Err(ChainError::NoSuchBlockError);
                 };
-                let Some(nakamoto_header) = header.anchored_header.as_stacks_nakamoto() else {
-                    return Err(ChainError::NoSuchBlockError);
-                };
-                NakamotoBlockStream::new(
-                    chainstate,
-                    block_id.clone(),
-                    nakamoto_header.consensus_hash.clone(),
-                    nakamoto_header.parent_block_id.clone(),
-                )
+                NakamotoBlockStream::new(chainstate, block_id.clone(), tenure_id, parent_block_id)
             });
 
         // start loading up the block
diff --git a/stackslib/src/net/api/getsortition.rs b/stackslib/src/net/api/getsortition.rs
index ccfa4efec9..9b22d8b82f 100644
--- a/stackslib/src/net/api/getsortition.rs
+++ b/stackslib/src/net/api/getsortition.rs
@@ -381,3 +381,34 @@ impl HttpResponse for GetSortitionHandler {
         Ok(HttpResponsePayload::try_from_json(sortition_info)?)
     }
 }
+
+impl StacksHttpRequest {
+    /// Make a new getsortition request to this endpoint
+    pub fn new_get_sortition(
+        host: PeerHost,
+        sort_key: &str,
+        sort_value: &str,
+    ) -> StacksHttpRequest {
+        StacksHttpRequest::new_for_peer(
+            host,
+            "GET".into(),
+            format!("{}/{}/{}", RPC_SORTITION_INFO_PATH, sort_key, sort_value),
+            HttpRequestContents::new(),
+        )
+        .expect("FATAL: failed to construct request from infallible data")
+    }
+
+    pub fn new_get_sortition_consensus(host: PeerHost, ch: &ConsensusHash) -> StacksHttpRequest {
+        Self::new_get_sortition(host, "consensus", &ch.to_string())
+    }
+}
+
+impl StacksHttpResponse {
+    pub fn decode_sortition_info(self) -> Result<Vec<SortitionInfo>, NetError> {
+        let contents = self.get_http_payload_ok()?;
+        let response_json: serde_json::Value = contents.try_into()?;
+        let response: Vec<SortitionInfo> = serde_json::from_value(response_json)
+            .map_err(|_e| Error::DecodeError(format!("Failed to decode JSON: {:?}", &_e)))?;
+        Ok(response)
+    }
+}
diff --git a/stackslib/src/net/chat.rs b/stackslib/src/net/chat.rs
index d4e3b8aab5..5cf32a8a56 100644
--- a/stackslib/src/net/chat.rs
+++ b/stackslib/src/net/chat.rs
@@ -1744,7 +1744,8 @@ impl ConversationP2P {
             &tip,
             sortdb,
             chainstate,
-            &network.stacks_tip.block_id(),
+            &network.stacks_tip.consensus_hash,
+            &network.stacks_tip.block_hash,
             reward_cycle,
         )?;
         let nakamoto_inv = NakamotoInvData::try_from(&bitvec_bools).map_err(|e| {
diff --git a/stackslib/src/net/download/nakamoto/tenure_downloader.rs b/stackslib/src/net/download/nakamoto/tenure_downloader.rs
index 92e032fa38..66504e01e6 100644
--- a/stackslib/src/net/download/nakamoto/tenure_downloader.rs
+++ b/stackslib/src/net/download/nakamoto/tenure_downloader.rs
@@ -376,8 +376,20 @@ impl NakamotoTenureDownloader {
         let mut expected_block_id = block_cursor;
         let mut count = 0;
         for block in tenure_blocks.iter() {
-            if &block.header.block_id() != expected_block_id {
+            // must be from this tenure
+            // This may not always be the case, since a remote peer could have processed a
+            // different Stacks micro-fork.  The consequence of erroring here (or below) is that we
+            // disconnect from the peer that served this to us.
+            if block.header.consensus_hash != self.tenure_id_consensus_hash {
                 warn!("Unexpected Nakamoto block -- not part of tenure";
+                      "block.header.consensus_hash" => %block.header.consensus_hash,
+                      "self.tenure_id_consensus_hash" => %self.tenure_id_consensus_hash,
+                      "state" => %self.state);
+                return Err(NetError::InvalidMessage);
+            }
+
+            if &block.header.block_id() != expected_block_id {
+                warn!("Unexpected Nakamoto block -- does not match cursor";
                       "expected_block_id" => %expected_block_id,
                       "block_id" => %block.header.block_id(),
                       "state" => %self.state);
diff --git a/stackslib/src/net/httpcore.rs b/stackslib/src/net/httpcore.rs
index 3b4bf8c9b9..c58355a6a9 100644
--- a/stackslib/src/net/httpcore.rs
+++ b/stackslib/src/net/httpcore.rs
@@ -532,6 +532,12 @@ impl StacksHttpRequest {
         self.preamble.add_header(hdr, value);
     }
 
+    /// Constructor to add headers
+    pub fn with_header(mut self, hdr: String, value: String) -> Self {
+        self.add_header(hdr, value);
+        self
+    }
+
     /// Get a ref to all request headers
     pub fn get_headers(&self) -> &BTreeMap<String, String> {
         &self.preamble.headers
diff --git a/stackslib/src/net/inv/nakamoto.rs b/stackslib/src/net/inv/nakamoto.rs
index 3b7a5050ba..8971a8230f 100644
--- a/stackslib/src/net/inv/nakamoto.rs
+++ b/stackslib/src/net/inv/nakamoto.rs
@@ -17,7 +17,7 @@
 use std::collections::{BTreeMap, HashMap, HashSet};
 
 use stacks_common::bitvec::BitVec;
-use stacks_common::types::chainstate::StacksBlockId;
+use stacks_common::types::chainstate::{BlockHeaderHash, StacksBlockId};
 use stacks_common::types::StacksEpochId;
 use stacks_common::util::{get_epoch_time_ms, get_epoch_time_secs};
 
@@ -113,6 +113,9 @@ pub struct InvGenerator {
     tip_ancestor_search_depth: u64,
     /// count cache misses for `processed_tenures`
     cache_misses: u128,
+    /// Disable caching (test only)
+    #[cfg(test)]
+    no_cache: bool,
 }
 
 impl InvGenerator {
@@ -122,6 +125,19 @@ impl InvGenerator {
             sortitions: HashMap::new(),
             tip_ancestor_search_depth: TIP_ANCESTOR_SEARCH_DEPTH,
             cache_misses: 0,
+            #[cfg(test)]
+            no_cache: false,
+        }
+    }
+
+    #[cfg(test)]
+    pub fn new_no_cache() -> Self {
+        Self {
+            processed_tenures: HashMap::new(),
+            sortitions: HashMap::new(),
+            tip_ancestor_search_depth: TIP_ANCESTOR_SEARCH_DEPTH,
+            cache_misses: 0,
+            no_cache: true,
         }
     }
 
@@ -149,28 +165,60 @@ impl InvGenerator {
     /// the maximum expected number of blocks to be processed in-between handling `GetNakamotoInv`
     /// messages.
     ///
-    /// If found, then return the ancestor block ID represented in `self.processed_tenures`.
+    /// If found, then return the ancestor block ID represented in `self.processed_tenures`, as
+    /// well as the list of any intermediate tenures between (and including) that of `tip_block_id`
+    /// and that of (and including) the highest-found ancestor.
+    ///
     /// If not, then return None.
     pub(crate) fn find_ancestor_processed_tenures(
         &self,
         chainstate: &StacksChainState,
         tip_block_id: &StacksBlockId,
-    ) -> Result<Option<StacksBlockId>, NetError> {
+    ) -> Result<Option<(StacksBlockId, Vec<ConsensusHash>)>, NetError> {
         let mut cursor = tip_block_id.clone();
+        let mut chs = vec![];
+        let Some(ch) =
+            NakamotoChainState::get_block_header_nakamoto_tenure_id(chainstate.db(), &cursor)?
+        else {
+            return Ok(None);
+        };
+        chs.push(ch);
         for _ in 0..self.tip_ancestor_search_depth {
             let parent_id_opt =
                 NakamotoChainState::get_nakamoto_parent_block_id(chainstate.db(), &cursor)?;
+
             let Some(parent_id) = parent_id_opt else {
                 return Ok(None);
             };
+
+            let Some(parent_ch) = NakamotoChainState::get_block_header_nakamoto_tenure_id(
+                chainstate.db(),
+                &parent_id,
+            )?
+            else {
+                return Ok(None);
+            };
+            chs.push(parent_ch);
+
             if self.processed_tenures.contains_key(&parent_id) {
-                return Ok(Some(parent_id));
+                return Ok(Some((parent_id, chs)));
             }
             cursor = parent_id;
         }
         Ok(None)
     }
 
+    #[cfg(not(test))]
+    fn test_clear_cache(&mut self) {}
+
+    /// Clear the cache (test only)
+    #[cfg(test)]
+    fn test_clear_cache(&mut self) {
+        if self.no_cache {
+            self.processed_tenures.clear();
+        }
+    }
+
     /// Get a processed tenure. If it's not cached, then load it from disk.
     ///
     /// Loading it is expensive, so once loaded, store it with the cached processed tenure map
@@ -188,23 +236,45 @@ impl InvGenerator {
     pub(crate) fn get_processed_tenure(
         &mut self,
         chainstate: &StacksChainState,
-        tip_block_id: &StacksBlockId,
+        tip_block_ch: &ConsensusHash,
+        tip_block_bh: &BlockHeaderHash,
         tenure_id_consensus_hash: &ConsensusHash,
     ) -> Result<Option<InvTenureInfo>, NetError> {
-        if self.processed_tenures.get(tip_block_id).is_none() {
+        let tip_block_id = StacksBlockId::new(tip_block_ch, tip_block_bh);
+        if self.processed_tenures.get(&tip_block_id).is_none() {
             // this tip has no known table.
             // does it have an ancestor with a table? If so, then move its ancestor's table to this
             // tip. Otherwise, make a new table.
-            if let Some(ancestor_tip_id) =
-                self.find_ancestor_processed_tenures(chainstate, tip_block_id)?
+            if let Some((ancestor_tip_id, intermediate_tenures)) =
+                self.find_ancestor_processed_tenures(chainstate, &tip_block_id)?
             {
-                let ancestor_tenures = self
+                // The table removals here are for cache maintenance.
+                //
+                // Between successive calls to this function, the Stacks tip (identified by
+                // `tip_block_ch` and `tip_block_bh`) can advance as more blocks are discovered.
+                // This means that tenures that had previously been treated as absent could now be
+                // present.  By evicting cached data for all tenures between (and including) the
+                // highest ancestor of the current Stacks tip, and the current Stacks tip, we force
+                // this code to re-evaluate the presence or absence of each potentially-affected
+                // tenure.
+                //
+                // First, remove the highest ancestor's table, so we can re-assign it to the new
+                // tip.
+                let mut ancestor_tenures = self
                     .processed_tenures
                     .remove(&ancestor_tip_id)
                     .unwrap_or_else(|| {
                         panic!("FATAL: did not have ancestor tip reported by search");
                     });
 
+                // Clear out any intermediate cached results for tenure presence/absence, including
+                // both that of the highest ancestor and the current tip.
+                for ch in intermediate_tenures.into_iter() {
+                    ancestor_tenures.remove(&ch);
+                }
+                ancestor_tenures.remove(tip_block_ch);
+
+                // Update the table so it is pointed to by the new tip.
                 self.processed_tenures
                     .insert(tip_block_id.clone(), ancestor_tenures);
             } else {
@@ -213,23 +283,25 @@ impl InvGenerator {
             }
         }
 
-        let Some(tenure_infos) = self.processed_tenures.get_mut(tip_block_id) else {
+        let Some(tenure_infos) = self.processed_tenures.get_mut(&tip_block_id) else {
             unreachable!("FATAL: inserted table for chain tip, but didn't get it back");
         };
 
-        // this tip has a known table
-        if let Some(loaded_tenure_info) = tenure_infos.get(tenure_id_consensus_hash) {
+        let ret = if let Some(loaded_tenure_info) = tenure_infos.get(tenure_id_consensus_hash) {
             // we've loaded this tenure info before for this tip
-            return Ok(loaded_tenure_info.clone());
+            Ok(loaded_tenure_info.clone())
         } else {
-            // we have not loaded the tenure info for this tip, so go get it
+            // we have not loaded the tenure info for this tip, or it was cleared via cache
+            // maintenance.  Either way, got get it from disk.
             let loaded_info_opt =
-                InvTenureInfo::load(chainstate, tip_block_id, &tenure_id_consensus_hash)?;
-            tenure_infos.insert(tenure_id_consensus_hash.clone(), loaded_info_opt.clone());
+                InvTenureInfo::load(chainstate, &tip_block_id, &tenure_id_consensus_hash)?;
 
+            tenure_infos.insert(tenure_id_consensus_hash.clone(), loaded_info_opt.clone());
             self.cache_misses = self.cache_misses.saturating_add(1);
-            return Ok(loaded_info_opt);
-        }
+            Ok(loaded_info_opt)
+        };
+        self.test_clear_cache();
+        ret
     }
 
     /// Get sortition info, loading it from our cache if needed
@@ -269,9 +341,11 @@ impl InvGenerator {
         tip: &BlockSnapshot,
         sortdb: &SortitionDB,
         chainstate: &StacksChainState,
-        nakamoto_tip: &StacksBlockId,
+        nakamoto_tip_ch: &ConsensusHash,
+        nakamoto_tip_bh: &BlockHeaderHash,
         reward_cycle: u64,
     ) -> Result<Vec<bool>, NetError> {
+        let nakamoto_tip = StacksBlockId::new(nakamoto_tip_ch, nakamoto_tip_bh);
         let ih = sortdb.index_handle(&tip.sortition_id);
 
         // N.B. reward_cycle_to_block_height starts at reward index 1
@@ -290,8 +364,12 @@ impl InvGenerator {
         let mut cur_height = reward_cycle_end_tip.block_height;
         let mut cur_consensus_hash = reward_cycle_end_tip.consensus_hash;
 
-        let mut cur_tenure_opt =
-            self.get_processed_tenure(chainstate, &nakamoto_tip, &cur_consensus_hash)?;
+        let mut cur_tenure_opt = self.get_processed_tenure(
+            chainstate,
+            nakamoto_tip_ch,
+            nakamoto_tip_bh,
+            &cur_consensus_hash,
+        )?;
 
         // loop variables and invariants:
         //
@@ -342,7 +420,8 @@ impl InvGenerator {
                     tenure_status.push(true);
                     cur_tenure_opt = self.get_processed_tenure(
                         chainstate,
-                        &nakamoto_tip,
+                        nakamoto_tip_ch,
+                        nakamoto_tip_bh,
                         &cur_tenure_info.parent_tenure_id_consensus_hash,
                     )?;
                 } else {
@@ -363,7 +442,8 @@ impl InvGenerator {
                 tenure_status.push(false);
                 cur_tenure_opt = self.get_processed_tenure(
                     chainstate,
-                    &nakamoto_tip,
+                    nakamoto_tip_ch,
+                    nakamoto_tip_bh,
                     &parent_sortition_consensus_hash,
                 )?;
             }
diff --git a/stackslib/src/net/p2p.rs b/stackslib/src/net/p2p.rs
index 4338671641..d7ea9684f3 100644
--- a/stackslib/src/net/p2p.rs
+++ b/stackslib/src/net/p2p.rs
@@ -1050,6 +1050,9 @@ impl PeerNetwork {
             if let Some(event_id) = self.events.get(&nk) {
                 let event_id = *event_id;
                 if let Some(convo) = self.peers.get_mut(&event_id) {
+                    if !convo.is_authenticated() {
+                        continue;
+                    }
                     // safety check -- don't send to someone who has already been a relayer
                     let mut do_relay = true;
                     if let Some(pubkey) = convo.ref_public_key() {


Test patch:
diff --git a/stackslib/src/net/tests/download/nakamoto.rs b/stackslib/src/net/tests/download/nakamoto.rs
index cc90d90011..0e48059796 100644
--- a/stackslib/src/net/tests/download/nakamoto.rs
+++ b/stackslib/src/net/tests/download/nakamoto.rs
@@ -229,7 +229,7 @@ fn test_nakamoto_tenure_downloader() {
     };
 
     let mut td = NakamotoTenureDownloader::new(
-        ch,
+        tenure_start_block.header.consensus_hash.clone(),
         tenure_start_block.header.block_id(),
         next_tenure_start_block.header.block_id(),
         naddr.clone(),
@@ -292,6 +292,7 @@ fn test_nakamoto_tenure_downloader() {
             .try_accept_tenure_blocks(vec![next_tenure_start_block.clone()])
             .is_err());
 
+        debug!("Try accept {:?}", &block);
         let res = td.try_accept_tenure_blocks(vec![block.clone()]);
         assert!(res.is_ok());
         assert!(res.unwrap().is_none());
diff --git a/stackslib/src/net/tests/inv/nakamoto.rs b/stackslib/src/net/tests/inv/nakamoto.rs
index 2f60027207..fac9623d3f 100644
--- a/stackslib/src/net/tests/inv/nakamoto.rs
+++ b/stackslib/src/net/tests/inv/nakamoto.rs
@@ -172,9 +172,11 @@ fn test_nakamoto_inv_10_tenures_10_sortitions() {
 
     let chainstate = &mut peer.stacks_node.as_mut().unwrap().chainstate;
     let sort_db = peer.sortdb.as_mut().unwrap();
-    let stacks_tip = peer.network.stacks_tip.block_id();
+    let stacks_tip_ch = peer.network.stacks_tip.consensus_hash.clone();
+    let stacks_tip_bh = peer.network.stacks_tip.block_hash.clone();
 
     let mut inv_generator = InvGenerator::new();
+    let mut inv_generator_no_cache = InvGenerator::new_no_cache();
 
     // processed 10 tenures
     let tip = SortitionDB::get_canonical_burn_chain_tip(sort_db.conn()).unwrap();
@@ -183,8 +185,29 @@ fn test_nakamoto_inv_10_tenures_10_sortitions() {
     // check the reward cycles
     for (rc, inv) in reward_cycle_invs.into_iter().enumerate() {
         let bitvec = inv_generator
-            .make_tenure_bitvector(&tip, sort_db, chainstate, &stacks_tip, rc as u64)
+            .make_tenure_bitvector(
+                &tip,
+                sort_db,
+                chainstate,
+                &stacks_tip_ch,
+                &stacks_tip_bh,
+                rc as u64,
+            )
             .unwrap();
+
+        let bitvec_no_cache = inv_generator_no_cache
+            .make_tenure_bitvector(
+                &tip,
+                sort_db,
+                chainstate,
+                &stacks_tip_ch,
+                &stacks_tip_bh,
+                rc as u64,
+            )
+            .unwrap();
+
+        assert_eq!(bitvec, bitvec_no_cache);
+
         debug!(
             "At reward cycle {}: {:?}, mesasge = {:?}",
             rc, &bitvec, &inv
@@ -234,9 +257,11 @@ fn test_nakamoto_inv_2_tenures_3_sortitions() {
 
     let chainstate = &mut peer.stacks_node.as_mut().unwrap().chainstate;
     let sort_db = peer.sortdb.as_mut().unwrap();
-    let stacks_tip = peer.network.stacks_tip.block_id();
+    let stacks_tip_ch = peer.network.stacks_tip.consensus_hash.clone();
+    let stacks_tip_bh = peer.network.stacks_tip.block_hash.clone();
 
     let mut inv_generator = InvGenerator::new();
+    let mut inv_generator_no_cache = InvGenerator::new_no_cache();
 
     // processed 3 sortitions
     let tip = SortitionDB::get_canonical_burn_chain_tip(sort_db.conn()).unwrap();
@@ -244,8 +269,28 @@ fn test_nakamoto_inv_2_tenures_3_sortitions() {
 
     for (rc, inv) in reward_cycle_invs.into_iter().enumerate() {
         let bitvec = inv_generator
-            .make_tenure_bitvector(&tip, sort_db, chainstate, &stacks_tip, rc as u64)
+            .make_tenure_bitvector(
+                &tip,
+                sort_db,
+                chainstate,
+                &stacks_tip_ch,
+                &stacks_tip_bh,
+                rc as u64,
+            )
             .unwrap();
+
+        let bitvec_no_cache = inv_generator_no_cache
+            .make_tenure_bitvector(
+                &tip,
+                sort_db,
+                chainstate,
+                &stacks_tip_ch,
+                &stacks_tip_bh,
+                rc as u64,
+            )
+            .unwrap();
+        assert_eq!(bitvec, bitvec_no_cache);
+
         debug!(
             "At reward cycle {}: {:?}, mesasge = {:?}",
             rc, &bitvec, &inv
@@ -287,9 +332,11 @@ fn test_nakamoto_inv_10_extended_tenures_10_sortitions() {
 
     let chainstate = &mut peer.stacks_node.as_mut().unwrap().chainstate;
     let sort_db = peer.sortdb.as_mut().unwrap();
-    let stacks_tip = peer.network.stacks_tip.block_id();
+    let stacks_tip_ch = peer.network.stacks_tip.consensus_hash.clone();
+    let stacks_tip_bh = peer.network.stacks_tip.block_hash.clone();
 
     let mut inv_generator = InvGenerator::new();
+    let mut inv_generator_no_cache = InvGenerator::new_no_cache();
 
     // processed 10 tenures
     let tip = SortitionDB::get_canonical_burn_chain_tip(sort_db.conn()).unwrap();
@@ -297,8 +344,27 @@ fn test_nakamoto_inv_10_extended_tenures_10_sortitions() {
 
     for (rc, inv) in reward_cycle_invs.into_iter().enumerate() {
         let bitvec = inv_generator
-            .make_tenure_bitvector(&tip, sort_db, chainstate, &stacks_tip, rc as u64)
+            .make_tenure_bitvector(
+                &tip,
+                sort_db,
+                chainstate,
+                &stacks_tip_ch,
+                &stacks_tip_bh,
+                rc as u64,
+            )
             .unwrap();
+        let bitvec_no_cache = inv_generator_no_cache
+            .make_tenure_bitvector(
+                &tip,
+                sort_db,
+                chainstate,
+                &stacks_tip_ch,
+                &stacks_tip_bh,
+                rc as u64,
+            )
+            .unwrap();
+        assert_eq!(bitvec, bitvec_no_cache);
+
         debug!("At reward cycle {}: {:?}", rc, &bitvec);
 
         if rc <= 6 {
@@ -1103,6 +1169,7 @@ fn test_nakamoto_make_tenure_inv_in_forks() {
     peer.mine_malleablized_blocks = false;
 
     let mut invgen = InvGenerator::new().with_tip_ancestor_search_depth(5);
+    let mut invgen_no_cache = InvGenerator::new_no_cache().with_tip_ancestor_search_depth(5);
 
     //
     // ---------------------- basic operations ----------------------
@@ -1119,9 +1186,13 @@ fn test_nakamoto_make_tenure_inv_in_forks() {
         .block_height_to_reward_cycle(first_burn_block_height, sort_tip.block_height)
         .unwrap();
 
+    let naka_tip_ch = peer.network.stacks_tip.consensus_hash.clone();
+    let naka_tip_bh = peer.network.stacks_tip.block_hash.clone();
     let naka_tip = peer.network.stacks_tip.block_id();
     let first_naka_tip = naka_tip.clone();
     let first_sort_tip = sort_tip.clone();
+    let first_naka_tip_ch = naka_tip_ch.clone();
+    let first_naka_tip_bh = naka_tip_bh.clone();
 
     // find the first block in this tenure
     let naka_tip_header = NakamotoChainState::get_block_header_nakamoto(chainstate.db(), &naka_tip)
@@ -1143,8 +1214,27 @@ fn test_nakamoto_make_tenure_inv_in_forks() {
     assert_eq!(invgen.cache_misses(), 0);
 
     let bits = invgen
-        .make_tenure_bitvector(&sort_tip, &sortdb, &chainstate, &naka_tip, tip_rc)
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc,
+        )
+        .unwrap();
+    let bits_no_cache = invgen_no_cache
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc,
+        )
         .unwrap();
+    assert_eq!(bits, bits_no_cache);
+
     debug!("test: Bits at rc {}: {:?}", tip_rc, &bits);
     debug!("test: invgen.cache_misses() = {}", invgen.cache_misses());
 
@@ -1152,8 +1242,27 @@ fn test_nakamoto_make_tenure_inv_in_forks() {
     assert_eq!(invgen.cache_misses(), 3);
 
     let bits = invgen
-        .make_tenure_bitvector(&sort_tip, &sortdb, &chainstate, &naka_tip, tip_rc)
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc,
+        )
         .unwrap();
+    let bits_no_cache = invgen_no_cache
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc,
+        )
+        .unwrap();
+    assert_eq!(bits, bits_no_cache);
+
     debug!("test: Bits at rc {}: {:?}", tip_rc, &bits);
     debug!("test: invgen.cache_misses() = {}", invgen.cache_misses());
 
@@ -1161,7 +1270,14 @@ fn test_nakamoto_make_tenure_inv_in_forks() {
     assert_eq!(invgen.cache_misses(), 3);
 
     let bits = invgen
-        .make_tenure_bitvector(&sort_tip, &sortdb, &chainstate, &naka_tip, tip_rc - 1)
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc - 1,
+        )
         .unwrap();
     debug!("test: Bits at rc {}: {:?}", tip_rc, &bits);
     debug!("test: invgen.cache_misses() = {}", invgen.cache_misses());
@@ -1173,8 +1289,27 @@ fn test_nakamoto_make_tenure_inv_in_forks() {
     assert_eq!(invgen.cache_misses(), 13);
 
     let bits = invgen
-        .make_tenure_bitvector(&sort_tip, &sortdb, &chainstate, &naka_tip, tip_rc - 1)
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc - 1,
+        )
+        .unwrap();
+    let bits_no_cache = invgen_no_cache
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc - 1,
+        )
         .unwrap();
+    assert_eq!(bits, bits_no_cache);
+
     debug!("test: Bits at rc {}: {:?}", tip_rc, &bits);
     debug!("test: invgen.cache_misses() = {}", invgen.cache_misses());
 
@@ -1202,6 +1337,8 @@ fn test_nakamoto_make_tenure_inv_in_forks() {
 
         peer.refresh_burnchain_view();
         let naka_tip = peer.network.stacks_tip.block_id();
+        let naka_tip_ch = peer.network.stacks_tip.consensus_hash.clone();
+        let naka_tip_bh = peer.network.stacks_tip.block_hash.clone();
         let sort_tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap();
         let tip_rc = sortdb
             .pox_constants
@@ -1209,14 +1346,20 @@ fn test_nakamoto_make_tenure_inv_in_forks() {
             .unwrap();
 
         let bits = invgen
-            .make_tenure_bitvector(&sort_tip, &sortdb, &chainstate, &naka_tip, tip_rc)
+            .make_tenure_bitvector(
+                &sort_tip,
+                &sortdb,
+                &chainstate,
+                &naka_tip_ch,
+                &naka_tip_bh,
+                tip_rc,
+            )
             .unwrap();
         debug!("test: Bits at rc {}: {:?}", tip_rc, &bits);
         debug!("test: invgen.cache_misses() = {}", invgen.cache_misses());
 
-        // only one additional cache miss
         expected_bits.push(true);
-        expected_cache_misses += 1;
+        expected_cache_misses += 2;
 
         assert_eq!(bits, expected_bits);
         assert_eq!(invgen.cache_misses(), expected_cache_misses);
@@ -1228,6 +1371,8 @@ fn test_nakamoto_make_tenure_inv_in_forks() {
 
     peer.refresh_burnchain_view();
     let naka_tip = peer.network.stacks_tip.block_id();
+    let naka_tip_ch = peer.network.stacks_tip.consensus_hash.clone();
+    let naka_tip_bh = peer.network.stacks_tip.block_hash.clone();
 
     //
     // ---------------------- the inv generator can track multiple forks at once ----------------------
@@ -1255,8 +1400,27 @@ fn test_nakamoto_make_tenure_inv_in_forks() {
     // load inv off of the canonical tip.
     // It should show a missed sortition.
     let bits = invgen
-        .make_tenure_bitvector(&sort_tip, &sortdb, &chainstate, &naka_tip, tip_rc)
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc,
+        )
         .unwrap();
+    let bits_no_cache = invgen_no_cache
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc,
+        )
+        .unwrap();
+    assert_eq!(bits, bits_no_cache);
+
     debug!(
         "test: Bits in fork on {} at rc {}: {:?}",
         &naka_tip, tip_rc, &bits
@@ -1267,7 +1431,7 @@ fn test_nakamoto_make_tenure_inv_in_forks() {
     );
 
     assert_eq!(bits, [true, true, true, true, true, false]);
-    assert_eq!(invgen.cache_misses(), 17);
+    assert_eq!(invgen.cache_misses(), 20);
 
     // load inv off of the non-canonical tip.
     // it should show the last 3 canonical tenures as missing, and this forked block as present
@@ -1276,10 +1440,23 @@ fn test_nakamoto_make_tenure_inv_in_forks() {
             &sort_tip,
             &sortdb,
             &chainstate,
-            &fork_naka_block.block_id(),
+            &fork_naka_block.header.consensus_hash,
+            &fork_naka_block.header.block_hash(),
+            tip_rc,
+        )
+        .unwrap();
+    let bits_no_cache = invgen
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &fork_naka_block.header.consensus_hash,
+            &fork_naka_block.header.block_hash(),
             tip_rc,
         )
         .unwrap();
+    assert_eq!(bits, bits_no_cache);
+
     debug!(
         "test: Bits in fork on {} at rc {}: {:?}",
         &fork_naka_block.block_id(),
@@ -1292,7 +1469,8 @@ fn test_nakamoto_make_tenure_inv_in_forks() {
     );
 
     assert_eq!(bits, [true, true, false, false, false, true]);
-    assert_eq!(invgen.cache_misses(), 21);
+    debug!("cache misses = {}", invgen.cache_misses());
+    assert_eq!(invgen.cache_misses(), 24);
 
     // add more to the fork
     peer.mine_nakamoto_on(vec![fork_naka_block.clone()]);
@@ -1306,6 +1484,8 @@ fn test_nakamoto_make_tenure_inv_in_forks() {
 
     peer.refresh_burnchain_view();
     let new_naka_tip = peer.network.stacks_tip.block_id();
+    let new_naka_tip_ch = peer.network.stacks_tip.consensus_hash.clone();
+    let new_naka_tip_bh = peer.network.stacks_tip.block_hash.clone();
     let sort_tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap();
     let tip_rc = sortdb
         .pox_constants
@@ -1319,8 +1499,27 @@ fn test_nakamoto_make_tenure_inv_in_forks() {
     // It should show two missed sortitions, for each fork.
     // only one additional cache miss
     let bits = invgen
-        .make_tenure_bitvector(&sort_tip, &sortdb, &chainstate, &naka_tip, tip_rc)
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc,
+        )
         .unwrap();
+    let bits_no_cache = invgen_no_cache
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc,
+        )
+        .unwrap();
+    assert_eq!(bits, bits_no_cache);
+
     debug!(
         "test: Bits in fork on {} at rc {}: {:?}",
         &naka_tip, tip_rc, &bits
@@ -1331,20 +1530,35 @@ fn test_nakamoto_make_tenure_inv_in_forks() {
     );
 
     assert_eq!(bits, [true, true, true, true, true, false, false]);
-    assert_eq!(invgen.cache_misses(), 22);
+    debug!("cache misses = {}", invgen.cache_misses());
+    assert_eq!(invgen.cache_misses(), 25);
 
     // load inv off of the non-canonical tip again.
     // it should show the last 3 last canonical tenures as missing, and this forked block as
-    // present. Only one additional cache miss should manifest.
+    // present. Two additional cache misses should manifest, since we invalidate the common
+    // parent's tenure data.
     let bits = invgen
         .make_tenure_bitvector(
             &sort_tip,
             &sortdb,
             &chainstate,
-            &fork_naka_block.block_id(),
+            &fork_naka_block.header.consensus_hash,
+            &fork_naka_block.header.block_hash(),
+            tip_rc,
+        )
+        .unwrap();
+    let bits_no_cache = invgen_no_cache
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &fork_naka_block.header.consensus_hash,
+            &fork_naka_block.header.block_hash(),
             tip_rc,
         )
         .unwrap();
+    assert_eq!(bits, bits_no_cache);
+
     debug!(
         "test: Bits in fork on {} at rc {}: {:?}",
         &fork_naka_block.block_id(),
@@ -1358,13 +1572,33 @@ fn test_nakamoto_make_tenure_inv_in_forks() {
 
     // only one more cache miss
     assert_eq!(bits, [true, true, false, false, false, true, true]);
-    assert_eq!(invgen.cache_misses(), 23);
+    debug!("cache misses = {}", invgen.cache_misses());
+    assert_eq!(invgen.cache_misses(), 27);
 
     // load inv off of the canonical tip again.
     // It should show two missed sortitions.
     let bits = invgen
-        .make_tenure_bitvector(&sort_tip, &sortdb, &chainstate, &naka_tip, tip_rc)
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc,
+        )
         .unwrap();
+    let bits_no_cache = invgen_no_cache
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc,
+        )
+        .unwrap();
+    assert_eq!(bits, bits_no_cache);
+
     debug!(
         "test: Bits in fork on {} at rc {}: {:?}",
         &naka_tip, tip_rc, &bits
@@ -1376,7 +1610,8 @@ fn test_nakamoto_make_tenure_inv_in_forks() {
 
     // no new cache misses
     assert_eq!(bits, [true, true, true, true, true, false, false]);
-    assert_eq!(invgen.cache_misses(), 23);
+    debug!("cache misses = {}", invgen.cache_misses());
+    assert_eq!(invgen.cache_misses(), 27);
 
     //
     // ---------------------- the inv generator will search only a maximum depth before giving up ----------------------
@@ -1397,6 +1632,8 @@ fn test_nakamoto_make_tenure_inv_in_forks() {
         peer.mine_nakamoto_on(vec![naka_block.clone()]);
     }
     let naka_tip = peer.network.stacks_tip.block_id();
+    let naka_tip_ch = peer.network.stacks_tip.consensus_hash.clone();
+    let naka_tip_bh = peer.network.stacks_tip.block_hash.clone();
     let sort_tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap();
 
     // new inv generator with a search depth of 3
@@ -1408,23 +1645,57 @@ fn test_nakamoto_make_tenure_inv_in_forks() {
             &first_sort_tip,
             &sortdb,
             &chainstate,
-            &first_naka_tip,
+            &first_naka_tip_ch,
+            &first_naka_tip_bh,
+            tip_rc,
+        )
+        .unwrap();
+    let bits_no_cache = invgen_no_cache
+        .make_tenure_bitvector(
+            &first_sort_tip,
+            &sortdb,
+            &chainstate,
+            &first_naka_tip_ch,
+            &first_naka_tip_bh,
             tip_rc,
         )
         .unwrap();
+    assert_eq!(bits, bits_no_cache);
+
     assert_eq!(bits, [true, true]);
+    debug!("cache misses = {}", invgen.cache_misses());
     assert_eq!(invgen.cache_misses(), 3);
 
     // load a descendant that is 6 blocks higher
     let bits = invgen
-        .make_tenure_bitvector(&sort_tip, &sortdb, &chainstate, &naka_tip, tip_rc)
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc,
+        )
+        .unwrap();
+    let bits_no_cache = invgen_no_cache
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc,
+        )
         .unwrap();
+    assert_eq!(bits, bits_no_cache);
+
     assert_eq!(
         bits,
         [true, true, true, true, true, false, false, true, true, true]
     );
 
     // all 10 tenures were loaded, because we had to search more than 5 blocks back
+    debug!("cache misses = {}", invgen.cache_misses());
     assert_eq!(invgen.cache_misses(), 12);
 
     // new inv generator with a search depth of 10
@@ -1436,24 +1707,58 @@ fn test_nakamoto_make_tenure_inv_in_forks() {
             &first_sort_tip,
             &sortdb,
             &chainstate,
-            &first_naka_tip,
+            &first_naka_tip_ch,
+            &first_naka_tip_bh,
+            tip_rc,
+        )
+        .unwrap();
+    let bits_no_cache = invgen_no_cache
+        .make_tenure_bitvector(
+            &first_sort_tip,
+            &sortdb,
+            &chainstate,
+            &first_naka_tip_ch,
+            &first_naka_tip_bh,
             tip_rc,
         )
         .unwrap();
+    assert_eq!(bits, bits_no_cache);
+
     assert_eq!(bits, [true, true]);
+    debug!("cache misses = {}", invgen.cache_misses());
     assert_eq!(invgen.cache_misses(), 3);
 
     // load a descendant that is 6 blocks higher
     let bits = invgen
-        .make_tenure_bitvector(&sort_tip, &sortdb, &chainstate, &naka_tip, tip_rc)
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc,
+        )
         .unwrap();
+    let bits_no_cache = invgen_no_cache
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc,
+        )
+        .unwrap();
+    assert_eq!(bits, bits_no_cache);
+
     assert_eq!(
         bits,
         [true, true, true, true, true, false, false, true, true, true]
     );
 
-    // reused old canonical tip information
-    assert_eq!(invgen.cache_misses(), 9);
+    // reused old canonical tip information, but still had an additional cache miss from the parent
+    debug!("cache misses = {}", invgen.cache_misses());
+    assert_eq!(invgen.cache_misses(), 10);
 }
 
 #[test]
@@ -1466,7 +1771,7 @@ fn test_nakamoto_make_tenure_inv_in_many_reward_cycles() {
     let bitvecs = vec![
         // full rc
         vec![true, true, true, true, true, true, true, true, true, true],
-        // sparce rc
+        // sparse rc
         vec![
             true, false, false, false, false, false, false, true, true, true,
         ],
@@ -1495,6 +1800,7 @@ fn test_nakamoto_make_tenure_inv_in_many_reward_cycles() {
     peer.mine_malleablized_blocks = false;
 
     let mut invgen = InvGenerator::new().with_tip_ancestor_search_depth(5);
+    let mut invgen_no_cache = InvGenerator::new_no_cache().with_tip_ancestor_search_depth(5);
 
     let sortdb = peer.sortdb_ref().reopen().unwrap();
     let (chainstate, _) = peer.chainstate_ref().reopen().unwrap();
@@ -1508,6 +1814,8 @@ fn test_nakamoto_make_tenure_inv_in_many_reward_cycles() {
         .unwrap();
 
     let naka_tip = peer.network.stacks_tip.block_id();
+    let naka_tip_ch = peer.network.stacks_tip.consensus_hash.clone();
+    let naka_tip_bh = peer.network.stacks_tip.block_hash.clone();
     let first_naka_tip = naka_tip.clone();
     let first_sort_tip = sort_tip.clone();
 
@@ -1531,44 +1839,121 @@ fn test_nakamoto_make_tenure_inv_in_many_reward_cycles() {
     assert_eq!(invgen.cache_misses(), 0);
 
     let bits = invgen
-        .make_tenure_bitvector(&sort_tip, &sortdb, &chainstate, &naka_tip, tip_rc)
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc,
+        )
+        .unwrap();
+    let bits_no_cache = invgen_no_cache
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc,
+        )
         .unwrap();
     debug!("test: Bits at rc {}: {:?}", tip_rc, &bits);
     debug!("test: invgen.cache_misses() = {}", invgen.cache_misses());
 
+    assert_eq!(bits, bits_no_cache);
     assert_eq!(bits, [true, true]);
+    debug!("cache misses = {}", invgen.cache_misses());
     assert_eq!(invgen.cache_misses(), 3);
 
     let bits = invgen
-        .make_tenure_bitvector(&sort_tip, &sortdb, &chainstate, &naka_tip, tip_rc - 1)
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc - 1,
+        )
+        .unwrap();
+    let bits_no_cache = invgen_no_cache
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc - 1,
+        )
         .unwrap();
     debug!("test: Bits at rc {}: {:?}", tip_rc - 1, &bits);
     debug!("test: invgen.cache_misses() = {}", invgen.cache_misses());
 
+    assert_eq!(bits, bits_no_cache);
     assert_eq!(
         bits,
         [true, true, true, true, true, true, true, true, true, true]
     );
+    debug!("cache misses = {}", invgen.cache_misses());
     assert_eq!(invgen.cache_misses(), 13);
 
     let bits = invgen
-        .make_tenure_bitvector(&sort_tip, &sortdb, &chainstate, &naka_tip, tip_rc - 2)
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc - 2,
+        )
+        .unwrap();
+    let bits_no_cache = invgen_no_cache
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc - 2,
+        )
         .unwrap();
+
     debug!("test: Bits at rc {}: {:?}", tip_rc - 2, &bits);
     debug!("test: invgen.cache_misses() = {}", invgen.cache_misses());
 
+    assert_eq!(bits, bits_no_cache);
     assert_eq!(
         bits,
         [true, true, false, false, false, false, false, false, true, true]
     );
+    debug!("cache misses = {}", invgen.cache_misses());
     assert_eq!(invgen.cache_misses(), 17);
 
     let bits = invgen
-        .make_tenure_bitvector(&sort_tip, &sortdb, &chainstate, &naka_tip, tip_rc - 3)
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc - 3,
+        )
+        .unwrap();
+
+    let bits_no_cache = invgen_no_cache
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc - 3,
+        )
         .unwrap();
     debug!("test: Bits at rc {}: {:?}", tip_rc - 3, &bits);
     debug!("test: invgen.cache_misses() = {}", invgen.cache_misses());
 
+    assert_eq!(bits, bits_no_cache);
     assert_eq!(
         bits,
         [true, true, false, true, false, true, false, true, false, true]
@@ -1576,96 +1961,424 @@ fn test_nakamoto_make_tenure_inv_in_many_reward_cycles() {
     assert_eq!(invgen.cache_misses(), 23);
 
     let bits = invgen
-        .make_tenure_bitvector(&sort_tip, &sortdb, &chainstate, &naka_tip, tip_rc - 4)
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc - 4,
+        )
+        .unwrap();
+
+    let bits_no_cache = invgen_no_cache
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc - 4,
+        )
         .unwrap();
+
     debug!("test: Bits at rc {}: {:?}", tip_rc - 4, &bits);
     debug!("test: invgen.cache_misses() = {}", invgen.cache_misses());
 
+    assert_eq!(bits, bits_no_cache);
     assert_eq!(
         bits,
         [true, true, true, false, false, false, false, false, false, true]
     );
+    debug!("cache misses = {}", invgen.cache_misses());
     assert_eq!(invgen.cache_misses(), 27);
 
     let bits = invgen
-        .make_tenure_bitvector(&sort_tip, &sortdb, &chainstate, &naka_tip, tip_rc - 5)
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc - 5,
+        )
+        .unwrap();
+    let bits_no_cache = invgen_no_cache
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc - 5,
+        )
         .unwrap();
     debug!("test: Bits at rc {}: {:?}", tip_rc - 5, &bits);
     debug!("test: invgen.cache_misses() = {}", invgen.cache_misses());
 
+    assert_eq!(bits, bits_no_cache);
     assert_eq!(
         bits,
         [false, false, true, true, true, true, true, true, true, true]
     );
+    debug!("cache misses = {}", invgen.cache_misses());
     assert_eq!(invgen.cache_misses(), 37);
 
     // load them all again.  cache misses should remain the same.
     let bits = invgen
-        .make_tenure_bitvector(&sort_tip, &sortdb, &chainstate, &naka_tip, tip_rc)
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc,
+        )
+        .unwrap();
+    let bits_no_cache = invgen_no_cache
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc,
+        )
         .unwrap();
     debug!("test: Bits at rc {}: {:?}", tip_rc, &bits);
     debug!("test: invgen.cache_misses() = {}", invgen.cache_misses());
 
+    assert_eq!(bits, bits_no_cache);
     assert_eq!(bits, [true, true]);
+    debug!("cache misses = {}", invgen.cache_misses());
     assert_eq!(invgen.cache_misses(), 37);
 
     let bits = invgen
-        .make_tenure_bitvector(&sort_tip, &sortdb, &chainstate, &naka_tip, tip_rc - 1)
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc - 1,
+        )
+        .unwrap();
+    let bits_no_cache = invgen_no_cache
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc - 1,
+        )
         .unwrap();
     debug!("test: Bits at rc {}: {:?}", tip_rc - 1, &bits);
     debug!("test: invgen.cache_misses() = {}", invgen.cache_misses());
 
+    assert_eq!(bits, bits_no_cache);
     assert_eq!(
         bits,
         [true, true, true, true, true, true, true, true, true, true]
     );
+    debug!("cache misses = {}", invgen.cache_misses());
     assert_eq!(invgen.cache_misses(), 37);
 
     let bits = invgen
-        .make_tenure_bitvector(&sort_tip, &sortdb, &chainstate, &naka_tip, tip_rc - 2)
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc - 2,
+        )
+        .unwrap();
+    let bits_no_cache = invgen_no_cache
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc - 2,
+        )
         .unwrap();
     debug!("test: Bits at rc {}: {:?}", tip_rc - 2, &bits);
     debug!("test: invgen.cache_misses() = {}", invgen.cache_misses());
 
+    assert_eq!(bits, bits_no_cache);
     assert_eq!(
         bits,
         [true, true, false, false, false, false, false, false, true, true]
     );
+    debug!("cache misses = {}", invgen.cache_misses());
     assert_eq!(invgen.cache_misses(), 37);
 
     let bits = invgen
-        .make_tenure_bitvector(&sort_tip, &sortdb, &chainstate, &naka_tip, tip_rc - 3)
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc - 3,
+        )
+        .unwrap();
+    let bits_no_cache = invgen_no_cache
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc - 3,
+        )
         .unwrap();
     debug!("test: Bits at rc {}: {:?}", tip_rc - 3, &bits);
     debug!("test: invgen.cache_misses() = {}", invgen.cache_misses());
 
+    assert_eq!(bits, bits_no_cache);
     assert_eq!(
         bits,
         [true, true, false, true, false, true, false, true, false, true]
     );
+    debug!("cache misses = {}", invgen.cache_misses());
     assert_eq!(invgen.cache_misses(), 37);
 
     let bits = invgen
-        .make_tenure_bitvector(&sort_tip, &sortdb, &chainstate, &naka_tip, tip_rc - 4)
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc - 4,
+        )
+        .unwrap();
+    let bits_no_cache = invgen_no_cache
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc - 4,
+        )
         .unwrap();
     debug!("test: Bits at rc {}: {:?}", tip_rc - 4, &bits);
     debug!("test: invgen.cache_misses() = {}", invgen.cache_misses());
 
+    assert_eq!(bits, bits_no_cache);
     assert_eq!(
         bits,
         [true, true, true, false, false, false, false, false, false, true]
     );
+    debug!("cache misses = {}", invgen.cache_misses());
     assert_eq!(invgen.cache_misses(), 37);
 
     let bits = invgen
-        .make_tenure_bitvector(&sort_tip, &sortdb, &chainstate, &naka_tip, tip_rc - 5)
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc - 5,
+        )
+        .unwrap();
+    let bits_no_cache = invgen_no_cache
+        .make_tenure_bitvector(
+            &sort_tip,
+            &sortdb,
+            &chainstate,
+            &naka_tip_ch,
+            &naka_tip_bh,
+            tip_rc - 5,
+        )
         .unwrap();
     debug!("test: Bits at rc {}: {:?}", tip_rc - 5, &bits);
     debug!("test: invgen.cache_misses() = {}", invgen.cache_misses());
 
+    assert_eq!(bits, bits_no_cache);
     assert_eq!(
         bits,
         [false, false, true, true, true, true, true, true, true, true]
     );
+    debug!("cache misses = {}", invgen.cache_misses());
     assert_eq!(invgen.cache_misses(), 37);
 }
+
+#[test]
+fn test_nakamoto_make_tenure_inv_from_old_tips() {
+    let sender_key = StacksPrivateKey::new();
+    let sender_addr = to_addr(&sender_key);
+    let initial_balances = vec![(sender_addr.to_account_principal(), 1000000000)];
+
+    let observer = TestEventObserver::new();
+    let bitvecs = vec![
+        // full rc
+        // item 0 is sortition 42
+        vec![true, true, true, true, true, true, true, true, true, true],
+        // sparse rc
+        // item 0 is sortition 52
+        vec![
+            true, false, false, false, false, false, false, true, true, true,
+        ],
+        // alternating rc
+        // item 0 is sortition 62
+        vec![
+            false, true, false, true, false, true, false, true, true, true,
+        ],
+        // sparse rc
+        // item 0 is sortition 72
+        vec![
+            false, false, false, false, false, false, true, true, true, true,
+        ],
+        // full rc
+        // item 0 is sortition 82
+        vec![true, true, true, true, true, true, true, true, true, true],
+    ];
+
+    // compute the rc-aligned bitvecs.
+    // bitvecs[i][0] starts at reward cycle index 2.
+    // aligned_bitvecs[i][0] starts at reward cycle index 0.
+    let mut aligned_bitvecs = vec![vec![false, false]];
+    let mut i = 2;
+    loop {
+        let bitvec_idx = (i - 2) / 10;
+        let bitvec_bit = (i - 2) % 10;
+        if bitvec_idx >= bitvecs.len() {
+            if let Some(ref mut last_bitvec) = aligned_bitvecs.last_mut() {
+                // last aligned bitvec has all `false`s
+                while last_bitvec.len() < 10 {
+                    last_bitvec.push(false);
+                }
+            }
+            break;
+        }
+
+        let aligned_bitvec_idx = i / 10;
+        let aligned_bitvec_bit = i % 10;
+        if aligned_bitvec_bit == 0 {
+            aligned_bitvecs.push(vec![]);
+        }
+
+        let bit = bitvecs[bitvec_idx][bitvec_bit];
+        aligned_bitvecs[aligned_bitvec_idx].push(bit);
+
+        i += 1;
+    }
+
+    assert_eq!(
+        aligned_bitvecs[0],
+        vec![false, false, true, true, true, true, true, true, true, true]
+    );
+    assert_eq!(
+        aligned_bitvecs[1],
+        vec![true, true, true, false, false, false, false, false, false, true]
+    );
+    assert_eq!(
+        aligned_bitvecs[2],
+        vec![true, true, false, true, false, true, false, true, false, true]
+    );
+    assert_eq!(
+        aligned_bitvecs[3],
+        vec![true, true, false, false, false, false, false, false, true, true]
+    );
+    assert_eq!(
+        aligned_bitvecs[4],
+        vec![true, true, true, true, true, true, true, true, true, true]
+    );
+    assert_eq!(
+        aligned_bitvecs[5],
+        vec![true, true, false, false, false, false, false, false, false, false]
+    );
+
+    let (mut peer, _) = make_nakamoto_peers_from_invs_and_balances(
+        function_name!(),
+        &observer,
+        10,
+        3,
+        bitvecs.clone(),
+        0,
+        initial_balances,
+    );
+    peer.refresh_burnchain_view();
+    peer.mine_malleablized_blocks = false;
+
+    let sortdb = peer.sortdb_ref().reopen().unwrap();
+    let (chainstate, _) = peer.chainstate_ref().reopen().unwrap();
+    let sort_tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap();
+
+    let mut invgen = InvGenerator::new().with_tip_ancestor_search_depth(5);
+    let mut invgen_no_cache = InvGenerator::new_no_cache().with_tip_ancestor_search_depth(5);
+
+    //
+    // ---------------------- querying each tip will report the successive inv bits ----------------------
+    //
+    let naka_tip = peer.network.stacks_tip.block_id();
+    let mut ancestor_tips = vec![];
+    let mut cursor = naka_tip.clone();
+    loop {
+        ancestor_tips.push(cursor.clone());
+        let Some(parent) =
+            NakamotoChainState::get_nakamoto_parent_block_id(chainstate.db(), &cursor).unwrap()
+        else {
+            break;
+        };
+        cursor = parent;
+    }
+    // last item is an epoch2 block, which we don't care about
+    ancestor_tips.pop();
+    ancestor_tips.reverse();
+
+    for tip in ancestor_tips.into_iter() {
+        debug!("load tip {}", &tip);
+        let hdr = NakamotoChainState::get_block_header_nakamoto(chainstate.db(), &tip)
+            .unwrap()
+            .unwrap();
+        let tip_ch = hdr.consensus_hash;
+        let tip_bh = hdr.anchored_header.block_hash();
+        let sn = SortitionDB::get_block_snapshot_consensus(sortdb.conn(), &tip_ch)
+            .unwrap()
+            .unwrap();
+        let rc = sortdb
+            .pox_constants
+            .block_height_to_reward_cycle(sortdb.first_block_height, sn.block_height)
+            .unwrap();
+        let rc_start_height = sortdb
+            .pox_constants
+            .reward_cycle_to_block_height(sortdb.first_block_height, rc)
+            - 1;
+        let bits = invgen
+            .make_tenure_bitvector(&sort_tip, &sortdb, &chainstate, &tip_ch, &tip_bh, rc)
+            .unwrap();
+
+        let bits_no_cache = invgen_no_cache
+            .make_tenure_bitvector(&sort_tip, &sortdb, &chainstate, &tip_ch, &tip_bh, rc)
+            .unwrap();
+
+        debug!("tip {}: consensus_hash={}, burn_height={}, reward_cycle={}, bits={:?}, bits_no_cache={:?}", &tip, &tip_ch, sn.block_height, rc, &bits, &bits_no_cache);
+        assert_eq!(bits, bits_no_cache);
+
+        // nakamoto starts at burn height 42, and has a reward cycle length of 10, so compute the range of bitvecs we need
+        assert_eq!(sortdb.pox_constants.reward_cycle_length, 10);
+        assert!(rc >= 4);
+
+        let mut expected_bits = aligned_bitvecs[(rc - 4) as usize].clone();
+        let from_bits = expected_bits.clone();
+
+        for i in (sn.block_height + 1 - rc_start_height)..10 {
+            expected_bits[i as usize] = false;
+        }
+
+        let bit_len = bits.len();
+        debug!(
+            "tip {}: from_bits={:?}, expected_bits={:?}, inv_bits={:?}, rc={}, block_height={}",
+            &tip, &from_bits, &expected_bits, &bits, rc, sn.block_height
+        );
+
+        assert_eq!(bits, expected_bits[0..bit_len]);
+    }
+}