• 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

79.81
/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

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

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

120
fn hex_deser_block<'de, D: serde::Deserializer<'de>>(d: D) -> Result<NakamotoBlock, D::Error> {
109,405✔
121
    let inst_str = String::deserialize(d)?;
109,405✔
122
    let bytes = hex_bytes(&inst_str).map_err(serde::de::Error::custom)?;
109,405✔
123
    NakamotoBlock::consensus_deserialize(&mut bytes.as_slice()).map_err(serde::de::Error::custom)
109,405✔
124
}
109,405✔
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
160
    T: Into<ChainError>,
161
{
162
    fn from(value: T) -> Self {
110✔
163
        let ce: ChainError = value.into();
110✔
164
        let reason_code = match ce {
110✔
165
            ChainError::DBError(db_error::NotFoundError) => ValidateRejectCode::NotFoundError,
90✔
166
            _ => ValidateRejectCode::ChainstateError,
20✔
167
        };
168
        Self {
110✔
169
            reason: format!("Chainstate Error: {ce}"),
110✔
170
            reason_code,
110✔
171
            failed_txid: None,
110✔
172
        }
110✔
173
    }
110✔
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
}
200

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

210
impl BlockValidateResponse {
211
    /// Get the signer signature hash from the response
212
    pub fn signer_signature_hash(&self) -> &Sha512Trunc256Sum {
52,878✔
213
        match self {
52,878✔
214
            BlockValidateResponse::Ok(o) => &o.signer_signature_hash,
51,885✔
215
            BlockValidateResponse::Reject(r) => &r.signer_signature_hash,
993✔
216
        }
217
    }
52,878✔
218
}
219

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

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

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

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

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

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

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

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

337
    let pox_addr_value = if let Value::Optional(data) = pox_addr_tuple {
799✔
338
        match data.data {
45✔
339
            None => return true,
31✔
340
            Some(ref inner) => inner.as_ref(),
14✔
341
        }
342
    } else {
343
        pox_addr_tuple
754✔
344
    };
345

346
    PoxAddress::try_from_pox_tuple(is_mainnet, pox_addr_value).is_some()
768✔
347
}
3,382,307✔
348

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

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

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

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

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

516
            Self::check_block_builds_on_highest_block_in_tenure(
23,800✔
517
                chainstate,
23,800✔
518
                sortdb,
23,800✔
519
                &parent_header.consensus_hash,
23,800✔
520
                &block.header.parent_block_id,
23,800✔
521
            )?;
10✔
522
        }
523
        Ok(())
38,843✔
524
    }
39,073✔
525

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

553
        fault_injection_validation_delay();
39,393✔
554

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

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

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

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

605
        let burn_dbconn: SortitionHandleConn = sortdb.index_handle(&sort_tip.sortition_id);
39,083✔
606
        let db_handle = sortdb.index_handle(&sort_tip.sortition_id);
39,083✔
607

608
        // (For the signer)
609
        // Verify that the block's tenure is on the canonical sortition history
610
        Self::check_block_has_valid_tenure(&db_handle, &self.block.header.consensus_hash)?;
39,083✔
611

612
        // (For the signer)
613
        // Verify that this block's parent is the highest such block we can build off of
614
        Self::check_block_has_valid_parent(chainstate, sortdb, &self.block)?;
39,073✔
615

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

633
        // Static validation checks
634
        NakamotoChainState::validate_normal_nakamoto_block_burnchain(
38,843✔
635
            chainstate.nakamoto_blocks_db(),
38,843✔
636
            &db_handle,
38,843✔
637
            expected_burn_opt,
38,843✔
638
            &self.block,
38,843✔
639
            mainnet,
38,843✔
640
            self.chain_id,
38,843✔
641
        )?;
20✔
642

643
        // Validate txs against chainstate
644

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

679
        if self.block.header.chain_length
38,821✔
680
            != parent_stacks_header.stacks_block_height.saturating_add(1)
38,821✔
681
        {
UNCOV
682
            warn!(
×
683
                "Rejected block proposal";
684
                "reason" => "Block height is non-contiguous with parent",
UNCOV
685
                "block_height" => self.block.header.chain_length,
×
UNCOV
686
                "parent_block_height" => parent_stacks_header.stacks_block_height,
×
687
            );
UNCOV
688
            return Err(BlockValidateRejectReason {
×
UNCOV
689
                reason_code: ValidateRejectCode::InvalidBlock,
×
UNCOV
690
                reason: "Block height is non-contiguous with parent".into(),
×
UNCOV
691
                failed_txid: None,
×
UNCOV
692
            });
×
693
        }
