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

stacks-network / stacks-core / 23943169302

03 Apr 2026 10:28AM UTC coverage: 77.573% (-8.1%) from 85.712%
23943169302

Pull #7076

github

7f2377
web-flow
Merge bb87ecec2 into c529ad924
Pull Request #7076: feat: sortition side-table copy and validation

3743 of 4318 new or added lines in 19 files covered. (86.68%)

19304 existing lines in 182 files now uncovered.

172097 of 221852 relevant lines covered (77.57%)

7722182.76 hits per line

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

19.74
/stackslib/src/net/api/getstackers.rs
1
// Copyright (C) 2024 Stacks Open Internet Foundation
2
//
3
// This program is free software: you can redistribute it and/or modify
4
// it under the terms of the GNU General Public License as published by
5
// the Free Software Foundation, either version 3 of the License, or
6
// (at your option) any later version.
7
//
8
// This program is distributed in the hope that it will be useful,
9
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
// GNU General Public License for more details.
12
//
13
// You should have received a copy of the GNU General Public License
14
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
15
use regex::{Captures, Regex};
16
use serde_json::json;
17
use stacks_common::types::chainstate::StacksBlockId;
18
use stacks_common::types::net::PeerHost;
19

20
use crate::burnchains::Burnchain;
21
use crate::chainstate::burn::db::sortdb::SortitionDB;
22
use crate::chainstate::coordinator::OnChainRewardSetProvider;
23
use crate::chainstate::stacks::boot::{PoxVersions, RewardSet};
24
use crate::chainstate::stacks::db::StacksChainState;
25
use crate::net::http::{
26
    parse_json, Error, HttpBadRequest, HttpRequest, HttpRequestContents, HttpRequestPreamble,
27
    HttpResponse, HttpResponseContents, HttpResponsePayload, HttpResponsePreamble,
28
};
29
use crate::net::httpcore::{
30
    HttpRequestContentsExtensions as _, RPCRequestHandler, StacksHttpRequest, StacksHttpResponse,
31
};
32
use crate::net::{Error as NetError, StacksNodeState, TipRequest};
33

34
#[derive(Clone, Default)]
35
pub struct GetStackersRequestHandler {
36
    cycle_number: Option<u64>,
37
}
38

39
#[derive(Debug, Serialize, Deserialize)]
40
pub struct GetStackersResponse {
41
    pub stacker_set: RewardSet,
42
}
43

44
pub enum GetStackersErrors {
45
    NotAvailableYet(crate::chainstate::coordinator::Error),
46
    Other(String),
47
}
48

49
impl GetStackersErrors {
50
    pub const NOT_AVAILABLE_ERR_TYPE: &str = "not_available_try_again";
51
    pub const OTHER_ERR_TYPE: &str = "other";
52

53
    pub fn error_type_string(&self) -> &'static str {
2✔
54
        match self {
2✔
55
            Self::NotAvailableYet(_) => Self::NOT_AVAILABLE_ERR_TYPE,
1✔
56
            Self::Other(_) => Self::OTHER_ERR_TYPE,
1✔
57
        }
58
    }
2✔
59
}
60

61
impl From<&str> for GetStackersErrors {
UNCOV
62
    fn from(value: &str) -> Self {
×
UNCOV
63
        GetStackersErrors::Other(value.into())
×
UNCOV
64
    }
×
65
}
66

67
impl std::fmt::Display for GetStackersErrors {
68
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2✔
69
        match self {
2✔
70
            GetStackersErrors::NotAvailableYet(e) => write!(f, "Could not read reward set. Prepare phase may not have started for this cycle yet. Err = {e:?}"),
1✔
71
            GetStackersErrors::Other(msg) => write!(f, "{msg}")
1✔
72
        }
73
    }
2✔
74
}
75

