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

stacks-network / stacks-core / 24140301216

08 Apr 2026 02:18PM UTC coverage: 46.817% (-38.9%) from 85.712%
24140301216

Pull #6959

github

279acf
web-flow
Merge efbee1783 into 882e27245
Pull Request #6959: Perf/cache epoch version in ClarityDatabase

66 of 149 new or added lines in 8 files covered. (44.3%)

85999 existing lines in 334 files now uncovered.

102056 of 217989 relevant lines covered (46.82%)

12315981.16 hits per line

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

6.94
/stackslib/src/net/api/blocksimulate.rs
1
// Copyright (C) 2025 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

16
use clarity::util::hash::bytes_to_hex;
17
use clarity::vm::types::PrincipalData;
18
use regex::{Captures, Regex};
19
use stacks_common::codec::{Error as CodecError, StacksMessageCodec, MAX_PAYLOAD_LEN};
20
use stacks_common::types::chainstate::StacksBlockId;
21
use stacks_common::types::net::PeerHost;
22
use stacks_common::util::hash::hex_bytes;
23
use url::form_urlencoded;
24

25
use crate::chainstate::burn::db::sortdb::SortitionDB;
26
use crate::chainstate::stacks::db::StacksChainState;
27
use crate::chainstate::stacks::{Error as ChainError, StacksTransaction};
28
use crate::net::api::blockreplay::{remine_nakamoto_block, RPCReplayedBlock};
29
use crate::net::http::{
30
    parse_json, Error, HttpContentType, HttpNotFound, HttpRequest, HttpRequestContents,
31
    HttpRequestPreamble, HttpResponse, HttpResponseContents, HttpResponsePayload,
32
    HttpResponsePreamble, HttpServerError,
33
};
34
use crate::net::httpcore::{RPCRequestHandler, StacksHttpResponse};
35
use crate::net::{Error as NetError, StacksHttpRequest, StacksNodeState};
36

37
#[derive(Clone, Serialize, Deserialize)]
38
pub struct RPCNakamotoBlockSimulateMint {
39
    pub principal: PrincipalData,
40
    pub amount: u128,
41
}
42

43
#[derive(Clone, Serialize, Deserialize)]
44
pub struct RPCNakamotoBlockSimulateBody {
45
    pub mint: Vec<RPCNakamotoBlockSimulateMint>,
46
    pub transactions_hex: Vec<String>,
47
}
48

49
#[derive(Clone)]
50
pub struct RPCNakamotoBlockSimulateRequestHandler {
51
    pub block_id: Option<StacksBlockId>,
52
    pub auth: Option<String>,
53
    pub profiler: bool,
54
    pub transactions: Vec<StacksTransaction>,
55
    pub mint: Vec<RPCNakamotoBlockSimulateMint>,
56
}
57

58
impl RPCNakamotoBlockSimulateRequestHandler {
59
    pub fn new(auth: Option<String>) -> Self {
970,340✔
60
        Self {
970,340✔
61
            block_id: None,
970,340✔
62
            auth,
970,340✔
63
            profiler: false,
970,340✔
64
            transactions: vec![],
970,340✔
65
            mint: vec![],
970,340✔
66
        }
970,340✔
67
    }
970,340✔
68

UNCOV
69
    fn parse_json(
×
UNCOV
70
        body: &[u8],
×
UNCOV
71
    ) -> Result<(Vec<StacksTransaction>, Vec<RPCNakamotoBlockSimulateMint>), Error> {
×
UNCOV
72
        let block_simulate_body: RPCNakamotoBlockSimulateBody = serde_json::from_slice(body)
×
UNCOV
73
            .map_err(|e| Error::DecodeError(format!("Failed to parse body: {e}")))?;
×
74

UNCOV
75
        let mut transactions = vec![];
×
76

UNCOV
77
        for tx_hex in block_simulate_body.transactions_hex {
×
UNCOV
78
            let tx_bytes =
×
UNCOV
79
                hex_bytes(&tx_hex).map_err(|_e| Error::DecodeError("Failed to parse tx".into()))?;
×
UNCOV
80
            let tx = StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).map_err(|e| {
×
81
                if let CodecError::DeserializeError(msg) = e {
×
82
                    Error::DecodeError(format!("Failed to deserialize transaction: {}", msg))
×
83
                } else {
84
                    e.into()
×
85
                }
86
            })?;
×
UNCOV
87
            transactions.push(tx);
×
88
        }
89

UNCOV
90
        Ok((transactions, block_simulate_body.mint))
×
UNCOV
91
    }
