Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions stacks-common/src/util/hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -777,9 +777,7 @@ mod test {
let tree = MerkleTree::new(&fixture.data);

assert_eq!(Some(tree.clone()), fixture.res);
if fixture.res.is_some() {
let nodes = fixture.res.unwrap().nodes;

if let Some(nodes) = fixture.res.map(|res| res.nodes) {
if !nodes.is_empty() {
assert_eq!(tree.root(), nodes[nodes.len() - 1][0]);
} else {
Expand Down
3 changes: 2 additions & 1 deletion stackslib/src/chainstate/burn/db/sortdb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2078,7 +2078,8 @@ impl<'a> SortitionHandleConn<'a> {
self.get_indexed(&self.context.chain_tip, key)
}

fn get_sortition_id_for_bhh(
/// Get the sortition ID for a particular burn header hash in the current fork.
pub fn get_sortition_id_for_bhh(
&self,
burn_header_hash: &BurnchainHeaderHash,
) -> Result<Option<SortitionId>, db_error> {
Expand Down
2 changes: 1 addition & 1 deletion stackslib/src/chainstate/stacks/db/transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1667,7 +1667,7 @@ pub mod test {
use clarity::vm::representations::{ClarityName, ContractName};
use clarity::vm::test_util::{UnitTestBurnStateDB, TEST_BURN_STATE_DB};
use clarity::vm::tests::TEST_HEADER_DB;
use clarity::vm::types::*;
use clarity::vm::types::ResponseData;
use rand::Rng;
use stacks_common::types::chainstate::SortitionId;
use stacks_common::util::hash::*;
Expand Down
292 changes: 228 additions & 64 deletions stackslib/src/net/api/gettenureblocks.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Stacks Open Internet Foundation
// Copyright (C) 2025-2026 Stacks Open Internet Foundation
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
Expand All @@ -12,16 +12,18 @@
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

use clarity::types::chainstate::StacksBlockId;
use regex::{Captures, Regex};
use serde::{Deserialize, Serialize};
use serde_json;
use stacks_common::types::chainstate::{BlockHeaderHash, ConsensusHash};
use stacks_common::types::net::PeerHost;

use crate::chainstate::burn::db::sortdb::SortitionDB;
use crate::chainstate::burn::db::DBConn;
use crate::chainstate::burn::BlockSnapshot;
use crate::chainstate::nakamoto::NakamotoChainState;
use crate::chainstate::stacks::db::{StacksBlockHeaderTypes, StacksChainState};
use crate::chainstate::stacks::db::{StacksBlockHeaderTypes, StacksChainState, StacksHeaderInfo};
use crate::chainstate::stacks::Error as ChainError;
use crate::net::http::{
parse_json, Error, HttpChunkGenerator, HttpNotFound, HttpRequest, HttpRequestContents,
Expand All @@ -31,6 +33,204 @@ use crate::net::http::{
use crate::net::httpcore::{request, RPCRequestHandler, StacksHttpRequest, StacksHttpResponse};
use crate::net::{Error as NetError, StacksNodeState};

/// Performs a MARF lookup to find the last snapshot with a sortition prior to the given burn block height.
/// Will return an empty consensus hash if no prior sorititon exists (i.e., if querying the Genesis tenure)
pub fn get_prior_last_sortition_consensus_hash(
sortdb: &SortitionDB,
block_snapshot: &BlockSnapshot,
preamble: &HttpRequestPreamble,
) -> Result<ConsensusHash, StacksHttpResponse> {
let handle = match sortdb.index_handle_at_ch(&block_snapshot.consensus_hash) {
Ok(handle) => handle,
Err(e) => {
let msg = format!(
"Failed to get sortition DB handle for tenure '{}': {e:?}",
block_snapshot.consensus_hash
);
error!("{msg}");
return Err(StacksHttpResponse::new_error(
preamble,
&HttpServerError::new(msg),
));
}
};

// Search backwards from the chain tip on the canonical fork to find the sortition
// that occurred BEFORE this burn block height (hence saturating_sub(1)).
let block_height = block_snapshot.block_height.saturating_sub(1);
match handle.get_last_snapshot_with_sortition(block_height.into()) {
Ok(last_sortition) => Ok(last_sortition.consensus_hash),
Err(e) => {
let msg = format!("Failed to get last sortition at block '{block_height}': {e:?}");
error!("{msg}");
Err(StacksHttpResponse::new_error(
preamble,
&HttpServerError::new(msg),
))
}
}
}

/// Retrieve the block snapshot for a given tenure consensus hash
pub fn get_block_snapshot_by_consensus_hash(
sortdb: &SortitionDB,
consensus_hash: &ConsensusHash,
preamble: &HttpRequestPreamble,
) -> Result<BlockSnapshot, StacksHttpResponse> {
let handle = sortdb.index_handle_at_ch(consensus_hash).map_err(|e| {
let msg = format!("Failed to get sortition DB handle for tenure '{consensus_hash}': {e:?}");
error!("{msg}");
StacksHttpResponse::new_error(preamble, &HttpServerError::new(msg))
})?;
match SortitionDB::get_block_snapshot_consensus(handle.conn(), consensus_hash) {
Ok(snap) => {
let Some(snap) = snap else {
let msg = format!("No sortition snapshot found for tenure '{consensus_hash}'");
debug!("{msg}");
return Err(StacksHttpResponse::new_error(
preamble,
&HttpNotFound::new(msg),
));
};
Ok(snap)
}
Err(e) => {
let msg =
format!("Failed to get sortition snapshot for tenure '{consensus_hash}': {e:?}");
error!("{msg}");
Err(StacksHttpResponse::new_error(
preamble,
&HttpServerError::new(msg),
))
}
}
}

/// Helper function to create RPCTenure from snapshot
pub fn create_rpc_tenure_from_snapshot(
snapshot: &BlockSnapshot,
last_sortition_ch: ConsensusHash,
) -> RPCTenure {
RPCTenure {
consensus_hash: snapshot.consensus_hash.clone(),
last_sortition_ch,
burn_block_height: snapshot.block_height.into(),
burn_block_hash: snapshot.burn_header_hash.to_hex(),
stacks_blocks: vec![],
}
}

/// Helper function to create RPCTenure from header info
pub fn create_rpc_tenure(
header_info: &StacksHeaderInfo,
last_sortition_ch: ConsensusHash,
) -> RPCTenure {
RPCTenure {
last_sortition_ch,
consensus_hash: header_info.consensus_hash.clone(),
burn_block_height: header_info.burn_header_height.into(),
burn_block_hash: header_info.burn_header_hash.to_hex(),
stacks_blocks: vec![],
}
}

/// Helper function to create tenure stream response
pub fn create_tenure_stream_response(
chainstate: &StacksChainState,
header_info: StacksHeaderInfo,
tenure: RPCTenure,
preamble: &HttpRequestPreamble,
) -> Result<RPCTenureStream, StacksHttpResponse> {
match RPCTenureStream::new(chainstate, header_info.index_block_hash(), tenure) {
Ok(stream) => Ok(stream),
Err(e) => {
let msg = format!("Failed to create tenure stream: {e:?}");
error!("{msg}");
Err(StacksHttpResponse::new_error(
preamble,
&HttpServerError::new(msg),
))
}
}
}

/// Possible replies for tenure blocks request:
/// either a stream of blocks, or an empty tenure in JSON.
pub enum TenureReply {
Stream(RPCTenureStream),
Json(RPCTenure),
}

/// Given a canonical tenure snapshot, try to stream blocks from the highest known header in that tenure;
/// otherwise, synthesize an empty tenure from the snapshot.
///
/// This helper encapsulates the common control flow for all "get tenure blocks" endpoints
/// (by consensus hash, burn block hash, or burn block height):
///
/// - The `snapshot` represents a *canonical* PoX sortition for a tenure, even if that tenure
/// ultimately contains **no Stacks blocks**.
/// - `last_sortition_ch` is the consensus hash of the most recent successful sortition
/// *prior* to this tenure, used to maintain a stable backward link between valid tenures.
///
/// The caller supplies `find_highest_header`, which performs the endpoint-specific lookup
/// for the highest known Stacks block header in this tenure. This allows the same response
/// logic to be reused across different query dimensions without duplicating error handling.
///
/// Behavior:
/// - If a block header is found, return a streaming response that incrementally emits all
/// blocks belonging to the tenure.
/// - If no block header exists (i.e., the tenure is valid but contains zero Stacks blocks),
/// return a fully materialized JSON tenure with an empty `stacks_blocks` array.
/// - If the header lookup fails, return a server error.
pub fn build_tenure_from_header_else_snapshot<F>(
chainstate: &StacksChainState,
snapshot: &BlockSnapshot,
last_sortition_ch: ConsensusHash,
preamble: &HttpRequestPreamble,
find_highest_header: F,
) -> Result<TenureReply, StacksHttpResponse>
where
F: FnOnce() -> Result<Option<StacksHeaderInfo>, ChainError>,
{
match find_highest_header() {
Ok(Some(header)) => {
let tenure = create_rpc_tenure(&header, last_sortition_ch);
let stream = create_tenure_stream_response(chainstate, header, tenure, preamble)?;
Ok(TenureReply::Stream(stream))
}
Ok(None) => Ok(TenureReply::Json(create_rpc_tenure_from_snapshot(
snapshot,
last_sortition_ch,
))),
Err(e) => {
let msg = format!("Failed to query tenure blocks: {e:?}");
error!("{msg}");
Err(StacksHttpResponse::new_error(
preamble,
&HttpServerError::new(msg),
))
}
}
}

pub fn encode_tenure_reply(
preamble: &HttpRequestPreamble,
reply: Result<TenureReply, StacksHttpResponse>,
) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> {
match reply {
Ok(TenureReply::Stream(stream)) => Ok((
HttpResponsePreamble::ok_json(preamble),
HttpResponseContents::from_stream(Box::new(stream)),
)),
Ok(TenureReply::Json(tenure)) => {
let body = HttpResponseContents::try_from_json(&tenure)
.map_err(|e| NetError::SendError(format!("Failed to encode JSON: {e:?}")))?;
Ok((HttpResponsePreamble::ok_json(preamble), body))
}
Err(e) => e.into(),
}
}

#[derive(Clone)]
pub struct RPCNakamotoTenureBlocksRequestHandler {
pub(crate) consensus_hash: Option<ConsensusHash>,
Expand All @@ -56,6 +256,9 @@ pub struct RPCTenureBlock {
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct RPCTenure {
pub consensus_hash: ConsensusHash,
/// The consensus hash corresponding to the last successful sortition prior to this tenure.
/// If no prior sorititions exist, it will be the Genesis tenure consensus hash.
pub last_sortition_ch: ConsensusHash,
pub burn_block_height: u64,
pub burn_block_hash: String,
pub stacks_blocks: Vec<RPCTenureBlock>,
Expand All @@ -78,19 +281,20 @@ pub struct RPCTenureStream {

impl RPCTenureStream {
/// Prepare for tenure streaming.
/// The tenure_first_chunk is created here and streamed at the first_next_block call
/// The tenure_first_chunk is created here and streamed at the first next_block call.
/// The HttpChunkGenerator trait implementation will take care of completing
/// the json stream (by clossing both the array and the object)
/// the json stream (by closing both the array and the object).
pub fn new(
chainstate: &StacksChainState,
block_id: StacksBlockId,
tenure: RPCTenure,
) -> Result<Self, ChainError> {
let headers_conn = chainstate.reopen_db()?;
let consensus_hash = tenure.consensus_hash;
let last_sortition_ch = tenure.last_sortition_ch;
let burn_block_height = tenure.burn_block_height;
let burn_block_hash = tenure.burn_block_hash;
let tenure_first_chunk = format!("{{\"consensus_hash\": \"{consensus_hash}\", \"burn_block_height\": {burn_block_height}, \"burn_block_hash\": \"{burn_block_hash}\", \"stacks_blocks\": [");
let tenure_first_chunk = format!("{{\"consensus_hash\": \"{consensus_hash}\", \"last_sortition_ch\": \"{last_sortition_ch}\", \"burn_block_height\": {burn_block_height}, \"burn_block_hash\": \"{burn_block_hash}\", \"stacks_blocks\": [");
Ok(RPCTenureStream {
headers_conn,
consensus_hash,
Expand Down Expand Up @@ -244,67 +448,27 @@ impl RPCRequestHandler for RPCNakamotoTenureBlocksRequestHandler {
.take()
.ok_or(NetError::SendError("`consensus_hash` not set".into()))?;

let stream_res =
node.with_node_state(|_network, sortdb, chainstate, _mempool, _rpc_args| {
let header_info =
match NakamotoChainState::find_highest_known_block_header_in_tenure(
&chainstate,
let reply = node.with_node_state(|_network, sortdb, chainstate, _mempool, _rpc_args| {
let snapshot =
get_block_snapshot_by_consensus_hash(sortdb, &consensus_hash, &preamble)?;
let last_sortition_ch =
get_prior_last_sortition_consensus_hash(sortdb, &snapshot, &preamble)?;
build_tenure_from_header_else_snapshot(
chainstate,
&snapshot,
last_sortition_ch,
&preamble,
|| {
NakamotoChainState::find_highest_known_block_header_in_tenure(
chainstate,
sortdb,
&consensus_hash,
) {
Ok(Some(header)) => header,
Ok(None) => {
let msg = format!("No blocks in tenure {consensus_hash}");
debug!("{msg}");
return Err(StacksHttpResponse::new_error(
&preamble,
&HttpNotFound::new(msg),
));
}
Err(e) => {
let msg = format!(
"Failed to query tenure blocks by consensus '{consensus_hash}': {e:?}"
);
error!("{msg}");
return Err(StacksHttpResponse::new_error(
&preamble,
&HttpServerError::new(msg),
));
}
};

let tenure = RPCTenure {
consensus_hash: header_info.consensus_hash.clone(),
burn_block_height: header_info.burn_header_height.into(),
burn_block_hash: header_info.burn_header_hash.to_hex(),
stacks_blocks: vec![],
};

match RPCTenureStream::new(chainstate, header_info.index_block_hash(), tenure) {
Ok(stream) => Ok(stream),
Err(e) => {
let msg = format!("Failed to create tenure stream: {e:?}");
error!("{msg}");
return Err(StacksHttpResponse::new_error(
&preamble,
&HttpServerError::new(msg),
));
}
}
});

let stream = match stream_res {
Ok(stream) => stream,
Err(e) => {
let msg = format!("Failed to create tenure stream: {e:?}");
error!("{msg}");
return e.into();
}
};
)
},
)
});

let preamble = HttpResponsePreamble::ok_json(&preamble);
let body = HttpResponseContents::from_stream(Box::new(stream));
Ok((preamble, body))
encode_tenure_reply(&preamble, reply)
}
}

Expand Down
Loading