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

stacks-network / stacks-core / 25584487095-1

08 May 2026 11:26PM UTC coverage: 85.6% (-0.1%) from 85.712%
25584487095-1

Pull #7166

github

0d0103
web-flow
Merge 0c313b8dc into 5afe38051
Pull Request #7166: Move composites into stacks-core and refactor workflows

187053 of 218521 relevant lines covered (85.6%)

16603804.77 hits per line

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

80.14
/stackslib/src/net/api/postblock_proposal.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::VecDeque;
18
use std::hash::{DefaultHasher, Hash, Hasher};
19
#[cfg(any(test, feature = "testing"))]
20
use std::sync::LazyLock;
21
use std::thread::{self, JoinHandle};
22
use std::time::{Duration, Instant};
23

24
use clarity::vm::contexts::AbortCallback;
25
use clarity::vm::costs::ExecutionCost;
26
use clarity::vm::events::StacksTransactionEvent;
27
use clarity::vm::types::{ResponseData, TupleData};
28
use clarity::vm::Value;
29
use regex::{Captures, Regex};
30
use serde::Deserialize;
31
use stacks_common::codec::{Error as CodecError, StacksMessageCodec, MAX_PAYLOAD_LEN};
32
use stacks_common::consts::CHAIN_ID_MAINNET;
33
use stacks_common::types::chainstate::{ConsensusHash, StacksBlockId};
34
use stacks_common::util::get_epoch_time_secs;
35
use stacks_common::util::hash::{hex_bytes, to_hex, Sha512Trunc256Sum};
36
#[cfg(any(test, feature = "testing"))]
37
use stacks_common::util::tests::TestFlag;
38

39
use crate::burnchains::Txid;
40
use crate::chainstate::burn::db::sortdb::{SortitionDB, SortitionHandleConn};
41
use crate::chainstate::nakamoto::miner::{
42
    make_mem_abort_callback, MinerTenureInfoCause, NakamotoBlockBuilder,
43
};
44
use crate::chainstate::nakamoto::{NakamotoBlock, NakamotoChainState, NAKAMOTO_BLOCK_VERSION};
45
use crate::chainstate::stacks::address::PoxAddress;
46
use crate::chainstate::stacks::boot::PoxVersions;
47
use crate::chainstate::stacks::db::{StacksBlockHeaderTypes, StacksChainState, StacksHeaderInfo};
48
use crate::chainstate::stacks::miner::{
49
    BlockBuilder, BlockLimitFunction, TransactionError, TransactionProblematic, TransactionResult,
50
    TransactionSkipped,
51
};
52
use crate::chainstate::stacks::{Error as ChainError, StacksTransaction, TransactionPayload};
53
use crate::clarity_vm::clarity::ClarityError;
54
use crate::config::DEFAULT_MAX_TENURE_BYTES;
55
use crate::core::mempool::ProposalCallbackReceiver;
56
use crate::net::connection::ConnectionOptions;
57
use crate::net::http::{
58
    http_reason, parse_json, Error, HttpContentType, HttpRequest, HttpRequestContents,
59
    HttpRequestPreamble, HttpResponse, HttpResponseContents, HttpResponsePayload,
60
    HttpResponsePreamble,
61
};
62
use crate::net::httpcore::RPCRequestHandler;
63
use crate::net::{Error as NetError, StacksNodeState};
64
use crate::util_lib::db::Error as db_error;
65

66
/// Test flag to stall block validation per endpoint with a matching passphrase
67
#[cfg(any(test, feature = "testing"))]
68
pub static TEST_VALIDATE_STALL: LazyLock<TestFlag<Vec<Option<String>>>> =
69
    LazyLock::new(TestFlag::default);
70

71
#[cfg(any(test, feature = "testing"))]
72
/// Artificial delay to add to block validation.
73
pub static TEST_VALIDATE_DELAY_DURATION_SECS: LazyLock<TestFlag<u64>> =
74
    LazyLock::new(TestFlag::default);
75

76
#[cfg(any(test, feature = "testing"))]
77
/// Mock for the set of transactions that must be replayed
78
pub static TEST_REPLAY_TRANSACTIONS: LazyLock<
79
    TestFlag<std::collections::VecDeque<StacksTransaction>>,
80
> = LazyLock::new(TestFlag::default);
81

82
#[cfg(any(test, feature = "testing"))]
83
/// Whether to reject any transaction while we're in a replay set.
84
pub static TEST_REJECT_REPLAY_TXS: LazyLock<TestFlag<bool>> = LazyLock::new(TestFlag::default);
85

86
// This enum is used to supply a `reason_code` for validation
87
//  rejection responses. This is serialized as an enum with string
88
//  type (in jsonschema terminology).
89
define_u8_enum![ValidateRejectCode {
90
    BadBlockHash = 0,
91
    BadTransaction = 1,
92
    InvalidBlock = 2,
93
    ChainstateError = 3,
94
    UnknownParent = 4,
95
    NonCanonicalTenure = 5,
96
    NoSuchTenure = 6,
97
    InvalidTransactionReplay = 7,
98
    InvalidParentBlock = 8,
99
    InvalidTimestamp = 9,
100
    NetworkChainMismatch = 10,
101
    NotFoundError = 11,
102
    ProblematicTransaction = 12
103
}];
104

105
pub static TOO_MANY_REQUESTS_STATUS: u16 = 429;
106

9,008✔
107
impl TryFrom<u8> for ValidateRejectCode {
9,008✔
108
    type Error = CodecError;
9,008✔
109
    fn try_from(value: u8) -> Result<Self, Self::Error> {
9,008✔
110
        Self::from_u8(value)
111
            .ok_or_else(|| CodecError::DeserializeError(format!("Unknown type prefix: {value}")))
112
    }
21,641✔
113
}
21,641✔
114

21,641✔
115
fn hex_ser_block<S: serde::Serializer>(b: &NakamotoBlock, s: S) -> Result<S::Ok, S::Error> {
21,641✔
116
    let inst = to_hex(&b.serialize_to_vec());
117
    s.serialize_str(inst.as_str())
86,461✔
118
}
86,461✔
119

86,461✔
120
fn hex_deser_block<'de, D: serde::Deserializer<'de>>(d: D) -> Result<NakamotoBlock, D::Error> {
86,461✔
121
    let inst_str = String::deserialize(d)?;
86,461✔
122
    let bytes = hex_bytes(&inst_str).map_err(serde::de::Error::custom)?;
123
    NakamotoBlock::consensus_deserialize(&mut bytes.as_slice()).map_err(serde::de::Error::custom)
124
}
125

126
/// A response for block proposal validation
127
///  that the stacks-node thinks should be rejected.
128
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
129
pub struct BlockValidateReject {
130
    pub signer_signature_hash: Sha512Trunc256Sum,
131
    pub reason: String,
132
    pub reason_code: ValidateRejectCode,
133
    /// The txid of the transaction that caused the block to be rejected, if any
134
    #[serde(default)]
135
    pub failed_txid: Option<Txid>,
136
}
137

138
#[derive(Debug, Clone, PartialEq)]
139
pub struct BlockValidateRejectReason {
140
    pub reason: String,
141
    pub reason_code: ValidateRejectCode,
142
    /// The txid of the transaction that caused the block to be rejected, if any
143
    pub failed_txid: Option<Txid>,
144
}
145

146
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
147
pub enum BlockProposalResult {
148
    Accepted,
149
    Error,
150
}
151

152
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
153
pub struct BlockProposalResponse {
154
    pub result: BlockProposalResult,
155
    pub message: String,
156
}
157