×
92

UNCOV
93
    pub fn block_simulate(
×
UNCOV
94
        &self,
×
UNCOV
95
        sortdb: &SortitionDB,
×
UNCOV
96
        chainstate: &mut StacksChainState,
×
UNCOV
97
    ) -> Result<RPCReplayedBlock, ChainError> {
×
UNCOV
98
        let Some(block_id) = &self.block_id else {
×
UNCOV
99
            return Err(ChainError::InvalidStacksBlock("block_id is None".into()));
×
100
        };
101

UNCOV
102
        let rpc_simulated_block = remine_nakamoto_block(
×
UNCOV
103
            block_id,
×
UNCOV
104
            sortdb,
×
UNCOV
105
            chainstate,
×
UNCOV
106
            self.profiler,
×
UNCOV
107
            |_| self.transactions.clone(),
×
UNCOV
108
            |tenure_tx| {
×
UNCOV
109
                if !self.mint.is_empty() {
×
UNCOV
110
                    tenure_tx.connection().as_transaction(|tx| {
×
UNCOV
111
                        tx.with_clarity_db(|ref mut db| {
×
UNCOV
112
                            for mint in &self.mint {
×
UNCOV
113
                                let mut balance = db.get_stx_balance_snapshot(&mint.principal)?;
×
UNCOV
114
                                balance.credit(mint.amount)?;
×
UNCOV
115
                                balance.save()?;
×
116
                            }
UNCOV
117
                            Ok(())
×
UNCOV
118
                        })
×
UNCOV
119
                    })?;
×
UNCOV
120
                }
×
UNCOV
121
                Ok(())
×
UNCOV
122
            },
×
UNCOV
123
        )?;
×
124

UNCOV
125
        Ok(rpc_simulated_block)
×
UNCOV
126
    }
×
127
}
128

129
/// Decode the HTTP request
130
impl HttpRequest for RPCNakamotoBlockSimulateRequestHandler {
131
    fn verb(&self) -> &'static str {
970,340✔
132
        "POST"
970,340✔
133
    }
970,340✔
134

135
    fn path_regex(&self) -> Regex {
1,940,680✔
136
        Regex::new(r#"^/v3/blocks/simulate/(?P<block_id>[0-9a-f]{64})$"#).unwrap()
1,940,680✔
137
    }
1,940,680✔
138

UNCOV
139
    fn metrics_identifier(&self) -> &str {
×
UNCOV
140
        "/v3/blocks/simulate/:block_id"
×
UNCOV
141
    }
×
142

143
    /// Try to decode this request.
144
    /// There's nothing to load here, so just make sure the request is well-formed.
UNCOV
145
    fn try_parse_request(
×
UNCOV
146
        &mut self,
×
UNCOV
147
        preamble: &HttpRequestPreamble,
×
UNCOV
148
        captures: &Captures,
×
UNCOV
149
        query: Option<&str>,
×
UNCOV
150
        body: &[u8],
×
UNCOV
151
    ) -> Result<HttpRequestContents, Error> {
×
152
        // If no authorization is set, then the block replay endpoint is not enabled
UNCOV
153
        let Some(password) = &self.auth else {
×
154
            return Err(Error::Http(400, "Bad Request.".into()));
×
155
        };
UNCOV
156
        let Some(auth_header) = preamble.headers.get("authorization") else {
×
UNCOV
157
            return Err(Error::Http(401, "Unauthorized".into()));
×
158
        };
UNCOV
159
        if auth_header != password {
×
160
            return Err(Error::Http(401, "Unauthorized".into()));
×
UNCOV
161
        }
×
162

UNCOV
163
        let block_id_str = captures
×
UNCOV
164
            .name("block_id")
×
UNCOV
165
            .ok_or_else(|| {
×
166
                Error::DecodeError("Failed to match path to block ID group".to_string())
×
167
            })?
×
UNCOV
168
            .as_str();
×
169

UNCOV
170
        let block_id = StacksBlockId::from_hex(block_id_str)
×
UNCOV
171
            .map_err(|_| Error::DecodeError("Invalid path: unparseable block id".to_string()))?;
×
172

UNCOV
173
        self.block_id = Some(block_id);
×
174

UNCOV
175
        if let Some(query_string) = query {
×
UNCOV
176
            for (key, value) in form_urlencoded::parse(query_string.as_bytes()) {
×
UNCOV
177
                if key == "profiler" {
×
UNCOV
178
                    if value == "1" {
×
UNCOV
179
                        self.profiler = true;
×
UNCOV
180
                        break;
×
181
                    }
×
182
                }
×
183
            }
UNCOV
184
        }
×
185

UNCOV
186
        if preamble.get_content_length() == 0 {
×
187
            return Err(Error::DecodeError(
×
188
                "Invalid Http request: expected non-zero-length body for block proposal endpoint"
×
189
                    .to_string(),
×
190
            ));
×
UNCOV
191
        }
×
UNCOV
192
        if preamble.get_content_length() > MAX_PAYLOAD_LEN {
×
193
            return Err(Error::DecodeError(
×
194
                "Invalid Http request: BlockProposal body is too big".to_string(),
×
195
            ));
×
UNCOV
196
        }
×
197

UNCOV
198
        (self.transactions, self.mint) = match preamble.content_type {
×
UNCOV
199
            Some(HttpContentType::JSON) => Self::parse_json(body)?,
×
200
            Some(_) => {
201
                return Err(Error::DecodeError(
×
202
                    "Wrong Content-Type for block proposal; expected application/json".to_string(),
×
203
                ))
×
204
            }
205
            None => {
206
                return Err(Error::DecodeError(
×
207
                    "Missing Content-Type for block simulation".to_string(),
×
208
                ))
×
209
            }
210
        };
211

UNCOV
212
        Ok(HttpRequestContents::new().query_string(query))
×
UNCOV
213
    }
×
214
}
215

