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

stacks-network / stacks-core / 25904007932-1

15 May 2026 06:31AM UTC coverage: 47.459% (-38.5%) from 85.959%
25904007932-1

Pull #7210

github

869a54
web-flow
Merge 27877974d into 1c7b8e6ac
Pull Request #7210: [wip] epoch 4 release branch

36 of 53 new or added lines in 1 file covered. (67.92%)

88645 existing lines in 346 files now uncovered.

104136 of 219422 relevant lines covered (47.46%)

12897381.15 hits per line

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

7.69
/stackslib/src/net/api/getattachmentsinv.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 std::collections::HashSet;
18

19
use regex::{Captures, Regex};
20
use stacks_common::types::chainstate::StacksBlockId;
21
use stacks_common::types::net::PeerHost;
22
use url::form_urlencoded;
23

24
use crate::net::atlas::{
25
    AttachmentPage, GetAttachmentsInvResponse, MAX_ATTACHMENT_INV_PAGES_PER_REQUEST,
26
};
27
use crate::net::http::{
28
    parse_json, Error, HttpBadRequest, HttpNotFound, HttpRequest, HttpRequestContents,
29
    HttpRequestPreamble, HttpResponse, HttpResponseContents, HttpResponsePayload,
30
    HttpResponsePreamble,
31
};
32
use crate::net::httpcore::{RPCRequestHandler, StacksHttpRequest, StacksHttpResponse};
33
use crate::net::{Error as NetError, StacksNodeState};
34

35
#[derive(Clone)]
36
pub struct RPCGetAttachmentsInvRequestHandler {
37
    pub index_block_hash: Option<StacksBlockId>,
38
    pub page_indexes: Option<Vec<u32>>,
39
}
40

41
impl RPCGetAttachmentsInvRequestHandler {
42
    pub fn new() -> Self {
1,079,741✔
43
        Self {
1,079,741✔
44
            index_block_hash: None,
1,079,741✔
45
            page_indexes: None,
1,079,741✔
46
        }
1,079,741✔
47
    }
1,079,741✔
48
}
49

50
/// Decode the HTTP request
51
impl HttpRequest for RPCGetAttachmentsInvRequestHandler {
52
    fn verb(&self) -> &'static str {
1,079,741✔
53
        "GET"
1,079,741✔
54
    }
1,079,741✔
55

56
    fn path_regex(&self) -> Regex {
2,159,482✔
57
        Regex::new("^/v2/attachments/inv$").unwrap()
2,159,482✔
58
    }
2,159,482✔
59

UNCOV
60
    fn metrics_identifier(&self) -> &str {
×
UNCOV
61
        "/v2/attachments/inv"
×
UNCOV
62
    }
×
63

64
    /// Try to decode this request.
65
    /// There's nothing to load here, so just make sure the request is well-formed.
UNCOV
66
    fn try_parse_request(
×
UNCOV
67
        &mut self,
×
UNCOV
68
        preamble: &HttpRequestPreamble,
×
UNCOV
69
        _captures: &Captures,
×
UNCOV
70
        query: Option<&str>,
×
UNCOV
71
        _body: &[u8],
×
UNCOV
72
    ) -> Result<HttpRequestContents, Error> {
×
UNCOV
73
        if preamble.get_content_length() != 0 {
×
74
            return Err(Error::DecodeError(
×
75
                "Invalid Http request: expected 0-length body".to_string(),
×
76
            ));
×
UNCOV
77
        }
×
78

UNCOV
79
        let query_str = if let Some(qs) = query {
×
UNCOV
80
            qs
×
81
        } else {
82
            return Err(Error::DecodeError(
×
83
                "Invalid Http request: expecting index_block_hash and pages_indexes".to_string(),
×
84
            ));
×
85
        };
86

UNCOV
87
        let mut index_block_hash = None;
×
UNCOV
88
        let mut page_indexes = HashSet::new();
×
89

90
        // expect index_block_hash= and page_indexes=
UNCOV
91
        for (key, value) in form_urlencoded::parse(query_str.as_bytes()) {
×
UNCOV
92
            if key == "index_block_hash" {
×
UNCOV
93
                index_block_hash = StacksBlockId::from_hex(&value).ok();
×
UNCOV
94
            } else if key == "pages_indexes" {
×
UNCOV
95
                let pages_indexes_value = value.to_string();
×
UNCOV
96
                for entry in pages_indexes_value.split(',') {
×
UNCOV
97
                    if let Ok(page_index) = entry.parse::<u32>() {
×
UNCOV
98
                        page_indexes.insert(page_index);
×
UNCOV
99
                    }
×
100
                }
101
            }
×
102
        }
103

UNCOV
104
        let index_block_hash = if let Some(ibh) = index_block_hash {
×
UNCOV
105
            ibh
×
106
        } else {
107
            return Err(Error::DecodeError(
×
108
                "Invalid Http request: expecting index_block_hash".to_string(),
×
109
            ));
×
110
        };
111

UNCOV
112
        if page_indexes.is_empty() {
×
113
            return Err(Error::DecodeError(
×
114
                "Invalid Http request: expecting pages_indexes".to_string(),
×
115
            ));
×
UNCOV
116
        }
×
117

UNCOV
118
        let mut page_index_list: Vec<u32> = page_indexes.into_iter().collect();
×
UNCOV
119
        page_index_list.sort();
×
120

UNCOV
121
        self.index_block_hash = Some(index_block_hash);
×
UNCOV
122
        self.page_indexes = Some(page_index_list);
×
123

UNCOV
124
        Ok(HttpRequestContents::new().query_string(query))
×
UNCOV
125
    }