38,821✔
694

695
        let tenure_change = self
38,821✔
696
            .block
38,821✔
697
            .txs
38,821✔
698
            .iter()
38,821✔
699
            .find(|tx| matches!(tx.payload, TransactionPayload::TenureChange(..)));
1,376,211✔
700
        let coinbase = self
38,821✔
701
            .block
38,821✔
702
            .txs
38,821✔
703
            .iter()
38,821✔
704
            .find(|tx| matches!(tx.payload, TransactionPayload::Coinbase(..)));
1,401,301✔
705
        let tenure_cause = tenure_change
38,821✔
706
            .and_then(|tx| match &tx.payload {
38,821✔
707
                TransactionPayload::TenureChange(tc) => Some(MinerTenureInfoCause::from(tc)),
26,390✔
UNCOV
708
                _ => None,
×
709
            })
26,390✔
710
            .unwrap_or_else(|| MinerTenureInfoCause::NoTenureChange);
38,821✔
711

712
        let replay_tx_exhausted = self.validate_replay(
38,821✔
713
            &parent_stacks_header,
38,821✔
714
            tenure_change,
38,821✔
715
            coinbase,
38,821✔
716
            tenure_cause,
38,821✔
717
            chainstate,
38,821✔
718
            &burn_dbconn,
38,821✔
719
        )?;
110✔
720

721
        let mut builder = NakamotoBlockBuilder::new(
38,711✔
722
            &parent_stacks_header,
38,711✔
723
            &self.block.header.consensus_hash,
38,711✔
724
            self.block.header.burn_spent,
38,711✔
725
            tenure_change,
38,711✔
726
            coinbase,
38,711✔
727
            self.block.header.pox_treatment.len(),
38,711✔
728
            None,
38,711✔
729
            None,
38,711✔
730
            Some(self.block.header.timestamp),
38,711✔
731
            u64::from(DEFAULT_MAX_TENURE_BYTES),
38,711✔
UNCOV
732
        )?;
×
733

734
        let mut miner_tenure_info =
38,711✔
735
            builder.load_tenure_info(chainstate, &burn_dbconn, tenure_cause)?;
38,711✔
736
        let burn_chain_height = miner_tenure_info.burn_tip_height;
38,711✔
737
        let mut tenure_tx = builder.tenure_begin(&burn_dbconn, &mut miner_tenure_info)?;
38,711✔
738

739
        let block_deadline = Instant::now() + Duration::from_secs(timeout_secs);
38,711✔
740
        let per_tx_max_execution_time = Duration::from_secs(max_tx_execution_time_secs);
38,711✔
741
        let mut receipts_total = 0u64;