216
impl RPCRequestHandler for RPCNakamotoBlockSimulateRequestHandler {
217
    /// Reset internal state
UNCOV
218
    fn restart(&mut self) {
×
UNCOV
219
        self.block_id = None;
×
UNCOV
220
    }
×
221

222
    /// Make the response
UNCOV
223
    fn try_handle_request(
×
UNCOV
224
        &mut self,
×
UNCOV
225
        preamble: HttpRequestPreamble,
×
UNCOV
226
        _contents: HttpRequestContents,
×
UNCOV
227
        node: &mut StacksNodeState,
×
UNCOV
228
    ) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> {
×
UNCOV
229
        let Some(block_id) = &self.block_id else {
×
230
            return Err(NetError::SendError("Missing `block_id`".into()));
×
231
        };
232

UNCOV
233
        let simulated_block_res =
×
UNCOV
234
            node.with_node_state(|_network, sortdb, chainstate, _mempool, _rpc_args| {
×
UNCOV
235
                self.block_simulate(sortdb, chainstate)
×
UNCOV
236
            });
×
237

238
        // start loading up the block
UNCOV
239
        let simulated_block = match simulated_block_res {
×
UNCOV
240
            Ok(simulated_block) => simulated_block,
×
241
            Err(ChainError::NoSuchBlockError) => {
UNCOV
242
                return StacksHttpResponse::new_error(
×
UNCOV
243
                    &preamble,
×
UNCOV
244
                    &HttpNotFound::new(format!("No such block {block_id}\n")),
×
245
                )
UNCOV
246
                .try_into_contents()
×
UNCOV
247
                .map_err(NetError::from)
×
248
            }
249
            Err(e) => {
×
250
                // nope -- error trying to check
251
                let msg = format!("Failed to simulate block {}: {:?}\n", &block_id, &e);
×
252
                warn!("{}", &msg);
×
253
                return StacksHttpResponse::new_error(&preamble, &HttpServerError::new(msg))
×
254
                    .try_into_contents()
×
255
                    .map_err(NetError::from);
×
256
            }
257
        };
258

UNCOV
259
        let preamble = HttpResponsePreamble::ok_json(&preamble);
×
UNCOV
260
        let body = HttpResponseContents::try_from_json(&simulated_block)?;
×
UNCOV
261
        Ok((preamble, body))
×
UNCOV
262
    }
×
263
}
264