×
126
}
127

128
impl RPCRequestHandler for RPCGetAttachmentsInvRequestHandler {
129
    /// Reset internal state
UNCOV
130
    fn restart(&mut self) {
×
UNCOV
131
        self.index_block_hash = None;
×
UNCOV
132
        self.page_indexes = None;
×
UNCOV
133
    }
×
134

UNCOV
135
    fn try_handle_request(
×
UNCOV
136
        &mut self,
×
UNCOV
137
        preamble: HttpRequestPreamble,
×
UNCOV
138
        _contents: HttpRequestContents,
×
UNCOV
139
        node: &mut StacksNodeState,
×
UNCOV
140
    ) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> {
×
UNCOV
141
        let index_block_hash = self
×
UNCOV
142
            .index_block_hash
×
UNCOV
143
            .take()
×
UNCOV
144
            .ok_or(NetError::SendError("Missing `index_block_hash`".into()))?;
×
UNCOV
145
        let page_indexes = self
×
UNCOV
146
            .page_indexes
×
UNCOV
147
            .take()
×
UNCOV
148
            .ok_or(NetError::SendError("Missing `page_indexes`".into()))?;
×
149

150
        // We are receiving a list of page indexes with a chain tip hash.
151
        // The amount of pages_indexes is capped by MAX_ATTACHMENT_INV_PAGES_PER_REQUEST (8)
152
        // Pages sizes are controlled by the constant ATTACHMENTS_INV_PAGE_SIZE (8), which
153
        // means that a `GET v2/attachments/inv` request can be requesting for a 64 bit vector
154
        // at once.
155
        // Since clients can be asking for non-consecutive pages indexes (1, 5_000, 10_000, ...),
156
        // we will be handling each page index separately.
157
        // We could also add the notion of "budget" so that a client could only get a limited number
158
        // of pages when they are spanning over many blocks.
UNCOV
159
        if page_indexes.len() > MAX_ATTACHMENT_INV_PAGES_PER_REQUEST {
×
160
            let msg = format!(
×
161
                "Number of attachment inv pages is limited by {} per request",
162
                MAX_ATTACHMENT_INV_PAGES_PER_REQUEST
163
            );
164
            warn!("{msg}");
×
165
            return StacksHttpResponse::new_error(&preamble, &HttpBadRequest::new(msg))
×
166
                .try_into_contents()
×
167
                .map_err(NetError::from);
×
UNCOV
168
        }
×
UNCOV
169
        if page_indexes.is_empty() {
×
170
            let msg = "Page indexes missing".to_string();
×
171
            warn!("{msg}");
×
172
            return StacksHttpResponse::new_error(&preamble, &HttpBadRequest::new(msg))
×
173
                .try_into_contents()
×
174
                .map_err(NetError::from);
×
UNCOV
175
        }
×
176

UNCOV
177
        let mut pages = vec![];
×
178

UNCOV
179
        for page_index in page_indexes.iter() {
×
UNCOV
180
            let page_res =
×
UNCOV
181
                node.with_node_state(|network, _sortdb, _chainstate, _mempool, _rpc_args| {
×
UNCOV
182
                    match network
×
UNCOV
183
                        .get_atlasdb()
×
UNCOV
184
                        .get_attachments_available_at_page_index(*page_index, &index_block_hash)
×
185
                    {
UNCOV
186
                        Ok(inventory) => Ok(AttachmentPage {
×
UNCOV
187
                            inventory,
×
UNCOV
188
                            index: *page_index,
×
UNCOV
189
                        }),
×
190
                        Err(e) => {
×
191
                            let msg = format!("Unable to read Atlas DB - {}", e);
×
192
                            warn!("{}", msg);
×
193
                            Err(msg)
×
194
                        }
195
                    }
UNCOV
196
                });
×
197

UNCOV
198
            match page_res {
×
UNCOV
199
                Ok(page) => {
×
UNCOV
200
                    pages.push(page);
×
UNCOV
201
                }
×
202
                Err(msg) => {
×
203
                    return StacksHttpResponse::new_error(&preamble, &HttpNotFound::new(msg))
×
204
                        .try_into_contents()
×
205
                        .map_err(NetError::from);
×
206
                }
207
            }
208
        }
209

UNCOV
210
        let content = GetAttachmentsInvResponse {
×
UNCOV
211
            block_id: index_block_hash.clone(),
×
UNCOV
212
            pages,
×
UNCOV
213
        };
×
214

UNCOV
215
        let preamble = HttpResponsePreamble::ok_json(&preamble);
×
UNCOV
216
        let body = HttpResponseContents::try_from_json(&content)?;
×
UNCOV
217
        Ok((preamble, body))
×
UNCOV
218
    }
×
219
}
220

