• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stacks-network / stacks-core / 23549394237

25 Mar 2026 03:32PM UTC coverage: 46.691% (-39.0%) from 85.712%
23549394237

Pull #6984

github

950b43
web-flow
Merge 3a953cd80 into 8b47a554a
Pull Request #6984: Epoch 3.5 activation

115 of 497 new or added lines in 21 files covered. (23.14%)

84896 existing lines in 340 files now uncovered.

101852 of 218140 relevant lines covered (46.69%)

12673023.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

4.95
/stackslib/src/net/api/getheaders.rs
1
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
2
// Copyright (C) 2020-2023 Stacks Open Internet Foundation
3
//
4
// This program is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// This program is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

17
use regex::{Captures, Regex};
18
use serde_json;
19
use stacks_common::types::chainstate::StacksBlockId;
20
use stacks_common::types::net::PeerHost;
21

22
use crate::chainstate::stacks::db::{ExtendedStacksHeader, StacksChainState};
23
use crate::chainstate::stacks::Error as ChainError;
24
use crate::net::http::{
25
    parse_json, Error, HttpBadRequest, HttpChunkGenerator, HttpContentType, HttpNotFound,
26
    HttpRequest, HttpRequestContents, HttpRequestPreamble, HttpResponse, HttpResponseContents,
27
    HttpResponsePayload, HttpResponsePreamble, HttpServerError,
28
};
29
use crate::net::httpcore::{
30
    request, HttpRequestContentsExtensions as _, RPCRequestHandler, StacksHttpRequest,
31
    StacksHttpResponse,
32
};
33
use crate::net::{Error as NetError, StacksNodeState, TipRequest, MAX_HEADERS};
34
use crate::util_lib::db::{DBConn, Error as DBError};
35

36
#[derive(Clone)]
37
pub struct RPCHeadersRequestHandler {
38
    pub quantity: Option<u32>,
39
}
40

41
impl RPCHeadersRequestHandler {
42
    pub fn new() -> Self {
981,714✔
43
        Self { quantity: None }
981,714✔
44
    }
981,714✔
45
}
46

47
#[derive(Debug)]
48
pub struct StacksHeaderStream {
49
    /// index block hash of the block to download
50
    pub index_block_hash: StacksBlockId,
51
    /// offset into whatever is being read (the blob, or the file in the chunk store)
52
    pub offset: u64,
53
    /// total number of bytes read.
54
    pub total_bytes: u64,
55
    /// number of headers remaining to stream
56
    pub num_headers: u32,
57

58
    /// header buffer data
59
    pub end_of_stream: bool,
60
    pub corked: bool,
61

62
    /// connection to the underlying chainstate
63
    chainstate_db: DBConn,
64
    blocks_path: String,
65
}
66

67
impl StacksHeaderStream {
UNCOV
68
    pub fn new(
×
UNCOV
69
        chainstate: &StacksChainState,
×
UNCOV
70
        tip: &StacksBlockId,
×
UNCOV
71
        num_headers_requested: u32,
×
UNCOV
72
    ) -> Result<Self, ChainError> {
×
UNCOV
73
        let header_info = StacksChainState::load_staging_block_info(chainstate.db(), tip)?
×
UNCOV
74
            .ok_or(ChainError::NoSuchBlockError)?;
×
75

UNCOV
76
        let num_headers = if header_info.height < (num_headers_requested as u64) {
×
UNCOV
77
            header_info.height as u32
×
78
        } else {
UNCOV
79
            num_headers_requested
×
80
        };
81

UNCOV
82
        let db = chainstate.reopen_db()?;
×
UNCOV
83
        let blocks_path = chainstate.blocks_path.clone();
×
84

UNCOV
85
        Ok(StacksHeaderStream {
×
UNCOV
86
            index_block_hash: tip.clone(),
×
UNCOV
87
            offset: 0,
×
UNCOV
88
            total_bytes: 0,
×
UNCOV
89
            num_headers,
×
UNCOV
90
            end_of_stream: false,
×
UNCOV
91
            corked: false,
×
UNCOV
92
            chainstate_db: db,
×
UNCOV
93
            blocks_path,
×
UNCOV
94
        })
×
UNCOV
95
    }
×
96
}
97

98
/// Decode the HTTP request
99
impl HttpRequest for RPCHeadersRequestHandler {
100
    fn verb(&self) -> &'static str {
981,714✔
101
        "GET"
981,714✔
102
    }
981,714✔
103