158
impl<T> From<T> for BlockValidateRejectReason
159
where
88✔
160
    T: Into<ChainError>,
88✔
161
{
88✔
162
    fn from(value: T) -> Self {
72✔
163
        let ce: ChainError = value.into();
16✔
164
        let reason_code = match ce {
165
            ChainError::DBError(db_error::NotFoundError) => ValidateRejectCode::NotFoundError,
88✔
166
            _ => ValidateRejectCode::ChainstateError,
88✔
167
        };
88✔
168
        Self {
88✔
169
            reason: format!("Chainstate Error: {ce}"),
88✔
170
            reason_code,
88✔
171
            failed_txid: None,
172
        }
173
    }
174
}
175

176
/// A response for block proposal validation
177
///  that the stacks-node thinks is acceptable.
178
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
179
pub struct BlockValidateOk {
180
    pub signer_signature_hash: Sha512Trunc256Sum,
181
    pub cost: ExecutionCost,
182
    pub size: u64,
183
    pub validation_time_ms: u64,
184
    /// If a block was validated by a transaction replay set,
185
    /// then this returns `Some` with the hash of the replay set.
186
    pub replay_tx_hash: Option<u64>,
187
    /// If a block was validated by a transaction replay set,
188
    /// then this is true if this block exhausted the set of transactions.
189
    pub replay_tx_exhausted: bool,
190
}
191

192
/// This enum is used for serializing the response to block
193
/// proposal validation.
194
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
195
#[serde(tag = "result")]
196
pub enum BlockValidateResponse {
197
    Ok(BlockValidateOk),
198
    Reject(BlockValidateReject),
199
}
3,854✔
200

3,854✔
201
impl From<Result<BlockValidateOk, BlockValidateReject>> for BlockValidateResponse {
3,790✔
202
    fn from(value: Result<BlockValidateOk, BlockValidateReject>) -> Self {
64✔
203
        match value {
204
            Ok(o) => BlockValidateResponse::Ok(o),
3,854✔
205
            Err(e) => BlockValidateResponse::Reject(e),
206
        }
207
    }
208
}
209

34,872✔
210
impl BlockValidateResponse {
34,872✔
211
    /// Get the signer signature hash from the response
34,242✔
212
    pub fn signer_signature_hash(&self) -> &Sha512Trunc256Sum {
630✔
213
        match self {
214
            BlockValidateResponse::Ok(o) => &o.signer_signature_hash,
34,872✔
215
            BlockValidateResponse::Reject(r) => &r.signer_signature_hash,
216
        }
217
    }
218
}
30,827✔
219

30,827✔
220
#[cfg(any(test, feature = "testing"))]
221
fn fault_injection_validation_stall(auth_token: Option<String>) {
64✔
222
    if TEST_VALIDATE_STALL.get().contains(&auth_token) {
66,336✔
223
        // Do an extra check just so we don't log EVERY time.
66,272✔
224
        warn!("Block validation is stalled due to testing directive."; "auth_token" => ?auth_token);
66,272✔
225
        while TEST_VALIDATE_STALL.get().contains(&auth_token) {
64✔
226
            std::thread::sleep(std::time::Duration::from_millis(10));
227
        }
228
        info!(
30,763✔
229
            "Block validation is no longer stalled due to testing directive. Continuing..."; "auth_token" => ?auth_token
30,827✔
230
        );
231
    }
232
}
233

234
#[cfg(not(any(test, feature = "testing")))]
235
fn fault_injection_validation_stall(_auth_token: Option<String>) {}
30,827✔
236

30,827✔
237
#[cfg(any(test, feature = "testing"))]
30,827✔
238
fn fault_injection_validation_delay() {
30,411✔
239
    let delay = TEST_VALIDATE_DELAY_DURATION_SECS.get();
416✔
240
    if delay == 0 {
416✔
241
        return;
416✔
242
    }
30,827✔
243
    warn!("Sleeping for {} seconds to simulate slow processing", delay);
244
    thread::sleep(Duration::from_secs(delay));
245
}
246

247
#[cfg(not(any(test, feature = "testing")))]
248
fn fault_injection_validation_delay() {}
496✔
249

496✔
250
#[cfg(any(test, feature = "testing"))]
496✔
251
fn fault_injection_reject_replay_txs() -> Result<(), BlockValidateRejectReason> {
64✔
252
    let reject = TEST_REJECT_REPLAY_TXS.get();
64✔
253
    if reject {
64✔
254
        Err(BlockValidateRejectReason {
64✔
255
            reason_code: ValidateRejectCode::InvalidTransactionReplay,
64✔
256
            reason: "Rejected by test flag".into(),
257
            failed_txid: None,
432✔
258
        })
259
    } else {
496✔
260
        Ok(())
261
    }
262
}
263

264
#[cfg(not(any(test, feature = "testing")))]
265
fn fault_injection_reject_replay_txs() -> Result<(), BlockValidateRejectReason> {
266
    Ok(())
267
}
268

269
/// Represents a block proposed to the `v3/block_proposal` endpoint for validation
270
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
271
pub struct NakamotoBlockProposal {
272
    /// Proposed block
273
    #[serde(serialize_with = "hex_ser_block", deserialize_with = "hex_deser_block")]
274
    pub block: NakamotoBlock,
275
    /// Identifies which chain block is for (Mainnet, Testnet, etc.)
276
    pub chain_id: u32,
277
    /// Optional transaction replay set
278
    pub replay_txs: Option<Vec<StacksTransaction>>,
677✔
279
}
677✔
280

×
281
fn match_result_ok(value: &Value) -> Option<&Value> {
282
    let Value::Response(ResponseData { committed, data }) = value else {
677✔
283
        return None;
×
284
    };
677✔
285
    if !committed {
677✔
286
        return None;
677✔
287
    }
288
    Some(data.as_ref())
1,354✔
289
}
1,354✔
290

1,354✔
291
fn match_tuple(value: &Value) -> Option<&TupleData> {
292
    if let Value::Tuple(data) = value {
×
293
        Some(data)
294
    } else {
1,354✔
295
        None
296
    }
2,740,979✔
297
}
2,740,979✔
298

299
pub fn is_event_pox_addr_valid(is_mainnet: bool, event: &StacksTransactionEvent) -> bool {
2,739,640✔
300
    let StacksTransactionEvent::SmartContractEvent(event) = event else {
301
        // only smart contract events are relevant, so everything else is "okay"
1,339✔
302
        return true;
303
    };
644✔
304
    if !event.key.0.is_boot() {
695✔
305
        // only boot code events are relevant, so everything else is "okay"
695✔
306
        return true;
307
    }
18✔
308
    if event.key.0.name.as_str() != PoxVersions::Pox4.get_name_str() {
677✔
309
        // only pox events are relevant
677✔
310
        return true;
311
    }
×
312
    if &event.key.1 != "print" {
677✔
313
        // only look at print events
677✔
314
        return true;
315
    }
×
316
    let Some(pox_event_tuple) = match_result_ok(&event.value) else {
317
        // only care about (okay ...) results
677✔
318
        return true;
319
    };
×
320
    let Some(outer_tuple_data) = match_tuple(&pox_event_tuple) else {
321
        // should be unreachable
677✔
322
        return true;
323
    };
×
324
    let Ok(data_tuple) = outer_tuple_data.get("data") else {
325
        // should be unreachable
677✔
326
        return true;
327
    };
×
328
    let Some(data_tuple_data) = match_tuple(&data_tuple) else {
329
        // should be unreachable
677✔
330
        return true;
331
    };
24✔
332
    let Ok(pox_addr_tuple) = data_tuple_data.get("pox-addr") else {
333
        // should be unreachable
334
        return true;
653✔
335
    };
39✔
336

25✔
337
    let pox_addr_value = if let Value::Optional(data) = pox_addr_tuple {
14✔
338
        match data.data {
339
            None => return true,
340
            Some(ref inner) => inner.as_ref(),
614✔
341
        }
342
    } else {
343
        pox_addr_tuple
628✔
344
    };
2,740,979✔
345

346
    PoxAddress::try_from_pox_tuple(is_mainnet, pox_addr_value).is_some()
347
}
30,827✔
348

