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

stacks-network / stacks-core / 25801484257-1

13 May 2026 01:15PM UTC coverage: 85.648% (-0.06%) from 85.712%
25801484257-1

Pull #7183

github

2d7e6d
web-flow
Merge 420cb597a into 31276d071
Pull Request #7183: Fix problematic transaction handling

110 of 130 new or added lines in 7 files covered. (84.62%)

5464 existing lines in 98 files now uncovered.

188263 of 219809 relevant lines covered (85.65%)

18940648.33 hits per line

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

82.95
/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::{self, json};
22
use stacks_common::codec::MAX_MESSAGE_LEN;
23
use stacks_common::types::net::PeerHost;
24
use stacks_common::util::secp256k1::MessageSignature;
25

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

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

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

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

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

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

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

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

89
        self.contract_identifier = Some(contract_identifier);
602,037✔
90
        self.chunk = Some(chunk);
602,037✔
91

92
        Ok(HttpRequestContents::new().query_string(query))
602,037✔
93
    }
602,037✔
94
}
95

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

103
impl StackerDBErrorCodes {
104
    pub fn code(&self) -> u32 {
32,666✔
105
        match self {
32,666✔
106
            Self::DataAlreadyExists => 0,
32,442✔
107
            Self::NoSuchSlot => 1,
182✔
108
            Self::BadSigner => 2,
42✔
109
        }
110
    }
32,666✔
111

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

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

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

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

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

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

212
                    let err_code = if slot_metadata_opt.is_some() {
16,333✔
213
                        if let NetError::BadSlotSigner(..) = e {
16,242✔
214
                            StackerDBErrorCodes::BadSigner
21✔
215
                        } else {
216
                            StackerDBErrorCodes::DataAlreadyExists
16,221✔
217
                        }
218
                    } else {
219
                        StackerDBErrorCodes::NoSuchSlot
91✔
220
                    };
221
                    let reason = serde_json::to_string(&err_code.clone().into_json())
16,333✔
222
                        .unwrap_or("(unable to encode JSON)".to_string());
16,333✔
223

224
                    let ack = StackerDBChunkAckData {
16,333✔
225
                        accepted: false,
16,333✔
226
                        reason: Some(reason),
16,333✔
227
                        metadata: slot_metadata_opt,
16,333✔
228
                        code: Some(err_code.code()),
16,333✔
229
                    };
16,333✔
230
                    return Ok(ack);
16,333✔
231
                }
585,702✔
232

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

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

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

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

269
                return Ok(ack);
585,702✔
270
            });
602,036✔
271

272
        let ack_resp = match ack_resp {
602,036✔
273
            Ok(ack) => ack,
602,035✔
274
            Err(response) => {
1✔
275
                return response.try_into_contents().map_err(NetError::from);
1✔
276
            }
277
        };
278

279
        if ack_resp.accepted {
602,035✔
280
            let push_chunk_data = StackerDBPushChunkData {
585,702✔
281
                contract_id: contract_identifier,
585,702✔
282
                rc_consensus_hash: node.with_node_state(|network, _, _, _, _| {
585,702✔
283
                    network.get_chain_view().rc_consensus_hash.clone()
585,702✔
284
                }),
585,702✔
285
                chunk_data: stackerdb_chunk,
585,702✔
286
            };
287
            node.set_relay_message(StacksMessageType::StackerDBPushChunk(push_chunk_data));
585,702✔
288
        }
16,333✔
289

290
        let preamble = HttpResponsePreamble::ok_json(&preamble);
602,035✔
291
        let body = HttpResponseContents::try_from_json(&ack_resp)?;
602,035✔
292
        Ok((preamble, body))
602,035✔
293
    }
602,036✔
294
}
295

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

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

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