104
    fn path_regex(&self) -> Regex {
1,963,428✔
105
        Regex::new(r#"^/v2/headers/(?P<quantity>[0-9]+)$"#).unwrap()
1,963,428✔
106
    }
1,963,428✔
107

UNCOV
108
    fn metrics_identifier(&self) -> &str {
×
UNCOV
109
        "/v2/headers/:height"
×
UNCOV
110
    }
×
111

112
    /// Try to decode this request.
113
    /// There's nothing to load here, so just make sure the request is well-formed.
UNCOV
114
    fn try_parse_request(
×
UNCOV
115
        &mut self,
×
UNCOV
116
        preamble: &HttpRequestPreamble,
×
UNCOV
117
        captures: &Captures,
×
UNCOV
118
        query: Option<&str>,
×
UNCOV
119
        _body: &[u8],
×
UNCOV
120
    ) -> Result<HttpRequestContents, Error> {
×
UNCOV
121
        if preamble.get_content_length() != 0 {
×
UNCOV
122
            return Err(Error::DecodeError(
×
UNCOV
123
                "Invalid Http request: expected 0-length body for GetInfo".to_string(),
×
UNCOV
124
            ));
×
UNCOV
125
        }
×
126

UNCOV
127
        let quantity = request::get_u32(captures, "quantity")?;
×
UNCOV
128
        self.quantity = Some(quantity);
×
129

UNCOV
130
        let contents = HttpRequestContents::new().query_string(query);
×
131

UNCOV
132
        Ok(contents)
×
UNCOV
133
    }
×
134
}
135

136
impl RPCRequestHandler for RPCHeadersRequestHandler {
137
    /// Reset internal state
UNCOV
138
    fn restart(&mut self) {
×
UNCOV
139
        self.quantity = None;
×
UNCOV
140
    }
×
141

142
    /// Make the response
UNCOV
143
    fn try_handle_request(
×
UNCOV
144
        &mut self,
×
UNCOV
145
        preamble: HttpRequestPreamble,
×
UNCOV
146
        contents: HttpRequestContents,
×
UNCOV
147
        node: &mut StacksNodeState,
×
UNCOV
148
    ) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> {
×
UNCOV
149
        let quantity = self
×
UNCOV
150
            .quantity
×
UNCOV
151
            .take()
×
UNCOV
152
            .ok_or(NetError::SendError("`quantity` not set".to_string()))?;
×
UNCOV
153
        if (quantity as usize) > MAX_HEADERS {
×
154
            return StacksHttpResponse::new_error(
×
155
                &preamble,
×
156
                &HttpBadRequest::new(format!(
×
157
                    "Invalid request: requested more than {} headers\n",
×
158
                    MAX_HEADERS
×
159
                )),
×
160
            )
161
            .try_into_contents()
×
162
            .map_err(NetError::from);
×
UNCOV
163
        }
×
164

165
        // find requested chain tip
UNCOV
166
        let tip = match node.load_stacks_chain_tip(&preamble, &contents) {
×
UNCOV
167
            Ok(tip) => tip,
×
168
            Err(error_resp) => {
×
169
                return error_resp.try_into_contents().map_err(NetError::from);
×
170
            }
171
        };
172

UNCOV
173
        let stream_res =
×
UNCOV
174
            node.with_node_state(|_network, _sortdb, chainstate, _mempool, _rpc_args| {
×
UNCOV
175
                StacksHeaderStream::new(chainstate, &tip, quantity)
×
UNCOV
176
            });
×
177

178
        // start loading headers
UNCOV
179
        let stream = match stream_res {
×
UNCOV
180
            Ok(stream) => stream,
×
181
            Err(ChainError::NoSuchBlockError) => {
UNCOV
182
                return StacksHttpResponse::new_error(
×
UNCOV
183
                    &preamble,
×
UNCOV
184
                    &HttpNotFound::new(format!("No such block {:?}\n", &tip)),
×
185
                )
UNCOV
186
                .try_into_contents()
×
UNCOV
187
                .map_err(NetError::from)
×
188
            }
189
            Err(e) => {
×
190
                // nope -- error trying to check
191
                let msg = format!("Failed to load block header: {:?}\n", &e);
×
192
                warn!("{}", &msg);
×
193
                return StacksHttpResponse::new_error(&preamble, &HttpServerError::new(msg))
×
194
                    .try_into_contents()
×
195
                    .map_err(NetError::from);
×
196
            }
197
        };
198

UNCOV
199
        let resp_preamble = HttpResponsePreamble::from_http_request_preamble(
×
UNCOV
200
            &preamble,
×
201
            200,
UNCOV
202
            "OK",
×
UNCOV
203
            None,
×
UNCOV
204
            HttpContentType::JSON,
×
205
        );
UNCOV
206
        Ok((
×
UNCOV
207
            resp_preamble,
×
UNCOV
208
            HttpResponseContents::from_stream(Box::new(stream)),
×
UNCOV
209
        ))
×
UNCOV
210
    }
×
211
}
212

213
/// Decode the HTTP response
214
impl HttpResponse for RPCHeadersRequestHandler {
215
    /// Decode this response from a byte stream.  This is called by the client to decode this
216
    /// message
UNCOV
217
    fn try_parse_response(
×
UNCOV
218
        &self,
×
UNCOV
219
        preamble: &HttpResponsePreamble,
×
UNCOV
220
        body: &[u8],
×
UNCOV
221
    ) -> Result<HttpResponsePayload, Error> {
×
UNCOV
222
        let headers: Vec<ExtendedStacksHeader> = parse_json(preamble, body)?;
×
UNCOV
223
        Ok(HttpResponsePayload::try_from_json(headers)?)
×
UNCOV
224
    }
×
225
}
226