30,827✔
349
impl NakamotoBlockProposal {
30,827✔
350
    fn spawn_validation_thread(
30,827✔
351
        self,
30,827✔
352
        sortdb: SortitionDB,
30,827✔
353
        mut chainstate: StacksChainState,
30,827✔
354
        receiver: Box<dyn ProposalCallbackReceiver>,
30,827✔
355
        connection_opts: &ConnectionOptions,
30,827✔
356
    ) -> Result<JoinHandle<()>, std::io::Error> {
30,827✔
357
        let timeout_secs = connection_opts.block_proposal_validation_timeout_secs;
30,827✔
358
        let max_tx_mem_bytes = connection_opts.block_proposal_max_tx_mem_bytes;
30,827✔
359
        let auth_token = connection_opts.auth_token.clone();
30,827✔
360
        thread::Builder::new()
30,827✔
361
            .name("block-proposal".into())
30,827✔
362
            .spawn(move || {
514✔
363
                let result = self
514✔
364
                    .validate(
514✔
365
                        &sortdb,
514✔
366
                        &mut chainstate,
514✔
367
                        timeout_secs,
30,827✔
368
                        max_tx_mem_bytes,
30,827✔
369
                        auth_token,
30,827✔
370
                    )
371
                    .map_err(|reason| BlockValidateReject {
372
                        signer_signature_hash: self.block.header.signer_signature_hash(),
373
                        reason_code: reason.reason_code,
374
                        reason: reason.reason,
375
                        failed_txid: reason.failed_txid,
376
                    });
377
                receiver.notify_proposal_result(result);
30,603✔
378
            })
30,603✔
379
    }
30,603✔
380

30,603✔
381
    /// DO NOT CALL FROM CONSENSUS CODE
30,603✔
382
    ///
30,603✔
383
    /// Check to see if a block builds atop the highest block in a given tenure.
30,603✔
384
    /// That is:
30,603✔
385
    /// - its parent must exist, and
386
    /// - its parent must be as high as the highest block in the given tenure.
30,603✔
387
    fn check_block_builds_on_highest_block_in_tenure(
×
388
        chainstate: &StacksChainState,
×
389
        sortdb: &SortitionDB,
×
390
        tenure_id: &ConsensusHash,
×
391
        parent_block_id: &StacksBlockId,
392
    ) -> Result<(), BlockValidateRejectReason> {
×
393
        let Some(highest_header) = NakamotoChainState::find_highest_known_block_header_in_tenure(
394
            chainstate, sortdb, tenure_id,
395
        )
396
        .map_err(|e| BlockValidateRejectReason {
397
            reason_code: ValidateRejectCode::ChainstateError,
×
398
            reason: format!("Failed to query highest block in tenure ID: {:?}", &e),
×
399
            failed_txid: None,
×
400
        })?
×
401
        else {
×
402
            warn!(
403
                "Rejected block proposal";
30,603✔
404
                "reason" => "Block is not a tenure-start block, and has an unrecognized tenure consensus hash",
30,603✔
405
                "consensus_hash" => %tenure_id,
406
            );
×
407
            return Err(BlockValidateRejectReason {
×
408
                reason_code: ValidateRejectCode::NoSuchTenure,
×
409
                reason: "Block is not a tenure-start block, and has an unrecognized tenure consensus hash".into(),
×
410
                failed_txid: None,
×
411
            });
412
        };
×
413
        let Some(parent_header) =
414
            NakamotoChainState::get_block_header(chainstate.db(), parent_block_id).map_err(
415
                |e| BlockValidateRejectReason {
416
                    reason_code: ValidateRejectCode::ChainstateError,
417
                    reason: format!("Failed to query block header by block ID: {:?}", &e),
×
418
                    failed_txid: None,
×
419
                },
×
420
            )?
×
421
        else {
×
422
            warn!(
423
                "Rejected block proposal";
30,603✔
424
                "reason" => "Block has no parent",
184✔
425
                "parent_block_id" => %parent_block_id
426
            );
427
            return Err(BlockValidateRejectReason {
428
                reason_code: ValidateRejectCode::UnknownParent,
184✔
429
                reason: "Block has no parent".into(),
184✔
430
                failed_txid: None,
431
            });
184✔
432
        };
184✔
433
        if parent_header.anchored_header.height() != highest_header.anchored_header.height() {
184✔
434
            warn!(
184✔
435
                "Rejected block proposal";
184✔
436
                "reason" => "Block's parent is not the highest block in this tenure",
30,419✔
437
                "consensus_hash" => %tenure_id,
30,419✔
438
                "parent_header.height" => parent_header.anchored_header.height(),
30,603✔
439
                "highest_header.height" => highest_header.anchored_header.height(),
440
            );
441
            return Err(BlockValidateRejectReason {
442
                reason_code: ValidateRejectCode::InvalidParentBlock,
30,611✔
443
                reason: "Block is not higher than the highest block in its tenure".into(),
30,611✔
444
                failed_txid: None,
30,611✔
445
            });
30,611✔
446
        }
447
        Ok(())
30,611✔
448
    }
8✔
449

450
    /// Verify that the block we received builds upon a valid tenure.
451
    /// Implemented as a static function to facilitate testing.
452
    pub(crate) fn check_block_has_valid_tenure(
453
        db_handle: &SortitionHandleConn,
8✔
454
        tenure_id: &ConsensusHash,
8✔
455
    ) -> Result<(), BlockValidateRejectReason> {
8✔
456
        // Verify that the block's tenure is on the canonical sortition history
8✔
457
        if !db_handle.has_consensus_hash(tenure_id)? {
8✔
458
            warn!(
30,603✔
459
                "Rejected block proposal";
30,603✔
460
                "reason" => "Block's tenure consensus hash is not on the canonical Bitcoin fork",
30,611✔
461
                "consensus_hash" => %tenure_id,
462
            );
463
            return Err(BlockValidateRejectReason {
464
                reason_code: ValidateRejectCode::NonCanonicalTenure,
465
                reason: "Tenure consensus hash is not on the canonical Bitcoin fork".into(),
466
                failed_txid: None,
467
            });
468
        }
30,603✔
469
        Ok(())
30,603✔
470
    }
30,603✔
471

30,603✔
472
    /// Verify that the block we received builds on the highest block in its tenure.
30,603✔
473
    /// * For tenure-start blocks, the parent must be as high as the highest block in the parent
30,603✔
474
    /// block's tenure.
30,603✔
475
    /// * For all other blocks, the parent must be as high as the highest block in the tenure.
30,603✔
476
    ///
30,603✔
477
    /// Implemented as a static function to facilitate testing
×
478
    pub(crate) fn check_block_has_valid_parent(
×
479
        chainstate: &StacksChainState,
×
480
        sortdb: &SortitionDB,
×
481
        block: &NakamotoBlock,
482
    ) -> Result<(), BlockValidateRejectReason> {
30,603✔
483
        let is_tenure_start =
484
            block
485
                .is_wellformed_tenure_start_block()
11,003✔
486
                .map_err(|_| BlockValidateRejectReason {
11,003✔
487
                    reason_code: ValidateRejectCode::InvalidBlock,
11,003✔
488
                    reason: "Block is not well-formed".into(),
11,003✔
489
                    failed_txid: None,
11,003✔
490
                })?;
176✔
491

492
        if !is_tenure_start {
493
            // this is a well-formed block that is not the start of a tenure, so it must build
494
            // atop an existing block in its tenure.
19,600✔
495
            Self::check_block_builds_on_highest_block_in_tenure(
19,600✔
496
                chainstate,
19,600✔
497
                sortdb,
×
498
                &block.header.consensus_hash,
19,600✔
499
                &block.header.parent_block_id,
×
500
            )?;
×
501
        } else {
×
502
            // this is a tenure-start block, so it must build atop a parent which has the
×
503
            // highest height in the *previous* tenure.
504
            let parent_header = NakamotoChainState::get_block_header(
19,600✔
505
                chainstate.db(),
19,600✔
506
                &block.header.parent_block_id,
19,600✔
507
            )?
19,600✔
508
            .ok_or_else(|| BlockValidateRejectReason {
19,600✔
509
                reason_code: ValidateRejectCode::UnknownParent,
8✔
510
                reason: "No parent block".into(),
511
                failed_txid: None,
30,419✔
512
            })?;
30,603✔
513

514
            Self::check_block_builds_on_highest_block_in_tenure(
515
                chainstate,
516
                sortdb,
517
                &parent_header.consensus_hash,
518
                &block.header.parent_block_id,
519
            )?;
520
        }
521
        Ok(())
522
    }
523

524
    /// Test this block proposal against the current chain state and
525
    /// either accept or reject the proposal
526
    ///
527
    /// This is done in 3 stages:
528
    /// - Static validation of the block, which checks the following:
529
    ///   - Block header is well-formed
30,827✔
530
    ///   - Transactions are well-formed
30,827✔
531
    ///   - Miner signature is valid
30,827✔
532
    /// - Validation of transactions by executing them agains current chainstate.
30,827✔
533
    ///   This is resource intensive, and therefore done only if previous checks pass
30,827✔
534
    ///
30,827✔
535
    /// During transaction replay, we also check that the block only contains the unmined
30,827✔
536
    /// transactions that need to be replayed, up until either:
30,827✔
537
    /// - The set of transactions that must be replayed is exhausted
30,827✔
538
    /// - A cost limit is hit
539
    pub fn validate(
30,827✔
540
        &self,
541
        sortdb: &SortitionDB,
30,827✔
542
        chainstate: &mut StacksChainState, // not directly used; used as a handle to open other chainstates
30,827✔
543
        timeout_secs: u64,
8✔
544
        max_tx_mem_bytes: u64,
545
        auth_token: Option<String>,
546
    ) -> Result<BlockValidateOk, BlockValidateRejectReason> {
8✔
547
        fault_injection_validation_stall(auth_token);
8✔
548
        let start = Instant::now();
8✔
549

8✔
550
        fault_injection_validation_delay();
551

8✔
552
        let mainnet = self.chain_id == CHAIN_ID_MAINNET;
8✔
553
        if self.chain_id != chainstate.chain_id || mainnet != chainstate.mainnet {
8✔
554
            warn!(
8✔
555
                "Rejected block proposal";
8✔
556
                "reason" => "Wrong network/chain_id",
30,819✔
557
                "expected_chain_id" => chainstate.chain_id,
558
                "expected_mainnet" => chainstate.mainnet,
559
                "received_chain_id" => self.chain_id,
560
                "received_mainnet" => mainnet,
561
            );
30,819✔
562
            return Err(BlockValidateRejectReason {
×
563
                reason_code: ValidateRejectCode::NetworkChainMismatch,
564
                reason: "Wrong network/chain_id".into(),
565
                failed_txid: None,
30,819✔
566
            });
567
        }
568

569
        // Check block version. If it's less than the compiled-in version, just emit a warning
570
        // because there's a new version of the node / signer binary available that really ought to
571
        // be used (hint, hint)
30,819✔
572
        if self.block.header.version != NAKAMOTO_BLOCK_VERSION {
30,819✔
573
            warn!("Proposed block has unexpected version. Upgrade your node and/or signer ASAP.";
30,819✔
574
                  "block.header.version" => %self.block.header.version,
×
575
                  "expected" => %NAKAMOTO_BLOCK_VERSION);
30,819✔
576
        }
136✔
577

136✔
578
        // open sortition view to the current burn view.
136✔
579
        // If the block has a TenureChange with an Extend cause, then the burn view is whatever is
136✔
580
        // indicated in the TenureChange.
581
        // Otherwise, it's the same as the block's parent's burn view.
30,611✔
582
        let parent_stacks_header = NakamotoChainState::get_block_header(
30,683✔
583
            chainstate.db(),
30,611✔
584
            &self.block.header.parent_block_id,
30,611✔
585
        )?
30,611✔
586
        .ok_or_else(|| BlockValidateRejectReason {
×
587
            reason_code: ValidateRejectCode::UnknownParent,
×
588
            reason: "Unknown parent block".into(),
×
589
            failed_txid: None,
×
590
        })?;
591

30,611✔
592
        let burn_view_consensus_hash =
30,611✔
593
            NakamotoChainState::get_block_burn_view(sortdb, &self.block, &parent_stacks_header)?;
594
        let sort_tip =
595
            SortitionDB::get_block_snapshot_consensus(sortdb.conn(), &burn_view_consensus_hash)?
596
                .ok_or_else(|| BlockValidateRejectReason {
30,611✔
597
                    reason_code: ValidateRejectCode::NoSuchTenure,
598
                    reason: "Failed to find sortition for block tenure".to_string(),
599
                    failed_txid: None,
600
                })?;
30,603✔
601

602
        let burn_dbconn: SortitionHandleConn = sortdb.index_handle(&sort_tip.sortition_id);
603
        let db_handle = sortdb.index_handle(&sort_tip.sortition_id);
604

605
        // (For the signer)
30,419✔
606
        // Verify that the block's tenure is on the canonical sortition history
30,419✔
607
        Self::check_block_has_valid_tenure(&db_handle, &self.block.header.consensus_hash)?;
30,419✔
608

×
609
        // (For the signer)
610
        // Verify that this block's parent is the highest such block we can build off of
611
        Self::check_block_has_valid_parent(chainstate, sortdb, &self.block)?;
612

×
613
        // get the burnchain tokens spent for this block. There must be a record of this (i.e.
×
614
        // there must be a block-commit for this), or otherwise this block doesn't correspond to
×
615
        // any burnchain chainstate.
×
616
        let expected_burn_opt =
×
617
            NakamotoChainState::get_expected_burns(&db_handle, chainstate.db(), &self.block)?;
30,419✔
618
        if expected_burn_opt.is_none() {
619
            warn!(
620
                "Rejected block proposal";
30,419✔
621
                "reason" => "Failed to find parent expected burns",
30,419✔
622
            );
30,419✔
623
            return Err(BlockValidateRejectReason {
30,419✔
624
                reason_code: ValidateRejectCode::UnknownParent,
30,419✔
625
                reason: "Failed to find parent expected burns".into(),
30,419✔
626
                failed_txid: None,
30,419✔
627
            });
16✔
628
        };
629

630
        // Static validation checks
631
        NakamotoChainState::validate_normal_nakamoto_block_burnchain(
632
            chainstate.nakamoto_blocks_db(),
633
            &db_handle,
634
            expected_burn_opt,
26,403✔
635
            &self.block,
30,403✔
636
            mainnet,
637
            self.chain_id,
26,403✔
638
        )?;
1✔
639

640
        // Validate txs against chainstate
641

1✔
642
        // Validate the block's timestamp. It must be:
1✔
643
        // - Greater than the parent block's timestamp
644
        // - At most 15 seconds into the future
1✔
645
        if let StacksBlockHeaderTypes::Nakamoto(parent_nakamoto_header) =
1✔
646
            &parent_stacks_header.anchored_header
1✔
647
        {
1✔
648
            if self.block.header.timestamp <= parent_nakamoto_header.timestamp {
1✔
649
                warn!(
26,402✔
650
                    "Rejected block proposal";
4,000✔
651
                    "reason" => "Block timestamp is not greater than parent block",
30,402✔
652
                    "block_timestamp" => self.block.header.timestamp,
1✔
653
                    "parent_block_timestamp" => parent_nakamoto_header.timestamp,
654
                );
655
                return Err(BlockValidateRejectReason {
1✔
656
                    reason_code: ValidateRejectCode::InvalidTimestamp,
1✔
657
                    reason: "Block timestamp is not greater than parent block".into(),
658
                    failed_txid: None,
1✔
659
                });
1✔
660
            }
1✔
661
        }
1✔
662
        if self.block.header.timestamp > get_epoch_time_secs() + 15 {
1✔
663
            warn!(
30,401✔
664
                "Rejected block proposal";
665
                "reason" => "Block timestamp is too far into the future",
30,401✔
666
                "block_timestamp" => self.block.header.timestamp,
30,401✔
667
                "current_time" => get_epoch_time_secs(),
668
            );
×
669
            return Err(BlockValidateRejectReason {
670
                reason_code: ValidateRejectCode::InvalidTimestamp,
671
                reason: "Block timestamp is too far into the future".into(),
×
672
                failed_txid: None,
×
673
            });
674
        }
×
675

×
676
        if self.block.header.chain_length
×
677
            != parent_stacks_header.stacks_block_height.saturating_add(1)
×
678
        {
×
679
            warn!(
30,401✔
680
                "Rejected block proposal";
681
                "reason" => "Block height is non-contiguous with parent",
30,401✔
682
                "block_height" => self.block.header.chain_length,
30,401✔
683
                "parent_block_height" => parent_stacks_header.stacks_block_height,
30,401✔
684
            );
30,401✔
685
            return Err(BlockValidateRejectReason {
1,115,489✔
686
                reason_code: ValidateRejectCode::InvalidBlock,
30,401✔
687
                reason: "Block height is non-contiguous with parent".into(),
30,401✔
688
                failed_txid: None,
30,401✔
689
            });
30,401✔
690
        }
1,136,073✔
691

30,401✔
692
        let tenure_change = self
30,401✔
693
            .block
21,672✔
694
            .txs
×
695
            .iter()
21,672✔
696
            .find(|tx| matches!(tx.payload, TransactionPayload::TenureChange(..)));
30,401✔
697
        let coinbase = self
698
            .block
30,401✔
699
            .txs
30,401✔
700
            .iter()
30,401✔
701
            .find(|tx| matches!(tx.payload, TransactionPayload::Coinbase(..)));
30,401✔
702
        let tenure_cause = tenure_change
30,401✔
703
            .and_then(|tx| match &tx.payload {
30,401✔
704
                TransactionPayload::TenureChange(tc) => Some(MinerTenureInfoCause::from(tc)),
30,401✔
705
                _ => None,
88✔
706
            })
707
            .unwrap_or_else(|| MinerTenureInfoCause::NoTenureChange);
30,313✔
708

30,313✔
709
        let replay_tx_exhausted = self.validate_replay(
30,313✔
710
            &parent_stacks_header,
30,313✔
711
            tenure_change,
30,313✔
712
            coinbase,
30,313✔
713
            tenure_cause,
30,313✔
714
            chainstate,
30,313✔
715
            &burn_dbconn,
30,313✔
716
        )?;
30,313✔
717

30,313✔
718
        let mut builder = NakamotoBlockBuilder::new(
×
719
            &parent_stacks_header,
720
            &self.block.header.consensus_hash,
30,313✔
721
            self.block.header.burn_spent,
30,313✔
722
            tenure_change,
30,313✔
723
            coinbase,
30,313✔
724
            self.block.header.pox_treatment.len(),
725
            None,
30,313✔
726
            None,
30,313✔
727
            Some(self.block.header.timestamp),
1,135,873✔
728
            u64::from(DEFAULT_MAX_TENURE_BYTES),
1,135,873✔
729
        )?;
730

1,135,873✔
731
        let mut miner_tenure_info =
732
            builder.load_tenure_info(chainstate, &burn_dbconn, tenure_cause)?;
1,135,873✔
733
        let burn_chain_height = miner_tenure_info.burn_tip_height;
1,135,873✔
734
        let mut tenure_tx = builder.tenure_begin(&burn_dbconn, &mut miner_tenure_info)?;
1,135,873✔
735

1,135,873✔
736
        let block_deadline = Instant::now() + Duration::from_secs(timeout_secs);
1,135,873✔
737
        let mut receipts_total = 0u64;
1,135,873✔
738
        for (i, tx) in self.block.txs.iter().enumerate() {
1,135,873✔
739
            let remaining = block_deadline.saturating_duration_since(Instant::now());
740

1,135,873✔
741
            let tx_len = tx.tx_len();
1,135,873✔
742

1,135,873✔
743
            if max_tx_mem_bytes > 0 {
1,135,873✔
744
                tenure_tx.set_abort_callback(make_mem_abort_callback(max_tx_mem_bytes));
1,135,873✔
745
            }
1,135,873✔
746

1,135,873✔
747
            let tx_result = builder.try_mine_tx_with_len(
1,135,873✔
748
                &mut tenure_tx,
×
749
                tx,
×
750
                tx_len,
×
751
                &BlockLimitFunction::NO_LIMIT_HIT,
×
752
                Some(remaining),
753
                &mut receipts_total,
1,135,873✔
754
            );
755

756
            tenure_tx.set_abort_callback(AbortCallback::None);
×
757

×
758
            let reason = match tx_result {
×
759
                TransactionResult::Success(success_result) => {
×
760
                    let all_events_valid = success_result
×
761
                        .receipt
×
762
                        .events
×
763
                        .iter()
×
764
                        .all(|event| is_event_pox_addr_valid(mainnet, event));
×
765
                    if !all_events_valid {
×
766
                        Some((
×
767
                            format!("Problematic tx {i}: contains invalid pox address"),
×
768
                            ValidateRejectCode::ProblematicTransaction,
769
                        ))
1,135,873✔
770
                    } else {
×
771
                        None
772
                    }
773
                }
774
                TransactionResult::Skipped(s) => Some((
775
                    format!("tx {i} skipped: {}", s.error),
×
776
                    ValidateRejectCode::BadTransaction,
×
777
                )),
×
778
                TransactionResult::ProcessingError(e) => Some((
×
779
                    format!("Error processing tx {i}: {}", e.error),
×
780
                    ValidateRejectCode::BadTransaction,
1,135,873✔
781
                )),
782
                TransactionResult::Problematic(p) => Some((
783
                    format!("Problematic tx {i}: {}", p.error),
30,313✔
784
                    ValidateRejectCode::ProblematicTransaction,
785
                )),
786
            };
787
            if let Some((reason, reject_code)) = reason {
30,313✔
788
                warn!(
30,313✔
789
                    "Rejected block proposal";
30,313✔
790
                    "reason" => %reason,
791
                    "tx" => ?tx,
792
                );
793
                return Err(BlockValidateRejectReason {
30,313✔
794
                    reason,
30,313✔
795
                    reason_code: reject_code,
30,313✔
796
                    failed_txid: Some(tx.txid()),
30,313✔
797
                });
30,313✔
798
            }
799
        }
800

30,313✔
801
        let mut block = builder.mine_nakamoto_block(&mut tenure_tx, burn_chain_height);
30,313✔
802
        // Override the block version with the one from the proposal. This must be
803
        // done before computing the block hash, because the block hash includes the
30,313✔
804
        // version in its computation.
×
805
        block.header.version = self.block.header.version;
806
        let size = builder.get_bytes_so_far();
807
        let cost = builder.tenure_finish(tenure_tx)?;
808

809
        // Clone signatures from block proposal
810
        // These have already been validated by `validate_nakamoto_block_burnchain()``
811
        block.header.miner_signature = self.block.header.miner_signature.clone();
812
        block
×
813
            .header
×
814
            .signer_signature
×
815
            .clone_from(&self.block.header.signer_signature);
×
816

×
817
        // Assuming `tx_merkle_root` has been checked we don't need to hash the whole block
30,313✔
818
        let expected_block_header_hash = self.block.header.block_hash();
819
        let computed_block_header_hash = block.header.block_hash();
30,313✔
820

821
        if computed_block_header_hash != expected_block_header_hash {
30,313✔
822
            warn!(
823
                "Rejected block proposal";
824
                "reason" => "Block hash is not as expected",
30,313✔
825
                "expected_block_header_hash" => %expected_block_header_hash,
30,313✔
826
                "computed_block_header_hash" => %computed_block_header_hash,
827
                "expected_block" => ?self.block,
30,313✔
828
                "computed_block" => ?block,
829
            );
30,313✔
830
            return Err(BlockValidateRejectReason {
1,135,873✔
831
                reason: "Block hash is not as expected".into(),
1,135,873✔
832
                reason_code: ValidateRejectCode::BadBlockHash,
1,135,873✔
833
                failed_txid: None,
834
            });
835
        }
30,313✔
836

837
        let validation_time_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
30,313✔
838

30,313✔
839
        info!(
30,313✔
840
            "Participant: validated anchored block";
30,313✔
841
            "block_header_hash" => %computed_block_header_hash,
30,313✔
842
            "height" => block.header.chain_length,
30,313✔
843
            "tx_count" => block.txs.len(),
30,313✔
844
            "parent_stacks_block_id" => %block.header.parent_block_id,
30,313✔
845
            "block_size" => size,
30,827✔
846
            "execution_cost" => %cost,
847
            "validation_time_ms" => validation_time_ms,
70,121✔
848
            "tx_fees_microstacks" => block.txs.iter().fold(0, |agg: u64, tx| {
70,121✔
849
                agg.saturating_add(tx.get_tx_fee())
1,824✔
850
            })
1,824✔
851
        );
1,824✔
852

1,824✔
853
        let replay_tx_hash = Self::tx_replay_hash(&self.replay_txs);
70,121✔
854

855
        Ok(BlockValidateOk {
856
            signer_signature_hash: block.header.signer_signature_hash(),
857
            cost,
858
            size,
859
            validation_time_ms,
860
            replay_tx_hash,
30,401✔
861
            replay_tx_exhausted,
30,401✔
862
        })
30,401✔
863
    }
30,401✔
864

30,401✔
865
    pub fn tx_replay_hash(replay_txs: &Option<Vec<StacksTransaction>>) -> Option<u64> {
30,401✔
866
        replay_txs.as_ref().map(|txs| {
30,401✔
867
            let mut hasher = DefaultHasher::new();
30,401✔
868
            txs.hash(&mut hasher);
30,401✔
869
            hasher.finish()
30,401✔
870
        })
30,401✔
871
    }
30,401✔
872

873
    /// Validate the block against the replay set.
30,401✔
874
    ///
29,553✔
875
    /// Returns a boolean indicating whether this block exhausts the replay set.
876
    ///
877
    /// Returns `false` if there is no replay set.
848✔
878
    fn validate_replay(
848✔
879
        &self,
848✔
880
        parent_stacks_header: &StacksHeaderInfo,
848✔
881
        tenure_change: Option<&StacksTransaction>,
848✔
882
        coinbase: Option<&StacksTransaction>,
848✔
883
        tenure_cause: MinerTenureInfoCause,
848✔
884
        // not directly used; used as a handle to open other chainstates
848✔
885
        chainstate_handle: &StacksChainState,
848✔
886
        burn_dbconn: &SortitionHandleConn,
848✔
887
    ) -> Result<bool, BlockValidateRejectReason> {
848✔
888
        let mut replay_txs_maybe: Option<VecDeque<StacksTransaction>> =
×
889
            self.replay_txs.clone().map(|txs| txs.into());
848✔
890

848✔
891
        let Some(ref mut replay_txs) = replay_txs_maybe else {
848✔
892
            return Ok(false);
848✔
893
        };
848✔
894

895
        let mut replay_builder = NakamotoBlockBuilder::new(
848✔
896
            &parent_stacks_header,
1,592✔
897
            &self.block.header.consensus_hash,
1,592✔
898
            self.block.header.burn_spent,
899
            tenure_change,
900
            coinbase,
901
            self.block.header.pox_treatment.len(),
902
            None,
496✔
903
            None,
1,640✔
904
            Some(self.block.header.timestamp),
905
            u64::from(DEFAULT_MAX_TENURE_BYTES),
906
        )?;
907
        let (mut replay_chainstate, _) = chainstate_handle.reopen()?;
1,144✔
908
        let mut replay_miner_tenure_info =
496✔
909
            replay_builder.load_tenure_info(&mut replay_chainstate, &burn_dbconn, tenure_cause)?;
496✔
910
        let mut replay_tenure_tx =
432✔
911
            replay_builder.tenure_begin(&burn_dbconn, &mut replay_miner_tenure_info)?;
912

913
        let mut total_receipts = 0;
914
        for (i, tx) in self.block.txs.iter().enumerate() {
915
            let tx_len = tx.tx_len();
×
916

×
917
            // If a list of replay transactions is set, this transaction must be the next
×
918
            // mineable transaction from this list.
919
            loop {
×
920
                if matches!(
×
921
                    tx.payload,
×
922
                    TransactionPayload::TenureChange(..) | TransactionPayload::Coinbase(..)
×
923
                ) {
×
924
                    // Allow this to happen, tenure extend checks happen elsewhere.
925
                    break;
432✔
926
                }
360✔
927
                fault_injection_reject_replay_txs()?;
72✔
928
                let Some(replay_tx) = replay_txs.pop_front() else {
929
                    // During transaction replay, we expect that the block only
930
                    // contains transactions from the replay set. Thus, if we're here,
931
                    // the block contains a transaction that is not in the replay set,
932
                    // and we should reject the block.
72✔
933
                    warn!("Rejected block proposal. Block contains transactions beyond the replay set.";
72✔
934
                        "txid" => %tx.txid(),
72✔
935
                        "tx_index" => i,
72✔
936
                    );
72✔
937
                    return Err(BlockValidateRejectReason {
72✔
938
                        reason_code: ValidateRejectCode::InvalidTransactionReplay,
72✔
939
                        reason: "Block contains transactions beyond the replay set".into(),
940
                        failed_txid: Some(tx.txid()),
72✔
941
                    });
×
942
                };
48✔
943
                if replay_tx.txid() == tx.txid() {
×
944
                    break;
945
                }
946

947
                // The included tx doesn't match the next tx in the
948
                // replay set. Check to see if the tx is skipped because
×
949
                // it was unmineable.
950
                let tx_result = replay_builder.try_mine_tx_with_len(
951
                    &mut replay_tenure_tx,
952
                    &replay_tx,
953
                    replay_tx.tx_len(),
954
                    &BlockLimitFunction::NO_LIMIT_HIT,
955
                    None,
956
                    &mut total_receipts,
×
957
                );
×
958
                match tx_result {
959
                    TransactionResult::Skipped(TransactionSkipped { error, .. })
×
960
                    | TransactionResult::ProcessingError(TransactionError { error, .. })
961
                    | TransactionResult::Problematic(TransactionProblematic { error, .. }) => {
962
                        // The tx wasn't able to be mined. Check the underlying error, to
963
                        // see if we should reject the block or allow the tx to be
964
                        // dropped from the replay set.
×
965

×
966
                        match error {
×
967
                            ChainError::CostOverflowError(..)
×
968
                            | ChainError::BlockTooBigError
×
969
                            | ChainError::BlockCostLimitError
970
                            | ChainError::ClarityError(ClarityError::CostError(..)) => {
971
                                // block limit reached; add tx back to replay set.
48✔
972
                                // BUT we know that the block should have ended at this point, so
48✔
973
                                // return an error.
974
                                let txid = replay_tx.txid();
975
                                replay_txs.push_front(replay_tx);
976

48✔
977
                                warn!("Rejecting block proposal. Next replay tx exceeds cost limits, so should have been in the next block.";
978
                                    "error" => %error,
979
                                    "txid" => %txid,
980
                                );
981

982
                                return Err(BlockValidateRejectReason {
24✔
983
                                    reason_code: ValidateRejectCode::InvalidTransactionReplay,
24✔
984
                                    reason: "Next replay tx exceeds cost limits, so should have been in the next block.".into(),
24✔
985
                                    failed_txid: None,
24✔
986
                                });
987
                            }
24✔
988
                            _ => {
24✔
989
                                info!("During replay block validation, allowing problematic tx to be dropped";
24✔
990
                                    "txid" => %replay_tx.txid(),
24✔
991
                                    "error" => %error,
24✔
992
                                );
993
                                // it's ok, drop it
994
                                continue;
995
                            }
996
                        }
997
                    }
998
                    TransactionResult::Success(_) => {
999
                        // Tx should have been included
1,504✔
1000
                        warn!("Rejected block proposal. Block doesn't contain replay transaction that should have been included.";
1,504✔
1001
                            "block_txid" => %tx.txid(),
1,504✔
1002
                            "block_tx_index" => i,
1,504✔
1003
                            "replay_txid" => %replay_tx.txid(),
1,504✔
1004
                        );
1,504✔
1005
                        return Err(BlockValidateRejectReason {
1,504✔
1006
                            reason_code: ValidateRejectCode::InvalidTransactionReplay,
1007
                            reason: "Transaction is not in the replay set".into(),
1008
                            failed_txid: Some(tx.txid()),
1009
                        });
760✔
1010
                    }
1011
                };
1012
            }
760✔
1013

624✔
1014
            // Apply the block's transaction to our block builder, but we don't
624✔
1015
            // actually care about the result - that happens in the main
624✔
1016
            // validation check.
624✔
1017
            let _tx_result = replay_builder.try_mine_tx_with_len(
624✔
1018
                &mut replay_tenure_tx,
624✔
1019
                tx,
624✔
1020
                tx_len,
624✔
1021
                &BlockLimitFunction::NO_LIMIT_HIT,
1022
                None,
624✔
1023
                &mut total_receipts,
48✔
1024
            );
248✔
1025
        }
64✔
1026

1027
        let no_replay_txs_remaining = replay_txs.is_empty();
312✔
1028

×
1029
        // Now, we need to check if the remaining replay transactions are unmineable.
1030
        let only_unmineable_remaining = !replay_txs.is_empty()
1031
            && replay_txs.iter().all(|tx| {
1032
                let tx_result = replay_builder.try_mine_tx_with_len(
1033
                    &mut replay_tenure_tx,
1034
                    &tx,
1035
                    tx.tx_len(),
1036
                    &BlockLimitFunction::NO_LIMIT_HIT,
1037
                    None,
1038
                    &mut total_receipts,
264✔
1039
                );
1040
                match tx_result {
1041
                    TransactionResult::Skipped(TransactionSkipped { error, .. })
624✔
1042
                    | TransactionResult::ProcessingError(TransactionError { error, .. })
1043
                    | TransactionResult::Problematic(TransactionProblematic { error, .. }) => {
760✔
1044
                        // If it's just a cost error, it's not unmineable.
30,401✔
1045
                        !matches!(
1046
                            error,
1047
                            ChainError::CostOverflowError(..)
1048
                                | ChainError::BlockTooBigError
1049
                                | ChainError::ClarityError(ClarityError::CostError(..))
1050
                                | ChainError::BlockCostLimitError
1051
                        )
1052
                    }
1053
                    TransactionResult::Success(_) => {
1054
                        // The tx could have been included, but wasn't. This is ok, but we
930,496✔
1055
                        // haven't exhausted the replay set.
930,496✔
1056
                        false
930,496✔
1057
                    }
930,496✔
1058
                }
930,496✔
1059
            });
930,496✔
1060

1061
        Ok(no_replay_txs_remaining || only_unmineable_remaining)
1062
    }
86,461✔
1063
}
86,461✔
1064