38,711✔
742
        for (i, tx) in self.block.txs.iter().enumerate() {
1,401,051✔
743
            // Enforce the overall block validation budget between txs. A tx
744
            // running over its own per-tx limit is the tx's fault and is
745
            // handled below; running out of overall budget is the block's
746
            // fault and shouldn't flag any specific tx as problematic.
747
            if Instant::now() >= block_deadline {
1,401,051✔
NEW
748
                warn!(
×
749
                    "Rejected block proposal";
750
                    "reason" => "Block validation timed out",
NEW
751
                    "next_tx_index" => i,
×
752
                );
NEW
753
                return Err(BlockValidateRejectReason {
×
NEW
754
                    reason: format!("Block validation timed out before tx {i} could be processed"),
×
NEW
755
                    reason_code: ValidateRejectCode::InvalidBlock,
×
NEW
756
                    failed_txid: None,
×
NEW
757
                });
×
758
            }
1,401,051✔
759

760
            let tx_len = tx.tx_len();
1,401,051✔
761

762
            if max_tx_mem_bytes > 0 {
1,401,051✔
763
                tenure_tx.set_abort_callback(make_mem_abort_callback(max_tx_mem_bytes));
1,401,051✔
764
            }
1,401,051✔
765

766
            let tx_result = builder.try_mine_tx_with_len(
1,401,051✔
767
                &mut tenure_tx,
1,401,051✔
768
                tx,
1,401,051✔
769
                tx_len,
1,401,051✔
770
                &BlockLimitFunction::NO_LIMIT_HIT,
1,401,051✔
771
                Some(per_tx_max_execution_time),
1,401,051✔
772
                &mut receipts_total,
1,401,051✔
773
            );
774

775
            tenure_tx.set_abort_callback(AbortCallback::None);
1,401,051✔
776

777
            let reason = match tx_result {
1,401,051✔
778
                TransactionResult::Success(success_result) => {
1,401,051✔
779
                    let all_events_valid = success_result
1,401,051✔
780
                        .receipt
1,401,051✔
781
                        .events
1,401,051✔
782
                        .iter()
1,401,051✔
783
                        .all(|event| is_event_pox_addr_valid(mainnet, event));
1,401,051✔
784
                    if !all_events_valid {
1,401,051✔
UNCOV
785
                        Some((
×
UNCOV
786
                            format!("Problematic tx {i}: contains invalid pox address"),
×
UNCOV
787
                            ValidateRejectCode::ProblematicTransaction,
×
UNCOV
788
                        ))
×
789
                    } else {
790
                        None
1,401,051✔
791
                    }
792
                }
UNCOV
793
                TransactionResult::Skipped(s) => Some((
×
794
                    format!("tx {i} skipped: {}", s.error),
×
UNCOV
795
                    ValidateRejectCode::BadTransaction,
×
UNCOV
796
                )),
×
UNCOV
797
                TransactionResult::ProcessingError(e) => Some((
×
UNCOV
798
                    format!("Error processing tx {i}: {}", e.error),
×
UNCOV
799
                    ValidateRejectCode::BadTransaction,
×
UNCOV
800
                )),
×
UNCOV
801
                TransactionResult::Problematic(p) => Some((
×
UNCOV
802
                    format!("Problematic tx {i}: {}", p.error),
×
UNCOV
803
                    ValidateRejectCode::ProblematicTransaction,
×
UNCOV
804
                )),
×
805
            };
806
            if let Some((reason, reject_code)) = reason {
1,401,051✔
UNCOV
807
                warn!(
×
808
                    "Rejected block proposal";
809
                    "reason" => %reason,
810
                    "tx" => ?tx,
811
                );
UNCOV
812
                return Err(BlockValidateRejectReason {
×
UNCOV
813
                    reason,
×
UNCOV
814
                    reason_code: reject_code,
×
UNCOV
815
                    failed_txid: Some(tx.txid()),
×
UNCOV
816
                });
×
817
            }
1,401,051✔
818
        }
819

820
        let mut block = builder.mine_nakamoto_block(&mut tenure_tx, burn_chain_height);
38,711✔
821
        // Override the block version with the one from the proposal. This must be
822
        // done before computing the block hash, because the block hash includes the
823
        // version in its computation.
824
        block.header.version = self.block.header.version;
38,711✔
825
        let size = builder.get_bytes_so_far();
38,711✔
826
        let cost = builder.tenure_finish(tenure_tx)?;
38,711✔
827

828
        // Clone signatures from block proposal
829
        // These have already been validated by `validate_nakamoto_block_burnchain()``
830
        block.header.miner_signature = self.block.header.miner_signature.clone();
38,711✔
831
        block
38,711✔
832
            .header
38,711✔
833
            .signer_signature
38,711✔
834
            .clone_from(&self.block.header.signer_signature);
38,711✔
835

836
        // Assuming `tx_merkle_root` has been checked we don't need to hash the whole block
837
        let expected_block_header_hash = self.block.header.block_hash();
38,711✔
838
        let computed_block_header_hash = block.header.block_hash();
38,711✔
839

840
        if computed_block_header_hash != expected_block_header_hash {
38,711✔
UNCOV
841
            warn!(
×
842
                "Rejected block proposal";
843
                "reason" => "Block hash is not as expected",
844
                "expected_block_header_hash" => %expected_block_header_hash,
845
                "computed_block_header_hash" => %computed_block_header_hash,
846
                "expected_block" => ?self.block,
847
                "computed_block" => ?block,
848
            );
UNCOV
849
            return Err(BlockValidateRejectReason {
×
UNCOV
850
                reason: "Block hash is not as expected".into(),
×
UNCOV
851
                reason_code: ValidateRejectCode::BadBlockHash,
×
UNCOV
852
                failed_txid: None,
×
853
            });
×
854
        }
38,711✔
855

856
        let validation_time_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
38,711✔
857