76
impl GetStackersResponse {
UNCOV
77
    pub fn load(
×
UNCOV
78
        sortdb: &SortitionDB,
×
UNCOV
79
        chainstate: &mut StacksChainState,
×
UNCOV
80
        tip: &StacksBlockId,
×
UNCOV
81
        burnchain: &Burnchain,
×
UNCOV
82
        cycle_number: u64,
×
UNCOV
83
    ) -> Result<Self, GetStackersErrors> {
×
UNCOV
84
        let cycle_start_height = burnchain.reward_cycle_to_block_height(cycle_number);
×
UNCOV
85
        let pox_contract_name = burnchain
×
UNCOV
86
            .pox_constants
×
UNCOV
87
            .active_pox_contract(cycle_start_height);
×
UNCOV
88
        let pox_version = PoxVersions::lookup_by_name(pox_contract_name)
×
UNCOV
89
            .ok_or("Failed to lookup PoX contract version at tip")?;
×
UNCOV
90
        if !matches!(pox_version, PoxVersions::Pox4) {
×
UNCOV
91
            return Err(
×
UNCOV
92
                "Active PoX contract version at tip is Pre-PoX-4, the signer set is not fetchable"
×
UNCOV
93
                    .into(),
×
UNCOV
94
            );
×
UNCOV
95
        }
×
96

UNCOV
97
        let provider = OnChainRewardSetProvider::new();
×
UNCOV
98
        let stacker_set = provider
×
UNCOV
99
            .read_reward_set_nakamoto(chainstate, cycle_number, sortdb, tip, true)
×
UNCOV
100
            .map_err(GetStackersErrors::NotAvailableYet)?;
×
101

UNCOV
102
        Ok(Self { stacker_set })
×
UNCOV
103
    }
×
104
}
105

106
/// Decode the HTTP request
107
impl HttpRequest for GetStackersRequestHandler {
108
    fn verb(&self) -> &'static str {
1,734✔
109
        "GET"
1,734✔
110
    }
1,734✔
111

112
    fn path_regex(&self) -> Regex {
3,468✔
113
        Regex::new(r#"^/v3/stacker_set/(?P<cycle_num>[0-9]{1,10})$"#).unwrap()
3,468✔
114
    }
3,468✔
115

UNCOV
116
    fn metrics_identifier(&self) -> &str {
×
UNCOV
117
        "/v3/stacker_set/:cycle_num"
×
UNCOV
118
    }
×
119

120
    /// Try to decode this request.
121
    /// There's nothing to load here, so just make sure the request is well-formed.
UNCOV
122
    fn try_parse_request(
×
UNCOV
123
        &mut self,
×
UNCOV
124
        preamble: &HttpRequestPreamble,
×
UNCOV
125
        captures: &Captures,
×
UNCOV
126
        query: Option<&str>,
×
UNCOV
127
        _body: &[u8],
×
UNCOV
128
    ) -> Result<HttpRequestContents, Error> {
×
UNCOV
129
        if preamble.get_content_length() != 0 {
×
130
            return Err(Error::DecodeError(
×
131
                "Invalid Http request: expected 0-length body".into(),
×
132
            ));
×
UNCOV
133
        }
×
134

UNCOV
135
        let Some(cycle_num_str) = captures.name("cycle_num") else {
×
136
            return Err(Error::DecodeError(
×
137
                "Missing in request path: `cycle_num`".into(),
×
138
            ));
×
139
        };
UNCOV
140
        let cycle_num = u64::from_str_radix(cycle_num_str.into(), 10)
×
UNCOV
141
            .map_err(|e| Error::DecodeError(format!("Failed to parse cycle number: {e}")))?;
×
142

UNCOV
143
        self.cycle_number = Some(cycle_num);
×
144

UNCOV
145
        Ok(HttpRequestContents::new().query_string(query))
×
UNCOV
146
    }
×
147
}
148