86,461✔
1065
#[derive(Clone, Default)]
86,461✔
1066
pub struct RPCBlockProposalRequestHandler {
1067
    pub block_proposal: Option<NakamotoBlockProposal>,
1068
    pub auth: Option<String>,
1069
}
1070

930,495✔
1071
impl RPCBlockProposalRequestHandler {
930,495✔
1072
    pub fn new(auth: Option<String>) -> Self {
930,495✔
1073
        Self {
1074
            block_proposal: None,
1,860,992✔
1075
            auth,
1,860,992✔
1076
        }
1,860,992✔
1077
    }
1078

86,460✔
1079
    /// Decode a JSON-encoded block proposal
86,460✔
1080
    fn parse_json(body: &[u8]) -> Result<NakamotoBlockProposal, Error> {
86,460✔
1081
        serde_json::from_slice(body)
1082
            .map_err(|e| Error::DecodeError(format!("Failed to parse body: {e}")))
1083
    }
1084
}
86,470✔
1085

86,470✔
1086
/// Decode the HTTP request
86,470✔
1087
impl HttpRequest for RPCBlockProposalRequestHandler {
86,470✔
1088
    fn verb(&self) -> &'static str {
86,470✔
1089
        "POST"
86,470✔
1090
    }
86,470✔
1091