265
impl StacksHttpRequest {
266
    /// Make a new block_replay request to this endpoint
UNCOV
267
    pub fn new_block_simulate(
×
UNCOV
268
        host: PeerHost,
×
UNCOV
269
        block_id: &StacksBlockId,
×
UNCOV
270
        transactions: &Vec<StacksTransaction>,
×
UNCOV
271
        mint: &Vec<RPCNakamotoBlockSimulateMint>,
×
UNCOV
272
    ) -> StacksHttpRequest {
×
UNCOV
273
        let transactions_hex = transactions
×
UNCOV
274
            .iter()
×
UNCOV
275
            .map(|transaction| bytes_to_hex(&transaction.serialize_to_vec()))
×
UNCOV
276
            .collect();
×
277

UNCOV
278
        let block_simulate_body = RPCNakamotoBlockSimulateBody {
×
UNCOV
279
            mint: mint.clone(),
×
UNCOV
280
            transactions_hex,
×
UNCOV
281
        };
×
282

UNCOV
283
        StacksHttpRequest::new_for_peer(
×
UNCOV
284
            host,
×
UNCOV
285
            "POST".into(),
×
UNCOV
286
            format!("/v3/blocks/simulate/{block_id}"),
×
UNCOV
287
            HttpRequestContents::new().payload_json(
×
UNCOV
288
                serde_json::to_value(block_simulate_body)
×
UNCOV
289
                    .expect("FATAL: failed to encode RPCNakamotoBlockSimulateBody"),
×
290
            ),
291
        )
UNCOV
292
        .expect("FATAL: failed to construct request from infallible data")
×
UNCOV
293
    }
×
294

UNCOV
295
    pub fn new_block_simulate_with_profiler(
×
UNCOV
296
        host: PeerHost,
×
UNCOV
297
        block_id: &StacksBlockId,
×
UNCOV
298
        profiler: bool,
×
UNCOV
299
        transactions: &Vec<StacksTransaction>,
×
UNCOV
300
        mint: &Vec<RPCNakamotoBlockSimulateMint>,
×
UNCOV
301
    ) -> StacksHttpRequest {
×
UNCOV
302
        let transactions_hex = transactions
×
UNCOV
303
            .iter()
×
UNCOV
304
            .map(|transaction| bytes_to_hex(&transaction.serialize_to_vec()))
×
UNCOV
305
            .collect();
×
306

UNCOV
307
        let block_simulate_body = RPCNakamotoBlockSimulateBody {
×
UNCOV
308
            mint: mint.clone(),
×
UNCOV
309
            transactions_hex,
×
UNCOV
310
        };
×
UNCOV
311
        StacksHttpRequest::new_for_peer(
×
UNCOV
312
            host,
×
UNCOV
313
            "POST".into(),
×
UNCOV
314
            format!("/v3/blocks/simulate/{block_id}"),
×
UNCOV
315
            HttpRequestContents::new()
×
UNCOV
316
                .query_arg(
×
UNCOV
317
                    "profiler".into(),
×
UNCOV
318
                    if profiler { "1".into() } else { "0".into() },
×
319
                )
UNCOV
320
                .payload_json(
×
UNCOV
321
                    serde_json::to_value(block_simulate_body)
×
UNCOV
322
                        .expect("FATAL: failed to encode RPCNakamotoBlockSimulateBody"),
×
323
                ),
324
        )
UNCOV
325
        .expect("FATAL: failed to construct request from infallible data")
×
UNCOV
326
    }
×
327
}
328

329
/// Decode the HTTP response
330
impl HttpResponse for RPCNakamotoBlockSimulateRequestHandler {
331
    /// Decode this response from a byte stream.  This is called by the client to decode this
332
    /// message
UNCOV
333
    fn try_parse_response(
×
UNCOV
334
        &self,
×
UNCOV
335
        preamble: &HttpResponsePreamble,
×
UNCOV
336
        body: &[u8],
×
UNCOV
337
    ) -> Result<HttpResponsePayload, Error> {
×
UNCOV
338
        let rpc_replayed_block: RPCReplayedBlock = parse_json(preamble, body)?;
×
UNCOV
339
        Ok(HttpResponsePayload::try_from_json(rpc_replayed_block)?)
×
UNCOV
340
    }
×
341
}
342

343
impl StacksHttpResponse {
UNCOV
344
    pub fn decode_simulated_block(self) -> Result<RPCReplayedBlock, NetError> {
×
UNCOV
345
        let contents = self.get_http_payload_ok()?;
×
UNCOV
346
        let response_json: serde_json::Value = contents.try_into()?;
×
UNCOV
347
        let replayed_block: RPCReplayedBlock = serde_json::from_value(response_json)
×
UNCOV
348
            .map_err(|_e| Error::DecodeError("Failed to decode JSON".to_string()))?;
×
UNCOV
349
        Ok(replayed_block)
×
UNCOV
350
    }
×
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