858
        info!(
38,711✔
859
            "Participant: validated anchored block";
860
            "block_header_hash" => %computed_block_header_hash,
861
            "height" => block.header.chain_length,
38,711✔
862
            "tx_count" => block.txs.len(),
38,711✔
863
            "parent_stacks_block_id" => %block.header.parent_block_id,
864
            "block_size" => size,
38,711✔
865
            "execution_cost" => %cost,
866
            "validation_time_ms" => validation_time_ms,
38,711✔
867
            "tx_fees_microstacks" => block.txs.iter().fold(0, |agg: u64, tx| {
1,401,051✔
868
                agg.saturating_add(tx.get_tx_fee())
1,401,051✔
869
            })
1,401,051✔
870
        );
871

872
        let replay_tx_hash = Self::tx_replay_hash(&self.replay_txs);
38,711✔
873

874
        Ok(BlockValidateOk {
38,711✔
875
            signer_signature_hash: block.header.signer_signature_hash(),
38,711✔
876
            cost,
38,711✔
877
            size,
38,711✔
878
            validation_time_ms,
38,711✔
879
            replay_tx_hash,
38,711✔
880
            replay_tx_exhausted,
38,711✔
881
        })
38,711✔
882
    }
39,393✔
883

884
    pub fn tx_replay_hash(replay_txs: &Option<Vec<StacksTransaction>>) -> Option<u64> {
89,821✔
885
        replay_txs.as_ref().map(|txs| {
89,821✔
886
            let mut hasher = DefaultHasher::new();
2,370✔
887
            txs.hash(&mut hasher);
2,370✔
888
            hasher.finish()
2,370✔
889
        })
2,370✔
890
    }
89,821✔
891

892
    /// Validate the block against the replay set.
893
    ///
894
    /// Returns a boolean indicating whether this block exhausts the replay set.
895
    ///
896
    /// Returns `false` if there is no replay set.
