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

Issue:
Add `tenure-height` to the /v2/info endpoint
In epoch 3.0, it will be useful to have the tenure height in the response from the /v2/info endpoint.


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5ccb9b5cac..eeb514f6b8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
     - `get-tenure-info?` added
     - `get-block-info?` removed
 - Added `/v3/signer/{signer_pubkey}/{reward_cycle}` endpoint
+- Added `tenure_height` to `/v2/info` endpoint
 - Added optional `timeout_ms` to `events_observer` configuration
 
 ## [2.5.0.0.7]
diff --git a/docs/rpc/api/core-node/get-info.example.json b/docs/rpc/api/core-node/get-info.example.json
index afc42e6f68..77ece128c3 100644
--- a/docs/rpc/api/core-node/get-info.example.json
+++ b/docs/rpc/api/core-node/get-info.example.json
@@ -11,6 +11,7 @@
   "stacks_tip": "b1807a2d3f7f8c7922f7c1d60d7c34145ade05d789640dc7dc9ec1021e07bb54",
   "stacks_tip_consensus_hash": "17f76e597bab45646956f38dd39573085d72cbc0",
   "unanchored_tip": "0000000000000000000000000000000000000000000000000000000000000000",
+  "tenure_height": 523,
   "exit_at_block_height": null,
   "is_fully_synced": false
 }
diff --git a/docs/rpc/api/core-node/get-info.schema.json b/docs/rpc/api/core-node/get-info.schema.json
index 16b560ed5e..e997a2d19c 100644
--- a/docs/rpc/api/core-node/get-info.schema.json
+++ b/docs/rpc/api/core-node/get-info.schema.json
@@ -17,6 +17,7 @@
     "stacks_tip",
     "stacks_tip_consensus_hash",
     "unanchored_tip",
+    "tenure_height",
     "exit_at_block_height",
     "is_fully_synced"
   ],
@@ -69,6 +70,10 @@
       "type": "string",
       "description": "the latest microblock hash if any microblocks were processed. if no microblock has been processed for the current block, a 000.., hex array is returned"
     },