221
/// Decode the HTTP response
222
impl HttpResponse for RPCGetAttachmentsInvRequestHandler {
UNCOV
223
    fn try_parse_response(
×
UNCOV
224
        &self,
×
UNCOV
225
        preamble: &HttpResponsePreamble,
×
UNCOV
226
        body: &[u8],
×
UNCOV
227
    ) -> Result<HttpResponsePayload, Error> {
×
UNCOV
228
        let pages: GetAttachmentsInvResponse = parse_json(preamble, body)?;
×
UNCOV
229
        Ok(HttpResponsePayload::try_from_json(pages)?)
×
UNCOV
230
    }
×
231
}
232

233
impl StacksHttpRequest {
234
    /// Make a new request for attachment inventory page
UNCOV
235
    pub fn new_getattachmentsinv(
×
UNCOV
236
        host: PeerHost,
×
UNCOV
237
        index_block_hash: StacksBlockId,
×
UNCOV
238
        page_indexes: HashSet<u32>,
×
UNCOV
239
    ) -> StacksHttpRequest {
×
UNCOV
240
        let page_list: Vec<String> = page_indexes.into_iter().map(|i| format!("{}", i)).collect();
×
UNCOV
241
        StacksHttpRequest::new_for_peer(
×
UNCOV
242
            host,
×
UNCOV
243
            "GET".into(),
×
UNCOV
244
            "/v2/attachments/inv".into(),
×
UNCOV
245
            HttpRequestContents::new()
×
UNCOV
246
                .query_arg("index_block_hash".into(), format!("{}", &index_block_hash))
×
UNCOV
247
                .query_arg("pages_indexes".into(), page_list[..].join(",")),
×
248
        )
UNCOV
249
        .expect("FATAL: failed to construct request from infallible data")
×
UNCOV
250
    }
×
251
}
252

253
impl StacksHttpResponse {
UNCOV
254
    pub fn decode_atlas_attachments_inv_response(
×
UNCOV
255
        self,
×
UNCOV
256
    ) -> Result<GetAttachmentsInvResponse, NetError> {
×
UNCOV
257
        let contents = self.get_http_payload_ok()?;
×
UNCOV
258
        let contents_json: serde_json::Value = contents.try_into()?;
×
UNCOV
259
        let resp: GetAttachmentsInvResponse = serde_json::from_value(contents_json)
×
UNCOV
260
            .map_err(|_e| NetError::DeserializeError("Failed to load from JSON".to_string()))?;
×
UNCOV
261
        Ok(resp)
×
UNCOV
262
    }
×
263
}
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