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

stacks-network / stacks-core / 23943270448

03 Apr 2026 10:32AM UTC coverage: 77.559% (-8.2%) from 85.712%
23943270448

Pull #7077

github

52f01d
web-flow
Merge fa3f939ed into c529ad924
Pull Request #7077: feat: add burnchain DB copy and validation

3654 of 4220 new or added lines in 18 files covered. (86.59%)

19324 existing lines in 182 files now uncovered.

171991 of 221755 relevant lines covered (77.56%)

7658447.9 hits per line

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

57.89
/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::costs::ExecutionCost;
25
use regex::{Captures, Regex};
26
use serde::Deserialize;
27
use stacks_common::codec::{Error as CodecError, StacksMessageCodec, MAX_PAYLOAD_LEN};
28
use stacks_common::consts::CHAIN_ID_MAINNET;
29
use stacks_common::types::chainstate::{ConsensusHash, StacksBlockId};
30
use stacks_common::util::get_epoch_time_secs;
31
use stacks_common::util::hash::{hex_bytes, to_hex, Sha512Trunc256Sum};
32
#[cfg(any(test, feature = "testing"))]
33
use stacks_common::util::tests::TestFlag;
34

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

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

62
#[cfg(any(test, feature = "testing"))]
63
/// Artificial delay to add to block validation.
64
pub static TEST_VALIDATE_DELAY_DURATION_SECS: LazyLock<TestFlag<u64>> =
65
    LazyLock::new(TestFlag::default);
66
#[cfg(any(test, feature = "testing"))]
67
/// Mock for the set of transactions that must be replayed
68
pub static TEST_REPLAY_TRANSACTIONS: LazyLock<
69
    TestFlag<std::collections::VecDeque<StacksTransaction>>,
70
> = LazyLock::new(TestFlag::default);
71

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

76
// This enum is used to supply a `reason_code` for validation
77
//  rejection responses. This is serialized as an enum with string
78
//  type (in jsonschema terminology).
79
define_u8_enum![ValidateRejectCode {
80
    BadBlockHash = 0,
81
    BadTransaction = 1,
82
    InvalidBlock = 2,
83
    ChainstateError = 3,
84
    UnknownParent = 4,
85
    NonCanonicalTenure = 5,
86
    NoSuchTenure = 6,
87
    InvalidTransactionReplay = 7,
88
    InvalidParentBlock = 8,
89
    InvalidTimestamp = 9,
90
    NetworkChainMismatch = 10,
91
    NotFoundError = 11
92
}];
93

94
pub static TOO_MANY_REQUESTS_STATUS: u16 = 429;
95

96
impl TryFrom<u8> for ValidateRejectCode {
97
    type Error = CodecError;
98
    fn try_from(value: u8) -> Result<Self, Self::Error> {
72✔
99
        Self::from_u8(value)
72✔
100
            .ok_or_else(|| CodecError::DeserializeError(format!("Unknown type prefix: {value}")))
72✔
101
    }
72✔
102
}
103

104
fn hex_ser_block<S: serde::Serializer>(b: &NakamotoBlock, s: S) -> Result<S::Ok, S::Error> {
7✔
105
    let inst = to_hex(&b.serialize_to_vec());
7✔
106
    s.serialize_str(inst.as_str())
7✔
107
}
7✔
108

109
fn hex_deser_block<'de, D: serde::Deserializer<'de>>(d: D) -> Result<NakamotoBlock, D::Error> {
5✔
110
    let inst_str = String::deserialize(d)?;
5✔
111
    let bytes = hex_bytes(&inst_str).map_err(serde::de::Error::custom)?;
5✔
112
    NakamotoBlock::consensus_deserialize(&mut bytes.as_slice()).map_err(serde::de::Error::custom)
5✔
113
}
5✔
114

115
/// A response for block proposal validation
116
///  that the stacks-node thinks should be rejected.
117
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
118
pub struct BlockValidateReject {
119
    pub signer_signature_hash: Sha512Trunc256Sum,
120
    pub reason: String,
121
    pub reason_code: ValidateRejectCode,
122
}
123

124
#[derive(Debug, Clone, PartialEq)]
125
pub struct BlockValidateRejectReason {
126
    pub reason: String,
127
    pub reason_code: ValidateRejectCode,
128
}
129

130
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
131
pub enum BlockProposalResult {
132
    Accepted,
133
    Error,
134
}
135

136
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
137
pub struct BlockProposalResponse {
138
    pub result: BlockProposalResult,
139
    pub message: String,
140
}
141

142
impl<T> From<T> for BlockValidateRejectReason
143
where
144
    T: Into<ChainError>,
145
{
UNCOV
146
    fn from(value: T) -> Self {
×
UNCOV
147
        let ce: ChainError = value.into();
×
UNCOV
148
        let reason_code = match ce {
×
UNCOV
149
            ChainError::DBError(db_error::NotFoundError) => ValidateRejectCode::NotFoundError,
×
UNCOV
150
            _ => ValidateRejectCode::ChainstateError,
×
151
        };
UNCOV
152
        Self {
×
UNCOV
153
            reason: format!("Chainstate Error: {ce}"),
×
UNCOV
154
            reason_code,
×
UNCOV
155
        }
×
UNCOV
156
    }
×
157
}
158

159
/// A response for block proposal validation
160
///  that the stacks-node thinks is acceptable.
161
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
162
pub struct BlockValidateOk {
163
    pub signer_signature_hash: Sha512Trunc256Sum,
164
    pub cost: ExecutionCost,
165
    pub size: u64,
166
    pub validation_time_ms: u64,
167
    /// If a block was validated by a transaction replay set,
168
    /// then this returns `Some` with the hash of the replay set.
169
    pub replay_tx_hash: Option<u64>,
170
    /// If a block was validated by a transaction replay set,
171
    /// then this is true if this block exhausted the set of transactions.
172
    pub replay_tx_exhausted: bool,
173
}
174

175
/// This enum is used for serializing the response to block
176
/// proposal validation.
177
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
178
#[serde(tag = "result")]
179
pub enum BlockValidateResponse {
180
    Ok(BlockValidateOk),
181
    Reject(BlockValidateReject),
182
}
183

184
impl From<Result<BlockValidateOk, BlockValidateReject>> for BlockValidateResponse {
185
    fn from(value: Result<BlockValidateOk, BlockValidateReject>) -> Self {
2✔
186
        match value {
2✔
187
            Ok(o) => BlockValidateResponse::Ok(o),
2✔
UNCOV
188
            Err(e) => BlockValidateResponse::Reject(e),
×
189
        }
190
    }
2✔
191
}
192

