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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
### Added

- When determining a global transaction replay set, the state evaluator now uses a longest-common-prefix algorithm to find a replay set in the case where a single replay set has less than 70% of signer weight.
- New endpoint /v3/tenures/blocks/ allowing retrieving the list of stacks blocks from a burn block

### Changed

Expand Down
23 changes: 23 additions & 0 deletions stackslib/src/chainstate/nakamoto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2862,6 +2862,29 @@ impl NakamotoChainState {
Self::get_block_header_nakamoto(chainstate_conn.sqlite(), &block_id)
}

/// Get all the blocks for a specific consensus hash and burnview
/// highest block on top
pub fn get_block_headers_in_tenure_at_burnview(
db: &Connection,
tenure_id: &ConsensusHash,
burn_view: &ConsensusHash,
) -> Result<Vec<StacksHeaderInfo>, ChainstateError> {
// see if we have a nakamoto block in this tenure
let qry = "
SELECT *
FROM nakamoto_block_headers
WHERE consensus_hash = ?1
AND burn_view = ?2
ORDER BY block_height DESC
";
let args = params![tenure_id, burn_view];
let out = query_rows(db, qry, args)?;
if !out.is_empty() {
return Ok(out);
}
Err(ChainstateError::NoSuchBlockError)
}

/// DO NOT USE IN CONSENSUS CODE. Different nodes can have different blocks for the same
/// tenure.
///
Expand Down
257 changes: 257 additions & 0 deletions stackslib/src/net/api/gettenureblocks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
// Copyright (C) 2025 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
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// 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_json;
use stacks_common::types::chainstate::{BlockHeaderHash, ConsensusHash};
use stacks_common::types::net::PeerHost;

use crate::chainstate::nakamoto::NakamotoChainState;
use crate::chainstate::stacks::db::{StacksBlockHeaderTypes, StacksHeaderInfo};
use crate::net::http::{
parse_json, Error, HttpNotFound, HttpRequest, HttpRequestContents, HttpRequestPreamble,
HttpResponse, HttpResponseContents, HttpResponsePayload, HttpResponsePreamble, HttpServerError,
};
use crate::net::httpcore::{request, RPCRequestHandler, StacksHttpRequest, StacksHttpResponse};
use crate::net::{Error as NetError, StacksNodeState};

#[derive(Clone)]
pub struct RPCNakamotoTenureBlocksRequestHandler {
pub(crate) consensus_hash: Option<ConsensusHash>,
}

impl RPCNakamotoTenureBlocksRequestHandler {
pub fn new() -> Self {
Self {
consensus_hash: None,
}
}
}

#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct RPCTenureBlock {
pub block_id: StacksBlockId,
pub block_hash: BlockHeaderHash,
pub parent_block_id: String,
pub height: u64,
}

#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct RPCTenure {
pub consensus_hash: ConsensusHash,
pub burn_block_height: u64,
pub burn_block_hash: String,
pub stacks_blocks: Vec<RPCTenureBlock>,
}

/// Decode the HTTP request
impl HttpRequest for RPCNakamotoTenureBlocksRequestHandler {
fn verb(&self) -> &'static str {
"GET"
}

fn path_regex(&self) -> Regex {
Regex::new(r#"^/v3/tenures/blocks/(?P<consensus_hash>[0-9a-f]{40})$"#).unwrap()
}

fn metrics_identifier(&self) -> &str {
"/v3/tenures/blocks/:consensus_hash"
}

/// Try to decode this request.
/// There's nothing to load here, so just make sure the request is well-formed.
fn try_parse_request(
&mut self,
preamble: &HttpRequestPreamble,
captures: &Captures,
query: Option<&str>,
_body: &[u8],
) -> Result<HttpRequestContents, Error> {
if preamble.get_content_length() != 0 {
return Err(Error::DecodeError(
"Invalid Http request: expected 0-length body".to_string(),
));
}
let consensus_hash = request::get_consensus_hash(captures, "consensus_hash")?;
self.consensus_hash = Some(consensus_hash);
Ok(HttpRequestContents::new().query_string(query))
}
}

