From bb61adea3cbccf1111991f8d8fb46cd1e3f99989 Mon Sep 17 00:00:00 2001 From: Siddharth Sharma Date: Thu, 9 Apr 2026 14:51:21 -0700 Subject: [PATCH 1/2] feat: add get_header(height) to Requester API Expose the existing BlockTree::header_at_height through the Requester -> ClientMessage -> Node channel. This is a zero-cost local lookup (no network fetch) that allows consumers to retrieve block headers at specific heights from the locally maintained header chain. --- src/client.rs | 16 ++++++++++++++++ src/messages.rs | 4 +++- src/node.rs | 13 ++++++++++++- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index 4bf37165..a35a158d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -7,6 +7,7 @@ use tokio::sync::mpsc::UnboundedSender; use tokio::sync::oneshot; use crate::chain::block_subsidy; +use crate::chain::IndexedHeader; use crate::messages::ClientRequest; use crate::{Event, HeaderCheckpoint, Info, TrustedPeer, Warning}; @@ -221,6 +222,21 @@ impl Requester { rx.await.map_err(|_| ClientError::RecvError) } + /// Look up a header at a specific height in the locally synced chain of most work. + /// Returns `None` if the height is not in the header chain. + /// + /// # Errors + /// + /// If the node has stopped running. + pub async fn get_header(&self, height: u32) -> Result, ClientError> { + let (tx, rx) = tokio::sync::oneshot::channel::>(); + let request = ClientRequest::new(height, tx); + self.ntx + .send(ClientMessage::GetHeader(request)) + .map_err(|_| ClientError::SendError)?; + rx.await.map_err(|_| ClientError::RecvError) + } + /// Check if the node is running. pub fn is_running(&self) -> bool { self.ntx.send(ClientMessage::NoOp).is_ok() diff --git a/src/messages.rs b/src/messages.rs index 7021c859..269c663a 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -7,7 +7,7 @@ use bitcoin::{ block::Header, p2p::message_network::RejectReason, BlockHash, FeeRate, Transaction, Wtxid, }; -use crate::chain::BlockHeaderChanges; +use crate::chain::{BlockHeaderChanges, IndexedHeader}; use crate::IndexedFilter; use crate::{chain::checkpoints::HeaderCheckpoint, IndexedBlock, TrustedPeer}; @@ -153,6 +153,8 @@ pub(crate) enum ClientMessage { GetBroadcastMinFeeRate(ClientRequest<(), FeeRate>), /// Get info on connections GetPeerInfo(ClientRequest<(), Vec<(AddrV2, ServiceFlags)>>), + /// Look up a header at a specific height in the chain of most work. + GetHeader(ClientRequest>), /// Send an empty message to see if the node is running. NoOp, } diff --git a/src/node.rs b/src/node.rs index 94bd4293..f08ed8ba 100644 --- a/src/node.rs +++ b/src/node.rs @@ -29,7 +29,7 @@ use crate::{ chain::Chain, checkpoints::HeaderCheckpoint, error::HeaderSyncError, - CFHeaderChanges, ChainState, FilterCheck, HeightMonitor, + CFHeaderChanges, ChainState, FilterCheck, HeightMonitor, IndexedHeader, }, error::FetchBlockError, messages::ClientRequest, @@ -268,6 +268,17 @@ impl Node { self.dialog.send_warning(Warning::ChannelDropped); }; } + ClientMessage::GetHeader(request) => { + let (height, oneshot) = request.into_values(); + let header = self + .chain + .header_chain + .header_at_height(height) + .map(|h| IndexedHeader::new(height, h)); + if oneshot.send(header).is_err() { + self.dialog.send_warning(Warning::ChannelDropped); + }; + } ClientMessage::NoOp => (), } } From 4ce805f9ea991d368d2dc62426d3e8106dce13ba Mon Sep 17 00:00:00 2001 From: Siddharth Sharma Date: Thu, 9 Apr 2026 15:14:10 -0700 Subject: [PATCH 2/2] test: add integration tests for get_header Extend various_client_methods to verify get_header returns the correct header at a known height and at the chain tip, and returns None for heights beyond the chain. --- tests/core.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/core.rs b/tests/core.rs index 5d62e984..74d690e1 100644 --- a/tests/core.rs +++ b/tests/core.rs @@ -253,6 +253,20 @@ async fn various_client_methods() { let peers = requester.peer_info().await.unwrap(); assert_eq!(peers.len(), 1); assert!(requester.is_running()); + // get_header should return a header within the synced range + let header_at_1 = requester.get_header(1).await.unwrap(); + assert!(header_at_1.is_some()); + let header_at_1 = header_at_1.unwrap(); + assert_eq!(header_at_1.height, 1); + // get_header at the tip should match the chain tip hash + let tip_header = requester.get_header(cp.height).await.unwrap(); + assert!(tip_header.is_some()); + let tip_header = tip_header.unwrap(); + assert_eq!(tip_header.height, cp.height); + assert_eq!(tip_header.block_hash(), cp.hash); + // get_header beyond the chain should return None + let too_high = requester.get_header(cp.height + 1).await.unwrap(); + assert!(too_high.is_none()); requester.shutdown().unwrap(); rpc.stop().unwrap(); }