897
    fn validate_replay(
38,821✔
898
        &self,
38,821✔
899
        parent_stacks_header: &StacksHeaderInfo,
38,821✔
900
        tenure_change: Option<&StacksTransaction>,
38,821✔
901
        coinbase: Option<&StacksTransaction>,
38,821✔
902
        tenure_cause: MinerTenureInfoCause,
38,821✔
903
        // not directly used; used as a handle to open other chainstates
38,821✔
904
        chainstate_handle: &StacksChainState,
38,821✔
905
        burn_dbconn: &SortitionHandleConn,
38,821✔
906
    ) -> Result<bool, BlockValidateRejectReason> {
38,821✔
907
        let mut replay_txs_maybe: Option<VecDeque<StacksTransaction>> =
38,821✔
908
            self.replay_txs.clone().map(|txs| txs.into());
38,821✔
909

910
        let Some(ref mut replay_txs) = replay_txs_maybe else {
38,821✔
911
            return Ok(false);
37,671✔
912
        };
913

914
        let mut replay_builder = NakamotoBlockBuilder::new(
1,150✔
915
            &parent_stacks_header,
1,150✔
916
            &self.block.header.consensus_hash,
1,150✔
917
            self.block.header.burn_spent,
1,150✔
918
            tenure_change,
1,150✔
919
            coinbase,
1,150✔
920
            self.block.header.pox_treatment.len(),
1,150✔
921
            None,
1,150✔
922
            None,
1,150✔
923
            Some(self.block.header.timestamp),
1,150✔
924
            u64::from(DEFAULT_MAX_TENURE_BYTES),
1,150✔
UNCOV
925
        )?;
×
926
        let (mut replay_chainstate, _) = chainstate_handle.reopen()?;
1,150✔
927
        let mut replay_miner_tenure_info =
1,150✔
928
            replay_builder.load_tenure_info(&mut replay_chainstate, &burn_dbconn, tenure_cause)?;
1,150✔
929
        let mut replay_tenure_tx =
1,150✔
930
            replay_builder.tenure_begin(&burn_dbconn, &mut replay_miner_tenure_info)?;
1,150✔
931

932
        let mut total_receipts = 0;
1,150✔
933
        for (i, tx) in self.block.txs.iter().enumerate() {
2,180✔
934
            let tx_len = tx.tx_len();
2,180✔
935

936
            // If a list of replay transactions is set, this transaction must be the next
937
            // mineable transaction from this list.
938
            loop {
939
                if matches!(
670✔
940
                    tx.payload,
2,240✔
941
                    TransactionPayload::TenureChange(..) | TransactionPayload::Coinbase(..)
942
                ) {
943
                    // Allow this to happen, tenure extend checks happen elsewhere.
944
                    break;
1,570✔
945
                }
670✔
946
                fault_injection_reject_replay_txs()?;
670✔
947
                let Some(replay_tx) = replay_txs.pop_front() else {
590✔
948
                    // During transaction replay, we expect that the block only
949
                    // contains transactions from the replay set. Thus, if we're here,
950
                    // the block contains a transaction that is not in the replay set,
951
                    // and we should reject the block.
UNCOV
952
                    warn!("Rejected block proposal. Block contains transactions beyond the replay set.";
×
UNCOV
953
                        "txid" => %tx.txid(),
×
UNCOV
954
                        "tx_index" => i,
×
955
                    );
UNCOV
956
                    return Err(BlockValidateRejectReason {
×
UNCOV
957
                        reason_code: ValidateRejectCode::InvalidTransactionReplay,
×
UNCOV
958
                        reason: "Block contains transactions beyond the replay set".into(),
×
UNCOV
959
                        failed_txid: Some(tx.txid()),
×
UNCOV
960
                    });
×
961
                };
962
                if replay_tx.txid() == tx.txid() {
590✔
963
                    break;
500✔
964
                }
90✔
965

966
                // The included tx doesn't match the next tx in the
967
                // replay set. Check to see if the tx is skipped because
968
                // it was unmineable.
969
                let tx_result = replay_builder.try_mine_tx_with_len(
90✔
970
                    &mut replay_tenure_tx,
90✔
971
                    &replay_tx,
90✔
972
                    replay_tx.tx_len(),
90✔
973
                    &BlockLimitFunction::NO_LIMIT_HIT,
90✔
974
                    None,
90✔
975
                    &mut total_receipts,
90✔
976
                );
977
                match tx_result {
90✔
UNCOV
978
                    TransactionResult::Skipped(TransactionSkipped { error, .. })
×
979
                    | TransactionResult::ProcessingError(TransactionError { error, .. })
60✔
UNCOV
980
                    | TransactionResult::Problematic(TransactionProblematic { error, .. }) => {
×
981
                        // The tx wasn't able to be mined. Check the underlying error, to
982
                        // see if we should reject the block or allow the tx to be
983
                        // dropped from the replay set.
984

UNCOV
985
                        match error {
×
986
                            ChainError::CostOverflowError(..)
987
                            | ChainError::BlockTooBigError
988
                            | ChainError::BlockCostLimitError
989
                            | ChainError::ClarityError(ClarityError::CostError(..)) => {
990
                                // block limit reached; add tx back to replay set.
991
                                // BUT we know that the block should have ended at this point, so
992
                                // return an error.
UNCOV
993
                                let txid = replay_tx.txid();
×
UNCOV
994
                                replay_txs.push_front(replay_tx);
×
995

996
                                warn!("Rejecting block proposal. Next replay tx exceeds cost limits, so should have been in the next block.";
×
997
                                    "error" => %error,
998
                                    "txid" => %txid,
999
                                );
1000

UNCOV
1001
                                return Err(BlockValidateRejectReason {
×
1002
                                    reason_code: ValidateRejectCode::InvalidTransactionReplay,
×
UNCOV
1003
                                    reason: "Next replay tx exceeds cost limits, so should have been in the next block.".into(),
×
UNCOV
1004
                                    failed_txid: None,
×
1005
                                });
×
1006
                            }
1007
                            _ => {
1008
                                info!("During replay block validation, allowing problematic tx to be dropped";
60✔
1009
                                    "txid" => %replay_tx.txid(),
60✔
1010
                                    "error" => %error,
1011
                                );
1012
                                // it's ok, drop it
1013
                                continue;
60✔
1014
                            }
1015
                        }
1016
                    }
1017
                    TransactionResult::Success(_) => {
1018
                        // Tx should have been included
1019
                        warn!("Rejected block proposal. Block doesn't contain replay transaction that should have been included.";
30✔
1020
                            "block_txid" => %tx.txid(),
30✔
1021
                            "block_tx_index" => i,
30✔
1022
                            "replay_txid" => %replay_tx.txid(),
30✔
1023
                        );
1024
                        return Err(BlockValidateRejectReason {
30✔
1025
                            reason_code: ValidateRejectCode::InvalidTransactionReplay,
30✔
1026
                            reason: "Transaction is not in the replay set".into(),
30✔
1027
                            failed_txid: Some(tx.txid()),
30✔
1028
                        });
30✔
1029
                    }
1030
                };
1031
            }
1032

1033
            // Apply the block's transaction to our block builder, but we don't
1034
            // actually care about the result - that happens in the main
1035
            // validation check.
1036
            let _tx_result = replay_builder.try_mine_tx_with_len(
2,070✔
1037
                &mut replay_tenure_tx,
2,070✔
1038
                tx,
2,070✔
1039
                tx_len,
2,070✔
1040
                &BlockLimitFunction::NO_LIMIT_HIT,
2,070✔
1041
                None,
2,070✔
1042
                &mut total_receipts,
2,070✔
1043
            );
1044
        }
1045

1046
        let no_replay_txs_remaining = replay_txs.is_empty();
1,040✔
1047

1048
        // Now, we need to check if the remaining replay transactions are unmineable.
1049
        let only_unmineable_remaining = !replay_txs.is_empty()
1,040✔
1050
            && replay_txs.iter().all(|tx| {
920✔
1051
                let tx_result = replay_builder.try_mine_tx_with_len(
920✔
1052
                    &mut replay_tenure_tx,
920✔
1053
                    &tx,
920✔
1054
                    tx.tx_len(),
920✔
1055
                    &BlockLimitFunction::NO_LIMIT_HIT,
920✔
1056
                    None,
920✔
1057
                    &mut total_receipts,
920✔
1058
                );
1059
                match tx_result {
920✔
1060
                    TransactionResult::Skipped(TransactionSkipped { error, .. })
60✔
1061
                    | TransactionResult::ProcessingError(TransactionError { error, .. })
420✔
1062
                    | TransactionResult::Problematic(TransactionProblematic { error, .. }) => {
40✔
1063
                        // If it's just a cost error, it's not unmineable.
1064
                        !matches!(
460✔
UNCOV
1065
                            error,
×
1066
                            ChainError::CostOverflowError(..)
1067
                                | ChainError::BlockTooBigError
1068
                                | ChainError::ClarityError(ClarityError::CostError(..))
1069
                                | ChainError::BlockCostLimitError
1070
                        )
1071
                    }
1072
                    TransactionResult::Success(_) => {
1073
                        // The tx could have been included, but wasn't. This is ok, but we
1074
                        // haven't exhausted the replay set.
1075
                        false
400✔
1076
                    }
1077
                }
1078
            });
920✔
1079

1080
        Ok(no_replay_txs_remaining || only_unmineable_remaining)
1,040✔
1081
    }