impl RPCRequestHandler for RPCNakamotoTenureBlocksRequestHandler {
/// Reset internal state
fn restart(&mut self) {
self.consensus_hash = None;
}

/// Make the response
fn try_handle_request(
&mut self,
preamble: HttpRequestPreamble,
_contents: HttpRequestContents,
node: &mut StacksNodeState,
) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> {
let consensus_hash = self
.consensus_hash
.take()
.ok_or(NetError::SendError("`consensus_hash` not set".into()))?;

let tenure_blocks_resp =
node.with_node_state(|_network, sortdb, chainstate, _mempool, _rpc_args| {
let header_info =
match 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we suppress the warning (clippy::uninlined_format_args) for stackslib, but I'd suggest to inline the format arguments:

format!("No blocks in tenure {consensus_hash}")

debug!("{}", &msg);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: as above, error!("{msg}")

return Err(StacksHttpResponse::new_error(
&preamble,
&HttpNotFound::new(msg),
));
}
Err(e) => {
let msg = format!(
"Failed to query tenure blocks by consensus '{}': {:?}",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: as above. "Failed to query tenure blocks by consensus '{consensus_hash}': {e:?}"

consensus_hash, &e
);
error!("{}", &msg);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: as above, error!("{msg}")

return Err(StacksHttpResponse::new_error(
&preamble,
&HttpServerError::new(msg),
));
}
};

let blocks = match NakamotoChainState::get_block_headers_in_tenure_at_burnview(
chainstate.db(),
&header_info.consensus_hash,
&header_info.burn_view.unwrap(),
) {
Ok(blocks) => blocks,
Err(e) => {
let msg = format!(
"Failed to query tenure blocks by consensus '{}': {:?}",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, as above: "Failed to query tenure blocks by consensus '{consensus_hash}': {e:?}"

consensus_hash, &e
);
error!("{}", &msg);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, as above: error!("{msg}");

return Err(StacksHttpResponse::new_error(
&preamble,
&HttpServerError::new(msg),
));
}
};

let mut canonical_blocks: Vec<StacksHeaderInfo> = vec![];
// now go back from the top to the bottom
let mut current_block_opt = blocks.first();
while let Some(current_block) = current_block_opt {
if let Some(nakamoto_block) = current_block.anchored_header.as_stacks_nakamoto()
{
let block = current_block.clone();
let block_parent_id = nakamoto_block.parent_block_id;
canonical_blocks.push(block);
current_block_opt = blocks
.iter()
.find(|block| block.index_block_hash() == block_parent_id);
} else {
break;
}
}

Ok((canonical_blocks, header_info))
});
Comment on lines +161 to +179
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check for my own comprehension: why is this needed if the return of get_block_headers_in_tenure_at_burnview is already ordered by height? I initially thought that there could be competing blocks at the same height and you were filtering for the ones that are part of the canonical fork, but Nakamoto shouldn't fork within a tenure right?


let (tenure_blocks, header_info): (Vec<RPCTenureBlock>, StacksHeaderInfo) =
match tenure_blocks_resp {
Ok((tenure_blocks, header_info)) => (
tenure_blocks
.into_iter()
.map(|header| RPCTenureBlock {
block_id: header.index_block_hash(),
block_hash: header.anchored_header.block_hash(),
parent_block_id: match header.anchored_header {
StacksBlockHeaderTypes::Nakamoto(nakamoto) => {
nakamoto.parent_block_id.to_hex()
}
StacksBlockHeaderTypes::Epoch2(epoch2) => {
epoch2.parent_block.to_hex()
}
},

height: header.stacks_block_height,
})
Comment on lines +186 to +199
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I'd have moved this into a impl From<StacksHeaderInfo> for RPCTenureBlock and replace this with a RCPTenureBlock::from but it's just a personal preference, feel free to ignore this comment if you don't feel like it wound't improve readability!

.collect(),
header_info,
),
Err(response) => {
return response.try_into_contents().map_err(NetError::from);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

map_err may be redundant, as try_into_contents already return a NetError?

}
};

let tenure = RPCTenure {
consensus_hash: header_info.consensus_hash,
burn_block_height: header_info.burn_header_height.into(),
burn_block_hash: header_info.burn_header_hash.to_hex(),
stacks_blocks: tenure_blocks,
};

let preamble = HttpResponsePreamble::ok_json(&preamble);
let body = HttpResponseContents::try_from_json(&tenure)?;
Ok((preamble, body))
}
}

/// Decode the HTTP response
impl HttpResponse for RPCNakamotoTenureBlocksRequestHandler {
fn try_parse_response(
&self,
preamble: &HttpResponsePreamble,
body: &[u8],
) -> Result<HttpResponsePayload, Error> {
let tenure: RPCTenure = parse_json(preamble, body)?;
Ok(HttpResponsePayload::try_from_json(tenure)?)
}
}

impl StacksHttpRequest {
/// Make a new getinfo request to this endpoint
pub fn new_get_tenure_blocks(
host: PeerHost,
consensus_hash: &ConsensusHash,
) -> StacksHttpRequest {
StacksHttpRequest::new_for_peer(
host,
"GET".into(),
format!("/v3/tenures/blocks/{}", consensus_hash),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: format!("/v3/tenures/blocks/{consensus_hash}")

HttpRequestContents::new(),
)
.expect("FATAL: failed to construct request from infallible data")
}
}

impl StacksHttpResponse {
pub fn decode_tenure_blocks(self) -> Result<RPCTenure, NetError> {
let contents = self.get_http_payload_ok()?;
let response_json: serde_json::Value = contents.try_into()?;
let tenure: RPCTenure = serde_json::from_value(response_json)
.map_err(|_e| Error::DecodeError("Failed to decode JSON".to_string()))?;
Ok(tenure)
}
}
2 changes: 2 additions & 0 deletions stackslib/src/net/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ pub mod getstackerdbmetadata;
pub mod getstackers;
pub mod getstxtransfercost;
pub mod gettenure;
pub mod gettenureblocks;
pub mod gettenureinfo;
pub mod gettenuretip;
pub mod gettransaction;
Expand Down Expand Up @@ -119,6 +120,7 @@ impl StacksHttp {
self.register_rpc_endpoint(gettenure::RPCNakamotoTenureRequestHandler::new());
self.register_rpc_endpoint(gettenureinfo::RPCNakamotoTenureInfoRequestHandler::new());
self.register_rpc_endpoint(gettenuretip::RPCNakamotoTenureTipRequestHandler::new());
self.register_rpc_endpoint(gettenureblocks::RPCNakamotoTenureBlocksRequestHandler::new());
self.register_rpc_endpoint(get_tenures_fork_info::GetTenuresForkInfo::default());
self.register_rpc_endpoint(
gettransaction_unconfirmed::RPCGetTransactionUnconfirmedRequestHandler::new(),
Expand Down
Loading
Loading