1092
    fn path_regex(&self) -> Regex {
86,470✔
1093
        Regex::new(r#"^/v3/block_proposal$"#).unwrap()
×
1094
    }
1095

86,470✔
1096
    fn metrics_identifier(&self) -> &str {
9✔
1097
        "/v3/block_proposal"
1098
    }
86,461✔
1099

×
1100
    /// Try to decode this request.
86,461✔
1101
    /// There's nothing to load here, so just make sure the request is well-formed.
86,461✔
1102
    fn try_parse_request(
×
1103
        &mut self,
×
1104
        preamble: &HttpRequestPreamble,
×
1105
        _captures: &Captures,
×
1106
        query: Option<&str>,
86,461✔
1107
        body: &[u8],
86,461✔
1108
    ) -> Result<HttpRequestContents, Error> {
×
1109
        // If no authorization is set, then the block proposal endpoint is not enabled
×
1110
        let Some(password) = &self.auth else {
×
1111
            return Err(Error::Http(400, "Bad Request.".into()));
86,461✔
1112
        };
1113
        let Some(auth_header) = preamble.headers.get("authorization") else {
86,461✔
1114
            return Err(Error::Http(401, "Unauthorized".into()));
86,461✔
1115
        };
1116
        if auth_header != password {
×
1117
            return Err(Error::Http(401, "Unauthorized".into()));
×
1118
        }
×
1119
        if preamble.get_content_length() == 0 {
1120
            return Err(Error::DecodeError(
1121
                "Invalid Http request: expected non-zero-length body for block proposal endpoint"
×
1122
                    .to_string(),
×
1123
            ));
×
1124
        }
1125
        if preamble.get_content_length() > MAX_PAYLOAD_LEN {
1126
            return Err(Error::DecodeError(
1127
                "Invalid Http request: BlockProposal body is too big".to_string(),
86,461✔
1128
            ));
×
1129
        }
×
1130

×
1131
        let block_proposal = match preamble.content_type {
86,461✔
1132
            Some(HttpContentType::JSON) => Self::parse_json(body)?,
1133
            Some(_) => {
86,461✔
1134
                return Err(Error::DecodeError(
86,461✔
1135
                    "Wrong Content-Type for block proposal; expected application/json".to_string(),
86,470✔
1136
                ))
1137
            }
1138
            None => {
1139
                return Err(Error::DecodeError(
1140
                    "Missing Content-Type for block proposal".to_string(),
1141
                ))
1142
            }
1143
        };
1144

1145
        if block_proposal.block.is_shadow_block() {
1146
            return Err(Error::DecodeError(
86,470✔
1147
                "Shadow blocks cannot be submitted for validation".to_string(),
86,470✔
1148
            ));
86,470✔
1149
        }
1150

1151
        self.block_proposal = Some(block_proposal);
86,460✔
1152
        Ok(HttpRequestContents::new().query_string(query))
86,460✔
1153
    }
86,460✔
1154
}
86,460✔
1155