38,821✔
1082
}
1083

1084
#[derive(Clone, Default)]
1085
pub struct RPCBlockProposalRequestHandler {
1086
    pub block_proposal: Option<NakamotoBlockProposal>,
1087
    pub auth: Option<String>,
1088
}
1089

1090
impl RPCBlockProposalRequestHandler {
1091
    pub fn new(auth: Option<String>) -> Self {
1,159,578✔
1092
        Self {
1,159,578✔
1093
            block_proposal: None,
1,159,578✔
1094
            auth,
1,159,578✔
1095
        }
1,159,578✔
1096
    }
1,159,578✔
1097

1098
    /// Decode a JSON-encoded block proposal
1099
    fn parse_json(body: &[u8]) -> Result<NakamotoBlockProposal, Error> {
109,405✔
1100
        serde_json::from_slice(body)
109,405✔
1101
            .map_err(|e| Error::DecodeError(format!("Failed to parse body: {e}")))
109,405✔
1102
    }
109,405✔
1103
}
1104

1105
/// Decode the HTTP request
1106
impl HttpRequest for RPCBlockProposalRequestHandler {
1107
    fn verb(&self) -> &'static str {
1,159,577✔
1108
        "POST"
1,159,577✔
1109
    }
1,159,577✔
1110

1111
    fn path_regex(&self) -> Regex {
2,319,156✔
1112
        Regex::new(r#"^/v3/block_proposal$"#).unwrap()
2,319,156✔
1113
    }
2,319,156✔
1114

1115
    fn metrics_identifier(&self) -> &str {
109,404✔
1116
        "/v3/block_proposal"
109,404✔
1117
    }
109,404✔
1118

1119
    /// Try to decode this request.
1120
    /// There's nothing to load here, so just make sure the request is well-formed.
1121
    fn try_parse_request(
109,416✔
1122
        &mut self,
109,416✔
1123
        preamble: &HttpRequestPreamble,
109,416✔
1124
        _captures: &Captures,
109,416✔
1125
        query: Option<&str>,
109,416✔
1126
        body: &[u8],
109,416✔
1127
    ) -> Result<HttpRequestContents, Error> {
109,416✔
1128
        // If no authorization is set, then the block proposal endpoint is not enabled
1129
        let Some(password) = &self.auth else {
109,416✔
UNCOV
1130
            return Err(Error::Http(400, "Bad Request.".into()));
×
1131
        };
1132
        let Some(auth_header) = preamble.headers.get("authorization") else {
109,416✔
1133
            return Err(Error::Http(401, "Unauthorized".into()));
11✔
1134
        };
1135
        if auth_header != password {
109,405✔
UNCOV
1136
            return Err(Error::Http(401, "Unauthorized".into()));
×
1137
        }
109,405✔
1138
        if preamble.get_content_length() == 0 {
109,405✔
UNCOV
1139
            return Err(Error::DecodeError(
×
UNCOV
1140
                "Invalid Http request: expected non-zero-length body for block proposal endpoint"
×
UNCOV
1141
                    .to_string(),
×
UNCOV
1142
            ));
×
1143
        }
109,405✔
1144
        if preamble.get_content_length() > MAX_PAYLOAD_LEN {
109,405✔
UNCOV
1145
            return Err(Error::DecodeError(
×
UNCOV
1146
                "Invalid Http request: BlockProposal body is too big".to_string(),
×
UNCOV
1147
            ));
×
1148
        }
109,405✔
1149

1150
        let block_proposal = match preamble.content_type {
109,405✔
1151
            Some(HttpContentType::JSON) => Self::parse_json(body)?,
109,405✔
1152
            Some(_) => {
UNCOV
1153
                return Err(Error::DecodeError(
×
UNCOV
1154
                    "Wrong Content-Type for block proposal; expected application/json".to_string(),
×
UNCOV
1155
                ))
×
1156
            }
1157
            None => {
UNCOV
1158
                return Err(Error::DecodeError(
×
UNCOV
1159
                    "Missing Content-Type for block proposal".to_string(),
×
UNCOV
1160
                ))
×
1161
            }
1162
        };
1163

1164
        if block_proposal.block.is_shadow_block() {
109,405✔
UNCOV
1165
            return Err(Error::DecodeError(
×
UNCOV
1166
                "Shadow blocks cannot be submitted for validation".to_string(),
×
UNCOV
1167
            ));
×
1168
        }
109,405✔
1169

1170
        self.block_proposal = Some(block_proposal);
109,405✔
1171
        Ok(HttpRequestContents::new().query_string(query))
109,405✔
1172
    }
109,416✔
1173
}
1174