193
impl BlockValidateResponse {
194
    /// Get the signer signature hash from the response
UNCOV
195
    pub fn signer_signature_hash(&self) -> &Sha512Trunc256Sum {
×
UNCOV
196
        match self {
×
UNCOV
197
            BlockValidateResponse::Ok(o) => &o.signer_signature_hash,
×
UNCOV
198
            BlockValidateResponse::Reject(r) => &r.signer_signature_hash,
×
199
        }
UNCOV
200
    }
×
201
}
202

203
#[cfg(any(test, feature = "testing"))]
204
fn fault_injection_validation_stall(auth_token: Option<String>) {
3✔
205
    if TEST_VALIDATE_STALL.get().contains(&auth_token) {
3✔
206
        // Do an extra check just so we don't log EVERY time.
UNCOV
207
        warn!("Block validation is stalled due to testing directive."; "auth_token" => ?auth_token);
×
UNCOV
208
        while TEST_VALIDATE_STALL.get().contains(&auth_token) {
×
UNCOV
209
            std::thread::sleep(std::time::Duration::from_millis(10));
×
UNCOV
210
        }
×
UNCOV
211
        info!(
×
212
            "Block validation is no longer stalled due to testing directive. Continuing..."; "auth_token" => ?auth_token
213
        );
214
    }
3✔
215
}
3✔
216

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

220
#[cfg(any(test, feature = "testing"))]
221
fn fault_injection_validation_delay() {
3✔
222
    let delay = TEST_VALIDATE_DELAY_DURATION_SECS.get();
3✔
223
    if delay == 0 {
3✔
224
        return;
3✔
UNCOV
225
    }
×
UNCOV
226
    warn!("Sleeping for {} seconds to simulate slow processing", delay);
×
UNCOV
227
    thread::sleep(Duration::from_secs(delay));
×
228
}
3✔
229

230
#[cfg(not(any(test, feature = "testing")))]
231
fn fault_injection_validation_delay() {}
232

233
#[cfg(any(test, feature = "testing"))]
UNCOV
234
fn fault_injection_reject_replay_txs() -> Result<(), BlockValidateRejectReason> {
×
UNCOV
235
    let reject = TEST_REJECT_REPLAY_TXS.get();
×
UNCOV
236
    if reject {
×
UNCOV
237
        Err(BlockValidateRejectReason {
×
UNCOV
238
            reason_code: ValidateRejectCode::InvalidTransactionReplay,
×
UNCOV
239
            reason: "Rejected by test flag".into(),
×
UNCOV
240
        })
×
241
    } else {
UNCOV
242
        Ok(())
×
243
    }
UNCOV
244
}
×
245

246
#[cfg(not(any(test, feature = "testing")))]
247
fn fault_injection_reject_replay_txs() -> Result<(), BlockValidateRejectReason> {
248
    Ok(())
249
}
250

251
/// Represents a block proposed to the `v3/block_proposal` endpoint for validation
252
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
253
pub struct NakamotoBlockProposal {
254
    /// Proposed block
255
    #[serde(serialize_with = "hex_ser_block", deserialize_with = "hex_deser_block")]
256
    pub block: NakamotoBlock,
257
    /// Identifies which chain block is for (Mainnet, Testnet, etc.)
258
    pub chain_id: u32,
259
    /// Optional transaction replay set
260
    pub replay_txs: Option<Vec<StacksTransaction>>,
261
}
262