86,460✔
1156
struct ProposalThreadInfo {
86,460✔
1157
    sortdb: SortitionDB,
86,460✔
1158
    chainstate: StacksChainState,
86,460✔
1159
    receiver: Box<dyn ProposalCallbackReceiver>,
86,460✔
1160
}
86,460✔
1161

1162
impl RPCRequestHandler for RPCBlockProposalRequestHandler {
86,460✔
1163
    /// Reset internal state
1164
    fn restart(&mut self) {
86,460✔
1165
        self.block_proposal = None
86,460✔
1166
    }
86,460✔
1167

86,460✔
1168
    /// Make the response
1169
    fn try_handle_request(
1170
        &mut self,
1171
        preamble: HttpRequestPreamble,
86,460✔
1172
        _contents: HttpRequestContents,
86,460✔
1173
        node: &mut StacksNodeState,
55,624✔
1174
    ) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> {
55,624✔
1175
        let block_proposal = self
55,624✔
1176
            .block_proposal
55,624✔
1177
            .take()
30,836✔
1178
            .ok_or(NetError::SendError("`block_proposal` not set".into()))?;
1179

30,836✔
1180
        info!(
30,836✔
1181
            "Received block proposal request";
30,836✔
1182
            "signer_signature_hash" => %block_proposal.block.header.signer_signature_hash(),
30,836✔
1183
            "block_header_hash" => %block_proposal.block.header.block_hash(),
30,836✔
1184
            "height" => block_proposal.block.header.chain_length,
30,836✔
1185
            "tx_count" => block_proposal.block.txs.len(),
1186
            "parent_stacks_block_id" => %block_proposal.block.header.parent_block_id,
9✔
1187
        );
9✔
1188

9✔
1189
        let res = node.with_node_state(|network, sortdb, chainstate, _mempool, rpc_args| {
9✔
1190
            if network.is_proposal_thread_running() {
30,827✔
1191
                return Err((
1192
                    TOO_MANY_REQUESTS_STATUS,
30,827✔
1193
                    NetError::SendError("Proposal currently being evaluated".into()),
30,827✔
1194
                ));
30,827✔
1195
            }
30,827✔
1196

30,827✔
1197
            if block_proposal
30,827✔
1198
                .block
×
1199
                .header
×
1200
                .timestamp
×
1201
                .saturating_add(network.get_connection_opts().block_proposal_max_age_secs)
×
1202
                < get_epoch_time_secs()
×
1203
            {
×
1204
                return Err((
×
1205
                    422,
30,827✔
1206
                    NetError::SendError("Block proposal is too old to process.".into()),
30,827✔
1207
                ));
30,827✔
1208
            }
30,827✔
1209

30,827✔
1210
            let (chainstate, _) = chainstate.reopen().map_err(|e| (400, NetError::from(e)))?;
30,827✔
1211
            let sortdb = sortdb.reopen().map_err(|e| (400, NetError::from(e)))?;
1212
            let receiver = rpc_args
30,827✔
1213
                .event_observer
×
1214
                .and_then(|observer| observer.get_proposal_callback_receiver())
×
1215
                .ok_or_else(|| {
×
1216
                    (
×
1217
                        400,
×
1218
                        NetError::SendError(
×
1219
                            "No `observer` registered for receiving proposal callbacks".into(),
×
1220
                        ),
30,827✔
1221
                    )
30,827✔
1222
                })?;
86,460✔
1223
            let thread_info = block_proposal
1224
                .spawn_validation_thread(
86,460✔
1225
                    sortdb,
1226
                    chainstate,
30,827✔
1227
                    receiver,
30,827✔
1228
                    network.get_connection_opts(),
30,827✔
1229
                )
30,827✔
1230
                .map_err(|_e| {
30,827✔
1231
                    (
30,827✔
1232
                        TOO_MANY_REQUESTS_STATUS,
1233
                        NetError::SendError(
55,633✔
1234
                            "IO error while spawning proposal callback thread".into(),
55,633✔
1235
                        ),
55,633✔
1236
                    )
55,633✔
1237
                })?;
55,633✔
1238
            network.set_proposal_thread(thread_info);
55,633✔
1239
            Ok(())
55,633✔
1240
        });
1241

1242
        match res {
86,460✔
1243
            Ok(_) => {
1244
                let preamble = HttpResponsePreamble::accepted_json(&preamble);
1245
                let body = HttpResponseContents::try_from_json(&serde_json::json!({
1246
                    "result": "Accepted",
1247
                    "message": "Block proposal is processing, result will be returned via the event observer"
3✔
1248
                }))?;
3✔
1249
                Ok((preamble, body))
3✔
1250
            }
3✔
1251
            Err((code, err)) => {
3✔
1252
                let preamble = HttpResponsePreamble::error_json(code, http_reason(code));
3✔
1253
                let body = HttpResponseContents::try_from_json(&serde_json::json!({
3✔
1254
                    "result": "Error",
3✔
1255
                    "message": format!("Could not process block proposal request: {err}")
1256
                }))?;
1257
                Ok((preamble, body))
1258
            }
1259
        }
1260
    }
1261
}
1262

1263
/// Decode the HTTP response
1264
impl HttpResponse for RPCBlockProposalRequestHandler {
1265
    fn try_parse_response(
1266
        &self,
1267
        preamble: &HttpResponsePreamble,
1268
        body: &[u8],
1269
    ) -> Result<HttpResponsePayload, Error> {
1270
        let response: BlockProposalResponse = parse_json(preamble, body)?;
1271
        HttpResponsePayload::try_from_json(response)
1272
    }
1273
}
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