1175
struct ProposalThreadInfo {
1176
    sortdb: SortitionDB,
1177
    chainstate: StacksChainState,
1178
    receiver: Box<dyn ProposalCallbackReceiver>,
1179
}
1180

1181
impl RPCRequestHandler for RPCBlockProposalRequestHandler {
1182
    /// Reset internal state
1183
    fn restart(&mut self) {
109,416✔
1184
        self.block_proposal = None
109,416✔
1185
    }
109,416✔
1186

1187
    /// Make the response
1188
    fn try_handle_request(
109,404✔
1189
        &mut self,
109,404✔
1190
        preamble: HttpRequestPreamble,
109,404✔
1191
        _contents: HttpRequestContents,
109,404✔
1192
        node: &mut StacksNodeState,
109,404✔
1193
    ) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> {
109,404✔
1194
        let block_proposal = self
109,404✔
1195
            .block_proposal
109,404✔
1196
            .take()
109,404✔
1197
            .ok_or(NetError::SendError("`block_proposal` not set".into()))?;
109,404✔
1198

1199
        info!(
109,404✔
1200
            "Received block proposal request";
1201
            "signer_signature_hash" => %block_proposal.block.header.signer_signature_hash(),
109,404✔
1202
            "block_header_hash" => %block_proposal.block.header.block_hash(),
109,404✔
1203
            "height" => block_proposal.block.header.chain_length,
109,404✔
1204
            "tx_count" => block_proposal.block.txs.len(),
109,404✔
1205
            "parent_stacks_block_id" => %block_proposal.block.header.parent_block_id,
1206
        );
1207

1208
        let res = node.with_node_state(|network, sortdb, chainstate, _mempool, rpc_args| {
109,404✔
1209
            if network.is_proposal_thread_running() {
109,404✔
1210
                return Err((
70,000✔
1211
                    TOO_MANY_REQUESTS_STATUS,
70,000✔
1212
                    NetError::SendError("Proposal currently being evaluated".into()),
70,000✔
1213
                ));
70,000✔
1214
            }
39,404✔
1215

1216
            if block_proposal
39,404✔
1217
                .block
39,404✔
1218
                .header
39,404✔
1219
                .timestamp
39,404✔
1220
                .saturating_add(network.get_connection_opts().block_proposal_max_age_secs)
39,404✔
1221
                < get_epoch_time_secs()
39,404✔
1222
            {
1223
                return Err((
11✔
1224
                    422,
11✔
1225
                    NetError::SendError("Block proposal is too old to process.".into()),
11✔
1226
                ));
11✔
1227
            }
39,393✔
1228

1229
            let (chainstate, _) = chainstate.reopen().map_err(|e| (400, NetError::from(e)))?;
39,393✔
1230
            let sortdb = sortdb.reopen().map_err(|e| (400, NetError::from(e)))?;
39,393✔
1231
            let receiver = rpc_args
39,393✔
1232
                .event_observer
39,393✔
1233
                .and_then(|observer| observer.get_proposal_callback_receiver())
39,393✔
1234
                .ok_or_else(|| {
39,393✔
UNCOV
1235
                    (
×
UNCOV
1236
                        400,
×
UNCOV
1237
                        NetError::SendError(
×
UNCOV
1238
                            "No `observer` registered for receiving proposal callbacks".into(),
×
UNCOV
1239
                        ),
×
UNCOV
1240
                    )
×
UNCOV
1241
                })?;
×
1242
            let thread_info = block_proposal
39,393✔
1243
                .spawn_validation_thread(
39,393✔
1244
                    sortdb,
39,393✔
1245
                    chainstate,
39,393✔
1246
                    receiver,
39,393✔
1247
                    network.get_connection_opts(),
39,393✔
1248
                )
1249
                .map_err(|_e| {
39,393✔
UNCOV
1250
                    (
×
UNCOV
1251
                        TOO_MANY_REQUESTS_STATUS,
×
UNCOV
1252
                        NetError::SendError(
×
UNCOV
1253
                            "IO error while spawning proposal callback thread".into(),
×
UNCOV
1254
                        ),
×
UNCOV
1255
                    )
×
UNCOV
1256
                })?;
×
1257
            network.set_proposal_thread(thread_info);
39,393✔
1258
            Ok(())
39,393✔
1259
        });
109,404✔
1260

1261
        match res {
109,404✔
1262
            Ok(_) => {
1263
                let preamble = HttpResponsePreamble::accepted_json(&preamble);
39,393✔
1264
                let body = HttpResponseContents::try_from_json(&serde_json::json!({
39,393✔
1265
                    "result": "Accepted",
39,393✔
1266
                    "message": "Block proposal is processing, result will be returned via the event observer"
39,393✔
1267
                }))?;
39,393✔
1268
                Ok((preamble, body))
39,393✔
1269
            }
1270
            Err((code, err)) => {
70,011✔
1271
                let preamble = HttpResponsePreamble::error_json(code, http_reason(code));
70,011✔
1272
                let body = HttpResponseContents::try_from_json(&serde_json::json!({
70,011✔
1273
                    "result": "Error",
70,011✔
1274
                    "message": format!("Could not process block proposal request: {err}")
70,011✔
1275
                }))?;
70,011✔
1276
                Ok((preamble, body))
70,011✔
1277
            }
1278
        }
1279
    }
109,404✔
1280
}
1281

1282
/// Decode the HTTP response
1283
impl HttpResponse for RPCBlockProposalRequestHandler {
1284
    fn try_parse_response(
3✔
1285
        &self,
3✔
1286
        preamble: &HttpResponsePreamble,
3✔
1287
        body: &[u8],
3✔
1288
    ) -> Result<HttpResponsePayload, Error> {
3✔
1289
        let response: BlockProposalResponse = parse_json(preamble, body)?;
3✔
1290
        HttpResponsePayload::try_from_json(response)
3✔
1291
    }
3✔
1292
}
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