227
/// Stream implementation for HeaderStreamData
228
impl HttpChunkGenerator for StacksHeaderStream {
UNCOV
229
    fn hint_chunk_size(&self) -> usize {
×
UNCOV
230
        4096
×
UNCOV
231
    }
×
232

233
    #[cfg_attr(test, mutants::skip)]
UNCOV
234
    fn generate_next_chunk(&mut self) -> Result<Vec<u8>, String> {
×
UNCOV
235
        if self.total_bytes == 0 {
×
236
            // headers are a JSON array.  Start by writing '[', then write each header, and
237
            // then write ']'
UNCOV
238
            test_debug!("Opening header stream");
×
UNCOV
239
            self.total_bytes += 1;
×
UNCOV
240
            return Ok(vec![b'[']);
×
UNCOV
241
        }
×
UNCOV
242
        if self.num_headers == 0 {
×
UNCOV
243
            test_debug!("End of header stream");
×
UNCOV
244
            self.end_of_stream = true;
×
UNCOV
245
        }
×
UNCOV
246
        if self.total_bytes > 0 && !self.end_of_stream && !self.corked {
×
247
            // have more data to send.
248
            // read next header as JSON
UNCOV
249
            match StacksChainState::read_extended_header(
×
UNCOV
250
                &self.chainstate_db,
×
UNCOV
251
                &self.blocks_path,
×
UNCOV
252
                &self.index_block_hash,
×
253
            ) {
UNCOV
254
                Ok(extended_header) => {
×
255
                    // serialize
UNCOV
256
                    let mut header_bytes = vec![];
×
UNCOV
257
                    serde_json::to_writer(&mut header_bytes, &extended_header).map_err(|e| {
×
258
                        let msg = format!("Failed to encoded Stacks header: {:?}", &e);
×
259
                        warn!("{}", &msg);
×
260
                        msg
×
261
                    })?;
×
262

263
                    // advance
UNCOV
264
                    self.index_block_hash = extended_header.parent_block_id;
×
UNCOV
265
                    self.num_headers -= 1;
×
266

UNCOV
267
                    if self.num_headers > 0 {
×
UNCOV
268
                        header_bytes.push(b',');
×
UNCOV
269
                    } else {
×
UNCOV
270
                        self.end_of_stream = true;
×
UNCOV
271
                    }
×
272

UNCOV
273
                    self.total_bytes += header_bytes.len() as u64;
×
UNCOV
274
                    return Ok(header_bytes);
×
275
                }
276
                Err(ChainError::DBError(DBError::NotFoundError)) => {
277
                    // end of headers
278
                    test_debug!("Header not found; ending stream");
×
279
                    self.end_of_stream = true;
×
280
                }
281
                Err(e) => {
×
282
                    warn!("Header DB error: {:?}", &e);
×
283
                    self.end_of_stream = true;
×
284
                    return Err(format!(
×
285
                        "Failed to read extended header {}: {:?}",
×
286
                        &self.index_block_hash, &e
×
287
                    ));
×
288
                }
289
            };
UNCOV
290
        }
×
UNCOV
291
        if self.end_of_stream && !self.corked {
×
292
            // sent all the headers we're gonna send.
UNCOV
293
            test_debug!("Corking header stream");
×
UNCOV
294
            self.corked = true;
×
UNCOV
295
            self.total_bytes += 1;
×
UNCOV
296
            return Ok(vec![b']']);
×
UNCOV
297
        }
×
298

UNCOV
299
        test_debug!("Header stream terminated");
×
300
        // end of stream and corked. we're done!
UNCOV
301
        return Ok(vec![]);
×
UNCOV
302
    }
×
303
}
304

305
impl StacksHttpRequest {
UNCOV
306
    pub fn new_getheaders(host: PeerHost, quantity: u64, tip_req: TipRequest) -> StacksHttpRequest {
×
UNCOV
307
        StacksHttpRequest::new_for_peer(
×
UNCOV
308
            host,
×
UNCOV
309
            "GET".into(),
×
UNCOV
310
            format!("/v2/headers/{}", quantity),
×
UNCOV
311
            HttpRequestContents::new().for_tip(tip_req),
×
312
        )
UNCOV
313
        .expect("FATAL: failed to construct request from infallible data")
×
UNCOV
314
    }
×
315
}
316

317
impl StacksHttpResponse {
UNCOV
318
    pub fn decode_stacks_headers(self) -> Result<Vec<ExtendedStacksHeader>, NetError> {
×
UNCOV
319
        let contents = self.get_http_payload_ok()?;
×
UNCOV
320
        let response_json: serde_json::Value = contents.try_into()?;
×
UNCOV
321
        let headers: Vec<ExtendedStacksHeader> = serde_json::from_value(response_json)
×
UNCOV
322
            .map_err(|_e| Error::DecodeError("Failed to decode JSON".to_string()))?;
×
UNCOV
323
        Ok(headers)
×
UNCOV
324
    }
×
325
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc