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

stacks-network / stacks-core / 23526303949

25 Mar 2026 05:23AM UTC coverage: 85.692% (-0.02%) from 85.712%
23526303949

Pull #7016

github

371660
web-flow
Merge 1d9ce4783 into 1e36cefa9
Pull Request #7016: fix: signature order check

1 of 1 new or added line in 1 file covered. (100.0%)

158 existing lines in 34 files now uncovered.

186535 of 217681 relevant lines covered (85.69%)

17330188.5 hits per line

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

82.49
/stackslib/src/net/api/poststackerdbchunk.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 clarity::vm::representations::{CONTRACT_NAME_REGEX_STRING, STANDARD_PRINCIPAL_REGEX_STRING};
18
use clarity::vm::types::QualifiedContractIdentifier;
19
use libstackerdb::{StackerDBChunkAckData, StackerDBChunkData};
20
use regex::{Captures, Regex};
21
use serde_json;
22
use serde_json::json;
23
use stacks_common::codec::MAX_MESSAGE_LEN;
24
use stacks_common::types::net::PeerHost;
25
use stacks_common::util::secp256k1::MessageSignature;
26

27
use crate::net::http::{
28
    parse_json, Error, HttpNotFound, HttpRequest, HttpRequestContents, HttpRequestPreamble,
29
    HttpResponse, HttpResponseContents, HttpResponsePayload, HttpResponsePreamble, HttpServerError,
30
};
31
use crate::net::httpcore::{request, RPCRequestHandler, StacksHttpRequest, StacksHttpResponse};
32
use crate::net::{Error as NetError, StackerDBPushChunkData, StacksMessageType, StacksNodeState};
33

34
#[derive(Clone)]
35
pub struct RPCPostStackerDBChunkRequestHandler {
36
    pub contract_identifier: Option<QualifiedContractIdentifier>,
37
    pub chunk: Option<StackerDBChunkData>,
38
}
39
impl RPCPostStackerDBChunkRequestHandler {
40
    pub fn new() -> Self {
995,588✔
41
        Self {
995,588✔
42
            contract_identifier: None,
995,588✔
43
            chunk: None,
995,588✔
44
        }
995,588✔
45
    }
995,588✔
46
}
47

48
/// Decode the HTTP request
49
impl HttpRequest for RPCPostStackerDBChunkRequestHandler {
50
    fn verb(&self) -> &'static str {
995,587✔
51
        "POST"
995,587✔
52
    }
995,587✔
53

54
    fn path_regex(&self) -> Regex {
1,991,175✔
55
        Regex::new(&format!(
1,991,175✔
56
            r#"^/v2/stackerdb/(?P<address>{})/(?P<contract>{})/chunks$"#,
1,991,175✔
57
            *STANDARD_PRINCIPAL_REGEX_STRING, *CONTRACT_NAME_REGEX_STRING
1,991,175✔
58
        ))
1,991,175✔
59
        .unwrap()
1,991,175✔
60
    }
1,991,175✔
61

62
    fn metrics_identifier(&self) -> &str {
514,374✔
63
        "/v2/stackerdb/:principal/:contract_name/chunks"
514,374✔
64
    }
514,374✔
65

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

81
        if preamble.get_content_length() > MAX_MESSAGE_LEN {
514,375✔
82
            return Err(Error::DecodeError(
×
83
                "Invalid Http request: PostStackerDBChunk body is too big".to_string(),
×
84
            ));
×
85
        }
514,375✔
86

87
        let contract_identifier = request::get_contract_address(captures, "address", "contract")?;
514,375✔
88
        let chunk: StackerDBChunkData = serde_json::from_slice(body).map_err(Error::JsonError)?;
514,375✔
89

90
        self.contract_identifier = Some(contract_identifier);
514,375✔
91
        self.chunk = Some(chunk);
514,375✔
92

93
        Ok(HttpRequestContents::new().query_string(query))
514,375✔
94
    }
514,375✔
95
}
96

97
#[derive(Debug, Clone, PartialEq)]
98
pub enum StackerDBErrorCodes {
99
    DataAlreadyExists,
100
    NoSuchSlot,
101
    BadSigner,
102
}
103

104
impl StackerDBErrorCodes {
105
    pub fn code(&self) -> u32 {
18,420✔
106
        match self {
18,420✔
107
            Self::DataAlreadyExists => 0,
18,380✔
108
            Self::NoSuchSlot => 1,
2✔
109
            Self::BadSigner => 2,
38✔
110
        }
111
    }
18,420✔
112

113
    #[cfg_attr(test, mutants::skip)]
114
    pub fn reason(&self) -> &'static str {
9,210✔
115
        match self {
9,210✔
116
            Self::DataAlreadyExists => "Data for this slot and version already exist",
9,190✔
117
            Self::NoSuchSlot => "No such StackerDB slot",
1✔
118
            Self::BadSigner => "Signature does not match slot signer",
19✔
119
        }
120
    }
9,210✔
121

122
    pub fn into_json(self) -> serde_json::Value {
9,210✔
123
        json!({
9,210✔
124
            "code": self.code(),
9,210✔
125
            "message": format!("{:?}", &self),
9,210✔
126
            "reason": self.reason()
9,210✔
127
        })
128
    }
9,210✔
129

130
    #[cfg_attr(test, mutants::skip)]
131
    pub fn from_code(code: u32) -> Option<Self> {
2,968✔
132
        match code {
2,968✔
133
            0 => Some(Self::DataAlreadyExists),
2,968✔
UNCOV
134
            1 => Some(Self::NoSuchSlot),
×
135
            2 => Some(Self::BadSigner),
×
136
            _ => None,
×
137
        }
138
    }
2,968✔
139
}
140

141
impl RPCRequestHandler for RPCPostStackerDBChunkRequestHandler {
142
    /// Reset internal state
143
    fn restart(&mut self) {
514,375✔
144
        self.contract_identifier = None;
514,375✔
145
        self.chunk = None;
514,375✔
146
    }
514,375✔
147

148
    /// Make the response.
149
    fn try_handle_request(
514,374✔
150
        &mut self,
514,374✔
151
        preamble: HttpRequestPreamble,
514,374✔
152
        _contents: HttpRequestContents,
514,374✔
153
        node: &mut StacksNodeState,
514,374✔
154
    ) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> {
514,374✔
155
        let contract_identifier = self
514,374✔
156
            .contract_identifier
514,374✔
157
            .take()
514,374✔
158
            .ok_or(NetError::SendError("`contract_identifier` not set".into()))?;
514,374✔
159
        let stackerdb_chunk = self
514,374✔
160
            .chunk
514,374✔
161
            .take()
514,374✔
162
            .ok_or(NetError::SendError("`chunk` not set".into()))?;
514,374✔
163

164
        let ack_resp =
514,374✔
165
            node.with_node_state(|network, _sortdb, _chainstate, _mempool, _rpc_args| {
514,374✔
166
                let tx = if let Ok(tx) = network.stackerdbs_tx_begin(&contract_identifier) {
514,374✔
167
                    tx
514,373✔
168
                } else {
169
                    return Err(StacksHttpResponse::new_error(
1✔
170
                        &preamble,
1✔
171
                        &HttpNotFound::new("StackerDB not found".to_string()),
1✔
172
                    ));
1✔
173
                };
174
                if let Err(_e) = tx.get_stackerdb_id(&contract_identifier) {
514,373✔
175
                    // shouldn't be necessary (this is checked against the peer network's configured DBs),
176
                    // but you never know.
177
                    return Err(StacksHttpResponse::new_error(
×
178
                        &preamble,
×
179
                        &HttpNotFound::new("StackerDB not found".to_string()),
×
180
                    ));
×
181
                }
514,373✔
182
                if let Err(e) = tx.try_replace_chunk(
514,373✔
183
                    &contract_identifier,
514,373✔
184
                    &stackerdb_chunk.get_slot_metadata(),
514,373✔
185
                    &stackerdb_chunk.data,
514,373✔
186
                ) {
514,373✔
187
                    test_debug!(
9,210✔
188
                        "Failed to replace chunk {}.{} in {}: {:?}",
189
                        stackerdb_chunk.slot_id,
190
                        stackerdb_chunk.slot_version,
191
                        &contract_identifier,
×
192
                        &e
×
193
                    );
194
                    let slot_metadata_opt =
9,210✔
195
                        match tx.get_slot_metadata(&contract_identifier, stackerdb_chunk.slot_id) {
9,210✔
196
                            Ok(slot_opt) => slot_opt,
9,210✔
197
                            Err(e) => {
×
198
                                // some other error
199
                                error!("Failed to load replaced StackerDB chunk metadata";
×
200
                                       "smart_contract_id" => contract_identifier.to_string(),
×
201
                                       "error" => format!("{:?}", &e)
×
202
                                );
203
                                return Err(StacksHttpResponse::new_error(
×
204
                                    &preamble,
×
205
                                    &HttpServerError::new(format!(
×
206
                                        "Failed to load StackerDB chunk for {}: {:?}",
×
207
                                        &contract_identifier, &e
×
208
                                    )),
×
209
                                ));
×
210
                            }
211
                        };
212

213
                    let err_code = if slot_metadata_opt.is_some() {
9,210✔
214
                        if let NetError::BadSlotSigner(..) = e {
9,209✔
215
                            StackerDBErrorCodes::BadSigner
19✔
216
                        } else {
217
                            StackerDBErrorCodes::DataAlreadyExists
9,190✔
218
                        }
219
                    } else {
220
                        StackerDBErrorCodes::NoSuchSlot
1✔
221
                    };
222
                    let reason = serde_json::to_string(&err_code.clone().into_json())
9,210✔
223
                        .unwrap_or("(unable to encode JSON)".to_string());
9,210✔
224

225
                    let ack = StackerDBChunkAckData {
9,210✔
226
                        accepted: false,
9,210✔
227
                        reason: Some(reason),
9,210✔
228
                        metadata: slot_metadata_opt,
9,210✔
229
                        code: Some(err_code.code()),
9,210✔
230
                    };
9,210✔
231
                    return Ok(ack);
9,210✔
232
                }
505,163✔
233

234
                let slot_metadata = if let Ok(Some(md)) =
505,163✔
235
                    tx.get_slot_metadata(&contract_identifier, stackerdb_chunk.slot_id)
505,163✔
236
                {
237
                    md
505,163✔
238
                } else {
239
                    return Err(StacksHttpResponse::new_error(
×
240
                        &preamble,
×
241
                        &HttpServerError::new(
×
242
                            "Failed to load slot metadata after storing chunk".to_string(),
×
243
                        ),
×
244
                    ));
×
245
                };
246

247
                if let Err(e) = tx.commit() {
505,163✔
248
                    return Err(StacksHttpResponse::new_error(
×
249
                        &preamble,
×
250
                        &HttpServerError::new(format!("Failed to commit StackerDB tx: {:?}", &e)),
×
251
                    ));
×
252
                }
505,163✔
253

254
                debug!(
505,163✔
255
                    "Wrote {}-byte chunk to {} slot {} version {}",
256
                    &stackerdb_chunk.data.len(),
×
257
                    &contract_identifier,
×
258
                    stackerdb_chunk.slot_id,
259
                    stackerdb_chunk.slot_version
260
                );
261

262
                // success!
263
                let ack = StackerDBChunkAckData {
505,163✔
264
                    accepted: true,
505,163✔
265
                    reason: None,
505,163✔
266
                    metadata: Some(slot_metadata),
505,163✔
267
                    code: None,
505,163✔
268
                };
505,163✔
269

270
                return Ok(ack);
505,163✔
271
            });
514,374✔
272

273
        let ack_resp = match ack_resp {
514,374✔
274
            Ok(ack) => ack,
514,373✔
275
            Err(response) => {
1✔
276
                return response.try_into_contents().map_err(NetError::from);
1✔
277
            }
278
        };
279

280
        if ack_resp.accepted {
514,373✔
281
            let push_chunk_data = StackerDBPushChunkData {
505,163✔
282
                contract_id: contract_identifier,
505,163✔
283
                rc_consensus_hash: node.with_node_state(|network, _, _, _, _| {
505,163✔
284
                    network.get_chain_view().rc_consensus_hash.clone()
505,163✔
285
                }),
505,163✔
286
                chunk_data: stackerdb_chunk,
505,163✔
287
            };
288
            node.set_relay_message(StacksMessageType::StackerDBPushChunk(push_chunk_data));
505,163✔
289
        }
9,210✔
290

291
        let preamble = HttpResponsePreamble::ok_json(&preamble);
514,373✔
292
        let body = HttpResponseContents::try_from_json(&ack_resp)?;
514,373✔
293
        Ok((preamble, body))
514,373✔
294
    }
514,374✔
295
}
296

297
/// Decode the HTTP response
298
impl HttpResponse for RPCPostStackerDBChunkRequestHandler {
299
    /// Decode this response from a byte stream.  This is called by the client to decode this
300
    /// message
301
    fn try_parse_response(
5✔
302
        &self,
5✔
303
        preamble: &HttpResponsePreamble,
5✔
304
        body: &[u8],
5✔
305
    ) -> Result<HttpResponsePayload, Error> {
5✔
306
        let ack: StackerDBChunkAckData = parse_json(preamble, body)?;
5✔
307
        Ok(HttpResponsePayload::try_from_json(ack)?)
5✔
308
    }
5✔
309
}
310

311
impl StacksHttpRequest {
312
    pub fn new_post_stackerdb_chunk(
7✔
313
        host: PeerHost,
7✔
314
        stackerdb_contract_id: QualifiedContractIdentifier,
7✔
315
        slot_id: u32,
7✔
316
        slot_version: u32,
7✔
317
        sig: MessageSignature,
7✔
318
        data: Vec<u8>,
7✔
319
    ) -> StacksHttpRequest {
7✔
320
        StacksHttpRequest::new_for_peer(
7✔
321
            host,
7✔
322
            "POST".into(),
7✔
323
            format!(
7✔
324
                "/v2/stackerdb/{}/{}/chunks",
325
                &stackerdb_contract_id.issuer, &stackerdb_contract_id.name
7✔
326
            ),
327
            HttpRequestContents::new().payload_json(
7✔
328
                serde_json::to_value(StackerDBChunkData {
7✔
329
                    slot_id,
7✔
330
                    slot_version,
7✔
331
                    sig,
7✔
332
                    data,
7✔
333
                })
7✔
334
                .expect("FATAL: failed to construct JSON from infallible structure"),
7✔
335
            ),
336
        )
337
        .expect("FATAL: failed to construct request from infallible data")
7✔
338
    }
7✔
339
}
340

341
impl StacksHttpResponse {
342
    /// Decode an HTTP response into a chunk
343
    /// If it fails, return Self::Error(..)
344
    pub fn decode_stackerdb_chunk_ack(self) -> Result<StackerDBChunkAckData, NetError> {
5✔
345
        let contents = self.get_http_payload_ok()?;
5✔
346
        let response_json: serde_json::Value = contents.try_into()?;
5✔
347
        let data: StackerDBChunkAckData = serde_json::from_value(response_json)
5✔
348
            .map_err(|_e| Error::DecodeError("Failed to decode JSON".to_string()))?;
5✔
349
        Ok(data)
5✔
350
    }
5✔
351
}
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