+    "tenure_height": {
+      "type": "integer",
+      "description": "the latest Stacks tenure height"
+    },
     "exit_at_block_height": {
       "type": "integer",
       "description": "the block height at which the testnet network will be reset. not applicable for mainnet"
diff --git a/stacks-signer/src/client/mod.rs b/stacks-signer/src/client/mod.rs
index 3fe9f09354..9885182d98 100644
--- a/stacks-signer/src/client/mod.rs
+++ b/stacks-signer/src/client/mod.rs
@@ -322,6 +322,7 @@ pub(crate) mod tests {
             stacks_tip_consensus_hash: generate_random_consensus_hash(),
             unanchored_tip: None,
             unanchored_seq: Some(0),
+            tenure_height: thread_rng().next_u64(),
             exit_at_block_height: None,
             is_fully_synced: false,
             genesis_chainstate_hash: Sha256Sum::zero(),
diff --git a/stackslib/src/net/api/getinfo.rs b/stackslib/src/net/api/getinfo.rs
index 237205f63a..d95b94803a 100644
--- a/stackslib/src/net/api/getinfo.rs
+++ b/stackslib/src/net/api/getinfo.rs
@@ -27,11 +27,12 @@ use stacks_common::util::hash::{Hash160, Sha256Sum};
 use crate::burnchains::affirmation::AffirmationMap;
 use crate::burnchains::Txid;
 use crate::chainstate::burn::db::sortdb::SortitionDB;
+use crate::chainstate::nakamoto::NakamotoChainState;
 use crate::chainstate::stacks::db::StacksChainState;
 use crate::core::mempool::MemPoolDB;
 use crate::net::http::{
     parse_json, Error, HttpRequest, HttpRequestContents, HttpRequestPreamble, HttpResponse,
-    HttpResponseContents, HttpResponsePayload, HttpResponsePreamble,
+    HttpResponseContents, HttpResponsePayload, HttpResponsePreamble, HttpServerError,
 };
 use crate::net::httpcore::{
     HttpPreambleExtensions, RPCRequestHandler, StacksHttpRequest, StacksHttpResponse,
@@ -81,6 +82,7 @@ pub struct RPCPeerInfoData {
     pub genesis_chainstate_hash: Sha256Sum,
     pub unanchored_tip: Option<StacksBlockId>,
     pub unanchored_seq: Option<u16>,
+    pub tenure_height: u64,
     pub exit_at_block_height: Option<u64>,
     pub is_fully_synced: bool,
     #[serde(default)]
@@ -106,6 +108,7 @@ impl RPCPeerInfoData {
         chainstate: &StacksChainState,
         exit_at_block_height: Option<u64>,
         genesis_chainstate_hash: &Sha256Sum,
+        coinbase_height: u64,
         ibd: bool,
     ) -> RPCPeerInfoData {
         let server_version = version_string(
@@ -148,7 +151,7 @@ impl RPCPeerInfoData {
             stacks_tip_consensus_hash: network.stacks_tip.consensus_hash.clone(),
             unanchored_tip: unconfirmed_tip,
             unanchored_seq: unconfirmed_seq,
-            exit_at_block_height: exit_at_block_height,
+            exit_at_block_height,
             is_fully_synced,
             genesis_chainstate_hash: genesis_chainstate_hash.clone(),
             node_public_key: Some(public_key_buf),
@@ -169,6 +172,7 @@ impl RPCPeerInfoData {
                     .map(|cid| format!("{}", cid))
                     .collect(),
             ),
+            tenure_height: coinbase_height,
         }
     }
 }
@@ -217,16 +221,28 @@ impl RPCRequestHandler for RPCPeerInfoRequestHandler {
         node: &mut StacksNodeState,
     ) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> {
         let ibd = node.ibd;
-        let rpc_peer_info =
+
+        let rpc_peer_info: Result<RPCPeerInfoData, StacksHttpResponse> =
             node.with_node_state(|network, _sortdb, chainstate, _mempool, rpc_args| {
-                RPCPeerInfoData::from_network(
+                let coinbase_height = network.stacks_tip.coinbase_height;
+
+                Ok(RPCPeerInfoData::from_network(
                     network,
                     chainstate,
                     rpc_args.exit_at_block_height.clone(),
                     &rpc_args.genesis_chainstate_hash,
+                    coinbase_height,
                     ibd,
-                )
+                ))
             });
+
+        let rpc_peer_info = match rpc_peer_info {
+            Ok(rpc_peer_info) => rpc_peer_info,
+            Err(response) => {
+                return response.try_into_contents().map_err(NetError::from);
+            }
+        };
+
         let mut preamble = HttpResponsePreamble::ok_json(&preamble);
         preamble.set_canonical_stacks_tip_height(Some(node.canonical_stacks_tip_height()));
         let body = HttpResponseContents::try_from_json(&rpc_peer_info)?;
diff --git a/stackslib/src/net/api/postmempoolquery.rs b/stackslib/src/net/api/postmempoolquery.rs
index 2155863220..25da52a66d 100644
--- a/stackslib/src/net/api/postmempoolquery.rs
+++ b/stackslib/src/net/api/postmempoolquery.rs
@@ -116,9 +116,9 @@ impl StacksMemPoolStream {
 
         Self {
             tx_query,
-            last_randomized_txid: last_randomized_txid,
+            last_randomized_txid,
             num_txs: 0,
-            max_txs: max_txs,
+            max_txs,
             coinbase_height,
             corked: false,
             finished: false,
@@ -275,14 +275,8 @@ impl RPCRequestHandler for RPCMempoolQueryRequestHandler {
             .ok_or(NetError::SendError("`mempool_query` not set".into()))?;
         let page_id = self.page_id.take();
 
-        let stream_res = node.with_node_state(|network, sortdb, chainstate, mempool, _rpc_args| {
-            let header = self.get_stacks_chain_tip(&preamble, sortdb, chainstate)
-                .map_err(|e| StacksHttpResponse::new_error(&preamble, &HttpServerError::new(format!("Failed to load chain tip: {:?}", &e))))?;
-
-            let coinbase_height = NakamotoChainState::get_coinbase_height(&mut chainstate.index_conn(), &header.index_block_hash())
-                .map_err(|e| StacksHttpResponse::new_error(&preamble, &HttpServerError::new(format!("Failed to load coinbase height: {:?}", &e))))?
-                .unwrap_or(0);
-
+        let stream_res = node.with_node_state(|network, _sortdb, _chainstate, mempool, _rpc_args| {
+            let coinbase_height = network.stacks_tip.coinbase_height;
             let max_txs = network.connection_opts.mempool_max_tx_query;
             debug!(
                 "Begin mempool query";
diff --git a/stackslib/src/net/p2p.rs b/stackslib/src/net/p2p.rs
index eb224f3e80..054fefaf1d 100644
--- a/stackslib/src/net/p2p.rs
+++ b/stackslib/src/net/p2p.rs
@@ -105,7 +105,7 @@ struct NetworkHandleServer {
 
 impl NetworkHandle {
     pub fn new(chan_in: SyncSender<NetworkRequest>) -> NetworkHandle {
-        NetworkHandle { chan_in: chan_in }
+        NetworkHandle { chan_in }
     }
 
     /// Send out a command to the p2p thread.  Do not bother waiting for the response.
@@ -175,7 +175,7 @@ impl NetworkHandle {
 
 impl NetworkHandleServer {
     pub fn new(chan_in: Receiver<NetworkRequest>) -> NetworkHandleServer {
-        NetworkHandleServer { chan_in: chan_in }
+        NetworkHandleServer { chan_in }
     }
 
     pub fn pair(bufsz: usize) -> (NetworkHandleServer, NetworkHandle) {
@@ -246,6 +246,7 @@ pub struct StacksTipInfo {
     pub consensus_hash: ConsensusHash,
     pub block_hash: BlockHeaderHash,
     pub height: u64,
+    pub coinbase_height: u64,
     pub is_nakamoto: bool,
 }
 
@@ -255,6 +256,7 @@ impl StacksTipInfo {
             consensus_hash: ConsensusHash([0u8; 20]),
             block_hash: BlockHeaderHash([0u8; 32]),
             height: 0,
+            coinbase_height: 0,
             is_nakamoto: false,
         }
     }
@@ -481,11 +483,11 @@ impl PeerNetwork {
         }
 
         let mut network = PeerNetwork {
-            peer_version: peer_version,
-            epochs: epochs,
+            peer_version,
+            epochs,
 
-            local_peer: local_peer,
-            chain_view: chain_view,
+            local_peer,
+            chain_view,
             chain_view_stable_consensus_hash: ConsensusHash([0u8; 20]),
             ast_rules: ASTRules::Typical,
             heaviest_affirmation_map: AffirmationMap::empty(),
@@ -504,8 +506,8 @@ impl PeerNetwork {
             tenure_start_block_id: StacksBlockId([0x00; 32]),
             current_reward_sets: BTreeMap::new(),
 
-            peerdb: peerdb,
-            atlasdb: atlasdb,
+            peerdb,
+            atlasdb,
 
             peers: PeerMap::new(),
             sockets: HashMap::new(),
@@ -521,8 +523,8 @@ impl PeerNetwork {
             p2p_network_handle: 0,
             http_network_handle: 0,
 
-            burnchain: burnchain,
-            connection_opts: connection_opts,
+            burnchain,
+            connection_opts,
 
             work_state: PeerNetworkWorkState::GetPublicIP,
             nakamoto_work_state: PeerNetworkWorkState::GetPublicIP,
@@ -553,8 +555,8 @@ impl PeerNetwork {
             attachments_downloader: None,
 
             stacker_db_syncs: Some(stacker_db_sync_map),
-            stacker_db_configs: stacker_db_configs,
-            stackerdbs: stackerdbs,
+            stacker_db_configs,
+            stackerdbs,
 
             prune_outbound_counts: HashMap::new(),
             prune_inbound_counts: HashMap::new(),
@@ -3491,7 +3493,7 @@ impl PeerNetwork {
                     }
                     let microblocks_data = MicroblocksData {
                         index_anchor_block: anchor_block_id.clone(),
-                        microblocks: microblocks,
+                        microblocks,
                     };
 
                     debug!(
@@ -4039,7 +4041,7 @@ impl PeerNetwork {
                             peer_version: nk.peer_version,
                             network_id: nk.network_id,
                             ts: get_epoch_time_secs(),
-                            pubkey: pubkey,
+                            pubkey,
                         },
                     );
 
@@ -4218,14 +4220,39 @@ impl PeerNetwork {
 
         let parent_tenure_start_header = NakamotoChainState::get_tenure_start_block_header(&mut chainstate.index_conn(), stacks_tip_block_id, &parent_header.consensus_hash)?
             .ok_or_else(|| {
-                debug!("{:?}: get_parent_stacks_tip: No tenure-start block for parent tenure {} off of child {} (parnet {})", self.get_local_peer(), &parent_header.consensus_hash, stacks_tip_block_id, &parent_block_id);
+                debug!("{:?}: get_parent_stacks_tip: No tenure-start block for parent tenure {} off of child {} (parent {})", self.get_local_peer(), &parent_header.consensus_hash, stacks_tip_block_id, &parent_block_id);
                 net_error::DBError(db_error::NotFoundError)
             })?;
 
+        let parent_stacks_tip_block_hash = parent_tenure_start_header.anchored_header.block_hash();
+        let parent_stacks_tip_block_id = StacksBlockId::new(
+            &parent_tenure_start_header.consensus_hash,
+            &parent_stacks_tip_block_hash,
+        );
+        let parent_coinbase_height = NakamotoChainState::get_coinbase_height(
+            &mut chainstate.index_conn(),
+            &parent_stacks_tip_block_id,
+        )?;
+
+        let coinbase_height = match parent_coinbase_height {
+            Some(cbh) => cbh,
+            None => {
+                if parent_tenure_start_header.is_epoch_2_block() {
+                    // The coinbase height is the same as the stacks block height as
+                    // every block contains a coinbase in epoch 2.x
+                    parent_tenure_start_header.stacks_block_height
+                } else {
+                    debug!("{:?}: get_parent_stacks_tip: No coinbase height found for nakamoto block {parent_stacks_tip_block_id}", self.get_local_peer());
+                    return Err(net_error::DBError(db_error::NotFoundError));
+                }
+            }
+        };
+
         let parent_stacks_tip = StacksTipInfo {
             consensus_hash: parent_tenure_start_header.consensus_hash,
-            block_hash: parent_tenure_start_header.anchored_header.block_hash(),
+            block_hash: parent_stacks_tip_block_hash,
             height: parent_tenure_start_header.anchored_header.height(),
+            coinbase_height,
             is_nakamoto: parent_tenure_start_header
                 .anchored_header
                 .as_stacks_nakamoto()
@@ -4377,6 +4404,25 @@ impl PeerNetwork {
             self.stacks_tip.is_nakamoto
         };
 
+        let stacks_tip_cbh = NakamotoChainState::get_coinbase_height(
+            &mut chainstate.index_conn(),
+            &new_stacks_tip_block_id,
+        )?;
+
+        let coinbase_height = match stacks_tip_cbh {
+            Some(cbh) => cbh,
+            None => {
+                if !stacks_tip_is_nakamoto {
+                    // The coinbase height is the same as the stacks block height as
+                    // every block contains a coinbase in epoch 2.x
+                    stacks_tip_height
+                } else {
+                    debug!("{:?}: No coinbase height found for nakamoto block {new_stacks_tip_block_id}", self.get_local_peer());
+                    return Err(net_error::DBError(db_error::NotFoundError));
+                }
+            }
+        };
+
         let need_stackerdb_refresh = canonical_sn.canonical_stacks_tip_consensus_hash
             != self.burnchain_tip.canonical_stacks_tip_consensus_hash
             || burnchain_tip_changed
@@ -4415,6 +4461,7 @@ impl PeerNetwork {
                         consensus_hash: FIRST_BURNCHAIN_CONSENSUS_HASH.clone(),
                         block_hash: FIRST_STACKS_BLOCK_HASH.clone(),
                         height: 0,
+                        coinbase_height: 0,
                         is_nakamoto: false,
                     }
                 }
@@ -4610,6 +4657,7 @@ impl PeerNetwork {
                 consensus_hash: stacks_tip_ch,
                 block_hash: stacks_tip_bhh,
                 height: stacks_tip_height,
+                coinbase_height,
                 is_nakamoto: stacks_tip_is_nakamoto,
             };
             self.parent_stacks_tip = parent_stacks_tip;
@@ -5184,7 +5232,7 @@ mod test {
                     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x7f,
                     0x00, 0x00, 0x01,
                 ]),
-                port: port,
+                port,
             },
             public_key: Secp256k1PublicKey::from_hex(
                 "02fa66b66f8971a8cd4d20ffded09674e030f0f33883f337f34b95ad4935bac0e3",


Test patch:
diff --git a/stackslib/src/net/api/tests/getinfo.rs b/stackslib/src/net/api/tests/getinfo.rs
index 7d8aeff01c..89054e3453 100644
--- a/stackslib/src/net/api/tests/getinfo.rs
+++ b/stackslib/src/net/api/tests/getinfo.rs
@@ -50,7 +50,7 @@ fn test_try_parse_request() {
         .try_parse_request(&parsed_preamble.expect_request(), &bytes[offset..])
         .unwrap();
 
-    // parsed request consumes headers that would not be in a constructed reqeuest
+    // parsed request consumes headers that would not be in a constructed request
     parsed_request.clear_headers();
     parsed_request.add_header(
         "X-Canonical-Stacks-Tip-Height".to_string(),
@@ -63,10 +63,10 @@ fn test_try_parse_request() {
 
 #[test]
 fn test_getinfo_compat() {
-    let old_getinfo_json = r#"{"peer_version":402653189,"pox_consensus":"b712eb731b613eebae814a8f416c5c15bc8391ec","burn_block_height":727631,"stable_pox_consensus":"53b5ed79842080500d7d83daa36aa1069dedf983","stable_burn_block_height":727624,"server_version":"stacks-node 0.0.1 (feat/faster-inv-generation:68f33190a, release build, linux [x86_64])","network_id":1,"parent_network_id":3652501241,"stacks_tip_height":52537,"stacks_tip":"b3183f2ac588e12319ff0fde78f97e62c92a218d87828c35710c29aaf7adbedc","stacks_tip_consensus_hash":"b712eb731b613eebae814a8f416c5c15bc8391ec","genesis_chainstate_hash":"74237aa39aa50a83de11a4f53e9d3bb7d43461d1de9873f402e5453ae60bc59b","unanchored_tip":"e76f68d607480e9984b4062b2691fb60a88423177898f5780b40ace17ae8982a","unanchored_seq":0,"exit_at_block_height":null,"is_fully_synced":false}"#;
-    let getinfo_no_pubkey_hash_json = r#"{"peer_version":402653189,"pox_consensus":"b712eb731b613eebae814a8f416c5c15bc8391ec","burn_block_height":727631,"stable_pox_consensus":"53b5ed79842080500d7d83daa36aa1069dedf983","stable_burn_block_height":727624,"server_version":"stacks-node 0.0.1 (feat/faster-inv-generation:68f33190a, release build, linux [x86_64])","network_id":1,"parent_network_id":3652501241,"stacks_tip_height":52537,"stacks_tip":"b3183f2ac588e12319ff0fde78f97e62c92a218d87828c35710c29aaf7adbedc","stacks_tip_consensus_hash":"b712eb731b613eebae814a8f416c5c15bc8391ec","genesis_chainstate_hash":"74237aa39aa50a83de11a4f53e9d3bb7d43461d1de9873f402e5453ae60bc59b","unanchored_tip":"e76f68d607480e9984b4062b2691fb60a88423177898f5780b40ace17ae8982a","unanchored_seq":0,"exit_at_block_height":null,"is_fully_synced":false,"node_public_key":"029b27d345e7bd2a6627262cefe6e97d9bc482f41ec32ec76a7bec391bb441798d"}"#;
-    let getinfo_no_pubkey_json = r#"{"peer_version":402653189,"pox_consensus":"b712eb731b613eebae814a8f416c5c15bc8391ec","burn_block_height":727631,"stable_pox_consensus":"53b5ed79842080500d7d83daa36aa1069dedf983","stable_burn_block_height":727624,"server_version":"stacks-node 0.0.1 (feat/faster-inv-generation:68f33190a, release build, linux [x86_64])","network_id":1,"parent_network_id":3652501241,"stacks_tip_height":52537,"stacks_tip":"b3183f2ac588e12319ff0fde78f97e62c92a218d87828c35710c29aaf7adbedc","stacks_tip_consensus_hash":"b712eb731b613eebae814a8f416c5c15bc8391ec","genesis_chainstate_hash":"74237aa39aa50a83de11a4f53e9d3bb7d43461d1de9873f402e5453ae60bc59b","unanchored_tip":"e76f68d607480e9984b4062b2691fb60a88423177898f5780b40ace17ae8982a","unanchored_seq":0,"exit_at_block_height":null,"is_fully_synced":false,"node_public_key_hash":"046e6f832a83ff0da4a550907d3a44412cc1e4bf"}"#;
-    let getinfo_full_json = r#"{"peer_version":402653189,"pox_consensus":"b712eb731b613eebae814a8f416c5c15bc8391ec","burn_block_height":727631,"stable_pox_consensus":"53b5ed79842080500d7d83daa36aa1069dedf983","stable_burn_block_height":727624,"server_version":"stacks-node 0.0.1 (feat/faster-inv-generation:68f33190a, release build, linux [x86_64])","network_id":1,"parent_network_id":3652501241,"stacks_tip_height":52537,"stacks_tip":"b3183f2ac588e12319ff0fde78f97e62c92a218d87828c35710c29aaf7adbedc","stacks_tip_consensus_hash":"b712eb731b613eebae814a8f416c5c15bc8391ec","genesis_chainstate_hash":"74237aa39aa50a83de11a4f53e9d3bb7d43461d1de9873f402e5453ae60bc59b","unanchored_tip":"e76f68d607480e9984b4062b2691fb60a88423177898f5780b40ace17ae8982a","unanchored_seq":0,"exit_at_block_height":null,"is_fully_synced":false,"node_public_key":"029b27d345e7bd2a6627262cefe6e97d9bc482f41ec32ec76a7bec391bb441798d","node_public_key_hash":"046e6f832a83ff0da4a550907d3a44412cc1e4bf"}"#;
+    let old_getinfo_json = r#"{"peer_version":402653189,"pox_consensus":"b712eb731b613eebae814a8f416c5c15bc8391ec","burn_block_height":727631,"stable_pox_consensus":"53b5ed79842080500d7d83daa36aa1069dedf983","stable_burn_block_height":727624,"server_version":"stacks-node 0.0.1 (feat/faster-inv-generation:68f33190a, release build, linux [x86_64])","network_id":1,"parent_network_id":3652501241,"stacks_tip_height":52537,"stacks_tip":"b3183f2ac588e12319ff0fde78f97e62c92a218d87828c35710c29aaf7adbedc","stacks_tip_consensus_hash":"b712eb731b613eebae814a8f416c5c15bc8391ec","genesis_chainstate_hash":"74237aa39aa50a83de11a4f53e9d3bb7d43461d1de9873f402e5453ae60bc59b","unanchored_tip":"e76f68d607480e9984b4062b2691fb60a88423177898f5780b40ace17ae8982a","unanchored_seq":0,"exit_at_block_height":null,"is_fully_synced":false, "tenure_height": 42}"#;
+    let getinfo_no_pubkey_hash_json = r#"{"peer_version":402653189,"pox_consensus":"b712eb731b613eebae814a8f416c5c15bc8391ec","burn_block_height":727631,"stable_pox_consensus":"53b5ed79842080500d7d83daa36aa1069dedf983","stable_burn_block_height":727624,"server_version":"stacks-node 0.0.1 (feat/faster-inv-generation:68f33190a, release build, linux [x86_64])","network_id":1,"parent_network_id":3652501241,"stacks_tip_height":52537,"stacks_tip":"b3183f2ac588e12319ff0fde78f97e62c92a218d87828c35710c29aaf7adbedc","stacks_tip_consensus_hash":"b712eb731b613eebae814a8f416c5c15bc8391ec","genesis_chainstate_hash":"74237aa39aa50a83de11a4f53e9d3bb7d43461d1de9873f402e5453ae60bc59b","unanchored_tip":"e76f68d607480e9984b4062b2691fb60a88423177898f5780b40ace17ae8982a","unanchored_seq":0,"exit_at_block_height":null,"is_fully_synced":false,"node_public_key":"029b27d345e7bd2a6627262cefe6e97d9bc482f41ec32ec76a7bec391bb441798d", "tenure_height": 42}"#;
+    let getinfo_no_pubkey_json = r#"{"peer_version":402653189,"pox_consensus":"b712eb731b613eebae814a8f416c5c15bc8391ec","burn_block_height":727631,"stable_pox_consensus":"53b5ed79842080500d7d83daa36aa1069dedf983","stable_burn_block_height":727624,"server_version":"stacks-node 0.0.1 (feat/faster-inv-generation:68f33190a, release build, linux [x86_64])","network_id":1,"parent_network_id":3652501241,"stacks_tip_height":52537,"stacks_tip":"b3183f2ac588e12319ff0fde78f97e62c92a218d87828c35710c29aaf7adbedc","stacks_tip_consensus_hash":"b712eb731b613eebae814a8f416c5c15bc8391ec","genesis_chainstate_hash":"74237aa39aa50a83de11a4f53e9d3bb7d43461d1de9873f402e5453ae60bc59b","unanchored_tip":"e76f68d607480e9984b4062b2691fb60a88423177898f5780b40ace17ae8982a","unanchored_seq":0,"exit_at_block_height":null,"is_fully_synced":false,"node_public_key_hash":"046e6f832a83ff0da4a550907d3a44412cc1e4bf", "tenure_height": 0}"#;
+    let getinfo_full_json = r#"{"peer_version":402653189,"pox_consensus":"b712eb731b613eebae814a8f416c5c15bc8391ec","burn_block_height":727631,"stable_pox_consensus":"53b5ed79842080500d7d83daa36aa1069dedf983","stable_burn_block_height":727624,"server_version":"stacks-node 0.0.1 (feat/faster-inv-generation:68f33190a, release build, linux [x86_64])","network_id":1,"parent_network_id":3652501241,"stacks_tip_height":52537,"stacks_tip":"b3183f2ac588e12319ff0fde78f97e62c92a218d87828c35710c29aaf7adbedc","stacks_tip_consensus_hash":"b712eb731b613eebae814a8f416c5c15bc8391ec","genesis_chainstate_hash":"74237aa39aa50a83de11a4f53e9d3bb7d43461d1de9873f402e5453ae60bc59b","unanchored_tip":"e76f68d607480e9984b4062b2691fb60a88423177898f5780b40ace17ae8982a","unanchored_seq":0,"exit_at_block_height":null,"is_fully_synced":false,"node_public_key":"029b27d345e7bd2a6627262cefe6e97d9bc482f41ec32ec76a7bec391bb441798d","node_public_key_hash":"046e6f832a83ff0da4a550907d3a44412cc1e4bf", "tenure_height": 2423}"#;
 
     // they all parse
     for json_obj in &[
@@ -102,4 +102,6 @@ fn test_try_make_response() {
         Some(1)
     );
     let resp = response.decode_peer_info().unwrap();
+
+    assert_eq!(resp.tenure_height, 1);
 }
diff --git a/stackslib/src/net/api/tests/gettenureinfo.rs b/stackslib/src/net/api/tests/gettenureinfo.rs
index db53a5daca..51a8a11785 100644
--- a/stackslib/src/net/api/tests/gettenureinfo.rs
+++ b/stackslib/src/net/api/tests/gettenureinfo.rs
@@ -25,7 +25,6 @@ use stacks_common::types::net::PeerHost;
 use stacks_common::types::Address;
 
 use super::test_rpc;
-use crate::net::api::getinfo::RPCPeerInfoData;
 use crate::net::api::tests::TestRPC;
 use crate::net::api::*;
 use crate::net::connection::ConnectionOptions;
diff --git a/stackslib/src/net/tests/relay/nakamoto.rs b/stackslib/src/net/tests/relay/nakamoto.rs
index fb9db70d5b..606f1f3fb2 100644
--- a/stackslib/src/net/tests/relay/nakamoto.rs
+++ b/stackslib/src/net/tests/relay/nakamoto.rs
@@ -54,7 +54,6 @@ use crate::chainstate::stacks::tests::{
 use crate::chainstate::stacks::{Error as ChainstateError, *};
 use crate::clarity_vm::clarity::ClarityConnection;
 use crate::core::*;
-use crate::net::api::getinfo::RPCPeerInfoData;
 use crate::net::asn::*;
 use crate::net::chat::*;
 use crate::net::codec::*;
diff --git a/testnet/stacks-node/src/tests/nakamoto_integrations.rs b/testnet/stacks-node/src/tests/nakamoto_integrations.rs
index 009797a376..37b7a6c329 100644
--- a/testnet/stacks-node/src/tests/nakamoto_integrations.rs
+++ b/testnet/stacks-node/src/tests/nakamoto_integrations.rs
@@ -5399,6 +5399,11 @@ fn check_block_heights() {
 
     let info = get_chain_info_result(&naka_conf).unwrap();
     info!("Chain info: {:?}", info);
+
+    // With the first Nakamoto block, the chain tip and the number of tenures
+    // must be the same (before Nakamoto every block counts as a tenure)
+    assert_eq!(info.tenure_height, info.stacks_tip_height);
+
     let mut last_burn_block_height;
     let mut last_stacks_block_height = info.stacks_tip_height as u128;
     let mut last_tenure_height = last_stacks_block_height as u128;
@@ -5532,6 +5537,9 @@ fn check_block_heights() {
         );
         last_tenure_height = bh1;
 
+        let info = get_chain_info_result(&naka_conf).unwrap();
+        assert_eq!(info.tenure_height, bh3 as u64);
+
         let sbh = heights3
             .get("stacks-block-height")
             .unwrap()
@@ -5640,6 +5648,9 @@ fn check_block_heights() {
                 "Tenure height should not have changed"
             );
 
+            let info = get_chain_info_result(&naka_conf).unwrap();
+            assert_eq!(info.tenure_height, bh3 as u64);
+
             let sbh = heights3
                 .get("stacks-block-height")
                 .unwrap()
@@ -5680,6 +5691,9 @@ fn check_block_heights() {
         "Should have mined 1 + (1 + interim_blocks_per_tenure) * tenure_count nakamoto blocks"
     );
 
+    let info = get_chain_info_result(&naka_conf).unwrap();
+    assert_eq!(info.tenure_height, block_height_pre_3_0 + tenure_count);
+
     coord_channel
         .lock()
         .expect("Mutex poisoned")