263
impl NakamotoBlockProposal {
264
    fn spawn_validation_thread(
3✔
265
        self,
3✔
266
        sortdb: SortitionDB,
3✔
267
        mut chainstate: StacksChainState,
3✔
268
        receiver: Box<dyn ProposalCallbackReceiver>,
3✔
269
        connection_opts: &ConnectionOptions,
3✔
270
    ) -> Result<JoinHandle<()>, std::io::Error> {
3✔
271
        let timeout_secs = connection_opts.block_proposal_validation_timeout_secs;
3✔
272
        let auth_token = connection_opts.auth_token.clone();
3✔
273
        thread::Builder::new()
3✔
274
            .name("block-proposal".into())
3✔
275
            .spawn(move || {
3✔
276
                let result = self
3✔
277
                    .validate(&sortdb, &mut chainstate, timeout_secs, auth_token)
3✔
278
                    .map_err(|reason| BlockValidateReject {
3✔
279
                        signer_signature_hash: self.block.header.signer_signature_hash(),
2✔
280
                        reason_code: reason.reason_code,
2✔
281
                        reason: reason.reason,
2✔
282
                    });
2✔
283
                receiver.notify_proposal_result(result);
3✔
284
            })
3✔
285
    }
3✔
286

287
    /// DO NOT CALL FROM CONSENSUS CODE
288
    ///
289
    /// Check to see if a block builds atop the highest block in a given tenure.
290
    /// That is:
291
    /// - its parent must exist, and
292
    /// - its parent must be as high as the highest block in the given tenure.
293
    fn check_block_builds_on_highest_block_in_tenure(
3✔
294
        chainstate: &StacksChainState,
3✔
295
        sortdb: &SortitionDB,
3✔
296
        tenure_id: &ConsensusHash,
3✔
297
        parent_block_id: &StacksBlockId,
3✔
298
    ) -> Result<(), BlockValidateRejectReason> {
3✔
299
        let Some(highest_header) = NakamotoChainState::find_highest_known_block_header_in_tenure(
3✔
300
            chainstate, sortdb, tenure_id,
3✔
301
        )
302
        .map_err(|e| BlockValidateRejectReason {
3✔
303
            reason_code: ValidateRejectCode::ChainstateError,
×
304
            reason: format!("Failed to query highest block in tenure ID: {:?}", &e),
×
305
        })?
×
306
        else {
307
            warn!(
×
308
                "Rejected block proposal";
309
                "reason" => "Block is not a tenure-start block, and has an unrecognized tenure consensus hash",
310
                "consensus_hash" => %tenure_id,
311
            );
312
            return Err(BlockValidateRejectReason {
×
313
                reason_code: ValidateRejectCode::NoSuchTenure,
×
314
                reason: "Block is not a tenure-start block, and has an unrecognized tenure consensus hash".into(),
×
315
            });
×
316
        };
317
        let Some(parent_header) =
3✔
318
            NakamotoChainState::get_block_header(chainstate.db(), parent_block_id).map_err(
3✔
319
                |e| BlockValidateRejectReason {
320
                    reason_code: ValidateRejectCode::ChainstateError,
×
321
                    reason: format!("Failed to query block header by block ID: {:?}", &e),
×
322
                },
×
323
            )?
×
324
        else {
325
            warn!(
×
326
                "Rejected block proposal";
327
                "reason" => "Block has no parent",
328
                "parent_block_id" => %parent_block_id
329
            );
330
            return Err(BlockValidateRejectReason {
×
331
                reason_code: ValidateRejectCode::UnknownParent,
×
332
                reason: "Block has no parent".into(),
×
333
            });
×
334
        };
335
        if parent_header.anchored_header.height() != highest_header.anchored_header.height() {
3✔
UNCOV
336
            warn!(
×
337
                "Rejected block proposal";
338
                "reason" => "Block's parent is not the highest block in this tenure",
339
                "consensus_hash" => %tenure_id,
UNCOV
340
                "parent_header.height" => parent_header.anchored_header.height(),
×
UNCOV
341
                "highest_header.height" => highest_header.anchored_header.height(),
×
342
            );
UNCOV
343
            return Err(BlockValidateRejectReason {
×
UNCOV
344
                reason_code: ValidateRejectCode::InvalidParentBlock,
×
UNCOV
345
                reason: "Block is not higher than the highest block in its tenure".into(),
×
UNCOV
346
            });
×
347
        }
3✔
348
        Ok(())
3✔
349
    }
3✔
350

351
    /// Verify that the block we received builds upon a valid tenure.
352
    /// Implemented as a static function to facilitate testing.
353
    pub(crate) fn check_block_has_valid_tenure(
3✔
354
        db_handle: &SortitionHandleConn,
3✔
355
        tenure_id: &ConsensusHash,
3✔
356
    ) -> Result<(), BlockValidateRejectReason> {
3✔
357
        // Verify that the block's tenure is on the canonical sortition history
358
        if !db_handle.has_consensus_hash(tenure_id)? {
3✔
UNCOV
359
            warn!(
×
360
                "Rejected block proposal";
361
                "reason" => "Block's tenure consensus hash is not on the canonical Bitcoin fork",
362
                "consensus_hash" => %tenure_id,
363
            );
UNCOV
364
            return Err(BlockValidateRejectReason {
×
UNCOV
365
                reason_code: ValidateRejectCode::NonCanonicalTenure,
×
UNCOV
366
                reason: "Tenure consensus hash is not on the canonical Bitcoin fork".into(),
×
UNCOV
367
            });
×
368
        }
3✔
369
        Ok(())
3✔
370
    }
3✔
371

372
    /// Verify that the block we received builds on the highest block in its tenure.
373
    /// * For tenure-start blocks, the parent must be as high as the highest block in the parent
374
    /// block's tenure.
375
    /// * For all other blocks, the parent must be as high as the highest block in the tenure.
376
    ///
377
    /// Implemented as a static function to facilitate testing
378
    pub(crate) fn check_block_has_valid_parent(
3✔
379
        chainstate: &StacksChainState,
3✔
380
        sortdb: &SortitionDB,
3✔
381
        block: &NakamotoBlock,
3✔
382
    ) -> Result<(), BlockValidateRejectReason> {
3✔
383
        let is_tenure_start =
3✔
384
            block
3✔
385
                .is_wellformed_tenure_start_block()
3✔
386
                .map_err(|_| BlockValidateRejectReason {
3✔
387
                    reason_code: ValidateRejectCode::InvalidBlock,
×
388
                    reason: "Block is not well-formed".into(),
×
389
                })?;
×
390

391
        if !is_tenure_start {
3✔
392
            // this is a well-formed block that is not the start of a tenure, so it must build
393
            // atop an existing block in its tenure.
394
            Self::check_block_builds_on_highest_block_in_tenure(
3✔
395
                chainstate,
3✔
396
                sortdb,
3✔
397
                &block.header.consensus_hash,
3✔
398
                &block.header.parent_block_id,
3✔
UNCOV
399
            )?;
×
400
        } else {
401
            // this is a tenure-start block, so it must build atop a parent which has the
402
            // highest height in the *previous* tenure.
UNCOV
403
            let parent_header = NakamotoChainState::get_block_header(
×
UNCOV
404
                chainstate.db(),
×
UNCOV
405
                &block.header.parent_block_id,
×
406
            )?
×
UNCOV
407
            .ok_or_else(|| BlockValidateRejectReason {
×
408
                reason_code: ValidateRejectCode::UnknownParent,
×
409
                reason: "No parent block".into(),
×
410
            })?;
×
411

UNCOV
412
            Self::check_block_builds_on_highest_block_in_tenure(
×
UNCOV
413
                chainstate,
×
UNCOV
414
                sortdb,
×
UNCOV
415
                &parent_header.consensus_hash,
×
UNCOV
416
                &block.header.parent_block_id,
×
417
            )?;
×
418
        }
419
        Ok(())
3✔
420
    }
3✔
421

422
    /// Test this block proposal against the current chain state and
423
    /// either accept or reject the proposal
424
    ///
425
    /// This is done in 3 stages:
426
    /// - Static validation of the block, which checks the following:
427
    ///   - Block header is well-formed
428
    ///   - Transactions are well-formed
429
    ///   - Miner signature is valid
430
    /// - Validation of transactions by executing them agains current chainstate.
431
    ///   This is resource intensive, and therefore done only if previous checks pass
432
    ///
433
    /// During transaction replay, we also check that the block only contains the unmined
434
    /// transactions that need to be replayed, up until either:
435
    /// - The set of transactions that must be replayed is exhausted
436
    /// - A cost limit is hit
437
    pub fn validate(
3✔
438
        &self,
3✔
439
        sortdb: &SortitionDB,
3✔
440
        chainstate: &mut StacksChainState, // not directly used; used as a handle to open other chainstates
3✔
441
        timeout_secs: u64,
3✔
442
        auth_token: Option<String>,
3✔
443
    ) -> Result<BlockValidateOk, BlockValidateRejectReason> {
3✔
444
        fault_injection_validation_stall(auth_token);
3✔
445
        let start = Instant::now();
3✔
446

447
        fault_injection_validation_delay();
3✔
448

449
        let mainnet = self.chain_id == CHAIN_ID_MAINNET;
3✔
450
        if self.chain_id != chainstate.chain_id || mainnet != chainstate.mainnet {
3✔
UNCOV
451
            warn!(
×
452
                "Rejected block proposal";
453
                "reason" => "Wrong network/chain_id",
UNCOV
454
                "expected_chain_id" => chainstate.chain_id,
×
UNCOV
455
                "expected_mainnet" => chainstate.mainnet,
×
UNCOV
456
                "received_chain_id" => self.chain_id,
×
UNCOV
457
                "received_mainnet" => mainnet,
×
458
            );
UNCOV
459
            return Err(BlockValidateRejectReason {
×
UNCOV
460
                reason_code: ValidateRejectCode::NetworkChainMismatch,
×
UNCOV
461
                reason: "Wrong network/chain_id".into(),
×
UNCOV
462
            });
×
463
        }
3✔
464

465
        // Check block version. If it's less than the compiled-in version, just emit a warning
466
        // because there's a new version of the node / signer binary available that really ought to
467
        // be used (hint, hint)
468
        if self.block.header.version != NAKAMOTO_BLOCK_VERSION {
3✔
469
            warn!("Proposed block has unexpected version. Upgrade your node and/or signer ASAP.";
×
470
                  "block.header.version" => %self.block.header.version,
471
                  "expected" => %NAKAMOTO_BLOCK_VERSION);
472
        }
3✔
473

474
        // open sortition view to the current burn view.
475
        // If the block has a TenureChange with an Extend cause, then the burn view is whatever is
476
        // indicated in the TenureChange.
477
        // Otherwise, it's the same as the block's parent's burn view.
478
        let parent_stacks_header = NakamotoChainState::get_block_header(
3✔
479
            chainstate.db(),
3✔
480
            &self.block.header.parent_block_id,
3✔
481
        )?
×
482
        .ok_or_else(|| BlockValidateRejectReason {
3✔
UNCOV
483
            reason_code: ValidateRejectCode::UnknownParent,
×
UNCOV
484
            reason: "Unknown parent block".into(),
×
UNCOV
485
        })?;
×
486

487
        let burn_view_consensus_hash =
3✔
488
            NakamotoChainState::get_block_burn_view(sortdb, &self.block, &parent_stacks_header)?;
3✔
489
        let sort_tip =
3✔
490
            SortitionDB::get_block_snapshot_consensus(sortdb.conn(), &burn_view_consensus_hash)?
3✔
491
                .ok_or_else(|| BlockValidateRejectReason {
3✔
492
                    reason_code: ValidateRejectCode::NoSuchTenure,
×
493
                    reason: "Failed to find sortition for block tenure".to_string(),
×
494
                })?;
×
495

496
        let burn_dbconn: SortitionHandleConn = sortdb.index_handle(&sort_tip.sortition_id);
3✔
497
        let db_handle = sortdb.index_handle(&sort_tip.sortition_id);
3✔
498

499
        // (For the signer)
500
        // Verify that the block's tenure is on the canonical sortition history
501
        Self::check_block_has_valid_tenure(&db_handle, &self.block.header.consensus_hash)?;
3✔
502

503
        // (For the signer)
504
        // Verify that this block's parent is the highest such block we can build off of
505
        Self::check_block_has_valid_parent(chainstate, sortdb, &self.block)?;
3✔
506

507
        // get the burnchain tokens spent for this block. There must be a record of this (i.e.
508
        // there must be a block-commit for this), or otherwise this block doesn't correspond to
509
        // any burnchain chainstate.
510
        let expected_burn_opt =
3✔
511
            NakamotoChainState::get_expected_burns(&db_handle, chainstate.db(), &self.block)?;
3✔
512
        if expected_burn_opt.is_none() {
3✔
513
            warn!(
×
514
                "Rejected block proposal";
515
                "reason" => "Failed to find parent expected burns",
516
            );
517
            return Err(BlockValidateRejectReason {
×
518
                reason_code: ValidateRejectCode::UnknownParent,
×
519
                reason: "Failed to find parent expected burns".into(),
×
520
            });
×
521
        };
3✔
522

523
        // Static validation checks
524
        NakamotoChainState::validate_normal_nakamoto_block_burnchain(
3✔
525
            chainstate.nakamoto_blocks_db(),
3✔
526
            &db_handle,
3✔
527
            expected_burn_opt,
3✔
528
            &self.block,
3✔
529
            mainnet,
3✔
530
            self.chain_id,
3✔
UNCOV
531
        )?;
×
532

533
        // Validate txs against chainstate
534

535
        // Validate the block's timestamp. It must be:
536
        // - Greater than the parent block's timestamp
537
        // - At most 15 seconds into the future
538
        if let StacksBlockHeaderTypes::Nakamoto(parent_nakamoto_header) =
3✔
539
            &parent_stacks_header.anchored_header
3✔
540
        {
541
            if self.block.header.timestamp <= parent_nakamoto_header.timestamp {
3✔
542
                warn!(
1✔
543
                    "Rejected block proposal";
544
                    "reason" => "Block timestamp is not greater than parent block",
545
                    "block_timestamp" => self.block.header.timestamp,
1✔
546
                    "parent_block_timestamp" => parent_nakamoto_header.timestamp,
1✔
547
                );
548
                return Err(BlockValidateRejectReason {
1✔
549
                    reason_code: ValidateRejectCode::InvalidTimestamp,
1✔
550
                    reason: "Block timestamp is not greater than parent block".into(),
1✔
551
                });
1✔
552
            }
2✔
UNCOV
553
        }
×
554
        if self.block.header.timestamp > get_epoch_time_secs() + 15 {
2✔
555
            warn!(
1✔
556
                "Rejected block proposal";
557
                "reason" => "Block timestamp is too far into the future",
558
                "block_timestamp" => self.block.header.timestamp,
1✔
559
                "current_time" => get_epoch_time_secs(),
1✔
560
            );
561
            return Err(BlockValidateRejectReason {
1✔
562
                reason_code: ValidateRejectCode::InvalidTimestamp,
1✔
563
                reason: "Block timestamp is too far into the future".into(),
1✔
564
            });
1✔
565
        }
1✔
566

567
        if self.block.header.chain_length
1✔
568
            != parent_stacks_header.stacks_block_height.saturating_add(1)
1✔
569
        {
570
            warn!(
×
571
                "Rejected block proposal";
572
                "reason" => "Block height is non-contiguous with parent",
573
                "block_height" => self.block.header.chain_length,
×
574
                "parent_block_height" => parent_stacks_header.stacks_block_height,
×
575
            );
576
            return Err(BlockValidateRejectReason {
×
577
                reason_code: ValidateRejectCode::InvalidBlock,
×
578
                reason: "Block height is non-contiguous with parent".into(),
×
579
            });
×
580
        }
1✔
581

582
        let tenure_change = self
1✔
583
            .block
1✔
584
            .txs
1✔
585
            .iter()
1✔
586
            .find(|tx| matches!(tx.payload, TransactionPayload::TenureChange(..)));
1✔
587
        let coinbase = self
1✔
588
            .block
1✔
589
            .txs
1✔
590
            .iter()
1✔
591
            .find(|tx| matches!(tx.payload, TransactionPayload::Coinbase(..)));
1✔
592
        let tenure_cause = tenure_change
1✔
593
            .and_then(|tx| match &tx.payload {
1✔
UNCOV
594
                TransactionPayload::TenureChange(tc) => Some(MinerTenureInfoCause::from(tc)),
×
595
                _ => None,
×
UNCOV
596
            })
×
597
            .unwrap_or_else(|| MinerTenureInfoCause::NoTenureChange);
1✔
598

599
        let replay_tx_exhausted = self.validate_replay(
1✔
600
            &parent_stacks_header,
1✔
601
            tenure_change,
1✔
602
            coinbase,
1✔
603
            tenure_cause,
1✔
604
            chainstate,
1✔
605
            &burn_dbconn,
1✔
UNCOV
606
        )?;
×
607

608
        let mut builder = NakamotoBlockBuilder::new(
1✔
609
            &parent_stacks_header,
1✔
610
            &self.block.header.consensus_hash,
1✔
611
            self.block.header.burn_spent,
1✔
612
            tenure_change,
1✔
613
            coinbase,
1✔
614
            self.block.header.pox_treatment.len(),
1✔
615
            None,
1✔
616
            None,
1✔
617
            Some(self.block.header.timestamp),
1✔
618
            u64::from(DEFAULT_MAX_TENURE_BYTES),
1✔
619
        )?;
×
620

621
        let mut miner_tenure_info =
1✔
622
            builder.load_tenure_info(chainstate, &burn_dbconn, tenure_cause)?;
1✔
623
        let burn_chain_height = miner_tenure_info.burn_tip_height;
1✔
624
        let mut tenure_tx = builder.tenure_begin(&burn_dbconn, &mut miner_tenure_info)?;
1✔
625

626
        let block_deadline = Instant::now() + Duration::from_secs(timeout_secs);
1✔
627
        let mut receipts_total = 0u64;
1✔
628
        for (i, tx) in self.block.txs.iter().enumerate() {
1✔
629
            let now = Instant::now();
1✔
630
            if now >= block_deadline {
1✔
631
                return Err(BlockValidateRejectReason {
×
632
                    reason: format!("Problematic tx {i}: execution time expired"),
×
633
                    reason_code: ValidateRejectCode::BadTransaction,
×
634
                });
×
635
            }
1✔
636
            let remaining = block_deadline.saturating_duration_since(now);
1✔
637

638
            let tx_len = tx.tx_len();
1✔
639

640
            let tx_result = builder.try_mine_tx_with_len(
1✔
641
                &mut tenure_tx,
1✔
642
                tx,
1✔
643
                tx_len,
1✔
644
                &BlockLimitFunction::NO_LIMIT_HIT,
1✔
645
                Some(remaining),
1✔
646
                &mut receipts_total,
1✔
647
            );
648
            let err = match tx_result {
1✔
649
                TransactionResult::Success(_) => Ok(()),
1✔
650
                TransactionResult::Skipped(s) => Err(format!("tx {i} skipped: {}", s.error)),
×
651
                TransactionResult::ProcessingError(e) => {
×
652
                    Err(format!("Error processing tx {i}: {}", e.error))
×
653
                }
654
                TransactionResult::Problematic(p) => {
×
655
                    Err(format!("Problematic tx {i}: {}", p.error))
×
656
                }
657
            };
658
            if let Err(reason) = err {
1✔
659
                warn!(
×
660
                    "Rejected block proposal";
661
                    "reason" => %reason,
662
                    "tx" => ?tx,
663
                );
664
                return Err(BlockValidateRejectReason {
×
665
                    reason,
×
666
                    reason_code: ValidateRejectCode::BadTransaction,
×
667
                });
×
668
            }
1✔
669
        }
670

671
        let mut block = builder.mine_nakamoto_block(&mut tenure_tx, burn_chain_height);
1✔
672
        // Override the block version with the one from the proposal. This must be
673
        // done before computing the block hash, because the block hash includes the
674
        // version in its computation.
675
        block.header.version = self.block.header.version;
1✔
676
        let size = builder.get_bytes_so_far();
1✔
677
        let cost = builder.tenure_finish(tenure_tx)?;
1✔
678

679
        // Clone signatures from block proposal
680
        // These have already been validated by `validate_nakamoto_block_burnchain()``
681
        block.header.miner_signature = self.block.header.miner_signature.clone();
1✔
682
        block
1✔
683
            .header
1✔
684
            .signer_signature
1✔
685
            .clone_from(&self.block.header.signer_signature);
1✔
686

687
        // Assuming `tx_merkle_root` has been checked we don't need to hash the whole block
688
        let expected_block_header_hash = self.block.header.block_hash();
1✔
689
        let computed_block_header_hash = block.header.block_hash();
1✔
690

691
        if computed_block_header_hash != expected_block_header_hash {
1✔
692
            warn!(
×
693
                "Rejected block proposal";
694
                "reason" => "Block hash is not as expected",
695
                "expected_block_header_hash" => %expected_block_header_hash,
696
                "computed_block_header_hash" => %computed_block_header_hash,
697
                "expected_block" => ?self.block,
698
                "computed_block" => ?block,
699
            );
700
            return Err(BlockValidateRejectReason {
×
701
                reason: "Block hash is not as expected".into(),
×
702
                reason_code: ValidateRejectCode::BadBlockHash,
×
703
            });
×
704
        }
1✔
705

706
        let validation_time_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
1✔
707

708
        info!(
1✔
709
            "Participant: validated anchored block";
710
            "block_header_hash" => %computed_block_header_hash,
711
            "height" => block.header.chain_length,
1✔
712
            "tx_count" => block.txs.len(),
1✔
713
            "parent_stacks_block_id" => %block.header.parent_block_id,
714
            "block_size" => size,
1✔
715
            "execution_cost" => %cost,
716
            "validation_time_ms" => validation_time_ms,
1✔
717
            "tx_fees_microstacks" => block.txs.iter().fold(0, |agg: u64, tx| {
1✔
718
                agg.saturating_add(tx.get_tx_fee())
1✔
719
            })
1✔
720
        );
721

722
        let replay_tx_hash = Self::tx_replay_hash(&self.replay_txs);
1✔
723

724
        Ok(BlockValidateOk {
1✔
725
            signer_signature_hash: block.header.signer_signature_hash(),
1✔
726
            cost,
1✔
727
            size,
1✔
728
            validation_time_ms,
1✔
729
            replay_tx_hash,
1✔
730
            replay_tx_exhausted,
1✔
731
        })
1✔
732
    }
3✔
733

734
    pub fn tx_replay_hash(replay_txs: &Option<Vec<StacksTransaction>>) -> Option<u64> {
1✔
735
        replay_txs.as_ref().map(|txs| {
1✔
UNCOV
736
            let mut hasher = DefaultHasher::new();
×
UNCOV
737
            txs.hash(&mut hasher);
×
UNCOV
738
            hasher.finish()
×
UNCOV
739
        })
×
740
    }
1✔
741

742
    /// Validate the block against the replay set.
743
    ///
744
    /// Returns a boolean indicating whether this block exhausts the replay set.
745
    ///
746
    /// Returns `false` if there is no replay set.
747
    fn validate_replay(
1✔
748
        &self,
1✔
749
        parent_stacks_header: &StacksHeaderInfo,
1✔
750
        tenure_change: Option<&StacksTransaction>,
1✔
751
        coinbase: Option<&StacksTransaction>,
1✔
752
        tenure_cause: MinerTenureInfoCause,
1✔
753
        // not directly used; used as a handle to open other chainstates
1✔
754
        chainstate_handle: &StacksChainState,
1✔
755
        burn_dbconn: &SortitionHandleConn,
1✔
756
    ) -> Result<bool, BlockValidateRejectReason> {
1✔
757
        let mut replay_txs_maybe: Option<VecDeque<StacksTransaction>> =
1✔
758
            self.replay_txs.clone().map(|txs| txs.into());
1✔
759

760
        let Some(ref mut replay_txs) = replay_txs_maybe else {
1✔
761
            return Ok(false);
1✔
762
        };
763

UNCOV
764
        let mut replay_builder = NakamotoBlockBuilder::new(
×
UNCOV
765
            &parent_stacks_header,
×
UNCOV
766
            &self.block.header.consensus_hash,
×
UNCOV
767
            self.block.header.burn_spent,
×
UNCOV
768
            tenure_change,
×
UNCOV
769
            coinbase,
×
UNCOV
770
            self.block.header.pox_treatment.len(),
×
UNCOV
771
            None,
×
UNCOV
772
            None,
×
UNCOV
773
            Some(self.block.header.timestamp),
×
UNCOV
774
            u64::from(DEFAULT_MAX_TENURE_BYTES),
×
775
        )?;
×
UNCOV
776
        let (mut replay_chainstate, _) = chainstate_handle.reopen()?;
×
UNCOV
777
        let mut replay_miner_tenure_info =
×
UNCOV
778
            replay_builder.load_tenure_info(&mut replay_chainstate, &burn_dbconn, tenure_cause)?;
×
UNCOV
779
        let mut replay_tenure_tx =
×
UNCOV
780
            replay_builder.tenure_begin(&burn_dbconn, &mut replay_miner_tenure_info)?;
×
781

UNCOV
782
        let mut total_receipts = 0;
×
UNCOV
783
        for (i, tx) in self.block.txs.iter().enumerate() {
×
UNCOV
784
            let tx_len = tx.tx_len();
×
785

786
            // If a list of replay transactions is set, this transaction must be the next
787
            // mineable transaction from this list.
788
            loop {
UNCOV
789
                if matches!(
×
UNCOV
790
                    tx.payload,
×
791
                    TransactionPayload::TenureChange(..) | TransactionPayload::Coinbase(..)
792
                ) {
793
                    // Allow this to happen, tenure extend checks happen elsewhere.
UNCOV
794
                    break;
×
UNCOV
795
                }
×
UNCOV
796
                fault_injection_reject_replay_txs()?;
×
UNCOV
797
                let Some(replay_tx) = replay_txs.pop_front() else {
×
798
                    // During transaction replay, we expect that the block only
799
                    // contains transactions from the replay set. Thus, if we're here,
800
                    // the block contains a transaction that is not in the replay set,
801
                    // and we should reject the block.
802
                    warn!("Rejected block proposal. Block contains transactions beyond the replay set.";
×
803
                        "txid" => %tx.txid(),
×
804
                        "tx_index" => i,
×
805
                    );
806
                    return Err(BlockValidateRejectReason {
×
807
                        reason_code: ValidateRejectCode::InvalidTransactionReplay,
×
808
                        reason: "Block contains transactions beyond the replay set".into(),
×
809
                    });
×
810
                };
UNCOV
811
                if replay_tx.txid() == tx.txid() {
×
UNCOV
812
                    break;
×
UNCOV
813
                }
×
814

815
                // The included tx doesn't match the next tx in the
816
                // replay set. Check to see if the tx is skipped because
817
                // it was unmineable.
UNCOV
818
                let tx_result = replay_builder.try_mine_tx_with_len(
×
UNCOV
819
                    &mut replay_tenure_tx,
×
UNCOV
820
                    &replay_tx,
×
UNCOV
821
                    replay_tx.tx_len(),
×
UNCOV
822
                    &BlockLimitFunction::NO_LIMIT_HIT,
×
UNCOV
823
                    None,
×
UNCOV
824
                    &mut total_receipts,
×
825
                );
UNCOV
826
                match tx_result {
×
827
                    TransactionResult::Skipped(TransactionSkipped { error, .. })
×
UNCOV
828
                    | TransactionResult::ProcessingError(TransactionError { error, .. })
×
829
                    | TransactionResult::Problematic(TransactionProblematic { error, .. }) => {
×
830
                        // The tx wasn't able to be mined. Check the underlying error, to
831
                        // see if we should reject the block or allow the tx to be
832
                        // dropped from the replay set.
833

834
                        match error {
×
835
                            ChainError::CostOverflowError(..)
836
                            | ChainError::BlockTooBigError
837
                            | ChainError::BlockCostLimitError
838
                            | ChainError::ClarityError(ClarityError::CostError(..)) => {
839
                                // block limit reached; add tx back to replay set.
840
                                // BUT we know that the block should have ended at this point, so
841
                                // return an error.
842
                                let txid = replay_tx.txid();
×
843
                                replay_txs.push_front(replay_tx);
×
844

845
                                warn!("Rejecting block proposal. Next replay tx exceeds cost limits, so should have been in the next block.";
×
846
                                    "error" => %error,
847
                                    "txid" => %txid,
848
                                );
849

850
                                return Err(BlockValidateRejectReason {
×
851
                                    reason_code: ValidateRejectCode::InvalidTransactionReplay,
×
852
                                    reason: "Next replay tx exceeds cost limits, so should have been in the next block.".into(),
×
853
                                });
×
854
                            }
855
                            _ => {
UNCOV
856
                                info!("During replay block validation, allowing problematic tx to be dropped";
×
UNCOV
857
                                    "txid" => %replay_tx.txid(),
×
858
                                    "error" => %error,
859
                                );
860
                                // it's ok, drop it
UNCOV
861
                                continue;
×
862
                            }
863
                        }
864
                    }
865
                    TransactionResult::Success(_) => {
866
                        // Tx should have been included
UNCOV
867
                        warn!("Rejected block proposal. Block doesn't contain replay transaction that should have been included.";
×
UNCOV
868
                            "block_txid" => %tx.txid(),
×
UNCOV
869
                            "block_tx_index" => i,
×
UNCOV
870
                            "replay_txid" => %replay_tx.txid(),
×
871
                        );
UNCOV
872
                        return Err(BlockValidateRejectReason {
×
UNCOV
873
                            reason_code: ValidateRejectCode::InvalidTransactionReplay,
×
UNCOV
874
                            reason: "Transaction is not in the replay set".into(),
×
UNCOV
875
                        });
×
876
                    }
877
                };
878
            }
879

880
            // Apply the block's transaction to our block builder, but we don't
881
            // actually care about the result - that happens in the main
882
            // validation check.
UNCOV
883
            let _tx_result = replay_builder.try_mine_tx_with_len(
×
UNCOV
884
                &mut replay_tenure_tx,
×
UNCOV
885
                tx,
×
UNCOV
886
                tx_len,
×
UNCOV
887
                &BlockLimitFunction::NO_LIMIT_HIT,
×
UNCOV
888
                None,
×
UNCOV
889
                &mut total_receipts,
×
890
            );
891
        }
892

UNCOV
893
        let no_replay_txs_remaining = replay_txs.is_empty();
×
894

895
        // Now, we need to check if the remaining replay transactions are unmineable.
UNCOV
896
        let only_unmineable_remaining = !replay_txs.is_empty()
×
UNCOV
897
            && replay_txs.iter().all(|tx| {
×
UNCOV
898
                let tx_result = replay_builder.try_mine_tx_with_len(
×
UNCOV
899
                    &mut replay_tenure_tx,
×
UNCOV
900
                    &tx,
×
UNCOV
901
                    tx.tx_len(),
×
UNCOV
902
                    &BlockLimitFunction::NO_LIMIT_HIT,
×
UNCOV
903
                    None,
×
UNCOV
904
                    &mut total_receipts,
×
905
                );
UNCOV
906
                match tx_result {
×
UNCOV
907
                    TransactionResult::Skipped(TransactionSkipped { error, .. })
×
UNCOV
908
                    | TransactionResult::ProcessingError(TransactionError { error, .. })
×
UNCOV
909
                    | TransactionResult::Problematic(TransactionProblematic { error, .. }) => {
×
910
                        // If it's just a cost error, it's not unmineable.
UNCOV
911
                        !matches!(
×
912
                            error,
×
913
                            ChainError::CostOverflowError(..)
914
                                | ChainError::BlockTooBigError
915
                                | ChainError::ClarityError(ClarityError::CostError(..))
916
                                | ChainError::BlockCostLimitError
917
                        )
918
                    }
919
                    TransactionResult::Success(_) => {
920
                        // The tx could have been included, but wasn't. This is ok, but we
921
                        // haven't exhausted the replay set.
UNCOV
922
                        false
×
923
                    }
924
                }
UNCOV
925
            });
×
926

UNCOV
927
        Ok(no_replay_txs_remaining || only_unmineable_remaining)
×
928
    }
1✔
929
}
930

931
#[derive(Clone, Default)]
932
pub struct RPCBlockProposalRequestHandler {
933
    pub block_proposal: Option<NakamotoBlockProposal>,
934
    pub auth: Option<String>,
935
}
936

937
impl RPCBlockProposalRequestHandler {
938
    pub fn new(auth: Option<String>) -> Self {
1,625✔
939
        Self {
1,625✔
940
            block_proposal: None,
1,625✔
941
            auth,
1,625✔
942
        }
1,625✔
943
    }
1,625✔
944

945
    /// Decode a JSON-encoded block proposal
946
    fn parse_json(body: &[u8]) -> Result<NakamotoBlockProposal, Error> {
5✔
947
        serde_json::from_slice(body)
5✔
948
            .map_err(|e| Error::DecodeError(format!("Failed to parse body: {e}")))
5✔
949
    }
5✔
950
}
951

952
/// Decode the HTTP request
953
impl HttpRequest for RPCBlockProposalRequestHandler {
954
    fn verb(&self) -> &'static str {
1,624✔
955
        "POST"
1,624✔
956
    }
1,624✔
957

958
    fn path_regex(&self) -> Regex {
3,250✔
959
        Regex::new(r#"^/v3/block_proposal$"#).unwrap()
3,250✔
960
    }
3,250✔
961

962
    fn metrics_identifier(&self) -> &str {
4✔
963
        "/v3/block_proposal"
4✔
964
    }
4✔
965

966
    /// Try to decode this request.
967
    /// There's nothing to load here, so just make sure the request is well-formed.
968
    fn try_parse_request(
6✔
969
        &mut self,
6✔
970
        preamble: &HttpRequestPreamble,
6✔
971
        _captures: &Captures,
6✔
972
        query: Option<&str>,
6✔
973
        body: &[u8],
6✔
974
    ) -> Result<HttpRequestContents, Error> {
6✔
975
        // If no authorization is set, then the block proposal endpoint is not enabled
976
        let Some(password) = &self.auth else {
6✔
977
            return Err(Error::Http(400, "Bad Request.".into()));
×
978
        };
979
        let Some(auth_header) = preamble.headers.get("authorization") else {
6✔
980
            return Err(Error::Http(401, "Unauthorized".into()));
1✔
981
        };
982
        if auth_header != password {
5✔
983
            return Err(Error::Http(401, "Unauthorized".into()));
×
984
        }
5✔
985
        if preamble.get_content_length() == 0 {
5✔
986
            return Err(Error::DecodeError(
×
987
                "Invalid Http request: expected non-zero-length body for block proposal endpoint"
×
988
                    .to_string(),
×
989
            ));
×
990
        }
5✔
991
        if preamble.get_content_length() > MAX_PAYLOAD_LEN {
5✔
992
            return Err(Error::DecodeError(
×
993
                "Invalid Http request: BlockProposal body is too big".to_string(),
×
994
            ));
×
995
        }
5✔
996

997
        let block_proposal = match preamble.content_type {
5✔
998
            Some(HttpContentType::JSON) => Self::parse_json(body)?,
5✔
999
            Some(_) => {
1000
                return Err(Error::DecodeError(
×
1001
                    "Wrong Content-Type for block proposal; expected application/json".to_string(),
×
1002
                ))
×
1003
            }
1004
            None => {
1005
                return Err(Error::DecodeError(
×
1006
                    "Missing Content-Type for block proposal".to_string(),
×
1007
                ))
×
1008
            }
1009
        };
1010

1011
        if block_proposal.block.is_shadow_block() {
5✔
1012
            return Err(Error::DecodeError(
×
1013
                "Shadow blocks cannot be submitted for validation".to_string(),
×
1014
            ));
×
1015
        }
5✔
1016

1017
        self.block_proposal = Some(block_proposal);
5✔
1018
        Ok(HttpRequestContents::new().query_string(query))
5✔
1019
    }
6✔
1020
}
1021

1022
struct ProposalThreadInfo {
1023
    sortdb: SortitionDB,
1024
    chainstate: StacksChainState,
1025
    receiver: Box<dyn ProposalCallbackReceiver>,
1026
}
1027

1028
impl RPCRequestHandler for RPCBlockProposalRequestHandler {
1029
    /// Reset internal state
1030
    fn restart(&mut self) {
6✔
1031
        self.block_proposal = None
6✔
1032
    }
6✔
1033

1034
    /// Make the response
1035
    fn try_handle_request(
4✔
1036
        &mut self,
4✔
1037
        preamble: HttpRequestPreamble,
4✔
1038
        _contents: HttpRequestContents,
4✔
1039
        node: &mut StacksNodeState,
4✔
1040
    ) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> {
4✔
1041
        let block_proposal = self
4✔
1042
            .block_proposal
4✔
1043
            .take()
4✔
1044
            .ok_or(NetError::SendError("`block_proposal` not set".into()))?;
4✔
1045

1046
        info!(
4✔
1047
            "Received block proposal request";
1048
            "signer_signature_hash" => %block_proposal.block.header.signer_signature_hash(),
4✔
1049
            "block_header_hash" => %block_proposal.block.header.block_hash(),
4✔
1050
            "height" => block_proposal.block.header.chain_length,
4✔
1051
            "tx_count" => block_proposal.block.txs.len(),
4✔
1052
            "parent_stacks_block_id" => %block_proposal.block.header.parent_block_id,
1053
        );
1054

1055
        let res = node.with_node_state(|network, sortdb, chainstate, _mempool, rpc_args| {
4✔
1056
            if network.is_proposal_thread_running() {
4✔
UNCOV
1057
                return Err((
×
UNCOV
1058
                    TOO_MANY_REQUESTS_STATUS,
×
UNCOV
1059
                    NetError::SendError("Proposal currently being evaluated".into()),
×
UNCOV
1060
                ));
×
1061
            }
4✔
1062

1063
            if block_proposal
4✔
1064
                .block
4✔
1065
                .header
4✔
1066
                .timestamp
4✔
1067
                .saturating_add(network.get_connection_opts().block_proposal_max_age_secs)
4✔
1068
                < get_epoch_time_secs()
4✔
1069
            {
1070
                return Err((
1✔
1071
                    422,
1✔
1072
                    NetError::SendError("Block proposal is too old to process.".into()),
1✔
1073
                ));
1✔
1074
            }
3✔
1075

1076
            let (chainstate, _) = chainstate.reopen().map_err(|e| (400, NetError::from(e)))?;
3✔
1077
            let sortdb = sortdb.reopen().map_err(|e| (400, NetError::from(e)))?;
3✔
1078
            let receiver = rpc_args
3✔
1079
                .event_observer
3✔
1080
                .and_then(|observer| observer.get_proposal_callback_receiver())
3✔
1081
                .ok_or_else(|| {
3✔
1082
                    (
×
1083
                        400,
×
1084
                        NetError::SendError(
×
1085
                            "No `observer` registered for receiving proposal callbacks".into(),
×
1086
                        ),
×
1087
                    )
×
1088
                })?;
×
1089
            let thread_info = block_proposal
3✔
1090
                .spawn_validation_thread(
3✔
1091
                    sortdb,
3✔
1092
                    chainstate,
3✔
1093
                    receiver,
3✔
1094
                    network.get_connection_opts(),
3✔
1095
                )
1096
                .map_err(|_e| {
3✔
1097
                    (
×
1098
                        TOO_MANY_REQUESTS_STATUS,
×
1099
                        NetError::SendError(
×
1100
                            "IO error while spawning proposal callback thread".into(),
×
1101
                        ),
×
1102
                    )
×
1103
                })?;
×
1104
            network.set_proposal_thread(thread_info);
3✔
1105
            Ok(())
3✔
1106
        });
4✔
1107

1108
        match res {
4✔
1109
            Ok(_) => {
1110
                let preamble = HttpResponsePreamble::accepted_json(&preamble);
3✔
1111
                let body = HttpResponseContents::try_from_json(&serde_json::json!({
3✔
1112
                    "result": "Accepted",
3✔
1113
                    "message": "Block proposal is processing, result will be returned via the event observer"
3✔
1114
                }))?;
3✔
1115
                Ok((preamble, body))
3✔
1116
            }
1117
            Err((code, err)) => {
1✔
1118
                let preamble = HttpResponsePreamble::error_json(code, http_reason(code));
1✔
1119
                let body = HttpResponseContents::try_from_json(&serde_json::json!({
1✔
1120
                    "result": "Error",
1✔
1121
                    "message": format!("Could not process block proposal request: {err}")
1✔
1122
                }))?;
1✔
1123
                Ok((preamble, body))
1✔
1124
            }
1125
        }
1126
    }
4✔
1127
}
1128

1129
/// Decode the HTTP response
1130
impl HttpResponse for RPCBlockProposalRequestHandler {
1131
    fn try_parse_response(
3✔
1132
        &self,
3✔
1133
        preamble: &HttpResponsePreamble,
3✔
1134
        body: &[u8],
3✔
1135
    ) -> Result<HttpResponsePayload, Error> {
3✔
1136
        let response: BlockProposalResponse = parse_json(preamble, body)?;
3✔
1137
        HttpResponsePayload::try_from_json(response)
3✔
1138
    }
3✔
1139
}
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