149
impl RPCRequestHandler for GetStackersRequestHandler {
150
    /// Reset internal state
UNCOV
151
    fn restart(&mut self) {
×
UNCOV
152
        self.cycle_number = None;
×
UNCOV
153
    }
×
154

155
    /// Make the response
UNCOV
156
    fn try_handle_request(
×
UNCOV
157
        &mut self,
×
UNCOV
158
        preamble: HttpRequestPreamble,
×
UNCOV
159
        contents: HttpRequestContents,
×
UNCOV
160
        node: &mut StacksNodeState,
×
UNCOV
161
    ) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> {
×
UNCOV
162
        let tip = match node.load_stacks_chain_tip(&preamble, &contents) {
×
UNCOV
163
            Ok(tip) => tip,
×
164
            Err(error_resp) => {
×
165
                return error_resp.try_into_contents().map_err(NetError::from);
×
166
            }
167
        };
UNCOV
168
        let Some(cycle_number) = self.cycle_number else {
×
169
            return StacksHttpResponse::new_error(
×
170
                    &preamble,
×
171
                    &HttpBadRequest::new_json(json!({"response": "error", "err_msg": "Failed to read cycle number in request"}))
×
172
                )
173
                    .try_into_contents()
×
174
                    .map_err(NetError::from);
×
175
        };
176

UNCOV
177
        let stacker_response =
×
UNCOV
178
            node.with_node_state(|network, sortdb, chainstate, _mempool, _rpc_args| {
×
UNCOV
179
                GetStackersResponse::load(
×
UNCOV
180
                    sortdb,
×
UNCOV
181
                    chainstate,
×
UNCOV
182
                    &tip,
×
UNCOV
183
                    network.get_burnchain(),
×
UNCOV
184
                    cycle_number,
×
185
                )
UNCOV
186
            });
×
187

UNCOV
188
        let response = match stacker_response {
×
UNCOV
189
            Ok(response) => response,
×
UNCOV
190
            Err(error) => {
×
UNCOV
191
                return StacksHttpResponse::new_error(
×
UNCOV
192
                    &preamble,
×
UNCOV
193
                    &HttpBadRequest::new_json(json!({
×
UNCOV
194
                        "response": "error",
×
UNCOV
195
                        "err_type": error.error_type_string(),
×
UNCOV
196
                        "err_msg": error.to_string()})),
×
197
                )
UNCOV
198
                .try_into_contents()
×
UNCOV
199
                .map_err(NetError::from)
×
200
            }
201
        };
202

UNCOV
203
        let preamble = HttpResponsePreamble::ok_json(&preamble);
×
UNCOV
204
        let body = HttpResponseContents::try_from_json(&response)?;
×
UNCOV
205
        Ok((preamble, body))
×
UNCOV
206
    }
×
207
}
208

209
impl HttpResponse for GetStackersRequestHandler {
210
    fn try_parse_response(
×
211
        &self,
×
212
        preamble: &HttpResponsePreamble,
×
213
        body: &[u8],
×
214
    ) -> Result<HttpResponsePayload, Error> {
×
215
        let response: GetStackersResponse = parse_json(preamble, body)?;
×
216
        Ok(HttpResponsePayload::try_from_json(response)?)
×
217
    }
×
218
}
219

220
impl StacksHttpRequest {
221
    /// Make a new getinfo request to this endpoint
222
    pub fn new_getstackers(
×
223
        host: PeerHost,
×
224
        cycle_num: u64,
×
225
        tip_req: TipRequest,
×
226
    ) -> StacksHttpRequest {
×
227
        StacksHttpRequest::new_for_peer(
×
228
            host,
×
229
            "GET".into(),
×
230
            format!("/v3/stacker_set/{cycle_num}"),
×
231
            HttpRequestContents::new().for_tip(tip_req),
×
232
        )
233
        .expect("FATAL: failed to construct request from infallible data")
×
234
    }
×
235
}
236

237
impl StacksHttpResponse {
238
    pub fn decode_stacker_set(self) -> Result<GetStackersResponse, NetError> {
×
239
        let contents = self.get_http_payload_ok()?;
×
240
        let response_json: serde_json::Value = contents.try_into()?;
×
241
        let response: GetStackersResponse = serde_json::from_value(response_json)
×
242
            .map_err(|_e| Error::DecodeError("Failed to decode JSON".to_string()))?;
×
243
        Ok(response)
×
244
    }
×
245
}
246

247
#[cfg(test)]
248
mod test {
249
    use super::GetStackersErrors;
250

251
    #[test]
252
    // Test the formatting and error type strings of GetStackersErrors
253
    fn get_stackers_errors() {
1✔
254
        let not_available_err = GetStackersErrors::NotAvailableYet(
1✔
255
            crate::chainstate::coordinator::Error::PoXNotProcessedYet,
1✔
256
        );
1✔
257
        let other_err = GetStackersErrors::Other("foo".into());
1✔
258

259
        assert_eq!(
1✔
260
            not_available_err.error_type_string(),
1✔
261
            GetStackersErrors::NOT_AVAILABLE_ERR_TYPE
262
        );
263
        assert_eq!(
1✔
264
            other_err.error_type_string(),
1✔
265
            GetStackersErrors::OTHER_ERR_TYPE
266
        );
267

268
        assert!(not_available_err
1✔
269
            .to_string()
1✔
270
            .starts_with("Could not read reward set"));
1✔
271
        assert_eq!(other_err.to_string(), "foo".to_string());
1✔
272
    }
1✔
273
}
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